Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
20
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

プッシュ通知をユーザー毎に異なるメッセージで送る by Amazon SNS (Android/iOS 両対応)

プッシュ通知はモバイルアプリにおいて、送信後ユーザーがアプリを開くきっかけともなり非常に重要な機能です。

Amazon SNSではこの機能に対応しており、実装例も数多くネット上で見つかります。全くの初めての場合、Amazon Mobile Hubで最低限実装されたコードを自動生成するサービスがあるのでそちらの方も利用してみると良いでしょう。

一方で、見つかる情報の多くはユーザー全員に同じメッセージを送るものであり、ユーザーの層毎に層の定義を柔軟に生成し異なるメッセージを送ることができれば、よりユーザーに刺さるメッセージを送ることができるようになるでしょう。

しかし、ネット上にあまり実装例が見当たらなかったので記事にさせて頂きます。

Amazon SNSではプッシュ通知を送るために、基本的には以下の処理を行うことで使用することができます。

  1. 初期化処理
  2. Endpointの作成
  3. Topicに対するsubscribeの処理

ここまではネット上で多く情報が見つかります。
以降、SNSで複雑な処理も可能なように、よく使用されると思われるSNSの以下のAPIの実装例を挙げます。

  • ListTopics API
  • CreateTopic API
  • CreatePlatformEndpoint API
  • ListSubscriptions API
  • Subscribe API
  • Unsubscribe API

実装は以下の言語となります。

端末 言語
Android Java
iOS Swift3

また、推奨アプローチとして、Endpointが失効してプッシュ通知が送れなくなるのを防ぐためにEndpointを安全に最新版に保つというものがあります。詳しい説明はこちらの方が分かりやすく参考にしてみてください。ここでは実装例を取り上げます。

最後に、作成したTopic毎にプッシュ通知を送るためのスクリプトの実装例(Ruby)も挙げます。

初期化処理

Cognitoで認証後、SNSの各種APIによる処理を行う。

Android

final CognitoCachingCredentialsProvider credentials = credentialsProvider =
                new CognitoCachingCredentialsProvider(context,
                        "AWS_ACCOUNT_ID",
                        "COGNITO_POOL_ID",
                        "UNAUTH_ROLE_ARN",
                        "AUTH_ROLE_ARN",
                        Regions.AP_NORTHEAST_1
                );

iOS

let credentialsProvider:AWSCognitoCredentialsProvider = AWSCognitoCredentialsProvider(regionType: .apNortheast1, identityPoolId: "us-east-1:xxxxxxxxxxxx(Identity pool ID )")
let configuration = AWSServiceConfiguration(region: AWSRegionType.apNortheast1, credentialsProvider: credentialsProvider)
configuration?.timeoutIntervalForResource = 1000
AWSServiceManager.default().defaultServiceConfiguration =
configuration
credentialsProvider.getIdentityId()

ListTopics API

Topicのリストを取得する。

Android

final AmazonSNSClient snsClient = new AmazonSNSClient(credentials);
ListTopicsResult listTopicsResult = snsClient.listTopics();
List<Topic> topics = listTopicsResult.getTopics();

iOS

let sns = AWSSNS.default()
let request: AWSSNSListTopicsInput = AWSSNSListTopicsInput()
sns.listTopics(request).continue(with: AWSExecutor.default(), with: { [weak self] (task: AWSTask) -> AnyObject? in
    guard let me = self else { return nil }
    if let error = task.error { print("Error: \(error)"); return nil; }

    guard let listTopicsResponse = task.result else { return nil }
    guard let topics = listTopicsResponse.topics else { return nil }

    print(topics)

    return nil
})

CreateTopic API

Topicを作成する。

Android

final AmazonSNSClient snsClient = new AmazonSNSClient(credentials);
CreateTopicRequest createTopicRequest = new CreateTopicRequest(topic);
CreateTopicResult createTopicResult = snsClient.createTopic(createTopicRequest);

iOS

let request: AWSSNSCreateTopicInput = AWSSNSCreateTopicInput()
request.name = topic
sns.createTopic(request)

CreatePlatformEndpoint API

Endpointを作成する。

Android

final GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context);
String deviceToken = gcm.register(gcmSenderID);
final AmazonSNSClient snsClient = new AmazonSNSClient(credentials);

CreatePlatformEndpointRequest createRequest = new CreatePlatformEndpointRequest();
createRequest.setToken(deviceToken);            createRequest.setPlatformApplicationArn(BuildConfig.AMAZON_SNS_PLATFORM_APPLICATION_ARN);
CreatePlatformEndpointResult platformEndpoint = snsClient.createPlatformEndpoint(createRequest);
String endPoint = platformEndpoint.getEndpointArn();

iOS

let sns = AWSSNS.default()
let input: AWSSNSCreatePlatformEndpointInput = AWSSNSCreatePlatformEndpointInput()
input.token = deviceToken
input.platformApplicationArn = SNS_ARN_DEV
sns.createPlatformEndpoint(input)

ListSubscriptions API

