FirebaseFunctionsでPush通知をHTTP関数呼び出しで送信する

# はじめに

今回はFirebaseFunctionsのhttp関数を使用して特定のデバイスにPush通知を送る実装を行なっていきます。実装前に下記の準備項目が必要になります。

### 事前準備

- Firebaseプロジェクト
- Firebase/Messaging導入済のiosプロジェクト
- APNsのFirebaseアップロード

# FirebaseCLIインストール

まずは、FirebaseCLIをインストールすることでFunctionsのDeployやプロジェクトの切り替えなどをCLIで操作できるようにします。今回はnpmでインストールを行います。

#### npmインストール

とりあえず最新のものをnodebrewで取得してきてPathを通すとこまで終わらせます。

```
    $ brew install nodebrew
    $ nodebrew install-binary 13.7.0
    $ nodebrew use  v7.0.0
    $ echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
    $ source ~/.bash_profile

```

#### firebase-toolsインストール
 1.npmでfirebase-toolsをインストールします。

```
    $ npm install -g firebase-tools
```

 2.firebase-toolsコマンドを使用して、操作を行うユーザの認証をします。下記のコマンドを実行するとWebブラウザが立ち上がるので、Firebaseプロジェクトで編集権限のあるアカウントでログインを行います。

```
    $ firebase login
```
 3.firebaseのプロジェクトを`use`コマンドを使って指定します。この操作によりfirebase/functionsなどのデプロイ先を変更できたりします。

```
    $ firebase use firebase_project_id
```

# Functionsプロジェクト作成

今回はFunctionsのみ使用するので下記のコマンドでプロジェクトを立ち上げます。

```
    $ firebase init functions
```

すると下記のような構造のプロジェクトが立ち上がるので、主に`index.js`を編集して関数を作成して行きます。
<img width="735" alt="スクリーンショット 2020-02-05 22.25.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292456/d31bc878-0fa9-85a2-a691-6dd814cfcfdd.png">

参照: https://firebase.google.com/docs/functions/get-started?hl=ja

# FirebaseAdminSDKインストール

 1.sdkの情報などを保存する`package.json`を作成します。

```
    $ npm init
```

 2.firebase-admin npmパッケージをインストールします。

```
   $ npm install firebase-admin --save
```

 3.次に`firebase-admin`を初期化をするためにローカルの環境変数にFirebaseサービスアカウントの秘密鍵を生成したファイルへのパスを指定します。これを設定することでSDKの初期化時にキーが参照され、プロジェクトでの認証が完了します。CIなどでブランチごとにDeploy先を変更させたい時はどうやって秘密鍵を参照させるのがベストなんでしょうか？

```
   $ export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/service-account-file.json"
```
参照: https://firebase.google.com/docs/admin/setup?hl=ja


 4.`index.js`に移動してsdkの初期化コードを追加します。

```index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
```

# node.jsの実装

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

admin.initializeApp();

//onRequestでhttpからの呼び出しを可能にします。
exports.push = functions.https.onRequest((request, response) => {
  if (request.query.device_token !== undefined && request.body.message !== undefined) {
    const device_token = request.query.device_token
    const message = request.body.message
    const payload = {
      notification: {
        body: message,
        badge: "1",
        sound:"default",
      }
    };
    switch (request.method) {
      case 'POST':
        push(device_token, payload, response);
        break
      default:
        response.status(400).send({ error: 'Invalid request method' })
        break
    }
  } else {
    response.status(400).send({ error: 'Invalid request parameters' })
  }
})

function push(token, payload, response) {

  const options = {
    priority: "high",
  };

  //FCMにAdminSDKを介してPush通知を送信します。
  admin.messaging().sendToDevice(token, payload, options)
  .then(pushResponse => {
    console.log("Successfully sent message:", pushResponse);
    response.status(200).send({message: 'Successfully sent message'})
  })
  .catch(error => {
    response.status(400).send({ error: 'Error sending message' })
  });
}
```

# swiftの実装

```AppDelegate.swift
import UIKit
import Firebase
import UserNotifications
import FirebaseMessaging

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    private var mainTabViewController: MainTabViewController?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        //環境ごとにプロジェクトを変えてるためplistを変更しています。
        let filePath = Bundle.main.path(forResource: Config.Server.instance.firebaseInfoPlistName, ofType:"plist")
        //Forced Unwrapping🚨
        FirebaseApp.configure(options: FirebaseOptions(contentsOfFile:filePath!)!)
        initFirebaseMessaging()
        initRemoteNotification(application)
        window = UIWindow(frame: UIScreen.main.bounds)
        window!.makeKeyAndVisible()
        navigate()
        return true
    }

    func navigate(_ isTrial: Bool = false) {
        guard let window = window else {
            assert(false)
            return
        }
        let previousVC = window.rootViewController
        for v in window.subviews {
            v.removeFromSuperview()
        }
        let vc = MainTabViewController()
        mainTabViewController = vc
        window.rootViewController = vc
        if let previousVC = previousVC {
            previousVC.dismiss(animated: false) {
                previousVC.view.removeFromSuperview()
            }
        }
    }

    private func initRemoteNotification(_ application: UIApplication) {
        UNUserNotificationCenter.current().delegate = self

        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        //TODO: Relocate requestAuthorization method.
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions, completionHandler: {_, _ in })
        application.registerForRemoteNotifications()
    }

    private func initFirebaseMessaging() {
        //DelegateでdeviceTokenの変更を監視します。
        Messaging.messaging().delegate = self
        //明示的にdeviceTokenを取得します。
        InstanceID.instanceID().instanceID { (result, error) in
          if let error = error {
            //TODO: Error handling.
            print("Error fetching remote instance ID: \(error)")
          } else if let result = result {
            //TODO: Send token to parnovi api for update user fcm token. if authorized == true
            print("Remote instance ID token: \(result.token)")
          }
        }
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.badge, .sound, .alert])
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        completionHandler()
    }
}

extension AppDelegate: MessagingDelegate {
    //Observe firebase messaging token.
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
    }
}
```

# FunctionsのDeploy

実際に関数をデプロイしてPush通知を送信してみます。

```
    $ firebase deploy --only functions
```
swiftの`InstanceID.instanceID().instanceID `で取得したDeviceTokenを使ってcurlで実際にPushを送信してみます。

```
    $ curl -X POST https://yout-functions-url/push?device_token=your-device-token -d "message=I love fishing🎣"
```

### 結果

![IMG_0964.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292456/a2d7f179-4c7a-acb4-d8b8-3cad394c1b93.jpeg)

# さいごに
今回はテスト的に実行できるようにするため、httpリクエストに認証は設定していませんでしたが、また実装し直したら編集しようと思います。また、CIなどを使ってfirebase/functionsなどをデプロイするとき、どのようにFirebaseプロジェクトの秘密鍵を参照させるのがベストなのでしょうか。。