Open10

LINE Messaging APIを使ってオウム返しbotを作成するメモ

Yuma ItoYuma Ito

手順

bot用のチャネルを作成

Messaging APIのページから「今すぐ始めよう」をクリックします。

(初めての場合はログインページが挟まるので、個人のLINEアカウントかビジネスアカウントでログインしてください)

新しいチャネル(今回ではbot)用の設定を行います。

プロバイダー、チャネルなどの用語は以下のページで説明されています。

https://developers.line.biz/ja/docs/line-developers-console/overview/

Yuma ItoYuma Ito

チャネルアクセストークン v2.1 を作成する

Messaging APIを利用するためには、チャネルアクセストークンが必要です。

https://developers.line.biz/ja/docs/messaging-api/channel-access-tokens/

3種類のチャネルアクセストークンがあるようですが、v2.1というチャネルアクセストークンが推奨されています。
チャネルアクセストークン v2.1 はJWTを使って任意の有効期間を指定できるトークンです。

チャネルアクセストークンの作成方法は以下です。
https://developers.line.biz/ja/docs/messaging-api/generate-json-web-token/

詳しくは上記ページに記載されているので、ここでは簡単な手順を説明します。

  1. アサーション署名キーのキーペアを生成する
    • Go, Python, ブラウザで生成する例が載っています。私はブラウザで生成しました。
  2. 「チャネル基本設定」>「アサーション署名キー」から生成した公開鍵を設定する
  3. JWTを生成する
    • Node.jsのライブラリnode-joseを利用しました。
  4. チャネルアクセストークンv2.1を発行する

チャネルアクセストークンの発行時にエラーが発生(メモ)

チャネルアクセストークンの発行を行うと以下のようなエラーレスポンスが返ってきました。

$ node crypt/make_token.js 
{ error: 'invalid_client', error_description: 'Invalid exp' }

JWTの有効期間payload.expが正しくないようで調べてみたところ、実行環境(WSL2)の時刻設定が過去の時刻となっておりエラーになっていました。

参考:WindowsとWSL2の時刻のずれを修正する - Qiita

また、JWTの有効期間が30分の場合、同様にInvalid expというエラーが発生したので、25分に縮めたところ、正常にチャネルアクセストークンを取得することができました。

Yuma ItoYuma Ito

バックエンドサーバーの実行環境にGASを利用しようとしたが断念した

サーバーの実行環境としてGAS (Google Apps Script) を利用しようとしていたが、以下の観点から利用しないことにした。

Yuma ItoYuma Ito

バックエンドサーバーにFirebaseを利用する

Firebaseの

  • Cloud Functions for Firebase(サーバーレスのサーバー)
  • Cloud FireStore (NoSQLのストレージ)
    • チャネルアクセストークンの保存

を利用することができそう。
Cloud FunctionsのGet Startedが完了したところで時間切れ。

やり残したこと

  • まずはオウム返しbotの作成
  • チャネルアクセストークンの保存と再生成処理
    • そもそもチャネルアクセストークンの仕組みについて理解したい
  • リッチメニューの作成
  • LIFFでGoogleフォームを表示
Yuma ItoYuma Ito

LINE botのWebhookから呼ぶ関数をデプロイ

firebase init functionsなど、プロジェクトの初期化は完了している前提。

webhookという関数をexportすることでwebhookという名のCloud Funtionを作成できる。

まずは文字列を単に返すようにする。

index.ts
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のコンソール画面から以下のように表示されればデプロイ完了。

関数のエンドポイントにアクセスすると、レスポンスが返ってくることも確認できる。

Yuma ItoYuma Ito

LINE Messaging APIのwebhook URLを設定する

関数のエンドポイントを設定して、「検証」ボタンを押す。「成功」が表示されればOK。

これでLINE Messaging APIからFirebaseの関数をwebhookで呼び出せるようになった。

Yuma ItoYuma Ito

LINE Messaging API SDK for nodejs を使ってオウム返しbotを作成する

ユーザーが送信したテキストメッセージをそのままオウム返しするbotを作成しようと思います。

まず、Messaging API設定のページから「Webhookの利用」をONにする

次に LINE Messaging API を簡単に利用するためにSDKをインストールする。
https://line.github.io/line-bot-sdk-nodejs/

npm install @line/bot-sdk --save

SDKにはTypeScriptの型定義も含まれている。

そして、以下のようなコードを書きます。

index.ts

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のパラメータは以下のドキュメントに記載されています。

https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objects

例としては、

{
  "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.typemessage
  • メッセージがテキストメッセージ:event.message.typeが"text"

の場合にレスポンスを返すようにします。(詳しいパラメータはドキュメントにすべて載っています)

最後に、メッセージを返信します。

lineClient.replyMessage(replyToken, {type: "text", text: message.text});

返信するために必要なパラメータはドキュメントを参照してください。

応答メッセージを送る | LINE Developers

LINE botの友達登録をする

やっとここまで来ました。
LINE Developersコンソールの「Messaging API設定>QRコード」をLINEアプリで読み込んでください。

友達登録をしてテキストメッセージを送ってみると・・・?

オウム返ししてくれました! 🎉

Yuma ItoYuma Ito

署名の検証を行う

まだまだ終わりではありません。
本番環境にて運用する場合にはセキュリティー対策を行う必要があります。
LINEプラットフォーム以外からの不正なリクエストを防ぐために署名の検証を行います。

詳しくは、署名を検証する | LINE Developers をご確認ください。

署名を検証するには3つの方法があります。

  • SDKのmiddlewereを利用する
    • Express などのWebフレームワークを利用している場合
  • SDKのvalidateSignature関数
    • 署名の検証の前にbody-parserでリクエストのパースを行う環境(Firebase Cloud Functionsなど)や任意のタイミングで検証したい場合
  • 自作の関数で検証する

今回の場合は、Cloud Functionsを使っているので、validateSignature関数を利用することにします。

index.ts
- 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のログを確認するとエラーが吐かれ、不正なリクエストを弾くことができました。