Open12

FCMの仕様理解

さしもんさしもん

アプリサーバーからリクエスト→FCM→APNs→クライアント(アプリとか)で使われる

さしもんさしもん

メッセージの種類

FCMでは2種類のメッセージをクライアントに送ることができる

  1. Notification messages(だけのもの)
  2. Data messages(だけのもの)
    *3つ目としてNotificationMessageだけどDataペイロードを持つものもある

Use notification messages when you want the FCM SDK to handle displaying a notification automatically when your app is running in the background. Use data messages when you want to process the messages with your own client app code.
FCM can send a notification message including an optional data payload. In such cases, FCM handles displaying the notification payload, and the client app handles the data payload.

Notification Message

Notification messages, sometimes thought of as "display messages." These are handled by the FCM SDK automatically.

とかいてあるしMessage Typeのセクションを読むに
これはおそらくプッシュ通知が届いたタイミングでOSが自動的に表示する通知になるはず

Notification messages have a predefined set of user-visible keys and an optional data payload of custom key-value pairs.

Notification messageでは予約語のkeyを使ってnotification bodyの中にタイトルとメッセージだけを送ることもできる(下記参考)

// 特定のデバイスに送る場合
{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "notification":{
      "title":"Portugal vs. Denmark",
      "body":"great match!"
    }
  }
}

Notification messages have a predefined set of user-visible keys and an optional data payload of custom key-value pairs.

その他に送りたいものがあれば、data payload(data bodyの中に)を使ってkey-value型で定義すれば他のデータも送ることもできるみたい(下記参照)

// 特定のデバイスに送る場合
{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "notification":{
      "title":"Portugal vs. Denmark",
      "body":"great match!"
    },
    "data" : {
      "Nick" : "Mario",
      "Room" : "PortugalVSDenmark"
    }
  }
}

Data messages

Client app is responsible for processing data messages. Data messages have only custom key-value pairs with no reserved key names (see below).
In a trusted environment such as Cloud Functions or your app server, use the Admin SDK or the FCM Server Protocols: Set the data key only.

と書いてあるし、Notification messageと違ってData messageは通知が届いてもアプリ側で表示しようとしない限り表示されない。
またこれの構造は予約後dataを使うのみで持たせたいデータはこのdata{}(= data body)内にディクショナリ型で定義する(以下参照)

{
  "message":{
    "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "data":{
      "Nick" : "Mario",
      "body" : "great match!",
      "Room" : "PortugalVSDenmark"
    }
  }
}

data field, which is interpreted by clients on all platforms that receive the message. On each platform, the client app receives the data payload in a callback function.
On each platform, the client app receives the data payload in a callback function.

このフィールドは、メッセージを受信するすべてのプラットフォーム上でクライアントによって解釈されます。クライアント アプリは、それぞれのプラットフォームのコールバック関数でデータ ペイロードを受信します。

共通の通知内容をもたせつつも、Android、iOSなどプラットフォーム別の設定をしたNotification messageを送る場合

data payloadを持たない場合

{
  "message":{
     "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
     "notification":{
       "title":"Match update",
       "body":"Arsenal goal in added time, score is now 3-0"
     },
     "android":{
       "ttl":"86400s",
       "notification"{
         "click_action":"OPEN_ACTIVITY_1"
       }
     },
     "apns": {
       "headers": {
         "apns-priority": "5",
       },
       "payload": {
         "aps": {
           "category": "NEW_MESSAGE_CATEGORY"
         }
       }
     },
     "webpush":{
       "headers":{
         "TTL":"86400"
       }
     }
   }
 }

data payloadをもつ場合

{
  "message":{
     "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
     "notification":{
       "title":"Match update",
       "body":"Arsenal goal in added time, score is now 3-0"
     },
    "data":{
      "Nick" : "Mario",
      "body" : "great match!",
      "Room" : "PortugalVSDenmark"
    },
     "android":{
       "ttl":"86400s",
       "notification"{
         "click_action":"OPEN_ACTIVITY_1"
       }
     },
     "apns": {
       "headers": {
         "apns-priority": "5",
       },
       "payload": {
         "aps": {
           "category": "NEW_MESSAGE_CATEGORY"
         }
       }
     },
     "webpush":{
       "headers":{
         "TTL":"86400"
       }
     }
   }
 }

アプリ側で受け取ったときのペイロード

上記に書いてきたのはアプリサーバーがFCMサーバーに送るときのリクエストデータ
iOSアプリ側がFCMから受け取るペイロードは以下のような形

  {
    "aps" : {
      "alert" : {
        "body" : "great match!",
        "title" : "Portugal vs. Denmark",
      },
      "badge" : 1,
    },
    "customKey" : "customValue"
  }

なのでたとえば
サーバー側が以下のようなリクエストを送った場合

