Swift
Firebase
cloudfunctions

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

やりたいこと

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

Cloud Functions で可能な処理

関心事が発生したときにユーザーに通知する

notify.png

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

Firestore設計

chatrooms(コレクション)
 ∟chatroom1(ドキュメント)
  ∟messages(コレクション)
   ∟message1(ドキュメント)
   ∟message2(ドキュメント)
   ∟message3(ドキュメント)

 ∟chatroom2(ドキュメント)
  ∟messages(コレクション)
   ∟message1(ドキュメント)
   ∟message2(ドキュメント)

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

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

アプリ設計

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

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

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

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

CloudFunctionsの設計

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

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

image.png

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

image.png
こんなファイル群が自動で生成されるはずです。

index.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 の拡張

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

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