LINE Messaging APIを使ってオウム返しbotを作成するメモ
LINE Messageing APIを使ってLINE botを作成してみようと思います。
Messaging APIの概要
手順
bot用のチャネルを作成
Messaging APIのページから「今すぐ始めよう」をクリックします。
(初めての場合はログインページが挟まるので、個人のLINEアカウントかビジネスアカウントでログインしてください)
新しいチャネル(今回ではbot)用の設定を行います。
プロバイダー、チャネルなどの用語は以下のページで説明されています。
チャネルアクセストークン v2.1 を作成する
Messaging APIを利用するためには、チャネルアクセストークンが必要です。
3種類のチャネルアクセストークンがあるようですが、v2.1というチャネルアクセストークンが推奨されています。
チャネルアクセストークン v2.1 はJWTを使って任意の有効期間を指定できるトークンです。
チャネルアクセストークンの作成方法は以下です。
詳しくは上記ページに記載されているので、ここでは簡単な手順を説明します。
- アサーション署名キーのキーペアを生成する
- Go, Python, ブラウザで生成する例が載っています。私はブラウザで生成しました。
- 「チャネル基本設定」>「アサーション署名キー」から生成した公開鍵を設定する
- JWTを生成する
- Node.jsのライブラリ
node-jose
を利用しました。
- Node.jsのライブラリ
- チャネルアクセストークンv2.1を発行する
- アクセストークン発行用のエンドポイントに生成したJWTを使ってリクエストします。
- チャネルアクセストークンv2.1を発行する Messaging APIリファレンス | LINE Developers
チャネルアクセストークンの発行時にエラーが発生(メモ)
チャネルアクセストークンの発行を行うと以下のようなエラーレスポンスが返ってきました。
$ node crypt/make_token.js
{ error: 'invalid_client', error_description: 'Invalid exp' }
JWTの有効期間payload.exp
が正しくないようで調べてみたところ、実行環境(WSL2)の時刻設定が過去の時刻となっておりエラーになっていました。
参考:WindowsとWSL2の時刻のずれを修正する - Qiita
また、JWTの有効期間が30分の場合、同様にInvalid exp
というエラーが発生したので、25分に縮めたところ、正常にチャネルアクセストークンを取得することができました。
以下のスクラップが参考になりました。
バックエンドサーバーの実行環境にGASを利用しようとしたが断念した
サーバーの実行環境としてGAS (Google Apps Script) を利用しようとしていたが、以下の観点から利用しないことにした。
- ES Modules (
import/export
) に対応していない。- 代替案はあるがnpmパッケージを利用する規模の開発には向いていなさそう。
- トークンの検証はSDKを利用したかった。
- https://github.com/google/clasp/blob/master/docs/esmodules.md
- LINE Messaging APIの仕様として、webhookのエンドポイントからは200コードが返却される必要があるが、GASではセキュリティ上302が返却されてしまうため
バックエンドサーバーにFirebaseを利用する
Firebaseの
- Cloud Functions for Firebase(サーバーレスのサーバー)
- Cloud FireStore (NoSQLのストレージ)
- チャネルアクセストークンの保存
を利用することができそう。
Cloud FunctionsのGet Startedが完了したところで時間切れ。
やり残したこと
- まずはオウム返しbotの作成
- チャネルアクセストークンの保存と再生成処理
- そもそもチャネルアクセストークンの仕組みについて理解したい
- リッチメニューの作成
- LIFFでGoogleフォームを表示
LINE botのWebhookから呼ぶ関数をデプロイ
firebase init functions
など、プロジェクトの初期化は完了している前提。
webhook
という関数をexport
することでwebhook
という名のCloud Funtionを作成できる。
まずは文字列を単に返すようにする。
import {https} from "firebase-functions";
export const webhook = https.onRequest((req, res) => {
res.send("HTTP POST request sent to the webhook URL!");
});
デプロイ
npx firebase deploy --only functions
=== Deploying to 'xxxxxxxxxxxxx'...
i deploying functions
Running command: npm --prefix "$RESOURCE_DIR" run lint
> lint
> eslint --ext .js,.ts .
Running command: npm --prefix "$RESOURCE_DIR" run build
> build
> tsc
✔ functions: Finished running predeploy script.
i functions: preparing codebase default for deployment
i functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i functions: ensuring required API cloudbuild.googleapis.com is enabled...
i artifactregistry: ensuring required API artifactregistry.googleapis.com is enabled...
✔ artifactregistry: required API artifactregistry.googleapis.com is enabled
✔ functions: required API cloudfunctions.googleapis.com is enabled
✔ functions: required API cloudbuild.googleapis.com is enabled
i functions: preparing functions directory for uploading...
i functions: packaged /home/xxxxxxxxxxxxxxx/functions (165.74 KB) for uploading
✔ functions: functions folder uploaded successfully
i functions: creating Node.js 16 function webhook(us-central1)...
✔ functions[webhook(us-central1)] Successful create operation.
Function URL (webhook(us-central1)): https://xxxxxxxxxxxxxxx.cloudfunctions.net/webhook
i functions: cleaning up build files...
✔ Deploy complete!
Firebaseのコンソール画面から以下のように表示されればデプロイ完了。
関数のエンドポイントにアクセスすると、レスポンスが返ってくることも確認できる。
LINE Messaging APIのwebhook URLを設定する
関数のエンドポイントを設定して、「検証」ボタンを押す。「成功」が表示されればOK。
これでLINE Messaging APIからFirebaseの関数をwebhookで呼び出せるようになった。
LINE Messaging API SDK for nodejs を使ってオウム返しbotを作成する
ユーザーが送信したテキストメッセージをそのままオウム返しするbotを作成しようと思います。
まず、Messaging API設定のページから「Webhookの利用」をONにする
次に LINE Messaging API を簡単に利用するためにSDKをインストールする。
npm install @line/bot-sdk --save
SDKにはTypeScriptの型定義も含まれている。
そして、以下のようなコードを書きます。
index.ts
import {https, logger} from "firebase-functions";
import {defineString} from "firebase-functions/params";
import {WebhookRequestBody, Client} from "@line/bot-sdk";
// 実行時に必要なパラメータを定義
const config = {
channelSecret: defineString("CHANNEL_SECRET"),
channelAccessToken: defineString("CHANNEL_ACCESS_TOKEN"),
};
export const webhook = https.onRequest((req, res) => {
res.send("HTTP POST request sent to the webhook URL!");
// LINE Messaging API Clientの初期化
const lineClient = new Client({
channelSecret: config.channelSecret.value(),
channelAccessToken: config.channelAccessToken.value(),
});
// ユーザーがbotに送ったメッセージをそのまま返す
const {events} = req.body as WebhookRequestBody;
events.forEach((event) => {
switch (event.type) {
case "message": {
const {replyToken, message} = event;
if (message.type === "text") {
lineClient.replyMessage(replyToken, {type: "text", text: message.text});
}
break;
}
default:
break;
}
});
});
そして、デプロイコマンドを実行すると途中で以下のような質問が来るのでチャネルシークレットとチャネルアクセストークンを入力します。
? Enter a string value for CHANNEL_SECRET: xxxx
? Enter a string value for CHANNEL_ACCESS_TOKEN: xxxxxx
CHANNEL_SECRET
はDeveloper Consoleの「チャネルシークレット」、CHANNEL_ACCESS_TOKEN
は先程取得したチャネルアクセストークンを設定します。
(本当はチャネルアクセストークンは有効期限があるので、固定の値にせず都度有効なトークンを取得する必要がありますが、一旦ここでは固定の値として設定します。)
入力が完了すると、.env.<project_ID>
という.env
ファイルが作成されます。
ここではCloud Functions実行時のパラメータ(環境構成)を設定しています。パラメータを変更したい場合は.env
ファイルを更新してください。
詳しくは、環境を構成する | Cloud Functions for Firebase を参照してください。
さて、コードの解説をします。
// 実行時に必要なパラメータを定義
const config = {
channelSecret: defineString("CHANNEL_SECRET"),
channelAccessToken: defineString("CHANNEL_ACCESS_TOKEN"),
};
ここでは実行時に必要なパラメータの設定をしています。関数のデプロイ時にこれらの値が.env
ファイルから読み込まれます。
// LINE Messaging API Clientの初期化
const lineClient = new Client({
channelSecret: config.channelSecret.value(),
channelAccessToken: config.channelAccessToken.value(),
});
LINE Messaging API にリクエストを送るためのクライアントの初期化を行います。
.value()
によってパラメータの読み込みをしています。
// ユーザーがbotに送ったメッセージをそのまま返す
const {events} = req.body as WebhookRequestBody;
Webhookによるリクエストのbody
のパラメータは以下のドキュメントに記載されています。
例としては、
{
"destination": "xxxxxxxxxx",
"events": [
{
"type": "message",
"message": {
"type": "text",
"id": "14353798921116",
"text": "Hello, world"
},
"timestamp": 1625665242211,
"source": {
"type": "user",
"userId": "U80696558e1aa831..."
},
"replyToken": "757913772c4646b784d4b7ce46d12671",
"mode": "active",
"webhookEventId": "01FZ74A0TDDPYRVKNK77XKC3ZR",
"deliveryContext": {
"isRedelivery": false
}
},
{
"type": "follow",
"timestamp": 1625665242214,
"source": {
"type": "user",
"userId": "Ufc729a925b3abef..."
},
"replyToken": "bb173f4d9cf64aed9d408ab4e36339ad",
"mode": "active",
"webhookEventId": "01FZ74ASS536FW97EX38NKCZQK",
"deliveryContext": {
"isRedelivery": false
}
},
{
"type": "unfollow",
"timestamp": 1625665242215,
"source": {
"type": "user",
"userId": "Ubbd4f124aee5113..."
},
"mode": "active",
"webhookEventId": "01FZ74B5Y0F4TNKA5SCAVKPEDM",
"deliveryContext": {
"isRedelivery": false
}
}
]
}
です。複数のイベントがほぼ同時に起きた際、Webhookのリクエストはまとめて送られることがあるようです。
events.forEach((event) => {
switch (event.type) {
case "message": {
const {replyToken, message} = event;
if (message.type === "text") {
lineClient.replyMessage(replyToken, {type: "text", text: message.text});
}
break;
}
...
})
今回はユーザーが投稿したテキストメッセージをオウム返ししたいので、events
配列の要素event
に対して
- ユーザーがメッセージ送信したイベント:
event.type
がmessage
- メッセージがテキストメッセージ:
event.message.typeが"text"
の場合にレスポンスを返すようにします。(詳しいパラメータはドキュメントにすべて載っています)
最後に、メッセージを返信します。
lineClient.replyMessage(replyToken, {type: "text", text: message.text});
返信するために必要なパラメータはドキュメントを参照してください。
LINE botの友達登録をする
やっとここまで来ました。
LINE Developersコンソールの「Messaging API設定>QRコード」をLINEアプリで読み込んでください。
友達登録をしてテキストメッセージを送ってみると・・・?
オウム返ししてくれました! 🎉
署名の検証を行う
まだまだ終わりではありません。
本番環境にて運用する場合にはセキュリティー対策を行う必要があります。
LINEプラットフォーム以外からの不正なリクエストを防ぐために署名の検証を行います。
詳しくは、署名を検証する | LINE Developers をご確認ください。
署名を検証するには3つの方法があります。
- SDKの
middlewere
を利用する- Express などのWebフレームワークを利用している場合
- SDKの
validateSignature
関数- 署名の検証の前に
body-parser
でリクエストのパースを行う環境(Firebase Cloud Functionsなど)や任意のタイミングで検証したい場合
- 署名の検証の前に
- 自作の関数で検証する
今回の場合は、Cloud Functionsを使っているので、validateSignature
関数を利用することにします。
- import {WebhookRequestBody, Client} from "@line/bot-sdk";
+ import {WebhookRequestBody, Client, SignatureValidationFailed, validateSignature} from "@line/bot-sdk";
...
export const webhook = https.onRequest((req, res) => {
res.send("HTTP POST request sent to the webhook URL!");
+ // 署名の検証
+ const channelSecret = config.channelSecret.value();
+ const signature = req.header("x-line-signature") ?? "";
+ if (!validateSignature(req.rawBody, channelSecret, signature)) {
+ throw new SignatureValidationFailed("invalid signature");
+ }
// LINE Messaging API Clientの初期化
...
});
検証
ヘッダーに無効な署名を記載を載せてPOSTリクエストを送ります
curl \
'https://<自分の環境のURL>/webhook' \
-H 'X-Line-Signature: invalid signature' \
-H 'Content-Type: application/json' \
-d '{"destination": "xxxxxxxxxx","events": [{"type": "message","message": {"type": "text","id": "14353798921116","text": "Hello, world"},"timestamp": 1625665242211,"source": {"type": "user","userId": "U80696558e1aa831..."},"replyToken": "757913772c4646b784d4b7ce46d12671","mode": "active","webhookEventId": "01FZ74A0TDDPYRVKNK77XKC3ZR","deliveryContext": {"isRedelivery": false}}]}'
Cloud Functionsのログを確認するとエラーが吐かれ、不正なリクエストを弾くことができました。