LoginSignup
6
2

More than 1 year has passed since last update.

LINE Messaging API × Firebase Cloud Functions リッチメニューを押したらFirestoreに値を保存する

Last updated at Posted at 2022-08-24

はじめに

LINE Messaging APIを使ってLINE botやLIFF(LINE Front-end Framework)のアプリを開発するための、最初の土台作りの部分は省略します。。。(他のいろいろなサイトで解説されているので:wink:

LINE公式アカウントの作成とFirebaseプロジェクトの作成などは頑張ってください!

実現したい動作としては👇のような感じ
①リッチメニューを押す
②設定しておいたメッセージがユーザー側から送信される
③Fiirestoreになんらかの値を保存

環境

・言語はTypeScript
・WebhookサーバーはFirebase Cloud Functions
Expressは使いません

Webhookとは?

わかりやすいサイトがあったので👇を参考に。ここの理解はとても大事です!

LINE公式アカウントに何らかのイベントが起こった場合に、別のサービスを行っているサーバーへデータを送信する(通知する)ことをwebhookと言います。
イベントっていうのは、「友だちに追加された」「ブロックされた」「メッセージを受信した」といったことを指しています。
このようなイベントが起こったことを、チャットボットのサーバーに通知してくれるのがwebhookです。
https://hokkaido-dc.com/digital/edward/linechatbptsample/

Screen Shot 2022-08-24 at 21.15.16.png
ここで言う「チャットボット用サーバー」が今回ではFirebase Cloud Functionsにあたります。

リッチメニューが押されたことをどう検知するか?

リッチメニューを押すとユーザーから何かしらのテキストが送信されます。

例えば「ぴえん🥺」と言うリッチメニューがあったとして、それを押すと「ぴえん🥺」がユーザー側から送信されます。

これが「リッチメニューによって送信されたメッセージ」なのか、「ユーザー自身が入力して送信されたメッセージ」なのか、どうやって判断すれば良いのか?
LINE API初心者だった頃は頭を悩ませました。
なのでLINE公式のQ&Aプラットフォームで質問してみることに。

すると一瞬で解決しました!!

・Messaging APIでリッチメニューを作成
・ポストバックアクションを使う

ここがポイントです:smile:

LINE APIのチャネルシークレットとチャネルアクセストークンをGoogle Cloud Secret Managerに設定

API キーなどの機密性の高い値は.envに入れるべきではないようなので、Google Cloud Secret Managerを使用します。

LINE developerからチャネルシークレットとチャネルアクセストークンを確認しましょう。

$ firebase functions:config:set line.channel_secret="channelSecret"

$ firebase functions:config:set line.channel_access_token="channelAccessToken"

これで値がセットできたか確認

$ firebase functions:config:get
{
  "line": {
    "channel_secret": "channelSecret",
    "channel_access_token": "channelAccessToken"
  }
}

Cloud Functionsへデプロイ

Cloud Functionsのコードを書く

書き方は本当人によっていろいろだな〜っと感じました。。
あくまでこれも1例です。

your_project/functions/src/index.ts
import * as functions from "firebase-functions";
import * as line from "@line/bot-sdk";
import * as admin from 'firebase-admin';
admin.initializeApp();
const db = admin.firestore();

interface Config {
  channelAccessToken: string;
  channelSecret: string;
}

const config: Config = {
  channelAccessToken: functions.config().line.channel_access_token,
  channelSecret: functions.config().line.channel_secret
};

const client = new line.Client(config);

export const lineWebhook = functions
    .region("asia-northeast1")
    .https.onRequest(async (request, response) => {
      const signature = request.get("x-line-signature");

      if (!signature || !line.validateSignature(
          request.rawBody, config.channelSecret, signature
      )) {
        throw new
        line.SignatureValidationFailed(
            "signature validation failedで失敗ですわ!",
            signature
        );
      }
      const res: line.WebhookEvent = request.body.events[0];
      
      try {
        await actionPien(res);
      } catch (err) {
        console.log(err);
      }
    });

const actionPien = async (event: line.WebhookEvent) => {
  const userId = event.source.userId!;
  if (event.type !== 'postback') {
    return;
  }
    try {
      const message: line.TextMessage[] = [
        {
          type: "text",
          text: event.postback.data
        },
        {
          type: "text",
          text: "ぴえん"
        }
      ];
      client.replyMessage(event.replyToken, message);
      return addPien(userId, event.timestamp);
    } catch (error) {
      console.error(JSON.stringify(error));
      return Promise.resolve(null);
    }
};

const addPien = async (userId: string, timestamp: number) => {
  const pienDate = timestampToTime(timestamp);
  const pienEvent = {
    pienTime: pienDate,
  };
  
  return db.collection(`users/${userId}/piens`).add(pienEvent);
}

// 引数 timestamp の単位はミリ秒
const timestampToTime = (timestamp: number) => {
  const date = new Date(timestamp);
  const yyyy = `${date.getFullYear()}`;
  // .slice(-2)で文字列中の末尾の2文字を取得する
  // `0${date.getHoge()}`.slice(-2) と書くことで0埋めをする
  const MM = `0${date.getMonth() + 1}`.slice(-2); // getMonth()の返り値は0が基点
  const dd = `0${date.getDate()}`.slice(-2);
  const HH = `0${date.getHours() + 9}`.slice(-2); // 日本時間になるよう+9h
  const mm = `0${date.getMinutes()}`.slice(-2);
  const ss = `0${date.getSeconds()}`.slice(-2);

  return `${yyyy}/${MM}/${dd} ${HH}:${mm}:${ss}`;
}

一旦デプロイします

$ firebase deploy --only functions

デプロイした関数のURLをwebhook URLに登録

Screen Shot 2022-08-24 at 22.24.17.png

Screen Shot 2022-08-24 at 22.28.02.png

これでCloud Functions側は完了です!

リッチメニューをデプロイ

ポストバックアクションを使うにはリッチメニューを自分でアップロードしなくてはなりません。。。
初見ではまぁまぁむずいです:sweat_smile:

リッチメニュー画像を用意

色々やり方はあると思いますが、canvaからテンプレをゲットして画像をkeynoteとかGoogleスライドで当てはめるやり方が楽かな〜っと。
比率をピッタシにするのは大変ですからね〜・・・

リッチメニューをデプロイ

こちらを参考にしながらやってみてください。
初めてcurlコマンドに触れる方は混乱すると思いますが、ターミナルにコマンド(めっちゃ長いけど)をコピペでぶち込んでEnter押すだけです!!

リッチメニューに変更を加える度に毎回するので、どこかにコピペして置いとくのがおすすめ。richMenuIdも毎回変わるのでご注意を)

