FCM(Firebase Cloud Messaging) の新APIに移行した
背景
FCM(Firebase Cloud Messaging)は、モバイルアプリやwebアプリのpush通知の配信を簡単に行える、Googleの提供するサービス。
関わっているサービスのiOSアプリとAndroidアプリのpush通知で、FCMのHTTP APIを用いている。 サーバは、Ruby on Rails。 FCM サーバーでは、プロトコルを3種類用意されている。
今回は、「レガシー HTTP プロトコル」から、「FCM HTTP v1 API」に移行した。
レガシープロトコルと新HTTPプロトコルの違い
FCMでは、送付対象を特定するのに、大きく2種類の方法がある。
- topic
- あるtopicを購読している全端末に対して、通知を行う。
- token
- 端末ごとに発行されるtokenを指定して、通知を行う。
ざっくりとした対応表が下表になる。
レガシー | FCM HTTP v1 | |
---|---|---|
単一のtopic | ○ | ○ |
複数のtopic | ✗ | ○ |
単一のtoken | ○ | ○ |
複数のtoken | ○(1リクエスト1000個まで) | ✗ |
要件と設計方針
- ユーザが設定画面でpush通知の対象を限定することができる。
- (非通知設定していない)全ユーザを対象とした毎朝、夜の通知を行いたい。
- アクセスが一度に来ると、サーバに負荷がかかるので、ある程度分散して通知を行いたい
- 特定のユーザのみを対象とした通知も行いたい。
旧プロトコルでは、複数のtokenを対象に1度のAPIリクエストで行えたため、全ユーザに対する通知は、tokenを1000件ずつに区切り、時間間隔を空けながら、送ることができていた。 しかし、上表の通り新プロトコルでは複数のtokenを指定することができなくなったため、tokenを1000件ずつにグループ化して、事前にtopicを登録して通知を行うようにした。
実装
移行方法は、こちらに記載されており、以下の3つの修正が必要。
以前の HTTP から HTTP v1 に移行する | Firebase
基本的には、以下のドキュメントを参考に進めればできるはずだが、ハマったところを中心に記載する。
アプリサーバーからの送信リクエストを作成する | Firebase
サーバー エンドポイントの更新
全然ハマるポイントがなさそうだが、結構ハマった。
// 旧エンドポイント https://fcm.googleapis.com/fcm/send // 新エンドポイント https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send
myproject-b5ae1の部分が、FirebaseでのプロジェクトIDを指定するのだが、これがわかりにくい。。 Firebaseのコンソールの「設定」の「クラウド メッセージング」タブ中の 送信者IDを指定する。
認証
新プロトコルに対応したGoogleの提供するSDKは、Rubyが非対応のため、標準のSDKではなく、認証用のライブラリを使用した。
サーバーに Firebase Admin SDK を追加する | Firebase
事前の準備として、Firebase admin SDKのサービスアカウントをFirebaseコンソールで作成し、キーのjsonファイルGCPコンソールで取得しておく。
# 1時間で有効期限が切れるので注意 def authorization_header authorizer = Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: File.open("#{Rails.root}/config/google-services.json"), scope: 'https://www.googleapis.com/auth/firebase.messaging' ) access_token = authorizer.fetch_access_token!['access_token'] "Bearer #{access_token}" end
ペイロード
ここに一番デバッグの時間がかかった。
REST Resource: projects.messages | Firebase
送信データ
今回の変更点の良い点は、iOSとAndroid、Webなどのマルチプラットフォームに柔軟に対応できる形式になった点である。 一方辛いのは、今までに使用できていたデータの形式が一部使えなくなったことである。 アプリは配布されているため、互換性のないpush通知のフォーマットになってしまうため、移行に時間がかかった。
def data args = {} args[:android] = { priority: 10, data: { image_url: image_url } } args[:apns] = { headers: { 'apns-priority': '10' }, payload: { aps: { 'mutable-content': 1, badge: 1, }, 'gcm.notification.image_url': image_url # イケてないkeyの命名だが、互換性を保つため } } # key, valueともにstringしか使えない。(ネストもできない) args[:data] = { url: target_url, notification_id: notification_id.to_s } args[:notification] = { title: title, body: body } # 送信先の指定: tokenかtopic、conditionのいずれか一つのみ指定 if token.present? args[:token] = token elsif topic.present? args[:topic] = topic elsif condition.present? args[:condition] = condition end { message: args } end
送信先の指定
前述した通り、今回はtokenに対してtopicを事前に登録しておく設計にした。 topicはクライアント側でしか実装できないかと思っていたが、サーバからもAPIで管理することができた。
Server Reference | Instance ID | Google Developers
紆余曲折あり、バッチで更新することになったので今回はbatchAdd、batchRemoveのAPIを使用した。 ここは、ドキュメント通りに実装するだけで良かったので、割愛する。
サンプルコード
細かい部分や処理は省略した。
あくまでも、送るのに最低限のコード。
class PushNotification include ActiveModel::Model KEY_FILE_PATH = "#{Rails.root}/config/google-services.json" FIREBASE_URI = URI.parse("https://fcm.googleapis.com/v1/projects/my_project_id/messages:send") attr_accessor :token, :body, :image_url #他にbody, titleなど validates :token, presence: true validates :body, presence: true def send_notification http = build_http req = build_request errors = [] if req.present? res = http.request(req) if res.code != '200' # エラーハンドリング end else # エラーハンドリング end return true if errors.empty? errors.unshift('プッシュ通知の送信に失敗しました。') self.error_message = errors.join("\n") false end private def build_http http = Net::HTTP.new(FIREBASE_URI.host, FIREBASE_URI.port) http.use_ssl = true http end def build_request return if invalid? req = Net::HTTP::Post.new(FIREBASE_URI.request_uri) req['Content-Type'] = 'application/json' req['Authorization'] = authorization req.body = data.to_json req end def data args = {} args[:android] = { priority: 10, data: { image_url: image_url } } args[:apns] = { headers: { 'apns-priority': '10' }, payload: { aps: { 'mutable-content': 1, badge: 1, }, 'gcm.notification.image_url': image_url # イケてないkeyの命名だが、互換性を保つため } } # key, valueともにstringしか使えない。(ネストもできない) args[:data] = { url: target_url, notification_id: notification_id.to_s } args[:notification] = { title: title, body: body } # 送信先の指定: tokenかtopic、conditionのいずれか一つのみ指定 if token.present? args[:token] = token elsif topic.present? args[:topic] = topic elsif condition.present? args[:condition] = condition end { message: args } end def authorization_header authorizer = Google::Auth::ServiceAccountCredentials.make_creds( json_key_io: File.open(KEY_FILE_PATH), scope: 'https://www.googleapis.com/auth/firebase.messaging' ) access_token = authorizer.fetch_access_token!['access_token'] "Bearer #{access_token}" end end