ITと筋トレの二刀流

未だゼロ刀流

ReactNative+ExpoでAndroid端末へプッシュ通知を送る その②〜AWS Lambdaを使ってプッシュ通知〜

Expoを使用して開発するReactNativeアプリから各端末へプッシュ通知を送る仕組みを開発しています。

前回の記事では、Expoを使ったプッシュ通知に必要なExpoプッシュトークンを取得するところまで書きました。↓

tatsuyashi.hatenablog.com

今回は、取得したプッシュトークンを使用して実際にプッシュ通知を送る方法を記載します。
なお、バックエンドは全てAWSにしているため、今回はAWS Lambdaを使ってプッシュ通知を送ってみました。

プッシュ通知の流れ

プッシュ通知を送るまでの流れは下記の3ステップです。

AWS Lambda (Node.js) でプッシュ通知

Node.jsでLambdaを書いてプッシュ通知をしようと思います。

Expoからプッシュ通知を送るためのSDKが各言語向けに用意されているのでそれを使用します。 ちなみにNode.jsの他にはPythonRuby、Go、Elixir、PHPなどのSDKが有志によって用意されています。

Push Notifications - Expo Documentation

実はNode.js向けSDKGithubのREADMEのサンプルコードにほぼ答えが載ってるのでした。

github.com

上記を参考にして使ったLambdaのソースを抜粋して載せていきます。

準備

expo-server-sdkを追加します。

npm install expo-server-sdk

外部モジュールに依存するのでLambdaはインラインではなくZip形式にしたものをアップロードします。

コード

完全に公開できるものではないので、一部抜粋となりますがご了承ください。

const { Expo } = require('expo-server-sdk');

// Create a new Expo SDK client
let expo = new Expo();

exports.handler = async (event, context) => {
    // 通知メッセージを取得
    const messages = await getMessages(event);
    
    // 通知する
    const tickets = await sendPushNotifications(messages);

    // 通知結果を保存する(必要に応じて)
    await saveTickets(tickets);
}

const getMessages = async (event) => {
    // ここに通知するメッセージを取得する処理を書く
    ...
    
    return [
        // 1つ1つのメッセージは以下の形式で
        {
                to: pushToken, // Expoプッシュトークン
                sound: 'default',  // 通知時の音を鳴らすかどうかの設定
                title: title,  // 通知タイトル
                body: body  // 通知本文
            }
    ]
}

const sendPushNotifications = async (messages) => {
    // The Expo push notification service accepts batches of notifications so
    // that you don't need to send 1000 requests to send 1000 notifications. We
    // recommend you batch your notifications to reduce the number of requests
    // and to compress them (notifications with similar content will get
    // compressed).
    let chunks = expo.chunkPushNotifications(messages);
    let tickets = [];

    // Send the chunks to the Expo push notification service. There are
    // different strategies you could use. A simple one is to send one chunk at a
    // time, which nicely spreads the load out over time:
    for (let chunk of chunks) {
        try {
            let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
            console.log(ticketChunk);
            tickets.push(...ticketChunk);
            // NOTE: If a ticket contains an error code in ticket.details.error, you
            // must handle it appropriately. The error codes are listed in the Expo
            // documentation:
            // https://docs.expo.io/versions/latest/guides/push-notifications#response-format
        } catch (error) {
            console.error(error);
        }
    }
    return tickets;
}

const saveTickets = async (tickets) => {
    // ticketsをデータベースに保存するような処理
}

上記ソースコードで言うと、sendPushNotificationsメソッドがプッシュ通知を送るメイン処理となります。

その前にgetMessagesメソッドでプッシュ通知の対象データを返してあげます。 1つ1つのメッセージに設定できる項目は下記に載っています。

Push Notifications - Expo Documentation

最後にsaveTicketsメソッドでプッシュ通知の結果をデータベースに保存します。
この処理を行っている理由ですが、SDKのREADMEには以下のコードも併記されています。

// Later, after the Expo push notification service has delivered the
// notifications to Apple or Google (usually quickly, but allow the the service
// up to 30 minutes when under load), a "receipt" for each notification is
// created. The receipts will be available for at least a day; stale receipts
// are deleted.
//
// The ID of each receipt is sent back in the response "ticket" for each
// notification. In summary, sending a notification produces a ticket, which
// contains a receipt ID you later use to get the receipt.
//
// The receipts may contain error codes to which you must respond. In
// particular, Apple or Google may block apps that continue to send
// notifications to devices that have blocked notifications or have uninstalled
// your app. Expo does not control this policy and sends back the feedback from
// Apple and Google so you can handle it appropriately.
let receiptIds = [];
for (let ticket of tickets) {
  // NOTE: Not all tickets have IDs; for example, tickets for notifications
  // that could not be enqueued will have error information and no receipt ID.
  if (ticket.id) {
    receiptIds.push(ticket.id);
  }
}

let receiptIdChunks = expo.chunkPushNotificationReceiptIds(receiptIds);
(async () => {
  // Like sending notifications, there are different strategies you could use
  // to retrieve batches of receipts from the Expo service.
  for (let chunk of receiptIdChunks) {
    try {
      let receipts = await expo.getPushNotificationReceiptsAsync(chunk);
      console.log(receipts);

      // The receipts specify whether Apple or Google successfully received the
      // notification and information about an error, if one occurred.
      for (let receipt of receipts) {
        if (receipt.status === 'ok') {
          continue;
        } else if (receipt.status === 'error') {
          console.error(`There was an error sending a notification: ${receipt.message}`);
          if (receipt.details && receipt.details.error) {
            // The error codes are listed in the Expo documentation:
            // https://docs.expo.io/versions/latest/guides/push-notifications#response-format
            // You must handle the errors appropriately.
            console.error(`The error code is ${receipt.details.error}`);
          }
        }
      }
    } catch (error) {
      console.error(error);
    }
  }
})();

プッシュ通知でエラーになった場合はなんらかの対応をしないといけないらしく、そのために上記のようなreceiptの取得処理を行わないといけないようです。

ただ、処理を行うにしても今回のLambdaとは別のLambdaにすべきで、プッシュ通知そのものとは切り離すべきですね。

まとめ

以上のようにして、Lambdaを使ってExpo経由でプッシュ通知を送ることができました。

今回のLambdaはDynamoDB StreamsをトリガーとするLambdaを書いていますが、プッシュ通知を送るケースとしては他にも

  • Kinesisを使ったリアルタイムストリーム処理
  • CloudWatchの定時実行イベント

などのトリガーでLambdaを動かすことがあると思いますので、そういったケースで今回のコードが参考になれば幸いです。