プッシュ通知はモバイルアプリにおいて、送信後ユーザーがアプリを開くきっかけともなり非常に重要な機能です。
Amazon SNSではこの機能に対応しており、実装例も数多くネット上で見つかります。全くの初めての場合、Amazon Mobile Hubで最低限実装されたコードを自動生成するサービスがあるのでそちらの方も利用してみると良いでしょう。
一方で、見つかる情報の多くはユーザー全員に同じメッセージを送るものであり、ユーザーの層毎に層の定義を柔軟に生成し異なるメッセージを送ることができれば、よりユーザーに刺さるメッセージを送ることができるようになるでしょう。
しかし、ネット上にあまり実装例が見当たらなかったので記事にさせて頂きます。
Amazon SNSではプッシュ通知を送るために、基本的には以下の処理を行うことで使用することができます。
- 初期化処理
- Endpointの作成
- 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/