Cloud Functionsでチャットのメッセージを監視＆プッシュ通知

#やりたいこと

アプリからメッセージを送信→DB（Firestore）にメッセージが保存される→それを監視していたCloudFunctionsがプッシュ通知を送る

[Cloud Functions で可能な処理](https://firebase.google.com/docs/functions/use-cases?authuser=0#notify_users_when_something_interesting_happens)
>関心事が発生したときにユーザーに通知する

![notify.png](https://qiita-image-store.s3.amazonaws.com/0/112614/1ea2e714-366e-b624-3300-7d27bc9ce699.png)

これと同じようなイメージ。



#Firestore設計
chatrooms（コレクション）
　∟chatroom1（ドキュメント）
　　∟messages（コレクション）
　　　∟message1（ドキュメント）
　　　∟message2（ドキュメント）
　　　∟message3（ドキュメント）

　∟chatroom2（ドキュメント）
　　∟messages（コレクション）
　　　∟message1（ドキュメント）
　　　∟message2（ドキュメント）
：
：


- ルートレベルにchatroomsコレクションがある
- chatroomsドキュメント配下にmessagesサブコレクションがある

このように設計しました。


#アプリ設計
設計というほどではないのですが、以下のように事前にトピックを購読させます。
タイミングとしてはLINEでいうところの「グループチャットに参加する」のようなものを契機にすればよいのではないでしょうか。

その際トピック名としてチャットルームのドキュメントIDを指定します。

```swift
Messaging.messaging().subscribe(toTopic: "/topics/\(chatroomドキュメントのid)")
```

こうすることで`/topics/{chatroomドキュメントのid}`宛てにFCMメッセージが送られた際にプッシュ通知を受け取れるようになります。

#CloudFunctionsの設計

CLIツールをインストールしていない場合はインストールしてください。
`$ npm install -g firebase-tools`

適当にディレクトリを作ってそこでプロジェクトを初期化します。
`$ firebase init functions`


![image.png](https://qiita-image-store.s3.amazonaws.com/0/112614/d67e2989-23d5-01d2-164f-0504a969ca5e.png)

プロジェクトを選択し、JavascriptかTypescriptを選択します。
今回はJavascriptを選択した前提で話を進めます。
そのあと色々聞かれますが全てEnterでOKです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/112614/f82e88b6-129c-d129-7fc3-78e520e6d0f6.png)
こんなファイル群が自動で生成されるはずです。

index.jsに以下を追記します。（公式の手順通りですが）
これで

```js:functions/index.js
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');

// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
```

これでFirestoreにアクセスできるようになります。
続いて肝心のプッシュ通知を行う関数を定義します。

Firestoreへの追加をトリガーにプッシュ通知を送っています。
こちらを参考にしました。
[Cloud Functions による Cloud Firestore の拡張](https://firebase.google.com/docs/firestore/extend-with-functions?authuser=0)


```js:functions/index.js
exports.sendNotifications = functions.firestore
    .document('/chatrooms/{chatroomId}/messages/{messageId}')
    .onCreate((event, snapshot) => { // 公式ではeventとなっていたが、そうするとevent.params.chatroomIdのように値を取ってこれなかった。

        var topic = snapshot.params.chatroomId; // こうすることで上で指定したドキュメントのパスの{chatroomId}部分が取得できる。

        var payload = {
          notification: {
            title: "新着メッセージ", // この辺りを動的に取ってくるケースが多いかな？
            body: "新着メッセージがあります"
          }
        };

        var options = {
          priority: "high"
        };

        // 指定したtopic宛てにメッセージを送る
        return admin.messaging().sendToTopic(topic, payload, options)
          .then(function(response) {
            return console.log("Successfully sent message:", response);
          })
          .catch(function(error) {
            return console.log("Error sending message:", error);
          });
    });
```

そしてデプロイします。

`$ firebase deploy --only functions`

上記のコードであればエラーは出ないと思いますが、当初自分はsendToTopic内のconsole.logにreturnをつけていなかったので以下のようにLintで怒られました。
`Each then() should return a value or throw  promise/always-return`

うまくデプロイが済めばこうなるはずです。

```
✔  functions: Finished running predeploy script.
i  functions: ensuring necessary APIs are enabled...
✔  functions: all necessary APIs are enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (49.67 KB) for uploading
✔  functions: functions folder uploaded successfully
i  functions: creating function sendNotifications...
✔  functions[generateThumbnail]: Successful delete operation. 
✔  functions[sendNotifications]: Successful create operation. 

✔  Deploy complete!
```

そしてアプリからメッセージを送信します。（当記事ではメッセージ送信箇所について言及していませんが、Firestoreのmessageドキュメントを生成する処理を実行すればOKです。アプリでそれを実装するのが面倒な場合はFirestoreのコンソールから直接messagesドキュメントを作っても動作確認は可能です。）
すると、そのチャットルームを購読している端末に通知がいくと思います。

Cloud Functionsのログにも結果が出力されているはずです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/112614/e8bd2c41-c23e-97a9-aa14-e63b8cb69509.png)

もっといい方法があれば教えていただきたいです。