今回は、LINE Bot 内で送信された画像をアプリケーション側で取得する方法を備忘録として整理していきます。Node.js & TypeScriptに構築されたアプリケーションをベースに、LINEBotのSDKを利用して画像を取得します。
実装の流れ
前回の記事を参考にNode.js + TypeScriptの実行環境を構築
前回の記事を参考にLINE Botをセットアップ
SDKを利用してメッセージの画像を取得する処理を実装
開発環境
- Node.js:
v18.15.0
- npm:
v9.5.0
- TypeScript:
v4.9.5
- OS:
MacOS Monterey
構築手順
実行環境の構築
まずはNode.js + TypeScriptで実行できる環境を構築。
構築方法は以下をベースに実施しましたので、まだ構築できていない方は参考にしてください。
LINEBotのセットアップ
次にLINEBotのセットアップをしていきますが、以前まとめた記事の内容をベースに進めていきます。
主な流れは以下です。
- Node.jsとTypeScriptによる実行環境構築
- expressによるAPIサーバーの構築
- ngrokを使ってローカル環境の外部公開
- LINE Messaging APIの設定
- LINEBot SDKを使ったLINEBotの実装
この時点での実行ファイルのソースコードは以下のようになります。
ポイントとしては、if (event.type !== 'message' || event.message.type !== 'image') return;
で対象を画像のみに絞り込むことです。
import { WebhookEvent, middleware } from '@line/bot-sdk';
import express, { Application, Request, Response } from 'express';
import { load } from 'ts-dotenv';
const env = load({
CHANNEL_ACCESS_TOKEN: String,
CHANNEL_SECRET: String,
BASE_URL: String,
PORT: Number,
USER_ID_LIST: String,
});
const BASE_URL = env.BASE_URL || 'http://localhost';
const PORT = env.PORT || 3000;
const USER_ID_LIST = env.USER_ID_LIST?.split(',') || [];
const lineMessagingClientConfig = {
channelAccessToken: env.CHANNEL_ACCESS_TOKEN || '',
channelSecret: env.CHANNEL_SECRET,
};
const app: Application = express();
app.get('/webhook', async (_: Request, res: Response): Promise<Response> => {
return res.status(200).send('Health OK');
});
app.post(
'/webhook',
middleware(lineMessagingClientConfig),
async (req: Request, res: Response): Promise<Response> => {
const events: WebhookEvent[] = req.body.events;
await Promise.all(
events.map(async (event: WebhookEvent) => {
const userId = event.source.userId;
if (!userId || !USER_ID_LIST.includes(userId))
throw new Error('不正なユーザーのためブロック');
if (event.type !== 'message' || event.message.type !== 'image') return; //対象を画像に絞る
//TODO:画像を取得する処理を実装
})
);
return res.status(200);
}
);
app.listen(PORT, () => {
console.log(`${BASE_URL}:${PORT}/`);
});
画像を取得する処理の実装
SDKで提供されているgetMessageContent
メソッドとStream & Chunkを利用して画像を取得していきます。
画像を取得するには画像が送信されたメッセージのIDが必要になるため、WebhookEvent
から画像メッセージであることを特定し、IDを抽出します。
最終的なソースコード
import { Client } from '@line/bot-sdk';
import { load } from 'ts-dotenv';
import * as fs from 'fs';
import axios from 'axios';
const env = load({
CHANNEL_ACCESS_TOKEN: String,
CHANNEL_SECRET: String,
});
interface LineMessagingClientConfig {
channelAccessToken: string;
channelSecret: string;
}
export default class LineMessagingClient {
private client: Client;
constructor() {
const config = {
channelAccessToken: env.CHANNEL_ACCESS_TOKEN,
channelSecret: env.CHANNEL_SECRET,
};
this.client = new Client(config);
}
public async fetchImageByStream(messageId: string): Promise<void> {
return new Promise((resolve, reject) => {
this.client.getMessageContent(messageId).then((stream) => {
const chunks: Buffer[] = [];
stream.on('data', (chunk) => {
chunks.push(chunk);
});
stream.on('end', () => {
const buffer = Buffer.concat(chunks);
fs.writeFileSync('./data/sample.jpg', buffer);
resolve();
});
stream.on('error', (err) => {
reject(err);
});
});
});
}
}
import LineMessagingClient from './line-message-client'; //追加
import { WebhookEvent, middleware } from '@line/bot-sdk';
import express, { Application, Request, Response } from 'express';
import { load } from 'ts-dotenv';
const env = load({
CHANNEL_ACCESS_TOKEN: String,
CHANNEL_SECRET: String,
BASE_URL: String,
PORT: Number,
USER_ID_LIST: String,
});
const BASE_URL = env.BASE_URL || 'http://localhost';
const PORT = env.PORT || 3000;
const USER_ID_LIST = env.USER_ID_LIST?.split(',') || [];
const lineMessagingClientConfig = {
channelAccessToken: env.CHANNEL_ACCESS_TOKEN || '',
channelSecret: env.CHANNEL_SECRET,
};
const lineMessagingClient = new LineMessagingClient(); //追加
const app: Application = express();
app.get('/webhook', async (_: Request, res: Response): Promise<Response> => {
return res.status(200).send('Health OK');
});
app.post(
'/webhook',
middleware(lineMessagingClientConfig),
async (req: Request, res: Response): Promise<Response> => {
const events: WebhookEvent[] = req.body.events;
await Promise.all(
events.map(async (event: WebhookEvent) => {
const userId = event.source.userId;
if (!userId || !USER_ID_LIST.includes(userId))
throw new Error('不正なユーザーのためブロック');
if (event.type !== 'message' || event.message.type !== 'image') return;
await lineMessagingClient.fetchImageByStream(event.message.id); //追加
})
);
return res.status(200);
}
);
app.listen(PORT, () => {
console.log(`${BASE_URL}:${PORT}/`);
});
処理フロー
- メッセージタイプを画像のみに絞り込めたら、
LineMessagingClient.fetchImageByStream
にmessageId
を渡して実行 getMessageContent(messageId)
を呼び出して、画像コンテンツのストリームを取得- ストリームが取得されたら、
stream.on('data', ...)
を使って、ストリームから読み取ったデータのチャンクを処理。各チャンクをchunks
配列に格納。 - ストリームの終わりに到達したら、
stream.on('end', ...)
イベントが発火。このイベント内で、Buffer.concat(chunks)
を使用して、チャンクの配列を単一のバッファに結合。 - 結合したバッファを使って、
fs.writeFileSync('./data/sample.jpg', buffer)
を呼び出し、指定されたパスに画像ファイルを保存。
StreamとChunkについて
StreamとChunkについて正しく理解できていなかったので整理します。
ストリーム(Stream)
ストリームは、データの連続的な流れを表現する抽象概念。例えば、ファイルからデータを読み込んだり、ネットワーク経由でデータを送受信する際に、ストリームを利用する。ストリームを使用することで、データを一度にすべて読み込むのではなく、小さな部分(チャンク)を順次処理可能。これにより、大量のデータを効率的に扱うことができる。
チャンク(Chunk)
チャンクは、ストリームからデータを読み込む際に、一度に処理されるデータの小さな部分のこと。ストリームからデータを読み込むとき、データ全体を一度に取得するのではなく、小さなチャンクに分割して処理する。これにより、メモリ使用量を抑えることができ、大量のデータを効率的に扱うことができる。
例えば、画像や動画をダウンロードする際に、ストリームとチャンクを利用します。ファイル全体を一度にダウンロードするのではなく、小さなチャンクに分けてダウンロードして、順次処理することで、ダウンロード中に画像や動画の一部を表示したり、再生を開始することが可能となる。
まとめると、
つまりは、ストリームはデータの連続的な流れを表現し、チャンクはその流れの中で一度に処理されるデータの小さな部分を指す。ストリームとチャンクを利用することで、大量のデータを効率的に扱い、リソースの使用量(メモリ)を抑えることができる。よって、メモリリークやパフォーマンス遅延を防ぐことが可能。
[番外編] APIから直接画像を取得する方法
直接APIのエンドポイントをたたいて画像を取得する方法もあったので、一応記載します。
public async fetchImageByAxios(messageId: string): Promise<void> {
try {
const url = `https://api-data.line.me/v2/bot/message/${messageId}/content`;
const res = await axios.get(url, {
responseType: 'arraybuffer',
headers: { Authorization: `Bearer ${env.CHANNEL_ACCESS_TOKEN}` },
});
fs.writeFileSync('./data/sample.jpg', Buffer.from(res.data));
} catch (err) {
console.error(err);
}
}
詳しくは公式ドキュメントに記載されていますが、画像が送信されているmessageId
が分かれば取得可能です。
エンドポイントURLにmessageId
を埋め込み、ヘッダーのAuthorization
にBearerトークンを渡してあげればOK。
まとめ
この記事では、LINEBotのメッセージから画像を取得する処理についてまとめてみました。LINE Botで画像をやり取りしたり、画像から情報を取得する際に活用できそうです。今後、画像以外のコンテンツについても別途整理できればと思います。この記事が少しでも誰かの参考になれば幸いです。ご覧いただきありがとうございました。