FCMで直接トークンを指定してマルチキャストでプッシュメッセージを送信する

こんにちは、エキサイト株式会社の平石です。エキサイトホールディングス Advent Calendar 2023の12日目を担当いたします。

今回は、FCMで直接トークンを指定してマルチキャストでプッシュメッセージを送信する方法をご紹介します。

はじめに

FCM (Firebase Cloud Messaging)は、メッセージを送信するための手段です。
主にiOSAndroid向けのプッシュ通知やブラウザで動作するWeb Pushを送信する際に利用できます。

通常は、メッセージの送信には、トピックを利用することが多いです。
トピックとは、ざっくりいうと「メッセージを受信するユーザーのグループ」のようなもので、ユーザーごとに生成されるトークンをトピックに紐付けて、そのトピックを対象にメッセージを送信します。 (パブリッシュ / サブスクライブ モデル)

しかし、稀になんらかの理由でトピックが作成されておらず、送信対象のユーザーのトークンだけが手元にあり、そのユーザーに対してメッセージを送信したいという場面があります。
また、なんらかの理由でトピックを利用せずに、自前でプッシュ対象を制御したい場面もあるかもしれません。

今回は、そのような場面に使える方法をご紹介します。

プッシュメッセージのマルチキャスト

その方法は、メッセージのマルチキャストです。
マルチキャストは、「複数の端末(ホスト)に対して同一データを一斉に送信する方法」です。

FCMでも、Firebase Admin SDKマルチキャストのためのメソッドが用意されており、これを利用することで指定したトークンを持つユーザーにメッセージを送信することができます。

早速ソースコードを見てみましょう。
今回はJavaで記述しますが、他の言語でも可能ですので公式ドキュメントも参考にしてください。

public void sendSeriesFollowPush() {
    final List<String> tokenList = /*トークンのリストを渡す*/;
    final String body = "プッシュメッセージ本文";
    final Map<String, String> data = Map.of(
            "article_id", "A123456789"
    );

    final Notification notification = Notification.builder()
            .setBody(body)
            .build();

    final MulticastMessage message = MulticastMessage.builder()
            .addAllTokens(tokenList)
            .putAllData(data)
            .setNotification(notification)
            .build();

    try {
        final BatchResponse batchResponse = firebaseMessaging.sendEachForMulticast(message);
    } catch (FirebaseMessagingException e) {
        throw new InternalServerErrorException("プッシュの送信に失敗しました。");
    }
}

※ firebaseMessagingは、別途Beanを作成し、DIしたものを利用。Bean定義は付録を参照。

基本的な構成は、トピックを利用する方法と変わりませんが、MulticastMessageクラスのインスタンスに対して各種の設定をセットしている点と、sendEachForMulticastメソッドを利用している点が大きな違いです。

通常は、Messageクラスを利用して以下のように記述します。

final Message message = Message.builder()
            .setTopic("topic")
            .putAllData(data)
            .setNotification(notification)
            .build();

これを見ると、setTopicでトピックをセットする代わりに、.addAllTokensトークンのリストを渡していることがわかります。

たったこれだけでトークンに対して直接メッセージを送信することが可能なのです。

なお、一度に指定できるトークンの数は最大500なので、それ以上のトークンを対象にメッセージを送信する必要がある場合には、この処理を繰り返す必要があります。

メッセージ送信時のレスポンス

sendMulticastメソッドを利用してメッセージを送信すると、BatchResponse型の戻り値が返ってきます。

これの中身を見てみましょう。

https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/messaging/BatchResponse

Publicメソッドとして、getFailureCount()getResponses()getSuccessCount()が用意されています。
getFailureCount()getSuccessCount()はその名の通り指定したトークンのうち、送信に失敗した数と成功した数を取得できます。

では、getResponses()はといいますと、各トークンごとの送信結果を持つSendResponse型のリストを取得することができるメソッドです。

https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/messaging/SendResponse

SendResponseの中身を見てみると、getException()getMessageId()isSuccessful()というメソッドがあり、それぞれ以下のようなものを取得できます。

  • getException() ・・・そのトークンに対する送信が失敗した場合に、発生した例外を取得できる
  • getMessageId()・・・送信処理が成功した場合のメッセージID
  • isSuccessful()・・・そのトークンに対する送信の成否

ここで、「そのトークン」に該当する情報がこのSendResponseクラスのインスタンスにはないことに注意してください。

公式ドキュメント)によると、

The responses list obtained by calling getResponses() on the return value corresponds to the order of tokens in the MulticastMessage.

とあり、どうやら「送信時にaddAllTokensで指定したトークンリストの順番と、getResponses()で取得できる送信結果のリストの順番は一致しているから、その2つを照らし合わせてくれ」という仕様なようです。

どのトークンに対する送信が失敗したのかを得るためには、2つのリストを突合させる必要があるため、少し面倒ですね......。

そのため、「送信が失敗したトークンに対しては今後送信しないようにするために削除したい」といった理由で、そのようなトークンを取得するためには、以下のようにする必要があります。

final List<Boolean> isSuccessfulList = batchResponse.getResponses().stream()
                .map(response -> response.isSuccessful())
                .toList();

final List<String> failureTokenList = IntStream.range(0, tokenList.size())
                .filter(index -> ! isSuccessfulList.get(index))
                .mapToObj(index -> tokenList.get(index))
                .toList();

おわりに

今回は、FCMでプッシュメッセージをマルチキャストで送信する方法を紹介しました。

基本的にはトピックで管理するのが便利ですが、このような送信方法が必要になった場合には、ぜひ活用してみてください。

では、また次回。

参考文献

付録

@Configuration
@RequiredArgsConstructor
public class FirebaseConfig {
    private static final String MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging";

    private final FirebaseProperty firebaseProperty;

    /**
     * Firebase用のプロパティ
     *
     * @param serviceAccountKey Firebaseのサービスアカウントキー
     */
    @ConfigurationProperties(prefix = "firebase")
    public record FirebaseProperty(String serviceAccountKey) {}

    /**
     * FirebaseMessagingのDIのためのBean登録
     *
     * @return FirebaseMessaging
     */
    @Bean
    public FirebaseMessaging firebaseMessaging() throws IOException {
        final String serviceAccountKeyString = firebaseProperty.serviceAccountKey();
        final InputStream serviceAccountKey = new ByteArrayInputStream(serviceAccountKeyString.getBytes(StandardCharsets.UTF_8));

        final GoogleCredentials googleCredentials = GoogleCredentials
                .fromStream(serviceAccountKey)
                .createScoped(List.of(MESSAGING_SCOPE));

        final FirebaseOptions options = FirebaseOptions
                .builder()
                .setCredentials(googleCredentials)
                .build();

        final FirebaseApp app = FirebaseApp.initializeApp(options);

        return FirebaseMessaging.getInstance(app);
    }
}

あくまで、一例ですので公式ドキュメントなども参考にしてください。