Cloud Functionsでリッチプッシュを送信するぞ

Cloud Functions、便利ですよね。

前回は[Cloud Functionsに色々な処理を任せてしまおう](https://qiita.com/torahack/items/761f1a7f1f2ec8070c98)という話をしました。

今回は、ほとんどどのアプリでも実装することになるであろう「Push通知」について解説します。

FCMトークンの設定や受信時の処理はモバイル側で実装する必要がありますが、「送信」についてはCloud Functionsで実装できます。

今回実装したい関数は以下のような要件と仮定します。


<ul>
 	<li>アプリ管理者がWebの管理画面から配信したいお知らせを登録する</li>
 	<li>お知らせ登録時にユーザーにプッシュ通知で配信される</li>
 	<li>画像や通知音付きのリッチプッシュにしたい</li>
</ul>

<h2>Cloud Functionsでリッチプッシュを送信する関数を作ろう</h2>
実際に僕が使っているコードを例にしながら解説していきます。
<h3>プッシュ通知を送信するトリガーの設定</h3>
「アプリ管理者によるニュース登録」をトリガーとしたいので、newsモデル配下にデータが作成された時に、Cloud Functionsが起動するように設定する。

該当のコードは以下の通りです。

```typescript:functions/src/index.ts
export const sendNotification = functions.database.ref("/news/{newsId}")
    .onCreate(async (snapshot: any, context: any) => {
```

<h3>Cloud Functionsからモバイルデバイスに送信するデータの作成</h3>
次に、Push通知で送信するデータを作ります。

FirebaseのFCMメッセージは2種類のデータを格納できます。

1. notification（通知メッセージ）
2. data（データメッセージ）

notificationには事前に定義された値を格納できます。

一方、dataは開発者が自由に値を設定できるオブジェクトです。

詳細は[Firebaseの公式ドキュメント](https://firebase.google.com/docs/cloud-messaging/concept-options?hl=ja)を参照してください。

弊社ではPush通知＝「バッジ＋画像＋通知音付きのリッチプッシュ」であることが多いので、その場合の具体例を以下に示します。


```typescript:functions/src/index.ts
const value = snapshot.val();    //新規作成されたニュースの値を格納
const message = {
    notification: {
        title: value.subject,    // 通知のタイトル
        body: value.description,    // 通知の本文
        sound: "default",    // 受信時の通知音
        mutable_content: 'true',    // 画像付きのリッチプッシュに必要
        content_available: 'true'    // アプリがバックグラウンドでも通知を届けるために必要
    },
    data: {
        "image-url": value.image_url    // Cloud Storageに格納している画像ファイルのURL
    }
};
```

<h3>Cloud Functionsからモバイルにリッチプッシュを送信するメソッドの使い方</h3>
Cloud FunctionsからモバイルにPush通知を送る時は、[Firebase Admin SDKのメッセージ](https://firebase.google.com/docs/cloud-messaging/send-message?hl=ja)を使います。

Push通知を一斉配信するために、sendAll()やsendMulticast()が使えたら便利なのですが、これらのメソッドが受け取れる値は<a href="https://firebase.google.com/docs/reference/admin/node/admin.messaging.html#message" target="_blank" rel="noopener noreferrer">Message型</a>だけなんですよね。

Message型はnotificationにsoundやmutable_contentといった値を設定できません。

つまり、<strong><span style="color: #e74c3c;">リッチプッシュを実装できない</span></strong>ということです。
（iOS/Androidアプリ側でこねくり回せばできるのかもしれません）

notificationにsoundなどの値を設定できるのは、<a href="https://firebase.google.com/docs/reference/admin/node/admin.messaging.MessagingPayload.html" target="_blank" rel="noopener noreferrer">MessagingPayload型</a>だけ。

そしてMessagingPayload型を渡せるのはSendToXXXXX()メソッドのみです。

ということで仕方なくここでは<a href="https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging.html#sendtodevice" target="_blank" rel="noopener noreferrer">SendToDevice()</a>を使います。

実際にメッセージを送信するための関数は以下のように実装しました。

```typescript:functions/src/index.ts
const sendMessage = (fcmToken: string, message: any, options: any) => {
    return admin.messaging().sendToDevice(fcmToken, message, options)
        .then(() => {
            return Promise.resolve(null)
        })
        .catch(() => {
            return Promise.reject(null)
        });
    };
```

この関数はPromiseを返します。
あとで`Promise.all()`を使って配信を並列処理にするためです。

<h3>Cloud Functionsからリッチプッシュ通知を一斉配信する方法</h3>
Push通知を一斉配信する箇所の処理の流れは以下の通りです。

1. userモデルから取得したデータからFCMトークンを取り出す
2. FCMトークンが登録されているユーザーなら、`promises`という配列に`sendMessage()`を引数と共に追加する
3. 1~2を全ユーザー分繰り返したら、最後に`Promise.all(promises)`で並列実行する。


実際のコードは以下のようになります。

```typescript:functions/src/index.ts
const users: any = await queryUserData(); // Userモデルからデータを取得
const userModelLength = users.numChildren(); // Userモデルのデータ数を取得

let promises: Array = [],
    counter = 0;

users.forEach((user: any) =&gt; {
    const userValue = user.val();
    const fcmToken: string = (userValue.fcmToken) ? userValue.fcmToken : "";
    if (fcmToken !== "") {
        promises.push(sendMessage(fcmToken, message, options))
    }

    counter += 1
    // 全ユーザーの繰り返し処理完了後にPromise.all()で実行する
    if (counter === userModelLength) { return Promise.all(promises) }
})
```

<h2>Cloud Functionsからモバイルにリッチプッシュ通知を送信するために気をつけること</h2>
このFunctionを実装する際にハマった点があるので共有します。


- FCMトークンが登録されていないユーザーには送信できないので、例外処理や条件分岐が必要
- `sound`や`mutable_content`などを設定したい時は`send()`メソッドは使えないので`SendToDevice()`メソッドを使う
- `Promise.all()`で並列処理にしないと、ユーザー数が多い場合、配信時間にタイムラグがでる
- ただし並列処理にする場合、Cloud Functionsのメモリ上限を気にしなきゃいけない

以上です。

Push通知が実装できると一気にアプリでできる幅が広がります。

トリガーを工夫すれば、色々なタイミングでPush通知を送ることができますよ。