Current section

Files

Jump to
pigeon lib pigeon fcm notification.ex
Raw

lib/pigeon/fcm/notification.ex

defmodule Pigeon.FCM.Notification do
@moduledoc """
Defines FCM notification struct and convenience constructor functions.
"""
defstruct collapse_key: nil,
dry_run: false,
message_id: nil,
payload: %{},
priority: :normal,
registration_id: nil,
response: [],
restricted_package_name: nil,
status: nil,
time_to_live: 2_419_200
alias Pigeon.FCM.Notification
@type t :: %__MODULE__{
collapse_key: nil | String.t(),
dry_run: boolean,
message_id: nil | String.t(),
payload: map,
priority: :normal | :high,
registration_id: String.t() | [String.t()],
response: [] | [regid_response, ...],
restricted_package_name: nil | String.t(),
status: status | nil,
time_to_live: non_neg_integer
}
@typedoc ~S"""
Status of FCM request
- `:success` - Notification was processed successfully
- `:timeout` - Worker did not respond within timeout. This is likely an
internal error
- `:unauthorized` - Bad FCM key
- `:malformed_json` - Push payload was invalid JSON
- `:internal_server_error` - FCM server encountered an error while trying
to process the request
- `:unavailable` - FCM server couldn't process the request in time
"""
@type status ::
:success
| :timeout
| :unauthorized
| :malformed_json
| :internal_server_error
| :unavailable
@typedoc ~S"""
FCM push response for individual registration IDs
- `{:success, "reg_id"}` - Push was successfully sent
- `{:update, {"reg_id", "new_reg_id"}}` - Push successful but user should
use new registration ID for future pushes
- `{regid_error_response, "reg_id"}` - Push attempted but server responded
with error
"""
@type regid_response ::
{:success, binary}
| {regid_error_response, binary}
| {:update, {binary, binary}}
@type regid_error_response ::
:device_message_rate_exceeded
| :internal_server_error
| :invalid_apns_credential
| :invalid_data_key
| :invalid_package_name
| :invalid_parameters
| :invalid_registration
| :invalid_ttl
| :message_too_big
| :missing_registration
| :mismatch_sender_id
| :not_registered
| :topics_message_rate_exceeded
| :unavailable
| :unknown_error
@chunk_size 1_000
@doc """
Creates `FCM.Notification` struct with device registration IDs and optional
notification and data payloads.
## Examples
iex> Pigeon.FCM.Notification.new("reg ID")
%Pigeon.FCM.Notification{
payload: %{},
registration_id: "reg ID",
priority: :normal
}
iex> Pigeon.FCM.Notification.new("reg ID", %{"body" => "test message"})
%Pigeon.FCM.Notification{
payload: %{"notification" => %{"body" => "test message"}},
registration_id: "reg ID",
priority: :normal
}
iex> Pigeon.FCM.Notification.new("reg ID", %{"body" => "test message"},
...> %{"key" => "value"})
%Pigeon.FCM.Notification{
payload: %{
"data" => %{"key" => "value"},
"notification" => %{"body" => "test message"}
},
registration_id: "reg ID",
priority: :normal
}
iex> regids = Enum.map(0..1_499, fn(_x) -> "reg ID" end)
iex> [n1 | [n2]] = Pigeon.FCM.Notification.new(regids,
...> %{"body" => "test message"}, %{"key" => "value"})
iex> Enum.count(n1.registration_id)
1000
iex> Enum.count(n2.registration_id)
500
"""
def new(registration_ids, notification \\ %{}, data \\ %{})
def new(reg_id, notification, data) when is_binary(reg_id) do
%Pigeon.FCM.Notification{registration_id: reg_id}
|> put_notification(notification)
|> put_data(data)
end
def new(reg_ids, notification, data) when length(reg_ids) < 1001 do
%Pigeon.FCM.Notification{registration_id: reg_ids}
|> put_notification(notification)
|> put_data(data)
end
def new(reg_ids, notification, data) do
reg_ids
|> chunk(@chunk_size, @chunk_size, [])
|> Enum.map(&new(&1, notification, data))
|> List.flatten()
end
defp chunk(collection, chunk_size, step, padding) do
if Kernel.function_exported?(Enum, :chunk_every, 4) do
Enum.chunk_every(collection, chunk_size, step, padding)
else
Enum.chunk(collection, chunk_size, step, padding)
end
end
@doc """
Updates `"data"` key in push payload.
## Examples
iex> put_data(%Pigeon.FCM.Notification{}, %{"key" => 1234})
%Pigeon.FCM.Notification{
payload: %{"data" => %{"key" => 1234}},
registration_id: nil
}
"""
def put_data(n, data), do: update_payload(n, "data", data)
@doc """
Updates `"notification"` key in push payload.
## Examples
iex> put_notification(%Pigeon.FCM.Notification{},
...> %{"body" => "message"})
%Pigeon.FCM.Notification{
payload: %{"notification" => %{"body" => "message"}},
registration_id: nil
}
"""
def put_notification(n, notification),
do: update_payload(n, "notification", notification)
@doc """
Updates `"priority"` key.
## Examples
iex> put_priority(%Pigeon.FCM.Notification{}, :normal)
%Pigeon.FCM.Notification{priority: :normal}
iex> put_priority(%Pigeon.FCM.Notification{}, :high)
%Pigeon.FCM.Notification{priority: :high}
iex> put_priority(%Pigeon.FCM.Notification{priority: :normal}, :bad)
%Pigeon.FCM.Notification{priority: :normal}
"""
def put_priority(n, :normal), do: %{n | priority: :normal}
def put_priority(n, :high), do: %{n | priority: :high}
def put_priority(n, _), do: n
@doc """
Updates `"time_to_live"` key. Time-to-live is measured in seconds.
## Examples
iex> put_time_to_live(%Pigeon.FCM.Notification{}, 60 * 60 * 24)
%Pigeon.FCM.Notification{time_to_live: 86_400}
"""
def put_time_to_live(n, ttl) when is_integer(ttl),
do: %{n | time_to_live: ttl}
@doc """
Sets `"dry_run"` key to true. Pushes will be processed but not actually
delivered to the device.
## Examples
iex> put_dry_run(%Pigeon.FCM.Notification{})
%Pigeon.FCM.Notification{dry_run: true}
"""
def put_dry_run(n), do: %{n | dry_run: true}
@doc """
Updates `"collapse_key"` key.
## Examples
iex> put_collapse_key(%Pigeon.FCM.Notification{}, "Updates available")
%Pigeon.FCM.Notification{collapse_key: "Updates available"}
"""
def put_collapse_key(n, key) when is_binary(key), do: %{n | collapse_key: key}
@doc """
Updates `"restricted_package_name"` key.
## Examples
iex> put_restricted_package_name(%Pigeon.FCM.Notification{}, "com.example.app")
%Pigeon.FCM.Notification{restricted_package_name: "com.example.app"}
"""
def put_restricted_package_name(n, name) when is_binary(name),
do: %{n | restricted_package_name: name}
defp update_payload(notification, _key, value) when value == %{},
do: notification
defp update_payload(notification, key, value) do
payload =
notification.payload
|> Map.put(key, value)
%{notification | payload: payload}
end
@doc ~S"""
Returns a list of successful registration IDs
## Examples
iex> n = %Pigeon.FCM.Notification{response: [
...> {:success, "regid1"}, {:invalid_registration, "regid2"},
...> {:success, "regid3"}, {:update, {"regid4", "new_regid4"}},
...> {:not_registered, "regid5"}, {:unavailable, "regid6"}]}
iex> success?(n)
["regid1", "regid3"]
"""
def success?(%Notification{response: response}) do
Keyword.get_values(response, :success)
end
@doc ~S"""
Returns a list of registration IDs and their corresponding new ID
## Examples
iex> n = %Pigeon.FCM.Notification{response: [
...> {:success, "regid1"}, {:invalid_registration, "regid2"},
...> {:success, "regid3"}, {:update, {"regid4", "new_regid4"}},
...> {:not_registered, "regid5"}, {:unavailable, "regid6"}]}
iex> update?(n)
[{"regid4", "new_regid4"}]
"""
def update?(%Notification{response: response}) do
Keyword.get_values(response, :update)
end
@doc ~S"""
Returns a list of registration IDs that should be retried
## Examples
iex> n = %Pigeon.FCM.Notification{response: [
...> {:success, "regid1"}, {:invalid_registration, "regid2"},
...> {:success, "regid3"}, {:update, {"regid4", "new_regid4"}},
...> {:not_registered, "regid5"}, {:unavailable, "regid6"}]}
iex> retry?(n)
["regid6"]
"""
def retry?(%{response: response}) do
Keyword.get_values(response, :unavailable)
end
@doc ~S"""
Returns a list of registration IDs that should be removed
## Examples
iex> n = %Pigeon.FCM.Notification{response: [
...> {:success, "regid1"}, {:invalid_registration, "regid2"},
...> {:success, "regid3"}, {:update, {"regid4", "new_regid4"}},
...> {:not_registered, "regid5"}, {:unavailable, "regid6"}]}
iex> remove?(n)
["regid2", "regid5"]
"""
def remove?(%{response: response}) do
response
|> Enum.filter(fn {k, _v} ->
k == :invalid_registration || k == :not_registered
end)
|> Keyword.values()
end
end
defimpl Pigeon.Encodable, for: Pigeon.FCM.Notification do
def binary_payload(notif) do
encode_requests(notif)
end
@doc false
def encode_requests(%{registration_id: regid} = notif)
when is_binary(regid) do
encode_requests(%{notif | registration_id: [regid]})
end
def encode_requests(%{registration_id: regid} = notif) when is_list(regid) do
regid
|> recipient_attr()
|> Map.merge(notif.payload)
|> encode_attr("priority", to_string(notif.priority))
|> encode_attr("time_to_live", notif.time_to_live)
|> encode_attr("collapse_key", notif.collapse_key)
|> encode_attr("restricted_package_name", notif.restricted_package_name)
|> encode_attr("dry_run", notif.dry_run)
|> Poison.encode!()
end
defp encode_attr(map, _key, nil), do: map
defp encode_attr(map, key, val) do
Map.put(map, key, val)
end
defp recipient_attr([regid]), do: %{"to" => regid}
defp recipient_attr(regid) when is_list(regid),
do: %{"registration_ids" => regid}
end