Cloud Function for Firebaseでチャットアプリのプッシュ通知を打つ

## はじめに

今記事は**Cloud Function for Firebaseを使って、チーム(LINEのチャット的なやつ)にコメントがきたらチームの参加者にプッシュ通知を打つ**という実装を行った時のメモです。

通知の設定などは今記事では触れず、**Firebase Realtime Databaseにコメントが追加されたことをトリガーにしてFirebase Cloud Messagingに通知を打つことを命令する**部分のコードを紹介します。

コードは2017年8月のものです。

## Cloud Function for Firebaseとは

以下の記事を参考にしてsetupしました。

[Cloud Functions for Firebaseとは？](http://qiita.com/koki_cheese/items/013d4e6ab5aefc792388)

っていうか、そもそもnpmってなんぞや？って感じだったのでnode.jsをインストールするところから。

[Macにnode.jsをインストールする手順。](http://qiita.com/akakuro43/items/600e7e4695588ab2958d)

ってことで、`v8.3.0`をインストール。

index.jsができたら準備完了。

```js:index.js
var functions = require('firebase-functions');

exports.helloWorld = functions.database.ref('/teams')
    .onWrite(event => {
      const val = event.data.val();
      console.log('helloWorld', val);
    })
```

↑Firebase Realtime Databaseの`/teams `にデータが書き込まれたら発火してHelloWorldが出るやつです。

## 実際に書いたnode.js

先に書いた `Hello World` が以下になりました。

```js:index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
var teamRef

admin.initializeApp(functions.config().firebase);

exports.commentPush = functions.database.ref('/teams/{teamId}/comments/{commentId}')
.onWrite(event => {

  const item = event.data;
  const userName = item.child("userName").val();
  const commenterUid = item.child("uid").val();
  const comment = item.child("comment").val();

  teamRef = item.ref.parent.parent
  teamRef.once('value').then(function(snapshot) {
    const teamName = snapshot.val().teamName;

    // 通知のJSON
    const payload = {
      notification: {
        title: teamName,
        body: userName + "さん：" + comment,
        badge: "1",
        sound:"default",
      }
    };

    // 通知を打つ対象を検索してそのtokenにたいして打つ
    const members = snapshot.val().members
    console.log("Successfully fetch members:", members);

    for (key in members) {
      console.log("Successfully fetch member:", members[key]);

      const uid = members[key].uid
      const name = members[key].name
      console.log("uid:", uid);

      // コメントした本人には通知を打たない
      if (uid == commenterUid) {
        console.log("本人には通知を打たない");
        continue;
      }

      // fcmTokenを取得
      getTargetFcmToken(uid, function(token){

        if (token == null) {
          // 通知OFFのユーザーには通知を打たない
          console.log(name, "に通知を打たない");
          return;
        }
        console.log("fcmToken:", token);

        // tokenが欲しい
        pushToDevice(token, payload);
      });
    };
  });
});

// TODO: uidを使ってuserのdatabaseを検索
var getTargetFcmToken = function(uid, callback){
  console.log("getTargetFcmToken:");

  const rootRef = teamRef.parent.parent;
  const userRef = rootRef.child("users").child(uid);

  userRef.once('value').then(function(snapshot) {
    const isOn = snapshot.val().commentPush;
    console.log("isOn:", isOn);

    if (isOn == false) {
      // 通知設定がOFFの場合
      console.log("return callback null");
      callback(null);
      return
    }
    const fcmToken = snapshot.val().fcmToken

    console.log("return callback fcmToken", fcmToken);
    callback(fcmToken);
  });
}

// 特定のfcmTokenに対してプッシュ通知を打つ
function pushToDevice(token, payload){
  console.log("pushToDevice:", token);

  // priorityをhighにしとくと通知打つのが早くなる
  const options = {
    priority: "high",
  };

  admin.messaging().sendToDevice(token, payload, options)
  .then(pushResponse => {
    console.log("Successfully sent message:", pushResponse);
  })
  .catch(error => {
    console.log("Error sending message:", error);
  });
}
```

これで、チームにコメントがあったときに、そのチームに参加している自分以外の人に対してプッシュ通知が打てました。以下で詳しく解説していきます。

## 通知を打つフロー

### Firebase Realtime Databaseに値が書き込まれる
まずは、Firebase Realtime Databaseの`teams/{id}/comments`に値が書き込まれます。

#### teams/{id}/commentsの様子
<img width="560" alt="スクリーンショット 2017-08-30 21.31.36.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/bc5b5781-9d8c-dfdb-55b7-d0f742d81d0d.png">

#### teams/{id}/membersの様子
<img width="683" alt="スクリーンショット 2017-08-30 21.31.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/26111442-6bae-dc95-9c14-ff7ee48734db.png">

teamのRefが値の書き込みイベントから取れるので、チームに参加している人のuidを取得します。

```js:index.js
// 通知を打つ対象を検索してそのtokenにたいして打つ
    const members = snapshot.val().members
    console.log("Successfully fetch members:", members);

    for (key in members) {
      console.log("Successfully fetch member:", members[key]);

      const uid = members[key].uid
      const name = members[key].name
      console.log("uid:", uid);

      // コメントした本人には通知を打たない
      if (uid == commenterUid) {
        console.log("本人には通知を打たない");
        continue;
      }

      // fcmTokenを取得 (このメソッドは次のステップで定義)
      getTargetFcmToken(uid, function(token){

        if (token == null) {
          // 通知OFFのユーザーには通知を打たない
          console.log(name, "に通知を打たない");
          return;
        }
        console.log("fcmToken:", token);

        // tokenに対して通知を打つ (このメソッドは次の次のステップで定義)
        pushToDevice(token, payload);
      });
    };
```

### user検索してfcmTokenを取り出す

uidを使ってFirebase Realtime Database内のuserを検索し、そのuserのfcmTokenを取得します。

```js
// TODO: uidを使ってuserのdatabaseを検索
var getTargetFcmToken = function(uid, callback){
  console.log("getTargetFcmToken:");

  const rootRef = teamRef.parent.parent;
  const userRef = rootRef.child("users").child(uid);

  userRef.once('value').then(function(snapshot) {
    const isOn = snapshot.val().commentPush;
    console.log("isOn:", isOn);

    if (isOn == false) {
      // 通知設定がOFFの場合
      console.log("return callback null");
      callback(null);
      return
    }
    const fcmToken = snapshot.val().fcmToken

    console.log("return callback fcmToken", fcmToken);
    callback(fcmToken);
  });
}
```

### 通知を打つ

fcmTokenを手に入れたのであとは、それに対して打つだけです。Firebase Cloud Messagingで打つためのメソッドが用意されています。

```js
// 特定のfcmTokenに対してプッシュ通知を打つ
function pushToDevice(token, payload){
  console.log("pushToDevice:", token);

  // priorityをhighにしとくと通知打つのが早くなるかも
  const options = {
    priority: "high",
  };

  admin.messaging().sendToDevice(token, payload, options)
  .then(pushResponse => {
    console.log("Successfully sent message:", pushResponse);
  })
  .catch(error => {
    console.log("Error sending message:", error);
  });
}
```

完成。

## まとめ

* iOS側で発行されたFirebase Cloud MessagingのfcmTokenをFirebase Realtime Databaseのuserに保存
* Firebase Cloud FunctionではuidをkeyにUserを検索し、fcmTokenを取り出す。
* そのfcmTokenに対してFirebase Cloud Messagingを使って通知打つ

## 追記

TypeScriptで、同様にして、Firestoreに値が書き込まれたらFCMで通知を打つサンプルコードを公開しました。

https://github.com/kboy-silvergym/CloudFunctions-Push-Sample