defmodule ExAws.SNS do
@moduledoc """
Operations on AWS Simple Notification Service (SNS).
See http://docs.aws.amazon.com/sns/latest/api/API_Operations.html for details.
"""
import ExAws.Utils, only: [camelize_key: 1, camelize_keys: 1]
## Topics
######################
@type topic_name :: binary
@type topic_arn :: binary
@type topic_attribute_name ::
:policy
| :display_name
| :delivery_policy
@type topic_attributes :: [
{atom, binary}
| {atom, boolean}
| {atom, integer}
]
@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()
@spec create_topic(topic_name :: topic_name, attributes :: topic_attributes) ::
ExAws.Operation.Query.t()
def create_topic(topic_name, attributes \\ []) do
params =
attributes
|> build_attrs()
|> Map.put("Name", topic_name)
request(:create_topic, params)
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, 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 | nil
) :: 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 | :filter_policy | :raw_message_delivery
@type subscribe_opt :: {:return_subscription_arn, boolean}
@doc "Create Subscription."
@spec subscribe(topic_arn :: binary, protocol :: binary, endpoint :: binary, [subscribe_opt]) ::
ExAws.Operation.Query.t()
def subscribe(topic_arn, protocol, endpoint, opts \\ []) do
params = %{
"TopicArn" => topic_arn,
"Protocol" => protocol,
"Endpoint" => endpoint,
"ReturnSubscriptionArn" => opts[:return_subscription_arn] || false
}
request(:subscribe, params)
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 endpoints 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