subscribeされているSubscriptionのリストを取得する。ただし、端末毎にsubscribeしているものを取り出すわけではなく、認証に使用したIAMでsubscribeしているもの全体になるので注意が必要。

Android

final AmazonSNSClient snsClient = new AmazonSNSClient(credentials);
ListSubscriptionsResult listSubscriptionsResult = snsClient.listSubscriptions();
List<Subscription> subscriptions = listSubscriptionResult.getSubscriptions();

iOS

let sns = AWSSNS.default()
let req: AWSSNSListSubscriptionsInput = AWSSNSListSubscriptionsInput()
sns.listSubscriptions(req).continue(with: AWSExecutor.default(), with: { (task: AWSTask) -> AnyObject? in
    if let error = task.error { print("Error: \(error)"); return nil; }
    let result: AWSSNSListSubscriptionsResponse? = task.result
    let subscriptions = result?.subscriptions

    print(subscriptions)
    return nil
})

Subscribe API

存在しているTopic名を指定してsubscribeする。

Android

String title = "トピック名";
String nesessaryTopicArn = SNS_TOPIC_PREFIX + title;
final AmazonSNSClient snsClient = new AmazonSNSClient(credentials);

SubscribeRequest subscribeRequest = new SubscribeRequest(nesessaryTopicArn, "application", endPoint);
SubscribeResult subscribeResult = snsClient.subscribe(subscribeRequest);
String subscriptionArn = subscribeResult.getSubscriptionArn();

iOS

let sns = AWSSNS.default()
let request: AWSSNSSubscribeInput = AWSSNSSubscribeInput()

request.topicArn = SNS_TOPIC_PREFIX + subscribeTopicName
request.protocols = "application"
request.endpoint = endPointArn

sns.subscribe(request).continue(with: AWSExecutor.default(), with: { (task: AWSTask) -> AnyObject? in
    if let error = task.error { print("Error: \(error)"); return nil; }
    guard let subscribeResponse = task.result else { return nil }
    guard let subscriptionArn = subscribeResponse.subscriptionArn else { return nil }

    print(subscriptionArn)    
    return nil
})

Unsubscribe API

subscribeしているSubscription ARNを指定してunsubscribeする。

Android

String subscriptionArn = "unsubscribeするトピックのSubscription ARN"
final AmazonSNSClient snsClient = new AmazonSNSClient(credentials);
UnsubscribeRequest request = new UnsubscribeRequest(subscriptionArn);
snsClient.unsubscribe(request);

iOS

let sns = AWSSNS.default()
let request: AWSSNSUnsubscribeInput = AWSSNSUnsubscribeInput()
request.subscriptionArn = subscriptionArn
sns.unsubscribe(request)

Endpointを安全に最新版に保つための処理

Endpointが有効でなくなる場合を考慮して以下のような処理をすることでより安全にSNSを使用することができる。

Android

Boolean updateNeeded = false, createNeeded = false;
final AmazonSNSClient snsClient = new AmazonSNSClient(credentials);

try {
    GetEndpointAttributesRequest getAttributesRequest = new GetEndpointAttributesRequest().withEndpointArn(endPoint);
    GetEndpointAttributesResult attributesResult = snsClient.getEndpointAttributes(getAttributesRequest);
    Log.d("Endpoint Attributes Result: " + attributesResult.toString());

    updateNeeded = !attributesResult.getAttributes().get("Token").equals(gcmToken)
            || !attributesResult.getAttributes().get("Enabled").equalsIgnoreCase("true");
} catch (NotFoundException e) {
    createNeeded = true;
    Log.d("No endpoint of SNS topic");
}

// Make PlatformEndpoint latest safely
if (createNeeded) {
    Logg.d("Invalidate and recreate endpoint");
    createEndpoint(snsClient, gcmToken);
}
if (updateNeeded) {
    Log.d("Set attributes for endpoint");
    setAttribute(snsClient, gcmToken, endPoint);
}

private static void setAttribute(AmazonSNSClient snsClient, String gcmToken, String endPoint) {
    Map attributes = new HashMap();
    attributes.put("Token", gcmToken);
    attributes.put("Enabled", "true");
    SetEndpointAttributesRequest setAttributesRequest = new SetEndpointAttributesRequest().withEndpointArn(endPoint).withAttributes(attributes);
    snsClient.setEndpointAttributes(setAttributesRequest);
}

private static String createEndpoint(AmazonSNSClient snsClient, String gcmToken) {
    String endPoint = "";
    try {
        CreatePlatformEndpointRequest createRequest = new CreatePlatformEndpointRequest();
        createRequest.setToken(gcmToken);
        createRequest.setPlatformApplicationArn(BuildConfig.AMAZON_SNS_PLATFORM_APPLICATION_ARN);
        CreatePlatformEndpointResult platformEndpoint = snsClient.createPlatformEndpoint(createRequest);
        endPoint = platformEndpoint.getEndpointArn();
        Log.d("Success to get endpoint: " + endPoint);
        return endPoint;
    } catch (Exception e) {
        Log.d("Failure to get endpoint: " + e);
    }
    return endPoint;
}

iOS

let sns = AWSSNS.default()

