Current section

Files

Jump to
pigeon lib pigeon fcm.ex
Raw

lib/pigeon/fcm.ex

defmodule Pigeon.FCM do
@moduledoc """
Firebase Cloud Messaging (FCM)
"""
require Logger
import Supervisor.Spec
alias Pigeon.FCM.{Config, Notification}
alias Pigeon.Worker
@typedoc ~S"""
Async callback for push notification response.
## Examples
handler = fn(n) ->
case n.status do
:success ->
bad_regids = FCM.Notification.remove?(n)
to_retry = FCM.Notification.retry?(n)
# Handle updated regids, remove bad ones, etc
:unauthorized ->
# Bad FCM key
error ->
# Some other error
end
end
n = Pigeon.FCM.Notification.new("device token", %{}, %{"message" => "test"})
Pigeon.FCM.push(n, on_response: handler)
"""
@type on_response :: ((Notification.t) -> no_return)
@typedoc ~S"""
Options for sending push notifications.
- `:to` - Defines worker to process push. Defaults to `:fcm_default`
- `:on_response` - Optional async callback triggered on receipt of push.
See `t:on_response/0`
- `:timeout` - Specifies timeout for push responses. Useful if sending large
batches synchronously.
"""
@type push_opts :: [
to: atom | pid | nil,
timeout: pos_integer | nil,
on_response: on_response | nil
]
@default_timeout 5_000
@default_worker :fcm_default
@doc ~S"""
Sends a push over FCM.
## Examples
iex> n = Pigeon.FCM.Notification.new("regId", %{}, %{"message" => "123"})
iex> Pigeon.FCM.push(n)
%Pigeon.FCM.Notification{message_id: nil,
payload: %{"data" => %{"message" => "123"}}, priority: :normal,
registration_id: "regId", status: :success, response:
[invalid_registration: "regId"]}
iex> n = Pigeon.FCM.Notification.new("regId", %{}, %{"message" => "123"})
iex> Pigeon.FCM.push(n, on_response: nil)
:ok
iex> n = Pigeon.FCM.Notification.new(["regId", "regId"], %{},
...> %{"message" => "123"})
iex> Pigeon.FCM.push(n)
%Pigeon.FCM.Notification{message_id: nil,
payload: %{"data" => %{"message" => "123"}}, priority: :normal,
registration_id: ["regId", "regId"], status: :success,
response: [invalid_registration: "regId",
invalid_registration: "regId"]}
iex> n = Pigeon.FCM.Notification.new(["regId", "regId"], %{},
...> %{"message" => "test"})
iex> notifs = Pigeon.FCM.push([n, n])
iex> Enum.map(notifs, & &1.response)
[[invalid_registration: "regId", invalid_registration: "regId"],
[invalid_registration: "regId", invalid_registration: "regId"]]
"""
@spec push(Notification.t, Keyword.t) :: Notification.t
@spec push([Notification.t, ...], Keyword.t) :: [Notification.t, ...]
def push(notification, opts \\ [])
def push(notification, opts) when is_list(notification) do
timeout = Keyword.get(opts, :timeout, @default_timeout)
if Keyword.has_key?(opts, :on_response) do
for n <- notification, do: send_push(n, opts[:on_response], opts)
:ok
else
notification
|> Enum.map(& Task.async(fn -> sync_push(&1, opts) end))
|> Task.yield_many(timeout)
|> Enum.map(&task_mapper(&1))
end
end
def push(notification, opts) do
if Keyword.has_key?(opts, :on_response) do
send_push(notification, opts[:on_response], opts)
:ok
else
sync_push(notification, opts)
end
end
defp task_mapper({task, result}) do
case result do
nil -> Task.shutdown(task, :brutal_kill)
{:ok, notif} -> notif
end
end
defp send_push(notifications, on_response, opts) when is_list(notifications) do
worker_name = opts[:to] || @default_worker
notifications
|> Enum.map(& cast_request(worker_name, &1, on_response, opts))
end
defp send_push(notification, on_response, opts) do
send_push([notification], on_response, opts)
end
defp cast_request(worker_name, request, on_response, opts) do
opts = Keyword.put(opts, :on_response, on_response)
Worker.send_push(worker_name, request, opts)
end
defp sync_push(notification, opts) do
timeout = Keyword.get(opts, :timeout, @default_timeout)
ref = :erlang.make_ref
pid = self()
on_response = fn(x) -> send pid, {ref, x} end
send_push(notification, on_response, opts)
receive do
{^ref, x} -> x
after
timeout -> %{notification | status: :timeout}
end
end
@doc ~S"""
Starts FCM worker connection with given config or name.
## Examples
iex> config = Pigeon.FCM.Config.new(:fcm_default)
iex> {:ok, pid} = Pigeon.FCM.start_connection(%{config | name: nil})
iex> Process.alive?(pid)
true
"""
def start_connection(opts \\ [])
def start_connection(name) when is_atom(name) do
worker = worker(Pigeon.Worker, [Config.new(name)], id: name)
Supervisor.start_child(:pigeon, worker)
end
def start_connection(%Config{} = config) do
Worker.start_link(config)
end
def start_connection(opts) do
opts
|> Config.new
|> start_connection()
end
@doc ~S"""
Stops existing FCM worker connection.
## Examples
iex> config = Pigeon.FCM.Config.new(:fcm_default)
iex> {:ok, pid} = Pigeon.FCM.start_connection(%{config | name: nil})
iex> Pigeon.FCM.stop_connection(pid)
:ok
iex> :timer.sleep(500)
iex> Process.alive?(pid)
false
"""
@spec stop_connection(atom | pid) :: :ok
def stop_connection(name), do: Worker.stop_connection(name)
end