{
  "message":{
     "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
     "notification":{
       "title":"Match update",
       "body":"Arsenal goal in added time, score is now 3-0"
     },
    "data":{
      "Nick" : "Mario",
      "body" : "great match!",
      "Room" : "PortugalVSDenmark"
    },
     "android":{
       "ttl":"86400s",
       "notification"{
         "click_action":"OPEN_ACTIVITY_1"
       }
     },
     "apns": {
       "headers": {
         "apns-priority": "5",
       },
       "payload": {
         "aps": {
           "title": "テストタイトル",
           "body": "テストボディ",
           "category": "NEW_MESSAGE_CATEGORY"
         }
       }
     }
   }
 }

おそらくアプリ側で受け取ったときの形は以下になるはず

  {
    "aps" : {
      "alert" : {
           "title": "テストタイトル",
           "body": "テストボディ",
           "category": "NEW_MESSAGE_CATEGORY"
      },
      "badge" : 1,
    },
    "Nick" : "Mario",
    "body" : "great match!",
    "Room" : "PortugalVSDenmark"
  }

なのでuserInfo["aps"]やuserInfo["Nick"]、userInfo["body"]みたいな形で各valueにアクセスできる

使用できる予約後

message body(= message{} )で使用できる予約後
https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages?hl=ja

プラットフォーム固有の予約後
https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW1

さしもんさしもん

プラットフォーム別という切り口

ここまでは各メッセージタイプという点での切り口だったけど、共通・プラットフォーム別という切り口もある

共通の通知内容をもたせつつも、プラットフォーム固有のブロックを持たせることで、受信時にAndroid、iOS、webなどの各種プラットフォームで正しく処理されるように、メッセージを柔軟にカスタマイズできます
特定のプラットフォームだけに値を送信する場合は、共通フィールドを使用せずにプラットフォーム固有のフィールドを使用してください。たとえば、Android を除いて Apple プラットフォームとウェブのみに通知を送信するには、2 つの異なるフィールド セット(Apple 用とウェブ用に 1 つずつ)を使用する必要があります。

プラットフォーム別に設定をしたNotification messageを送る場合についてはサーバー側の方のドキュメントにも書いてある

  • Firebase Admin SDK と FCM v1 HTTP プロトコルは、いずれも message オブジェクト内のすべてのフィールドをメッセージ リクエストで設定できるようになっている

  • プラットフォーム固有のブロック(apns{}やandroid{}など)を使用すると、受信時に各種プラットフォームで正しく処理されるように、メッセージを柔軟にカスタマイズできる

実際に共通のフィールドで持ちつつも、各プラットフォームごとに値を設定している例に関しては
ここを参照

さしもんさしもん

プッシュ通知のターゲット

アプリサーバーがFCMサーバーとやりとりする方法は主に以下の2つのどちらか
- Firebase Admin SDKを使ったやり方
- FCM HTTP v1 APIをつかったやり方
* legacy HTTP protocolやXMPP server protocolを使ったやり方もあるがFirebase Admin SDKを除けばFCM HTTP v1 APIが推奨されている
* Firebase Admin SDK > FCM HTTP v1 API > legacy HTTP protocol == XMPP server protocol の順で推奨されている様子

これらの中からいずれかの方法をつかってアプリサーバーはFCMサーバーとやりとりをするわけだけど
プッシュ通知を送るターゲットはいくつかに分類できる

  • Device registration token(特定のデバイスにメッセージを送信する)
  • Device group name (legacy protocols and Firebase Admin SDK for Node.js only)(複数のデバイスにメッセージを送信する)
  • Topic name(特定のTopic nameを持つユーザーにのみメッセージを送信する)
  • Condition

特定のデバイスにメッセージを送信する場合

デバイスの登録トークンを渡す

POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA

{
   "message":{
      "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
      "notification":{
        "body":"This is an FCM notification message!",
        "title":"FCM Message"
      }
   }
}

*curlコマンドでプッシュ通知を飛ばすのはここでは触れない予定

複数のデバイスにメッセージを送信する場合

HTTPバッチリクエストを作成する(以下参照)
*バッチ リクエストとは、複数の API 呼び出しを 1 つの HTTP リクエストにまとめたもの

--subrequest_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA

POST /v1/projects/myproject-b5ae1/messages:send
Content-Type: application/json
accept: application/json

{
  "message":{
     "token":"bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
     "notification":{
       "title":"FCM Message",
       "body":"This is an FCM notification message!"
     }
  }
}

...

--subrequest_boundary
Content-Type: application/http
Content-Transfer-Encoding: binary
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA

POST /v1/projects/myproject-b5ae1/messages:send
Content-Type: application/json
accept: application/json

{
  "message":{
     "token":"cR1rjyj4_Kc:APA91bGusqbypSuMdsh7jSNrW4nzsM...",
     "notification":{
       "title":"FCM Message",
       "body":"This is an FCM notification message!"
     }
  }
}
--subrequest_boundary--