let request: AWSSNSGetEndpointAttributesInput = AWSSNSGetEndpointAttributesInput()
request.endpointArn = arn
sns.getEndpointAttributes(request).continue(with: AWSExecutor.default(), with: { [weak self] (task: AWSTask) in
    guard let me = self else { return nil }
    if let error = task.error {
        print("Error: Endpoint attributes error. The device should create endpoint: \(error)");
        me.createEndpoint()
        return nil
    }

    guard let endpointAttributesResult = task.result else { return nil }
    let attributes: Dictionary<String, String>? = endpointAttributesResult.attributes
    if !(attributes?["token"] == endPointArn) || !(attributes?["Enabled"] == "true") {
        print("Error: Device token is old or not valid. The device should set latest information");
        me.setAttribute()
    }

    return nil
})


fileprivate func createEndpoint() {
    let sns = AWSSNS.default()
    let input: AWSSNSCreatePlatformEndpointInput = AWSSNSCreatePlatformEndpointInput()
    input.token = endPointArn
    input.platformApplicationArn = SNS_ARN_DEV

    sns.createPlatformEndpoint(input)
}

fileprivate func setAttribute() {
    let sns = AWSSNS.default()
    let request: AWSSNSSetEndpointAttributesInput = AWSSNSSetEndpointAttributesInput() 
    request.attributes = ["Token": endPointArn, "Enabled": "true"]
    sns.setEndpointAttributes(request)
}

RubyによるTopicに応じたプッシュ通知送信処理

以下のようなファイル構成で実装を行う。

Gemfile
.env
push.rb
message/2016/10.ini
             11.ini
             12.ini

bundlerを使用する。インストールしていない場合は以下のコマンドを実行する。

gem install bundler

対象のレポジトリで以下のようにbundlerの初期化処理を実行する。

bundle init

Gemfileが作成されるので、AWSのSDKとInifileをインストールを記述する。

# frozen_string_literal: true
# A sample Gemfile
source "https://rubygems.org"

# gem "rails"
gem 'aws-sdk', '~> 2'
gem 'inifile'

以下のコマンドを実行して対象ファイルのインストールを行う。

bundle install

.envファイルを以下の通りに作成し環境変数を定義する。

export ACCESS_KEY='AKIA****************'
export SECRET_KEY='*****************************************'

作成後、以下のコマンドを実行して環境変数を読み込む。

source .env

メッセージは年/月.iniで日ごとにTopic毎のメッセージを管理する実装である。以下のように作成する。
例えば、以下の[18]は2016/10/18に向けたメッセージを表す。

[18]
Defualt_Topic = アプリをやろう
Topic1 = Topic1を今すぐチェック!
Topic2 = Topic2は明日間もなく!
Topic3 = Topic3をチェックしておこう!

[19]
Defualt_Topic = アプリ今日はもう確認したかな
Topic1 = Topic1のユーザーはこれ必見
Topic2 = Topic2が今話題
Topic3 = Topic3が熱い

以下、本体となるRubyのスクリプトである。

require 'date'
require 'aws-sdk'
require 'inifile'

FILE_PATH = "./message"
FILE_IDENTIFIER = ".ini"

SNS_PREFIX = "arn:aws:sns:ap-northeast-1:<アカウント番号>:"
DEFAULT_TOPIC = "デフォルトに設定するトピック名"

class Message
    def initialize
        today = Date.today
        year = today.year
        month = today.month
        filename = FILE_PATH + '/' + year.to_s + '/' + month.to_s + FILE_IDENTIFIER
        @init = IniFile.new(:filename => filename, :encoding => 'UTF-8')
    end

    def show(name)
        day = Date.today.day
        topic = decide_topic(name)
        message = @init[day][topic]

        if message.nil? then
            @init[day][DEFAULT_TOPIC]
        else
            message
        end
    end

    def decide_topic(name)
        name
    end
end

class PushManager
    def initialize
        Aws.config.update({
          region: 'ap-northeast-1',
          credentials: Aws::Credentials.new(ENV['ACCESS_KEY'], ENV['SECRET_KEY'])
        })

        @client = Aws::SNS::Client.new
        response = @client.list_topics
        @topics = response.topics
    end

    def publish
        @topics.each { |topic|
            category = topic.topic_arn[SNS_PREFIX.length..-1]

            topic_arn = SNS_PREFIX + category
            message = Message.new
            p category + ' :  ' + message.show(category)
            @client.publish(
                :topic_arn => topic_arn,
                :message => message.show(category)
            )
        }
    end
end

manager = PushManager.new
manager.publish

参考

SNS APIのドキュメント
http://docs.aws.amazon.com/ja_jp/sns/latest/api/Welcome.html
SNS SDKのドキュメント
http://docs.aws.amazon.com/AWSAndroidSDK/latest/javadoc/
SNSのモバイルトークン管理のベストプラクティス
http://dev.classmethod.jp/cloud/aws/sns-mobile-token/
https://aws.amazon.com/jp/blogs/mobile/mobile-token-management-with-amazon-sns/

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
20
Help us understand the problem. What are the problem?