defmodule ExAws.SNS do
import ExAws.Utils, only: [camelize_key: 1, camelize_keys: 1]
@moduledoc """
Operations on AWS SNS
http://docs.aws.amazon.com/sns/latest/api/API_Operations.html
"""
## Topics
######################
@type topic_name :: binary
@type topic_arn :: binary
@type topic_attribute_name ::
:policy |
:display_name |
:delivery_policy
@doc "List topics"
@spec list_topics() :: ExAws.Operation.Query.t
@spec list_topics(opts :: [next_token: binary]) :: ExAws.Operation.Query.t
def list_topics(opts \\ []) do
opts = opts
|> Map.new
|> camelize_keys
request(:list_topics, opts)
end
@doc "Create topic"
@spec create_topic(topic_name :: topic_name) :: ExAws.Operation.Query.t
def create_topic(topic_name) do
request(:create_topic, %{"Name" => topic_name})
end
@doc "Get topic attributes"
@spec get_topic_attributes(topic_arn :: topic_arn) :: ExAws.Operation.Query.t
def get_topic_attributes(topic_arn) do
request(:get_topic_attributes, %{"TopicArn" => topic_arn})
end
@doc "Set topic attributes"
@spec set_topic_attributes(attribute_name :: topic_attribute_name,
attribute_value :: binary,
topic_arn :: topic_arn) :: ExAws.Operation.Query.t
def set_topic_attributes(attribute_name, attribute_value, topic_arn) do
request(:set_topic_attributes, %{
"AttributeName" => attribute_name |> camelize_key,
"AttributeValue" => attribute_value,
"TopicArn" => topic_arn
})
end
@doc "Delete topic"
@spec delete_topic(topic_arn :: topic_arn) :: ExAws.Operation.Query.t
def delete_topic(topic_arn) do
request(:delete_topic, %{"TopicArn" => topic_arn})
end
@type message_attribute :: %{
:name => binary,
:data_type => :string | :number | :binary,
:value => {:string, binary} | {:binary, binary}
}
@type publish_opts :: [
{:message_attributes, [message_attribute]} |
{:message_structure, :json} |
{:subject, binary} |
{:phone_number, binary} |
{:target_arn, binary} |
{:topic_arn, binary}]
@doc """
Publish message to a target/topic ARN
You must set either :phone_number, :target_arn or :topic_arn but only one, via the options argument.
Do NOT assume that because your message is a JSON blob that you should set
message_structure: to :json. This has a very specific meaning, please see
http://docs.aws.amazon.com/sns/latest/api/API_Publish.html for details.
"""
@spec publish(message :: binary, opts :: publish_opts) :: ExAws.Operation.Query.t
def publish(message, opts) do
opts = opts |> Map.new
message_attrs = opts
|> Map.get(:message_attributes, [])
|> build_message_attributes
params = opts
|> Map.drop([:message_attributes])
|> camelize_keys
|> Map.put("Message", message)
|> Map.merge(message_attrs)
request(:publish, params)
end
defp build_message_attributes(attrs) do
attrs
|> Stream.with_index
|> Enum.reduce(%{}, &build_message_attribute/2)
end
def build_message_attribute({%{name: name, data_type: data_type, value: {value_type, value}}, i}, params) do
param_root = "MessageAttributes.entry.#{i + 1}"
value_type = value_type |> to_string |> String.capitalize
params
|> Map.put(param_root <> ".Name", name)
|> Map.put(param_root <> ".Value.#{value_type}Value", value)
|> Map.put(param_root <> ".Value.DataType", data_type |> to_string |> String.capitalize)
end
## Platform
######################
@type platform_application_arn:: binary
@doc "Create plaform application"
@spec create_platform_application(name :: binary, platform :: binary, attributes :: %{String.t => String.t}) :: ExAws.Operation.Query.t
def create_platform_application(name, platform, attributes) do
attributes =
attributes
|> build_kv_attrs
|> Map.merge(%{
"Name" => name,
"Platform" => platform,
})
request(:create_platform_application, attributes)
end
@doc "Delete platform application"
@spec delete_platform_application(platform_application_arn :: platform_application_arn) :: ExAws.Operation.Query.t
def delete_platform_application(platform_application_arn) do
request(:delete_platform_application, %{
"PlatformApplicationArn" => platform_application_arn
})
end
@doc "List platform applications"
@spec list_platform_applications() :: ExAws.Operation.Query.t
def list_platform_applications() do
request(:list_platform_applications, %{})
end
@spec list_platform_applications(next_token :: binary) :: ExAws.Operation.Query.t
def list_platform_applications(next_token) do
request(:list_platform_applications, %{"NextToken" => next_token})
end
@doc "Create platform endpoint"
@spec create_platform_endpoint(platform_application_arn :: platform_application_arn,
token :: binary) :: ExAws.Operation.Query.t
@spec create_platform_endpoint(platform_application_arn :: platform_application_arn,
token :: binary,
custom_user_data :: binary) :: ExAws.Operation.Query.t
def create_platform_endpoint(platform_application_arn, token, custom_user_data \\ nil) do
attrs = %{
"PlatformApplicationArn" => platform_application_arn,
"Token" => token
}
attrs = if custom_user_data do
Map.put(attrs, "CustomUserData", custom_user_data)
else
attrs
end
request(:create_platform_endpoint, attrs)
end
@doc "Get platform application attributes"
@spec get_platform_application_attributes(platform_application_arn :: platform_application_arn) :: ExAws.Operation.Query.t
def get_platform_application_attributes(platform_application_arn) do
request(:get_platform_application_attributes, %{"PlatformApplicationArn" => platform_application_arn})
end
## Subscriptions
######################
@type subscription_attribute_name :: :delivery_policy | :raw_message_delivery
@doc "Create Subscription"
@spec subscribe(topic_arn :: binary, protocol :: binary, endpoint :: binary) :: ExAws.Operation.Query.t
def subscribe(topic_arn, protocol, endpoint) do
request(:subscribe, %{
"TopicArn" => topic_arn,
"Protocol" => protocol,
"Endpoint" => endpoint,
})
end
@doc "Confirm Subscription"
@spec confirm_subscription(topic_arn :: binary, token :: binary, authenticate_on_unsubscribe :: boolean) :: ExAws.Operation.Query.t
def confirm_subscription(topic_arn, token, authenticate_on_unsubscribe \\ false) do
request(:confirm_subscription, %{
"TopicArn" => topic_arn,
"Token" => token,
"AuthenticateOnUnsubscribe" => to_string(authenticate_on_unsubscribe),
})
end
@doc "List Subscriptions"
@spec list_subscriptions() :: ExAws.Operation.Query.t
def list_subscriptions() do
request(:list_subscriptions, %{})
end
@spec list_subscriptions(next_token :: binary) :: ExAws.Operation.Query.t
def list_subscriptions(next_token) do
request(:list_subscriptions, %{"NextToken" => next_token})
end
@type list_subscriptions_by_topic_opt :: {:next_token, binary}
@doc "List Subscriptions by Topic"
@spec list_subscriptions_by_topic(topic_arn :: topic_arn) :: ExAws.Operation.Query.t
@spec list_subscriptions_by_topic(topic_arn :: topic_arn, [list_subscriptions_by_topic_opt]) :: ExAws.Operation.Query.t
def list_subscriptions_by_topic(topic_arn, opts \\ []) do
params = case opts do
[next_token: next_token] ->
%{"TopicArn" => topic_arn, "NextToken" => next_token}
_ ->
%{"TopicArn" => topic_arn}
end
request(:list_subscriptions_by_topic, params)
end
@doc "Unsubscribe"
@spec unsubscribe(subscription_arn :: binary) :: ExAws.Operation.Query.t
def unsubscribe(subscription_arn) do
request(:unsubscribe, %{
"SubscriptionArn" => subscription_arn
})
end
@doc "Get subscription attributes"
@spec get_subscription_attributes(subscription_arn :: binary) :: ExAws.Operation.Query.t
def get_subscription_attributes(subscription_arn) do
request(:get_subscription_attributes, %{
"SubscriptionArn" => subscription_arn
})
end
@doc "Set subscription attributes"
@spec set_subscription_attributes(attribute_name :: subscription_attribute_name,
attribute_value :: binary,
subscription_arn :: binary) :: ExAws.Operation.Query.t
def set_subscription_attributes(attribute_name, attribute_value, subscription_arn) do
request(:set_subscription_attributes, %{
"AttributeName" => attribute_name |> camelize_key,
"AttributeValue" => attribute_value,
"SubscriptionArn" => subscription_arn
})
end
@doc "List phone numbers opted out"
@spec list_phone_numbers_opted_out() :: ExAws.Operation.Query.t
def list_phone_numbers_opted_out() do
request(:list_phone_numbers_opted_out, %{})
end
@spec list_phone_numbers_opted_out(next_token :: binary) :: ExAws.Operation.Query.t
def list_phone_numbers_opted_out(next_token) do
request(:list_phone_numbers_opted_out, %{"nextToken" => next_token})
end
@doc "Opt in phone number"
@spec opt_in_phone_number(phone_number :: binary) :: ExAws.Operation.Query.t
def opt_in_phone_number(phone_number) do
request(:opt_in_phone_number, %{"phoneNumber" => phone_number})
end
## Endpoints
######################
@type endpoint_arn :: binary
@type endpoint_attributes :: [
{:token, binary}
| {:enabled, boolean}
| {:custom_user_data, binary}
]
@doc "Get endpoint attributes"
@spec get_endpoint_attributes(endpoint_arn :: endpoint_arn) :: ExAws.Operation.Query.t
def get_endpoint_attributes(endpoint_arn) do
request(:get_endpoint_attributes, %{"EndpointArn" => endpoint_arn})
end
@doc "Set endpoint attributes"
@spec set_endpoint_attributes(endpoint_arn :: endpoint_arn, attributes :: endpoint_attributes) :: ExAws.Operation.Query.t
def set_endpoint_attributes(endpoint_arn, attributes) do
params =
attributes
|> build_attrs
request(:set_endpoint_attributes, Map.put(params, "EndpointArn", endpoint_arn))
end
@doc "Delete endpoint"
@spec delete_endpoint(endpoint_arn :: endpoint_arn) :: ExAws.Operation.Query.t
def delete_endpoint(endpoint_arn) do
request(:delete_endpoint, %{
"EndpointArn" => endpoint_arn
})
end
@type list_endpoints_by_platform_application_opt :: {:next_token, binary}
@doc "List endpooints and endpoint attributes for devices in a supported push notification service"
@spec list_endpoints_by_platform_application(topic_arn :: topic_arn) :: ExAws.Operation.Query.t
@spec list_endpoints_by_platform_application(topic_arn :: topic_arn, [list_endpoints_by_platform_application_opt]) :: ExAws.Operation.Query.t
def list_endpoints_by_platform_application(platform_application_arn, opts \\ []) do
params = case opts do
[next_token: next_token] ->
%{ "PlatformApplicationArn" => platform_application_arn,
"NextToken" => next_token }
_ ->
%{ "PlatformApplicationArn" => platform_application_arn }
end
request(:list_endpoints_by_platform_application, params)
end
## Messages
######################
@notification_params ["Message", "MessageId", "Subject", "Timestamp", "TopicArn", "Type"]
@optional_notification_params ["Subject"]
@confirmation_params ["Message", "MessageId", "SubscribeURL", "Timestamp", "Token", "TopicArn", "Type"]
@signature_params ["SignatureVersion", "Signature", "SigningCertURL"]
@message_types ["SubscriptionConfirmation", "UnsubscribeConfirmation", "Notification"]
@doc "Verify message signature"
@spec verify_message(message_params :: %{String.t => String.t}) :: [:ok | {:error, String.t}]
def verify_message(message_params) do
with :ok <- validate_message_params(message_params),
:ok <- validate_signature_version(message_params["SignatureVersion"]),
{:ok, public_key} <- ExAws.SNS.PublicKeyCache.get(message_params["SigningCertURL"]) do
message_params
|> get_string_to_sign
|> verify(message_params["Signature"], public_key)
end
end
defp validate_message_params(message_params) do
with {:ok, required_params} <- get_required_params(message_params["Type"]) do
case required_params -- Map.keys(message_params) do
[] -> :ok
missing_params -> {:error, "The following parameters are missing: #{inspect missing_params}"}
end
end
end
defp get_required_params(message_type) do
case message_type do
"Notification" -> {:ok, (@notification_params -- @optional_notification_params) ++ @signature_params}
"SubscriptionConfirmation" -> {:ok, @confirmation_params ++ @signature_params}
"UnsubscribeConfirmation" -> {:ok, @confirmation_params ++ @signature_params}
type when is_binary(type) -> {:error, "Invalid Type, expected one of #{inspect @message_types}"}
type when is_nil(type) -> {:error, "Missing message type parameter (Type)"}
type -> {:error, "Invalid message type's type, expected a String, got #{inspect type}"}
end
end
defp validate_signature_version(version) do
case version do
"1" -> :ok
val when is_binary(val) -> {:error, "Unsupported SignatureVersion, expected \"1\", got #{version}"}
_ -> {:error, "Invalid SignatureVersion format, expected a String, got #{version}"}
end
end
defp get_string_to_sign(message_params) do
message_params
|> Map.take(get_params_to_sign(message_params["Type"]))
|> Enum.map(fn {key, value} -> [to_string(key), "\n", to_string(value), "\n"] end)
|> IO.iodata_to_binary
end
defp get_params_to_sign(message_type) do
case message_type do
"Notification" -> @notification_params
"SubscriptionConfirmation" -> @confirmation_params
"UnsubscribeConfirmation" -> @confirmation_params
end
end
defp verify(message, signature, public_key) do
case :public_key.verify(message, :sha, Base.decode64!(signature), public_key) do
true -> :ok
false -> {:error, "Signature is invalid"}
end
end
## Request
######################
defp request(action, params) do
action_string = action |> Atom.to_string |> Macro.camelize
%ExAws.Operation.Query{
path: "/",
params: params |> Map.put("Action", action_string),
service: :sns,
action: action,
parser: &ExAws.SNS.Parsers.parse/2
}
end
defp build_attrs(attrs) do
attrs
|> Enum.with_index(1)
|> Enum.map(&build_attr/1)
|> Enum.reduce(%{}, &Map.merge(&1, &2))
end
defp build_attr({{name, value}, index}) do
prefix = "Attributes.entry.#{index}."
%{}
|> Map.put(prefix <> "name", format_param_key(name))
|> Map.put(prefix <> "value", value)
end
defp build_kv_attrs(attrs) do
attrs
|> Enum.with_index(1)
|> Enum.map(&build_kv_attr/1)
|> Enum.reduce(%{}, &Map.merge(&1, &2))
end
defp build_kv_attr({{key, value}, index}) do
prefix = "Attributes.entry.#{index}."
%{}
|> Map.put(prefix <> "key", key)
|> Map.put(prefix <> "value", value)
end
defp format_param_key("*"), do: "*"
defp format_param_key(key) do
key
|> Atom.to_string
|> ExAws.Utils.camelize
end
end