Ruby から Firebase Cloud Messaging を利用してプッシュ通知を配信する

こんにちは。スタメンで主にバックエンドの開発を担当しています、河井です。

この度 Firebase Cloud Messaging (以下 FCM)を使ってプッシュ通知機能を実装したのですが、具体的な実装まで踏み込んだ情報があまりなかったのでまとめようと思います。

FCM 選定の背景

世の中にプッシュ通知のサービスは多くあり、ユーザーをセグメントに分割して一斉に送信したり、配信後の分析を詳細にできたりと、それぞれ特徴をもっています。

弊社サービスの TUNAG でプッシュ通知したいケースとしては、チャットの新着通知やタイムラインでのメンションなど、通知相手が明確に定まっていて確実に通知したいものしかありません。そのため、マーケティング支援的な機能よりもとにかく端末指定で速く届けたいというモチベーションがありました。

また、モバイルアプリのためにもともと Firebase を使っていて、Firebase SDK がすでに導入されていました。

以上のような理由から Firebase Cloud Messaging を選定しました。

実装

FCM を使って通知を送る手段としては REST API を直接叩く方法と AdminSDK を使う方法があります。

AdminSDK が Ruby に対応していないため、Rails アプリケーションなどから利用したい場合は REST API を直接利用して送信します。FCM の REST API にもまた2種類あり、レガシーAPI と v1 API が存在しますが、ここでは HTTP v1 API を使います。

事前準備

エンドポイントの設定

リクエストを送信するエンドポイントは

POST https://fcm.googleapis.com/v1/projects/プロジェクトID/messages:send

の形式で設定します。プロジェクトIDは、Firebase コンソールの「プロジェクトの設定」→「全般」で確認できます。ここでは test-project とします。

秘密鍵の取得

Firebase コンソールの「プロジェクトの設定」→「サービスアカウント」から秘密鍵をダウンロード。パスは './firebase-test-key.json' とします。

メッセージの送り先

FCM では instance_id というトークンを指定してメッセージを送信します。 iOS のデバイストークンはそのままの形では使うことができないので、Firebase SDK を使って instance_id を生成します。 ここでは 'test_instance_id' とします。

また、今回は使っていませんが、すでに保存済みのデバイストークンを instance_id に変換できる API が用意されています。 Instance ID API リファレンスページの "Create registration tokens for APNs tokens" の項目から確認できます。

通知までの流れ

OAuth 2.0 認証キーを取得

authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
  json_key_io: './firebase-test-key.json',
  scope: 'https://www.googleapis.com/auth/firebase.messaging'
)
access_token = authorizer.fetch_access_token!
access_key = "#{access_token['token_type']} #{access_token['access_token']}"

リクエストボディの設定

基本形は次のようになります。

body = {
  message: {
    token: 'test_instance_id',               # 送り先の instance_id
    notification: {
      title: 'test push notification'        # プッシュ通知タイトル
      body: 'notification message from fcm.' # プッシュ通知の内容
    }
  }
}

なお、HTTP v1 API では複数端末のトークンを指定することができません。複数端末に通知するときはトピックに対して通知する必要があります。(AdminSDK や レガシーAPI では複数のトークンを指定できるようです。)

OS毎にカスタマイズしたいときは次のようにして実現できます。

body = {
  message: {
    token: token,
    notification: {
      title: 'test push notification'
      body: 'notification message from fcm.'
    },
    apns: {
      payload: {
        aps: {
          sound: 'default', # 通知音
          badge: 1          # iOS のホーム画面でのバッジ更新用
        }
      }
    }
    android: {
      notification: {
        channel_id: 'test_category' # Android 向けの通知カテゴリ
      }
    }
  }
}

iOS、Andoroid のほか、ブラウザプッシュにも対応しています。

リクエストの送信

ここでは Ruby 組み込みの HTTP クライアントを使います。

uri = URI.parse('https://fcm.googleapis.com/v1/projects/test-project/messages:send')
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
req = Net::HTTP::Post.new(uri)
req['Content-Type'] = 'application/json'
req['Authorization'] = access_key
req.body = body

res = https.request(req)

ステータスコードが 200 であれば送信成功です。 それ以外であればエラーコード表を見てリトライやエラー通知など適切に対処します。

補足: 認証キーの保持について

OAuth2.0 認証キーを通知の度に取得すると、時間が余計にかかってしまうという問題がありました。 この認証キーは有効期限付きで、1時間はもつので、うまく保持できればキーのリクエストは1時間に1回で済みます。 そこで、以下のようなメソッドを定義して、インスタンス変数にアクセスキーを保持します。

def get_access_key
  authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
    json_key_io: StringIO.new(sdk_hash.to_json),
    scope: 'https://www.googleapis.com/auth/firebase.messaging'
  )
  access_token = authorizer.fetch_access_token!
  "#{access_token['token_type']} #{access_token['access_token']}"
end

def access_key
  @access_key ||= get_access_key
end

これで連続して通知するときに毎回キーをリクエストしなくて済むようになります。 制限時間があるので、1時間たつと送信リクエストがエラーになります。 リトライ処理の中で認証エラー(401)のときは @access_key = nil としてからリトライすることで、期限切れにも対応します。

おわりに

Ruby で FCM の HTTP v1 API を使ってプッシュ通知を送信する方法を解説しました。 FCM は通知速度が速く、通知先のデバイスの違いにも対応が容易です。加えて、すべての機能を無料で使うことが出来るのも嬉しい点ですね。 同じような状況の方がいらっしゃいましたら参考にしていただけると幸いです。

最後に、株式会社スタメンでは一緒に働くエンジニアを募集しています。ご興味のある方はぜひエンジニア採用サイトをご覧ください。

アイキャッチ Photo by Jamie Street on Unsplash