Skip to content

プッシュ通知の実装について

tateisu edited this page Dec 18, 2019 · 14 revisions

構成

図

登場するソフトウェア

  • SNSサーバ:皆様のアカウントが存在するMastodonやMisskeyなどのサーバ。
  • アプリサーバ:この図の真ん中で色々するサーバ。
  • FCM(Firebase Cloud Messaging)サービス:アプリサーバから端末上のアプリに通知やデータメッセージを送るサービス。
  • アプリ:皆様のお手元の端末にインストールされたSubway Tooter アプリ。

基本的な処理の流れ(A)

  • アプリは認証情報を使ってSNSサーバに通知購読リクエストを送る。その際にアプリサーバのエンドポイントURLを伝える。
  • アプリはアプリサーバに通知の配送に必要な情報を送る。FCM端末ID、ユーザのacct、インストールID、SNSサーバのVAPID公開鍵など。
  • SNSサーバはイベント発生時にアプリサーバのエンドポイントURLにHTTPリクエストを送る。
  • アプリサーバは受け取ったデータを検証して、FCMのAPIキーや送り先の端末IDと一緒にFCMサービスに投げる。
  • FCMサービスはデータを端末に届ける。
  • アプリはデータを受信したら通知を更新する。

基本的な処理の流れ(B)

この方法は安全ではないのでもう使われていない

  • アプリはアクセストークン、acct、FCM端末IDなどをアプリサーバに送る。
  • アプリサーバはアクセストークンを使ってSNSサーバのストリーミングAPIに接続し、イベントを聴き続ける。
  • アプリサーバは受け取ったデータを検証して、FCMのAPIキーや送り先の端末IDと一緒にFCMサービスに投げる。
  • FMCサービスはデータを端末に届ける。
  • アプリはデータを受信したら通知を更新する。

アプリサーバ

SubwayTooter のアプリサーバのソースコードは https://github.com/tateisu/PushToFCM で公開されている。 アプリサーバは後述する方法で通知イベントを取得した後に、それをFCM経由で端末上のアプリに転送する。

Firebase Cloud Messaging(FCM)

FCMは通知やデータを端末上のアプリに送るサービスで、モバイル回線のデバイスをスリープから起こすことができる。この特徴は他のOSS製品にはないため、妥協して OSSではないライブラリ firebase-messaging をアプリに含めることになった。

WebPushのペイロードサイズが最低4KBなのに対して、FCMのペイロードサイズは昔は上限3KB程度だった。今は上限4KBに増えたようだ。

十分な大きさのペイロードサイズが保証されない都合で、結局アプリサーバ上では受信したデータをデコードしないしペイロードをデバイスに送らず単なるトリガとして使っている。多分「ペイロードが十分に小さいならそのまま送る、ペイロードが大きすぎるならアプリサーバに保存してそのIDを端末に送る」などすれば通知データをそのままアプリに送れて通知チェックのネットワーク負荷を軽減できると思うが、現時点では実装されていない。

FCMは端末ごとにデバイストークンを発行するが、これはFCM側の都合で随時リフレッシュされる。アプリは新しいデバイストークンを受け取ったらアプリサーバや購読先にそれを伝える必要がある。

Mastodon

Streaming Listener (廃止)

昔のMastodonはPush購読APIがなかった。そこでアクセストークンをアプリからアプリサーバに送信して、アプリサーバがストリーミングを聴き続けることで通知イベントを取得していた。

この方法はストリーミングを聴き続けるサーバの負荷が高いため、SubwayTooterはサーバ管理者や一般ユーザが個別にStreaming Listenerを建てることでリアルタイム通知を行えるようにした。

また、この方法は「アクセストークンをアプリサーバに渡す」というのが根本的に安全ではないため、その後Push購読APIが提供されるようになると使われなくなった。Push購読が利用できるなら自動的に切り替えて、ある程度の移行期間の後に廃止された。

Push購読API