ちなみに私のリッチメニューはこんな感じ

ボタン3つのリッチメニューです。

{
  "size":{
      "width":2500,
      "height":1686
  },
  "selected": true,
  "name": "ぴえん",
  "chatBarText": "メニュー",
  "areas": [
    {
      "bounds": {
        "x": 0,
        "y": 0,
        "width": 1635,
        "height": 1686
      },
      "action": {
        "type": "postback",
        "data": "pien",
        "displayText": "\ぴえんっ🥺/\nどんなできごとがありましたか?\nよければLINEでお返事ください😌💫"
      }
    },
    {
      "bounds": {
        "x": 865,
        "y": 0,
        "width": 865,
        "height": 843
      },
      "action": {
        "type": "uri",
        "label": "動画だぜ",
        "uri": "https://www.youtube.com"
      }
    },
    {
      "bounds": {
        "x": 865,
        "y": 843,
        "width": 865,
        "height": 843
      },
      "action": {
        "type": "uri",
        "label": "Twitterだぜ",
        "uri": "https://twitter.com"
      }
    }
  ]
}

\nで改行できます

完成

これでリッチメニューの一番でかいボタンを押すと
①リッチメニューのdisplayTextに設定したメッセージがユーザーから送信される
②postbackを検知してbotがリッチメニューのdataと「ぴえん」のメッセージを送信
③Firestoreに値が保存される。
Screen Shot 2022-08-24 at 22.49.48.png

参考

今後プログラムが大きなってきたときに、index.tsに集中せずファイルを分割するやり方でとても参考になりました!!!

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2