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

# はじめに
LINE Messaging APIを使ってLINE botや[LIFF](https://developers.line.biz/ja/docs/liff/overview/)（LINE Front-end Framework）のアプリを開発するための、最初の土台作りの部分は省略します。。。（他のいろいろなサイトで解説されているので:wink:）


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

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

## 環境
・言語はTypeScript
・WebhookサーバーはFirebase Cloud Functions
・[Express](https://expressjs.com)は使いません

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

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

![Screen Shot 2022-08-24 at 21.15.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/e7401b8d-0fa4-74cc-21c8-05a8a9d5483b.png)
**ここで言う「チャットボット用サーバー」が今回ではFirebase Cloud Functionsにあたります。**

## リッチメニューが押されたことをどう検知するか？
リッチメニューを押すとユーザーから何かしらのテキストが送信されます。

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

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

https://www.line-community.me/en/question/62ecf90856453b191e4b2943

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

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

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


# LINE APIのチャネルシークレットとチャネルアクセストークンをGoogle Cloud Secret Managerに設定
API キーなどの機密性の高い値は`.env`に入れるべきではないようなので、Google Cloud Secret Managerを使用します。

https://firebase.google.com/docs/functions/config-env

[LINE developer](https://developers.line.biz/console/)からチャネルシークレットとチャネルアクセストークンを確認しましょう。

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

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

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

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

# Cloud Functionsへデプロイ
## Cloud Functionsのコードを書く
書き方は本当人によっていろいろだな〜っと感じました。。
あくまでこれも１例です。

```typescript: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) と書くことで０埋めをする
  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}`;
}
```

一旦デプロイします

```console
$ firebase deploy --only functions
```

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

![Screen Shot 2022-08-24 at 22.24.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/24514dd2-2c71-3b9c-14eb-798a0117c879.png)

![Screen Shot 2022-08-24 at 22.28.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/b4f9a567-cd7d-13a3-b5e6-a648cf325411.png)

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


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

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

https://www.canva.com/ja_jp/line-rich-menu/templates/

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

https://zenn.dev/sa8kat/articles/869ae291555715

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

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


## ちなみに私のリッチメニューはこんな感じ
ボタン3つのリッチメニューです。

```json
{
  "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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167220/748275c7-a049-c9a6-f23d-046fe8227cf8.png)


# 参考

https://qiita.com/Ryo9597/items/660d988858c573c3d58e

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