Push購読API https://github.com/tootsuite/mastodon/pull/7445 が2018年5月11日にmasterブランチにマージされて、クライアントアプリがpush通知を購読するとアプリサーバに通知コールバックが届くようになった。

WebPush標準の署名や暗号化が色々とややこしかったが割愛。

マストドンのPush購読にはやや特徴がある。

  • アクセストークンごとに購読を一つしか持てない。
  • アクセストークンはクライアントIDとユーザIDのペアに対して一意に決定される。
  • よって、まだ他のデバイスで使われていないアクセストークンを確実に得るには新しいクライアントを登録して再度OAuth認証を行う必要がある。
  • 結果、クライアント登録が浪費される。避けられない。

アプリデータのバックアップからの復元やエクスポート、インポートでアクセストークンは他のデバイスでも使われる。まあアプリデータを復元したらとりあえず購読以外は普通に行える方が便利だろう。しかし上記の制限により、Push購読までは復元できない。「どのデバイスが最初にそのアクセストークンでPush購読を行ったか」をアプリサーバ上で管理して、すでに他のデバイス(install id)で使われたことのあるアクセストークンではPush購読を行わないようになっている。

この制限をユーザにどう理解してもらうかが難しかった。なんらか理由で復元した後に「なぜかリアルタイム通知が届かない」と思われる。対策として Subway Tooter 3.9.8 でアカウント選択ダイアログにPush購読のエラーを表示するようにした。手動でのアクセストークン更新が必要なのは変わらないが、OAuthの性質上これは避けられないだろう。

Misskey

サーバ側の関連コード

サーバ公開鍵の検証

昔はサーバ公開鍵を返さない実装だった https://github.com/syuilo/misskey/issues/2541https://github.com/syuilo/misskey/commit/4c6fb60dd25d7e2865fc7c4d97728593ffc3c902 でサーバ公開鍵が返されるようになった(A)。

Subway Tooter 3.9.8 以降、関連コミット https://github.com/tateisu/SubwayTooter/commit/b74600dacf3fa768e25cb704e145290b47bfd1d5https://github.com/tateisu/PushToFCM/commit/83e56ec999ba27a5b507cf7cbd3b958bb631e55a で購読時に渡されたサーバ公開鍵と通知コールバックに使われたサーバ公開鍵を比較検証するようになった(B)。 Aが2018/9/1、Bが2019/12/18で対応時期が大分ずれたが、そんなこともあるさ…。

購読の重複と解除

購読APIは userId, endpoint, auth, publickey が同じリクエストの場合はDBを更新しない。Mastodonと異なり1ユーザがいくつでも購読を持てる。一方で明示的に購読を削除するAPIはない。 https://github.com/syuilo/misskey/issues/2540

購読が解除はendpoint URLへのリクエストが410を返した場合に行われる。 https://github.com/syuilo/misskey/blob/develop/src/services/push-notification.ts

Subway Tooter 3.9.8 以降、関連コミット https://github.com/tateisu/SubwayTooter/commit/df18fd6d3655043a9d3c9fff5671771c53720892https://github.com/tateisu/PushToFCM/commit/63e6eb9bd8db5daa6596ab22c4e0be2fad936311 で、あるacctとFCM device ID の組み合わせに対して endpoint URL を一つだけアプリサーバに保持して、endpoint がマッチしない通知コールバックに対して410を返すようになった。

動作確認時の注意

https://github.com/syuilo/misskey/blob/master/src/services/create-notification.ts#L46 を見ると通知には未読/既読の概念があり、イベント発生から2秒間未読のままでないとプッシュ通知が発生しない。

MisskeyのWebUIの通知表示は未読状態を変えてしまうため、アプリのプッシュ通知を動作確認する際はMisskeyのWebUIを開いていてはいけない。

Subway Tooter でプッシュ通知を試すにはSTの画面を非表示にする必要があるので WebUIを使って投稿していたが、WebUIを開いていると通知はすぐ既読になるのでプッシュ通知が発生せずに悩まされた。 この問題を回避するため、WebUIを閉じてAndroidデバイス2つでSubway Tooter 使うなどした。