特定のTopic nameを持つユーザーにのみメッセージを送信する

クライアント側でトピックにクライアントアプリインスタンスをサブスクライブするか、サーバーAPIを介してトピックを作成した後、トピックにメッセージを送信できます。
詳細: https://firebase.google.com/docs/cloud-messaging/ios/topic-messaging?hl=ja

リクエストデータ

POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA
{
  "message":{
    "topic" : "foo-bar",
    "notification" : {
      "body" : "This is a Firebase Cloud Messaging Topic Message!",
      "title" : "FCM Message"
      }
   }
}

複数のtopicを組み合わせ該当するユーザーにメッセージを送信する場合

トピックの組み合わせにメッセージを送信するには、Conditionを指定します
たとえば次の条件では、TopicA に加えて TopicB と TopicC のどちらか一方にも登録されているデバイスにメッセージが送信される
*条件式には最大 5 つのトピックを含めることができる

"'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"

topicを組み合わせてconditionを設定する場合のリクエスト

POST https://fcm.googleapis.com/v1/projects/myproject-b5ae1/messages:send HTTP/1.1

Content-Type: application/json
Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA
{
   "message":{
    "condition": "'dogs' in topics || 'cats' in topics",
    "notification" : {
      "body" : "This is a Firebase Cloud Messaging Topic Message!",
      "title" : "FCM Message",
    }
  }
}
さしもんさしもん

トピックに対して送信する場合の疑問

アプリサーバーからFCMに対して送られるペイロードに含まれているtopicが購読されていれば、登録トークン必要なくアプリは通知を受け取れるのか疑問だった

subscribeToTopic:topic を呼び出す前には、コールバック didReceiveRegistrationToken を介してクライアント アプリ インスタンスがすでに登録トークンを受信していることを確認します。

けど上記の内容がドキュメントに書かれていたから多分無理そう

これは推測だけどやっぱりプッシュ通知送るために端末識別は通常通り必要で
トピック使ったプッシュ通知は特定のデバイスにメッセージを送ることのアンド条件みたいな感じなんだろうな
トピックを購読するときにクライアントアプリインスタンスがすでに登録トークンを受信していることを確認するっていう一文から見るに裏で紐づけてるんだと思う

さしもんさしもん

FCMトークン?登録トークン?Registrationトークン?

FCMの開発しているとFCMトークンとか登録トークンとかRegistrationトークンとか出てくる
名前的に登録トークンとRegistrationトークンは一緒だとして
FCMトークンとは違うから二つあるのかと思っていたけど多分違った

理由は公式ドキュメントみると

FCM は FIRMessagingDelegate の messaging:didReceiveRegistrationToken: メソッドによって登録トークンを提供します。

って書いてあってこのAPIをみるとインターフェースが
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?)
ってなっていて外部引数名と内部引数名を見てわかる通り
RegistrationToken(登録トークン) = fcmTokenだから

人によってfcmトークンって呼んだりするけど、ドキュメントにはRegistrationTokenって書いてあるから呼ぶとしてもせめて登録トークンって呼んでほしい...

さしもんさしもん

でこの登録トークンを使うことで特定のターゲットに対してメッセージを送る、ということができるっぽい

このトークンを使用すると、ターゲットとする通知をアプリの特定のインスタンスに送信できます。

さしもんさしもん

登録トークン取得

起動時・変更時・無効化時に取得したい場合
デリゲートメソッドmessaging:didReceiveRegistrationToken:

任意の場所で取得したい場合
Messaging.messaging().token {}

さしもんさしもん

トークン更新のモニタリング

登録トークンは変更されることがあります
ケースは以下

アプリを新しいデバイスで復元した場合
ユーザーがアプリをアンインストール / 再インストールした場合
ユーザーがアプリのデータを消去する場合

以下の通り、これをmessaging:didReceiveRegistrationTokenを使うことで監視できる

Apple プラットフォームが通常、アプリ起動時に APNs デバイス トークンを配信するのと同じ方法で、FCM は FIRMessagingDelegate の messaging:didReceiveRegistrationToken: メソッドによって登録トークンを提供します。FCM SDK は、アプリの初期起動時ならびにトークンが更新または無効化されるたびに、新規または既存のトークンを取得します。いずれの場合も、FCM SDK は有効なトークンを使用して messaging:didReceiveRegistrationToken: を呼び出します。

SwiftUIアプリの場合

SwiftUIアプリの場合なんか考慮が必要そうな一文があった

メソッドの実装入れ替えを無効にした場合や、SwiftUI アプリを作成している場合は、明示的に APNs トークンを FCM 登録トークンにマッピングする必要があります。application(_:didRegisterForRemoteNotificationsWithDeviceToken:) メソッドを実装して APNs トークンを取得し、Messaging の apnsToken プロパティを設定します。