112 lines
3.6 KiB
Elixir
112 lines
3.6 KiB
Elixir
defmodule WhoNeedHelp.Push.FCMAdapter do
|
|
@moduledoc false
|
|
@behaviour WhoNeedHelp.Push.DeviceAdapter
|
|
|
|
alias WhoNeedHelp.Notifications.{Notification, PushDevice}
|
|
|
|
@retryable_statuses [408, 425, 429, 500, 502, 503, 504]
|
|
|
|
@impl true
|
|
def deliver(%Notification{} = notification, %PushDevice{} = device, opts) do
|
|
with {:ok, project_id} <- fetch_binary(opts, :project_id),
|
|
{:ok, token} <- fetch_access_token(opts),
|
|
{:ok, endpoint} <- endpoint(opts, project_id) do
|
|
payload = payload(notification, device)
|
|
|
|
case Req.post(endpoint,
|
|
json: payload,
|
|
headers: [{"authorization", "Bearer " <> token}],
|
|
retry: false,
|
|
receive_timeout: Keyword.get(opts, :receive_timeout, 10_000),
|
|
connect_options: [timeout: Keyword.get(opts, :connect_timeout, 5_000)]
|
|
) do
|
|
{:ok, %Req.Response{status: status, body: %{"name" => name}}}
|
|
when status in 200..299 ->
|
|
{:ok, %{id: name, duplicate: false}}
|
|
|
|
{:ok, %Req.Response{status: status, body: body}} when status in @retryable_statuses ->
|
|
{:error, {:retryable, status, sanitize(body)}}
|
|
|
|
{:ok, %Req.Response{status: status, body: body}} ->
|
|
{:error, {:rejected, status, sanitize(body)}}
|
|
|
|
{:error, error} ->
|
|
{:error, {:transport, transport_reason(error)}}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def payload(%Notification{} = notification, %PushDevice{} = device) do
|
|
%{
|
|
"message" => %{
|
|
"fid" => device.token,
|
|
"data" =>
|
|
notification.data
|
|
|> stringify_values()
|
|
|> Map.put("path", notification.path)
|
|
|> Map.put("notification_id", notification.id)
|
|
|> Map.put("title", notification.title)
|
|
|> Map.put("body", notification.body),
|
|
"android" => %{
|
|
"priority" => priority(notification.kind),
|
|
"ttl" => "300s"
|
|
}
|
|
}
|
|
}
|
|
end
|
|
|
|
defp fetch_access_token(opts) do
|
|
case Keyword.get(opts, :access_token) do
|
|
token when is_binary(token) and token != "" ->
|
|
{:ok, token}
|
|
|
|
_missing ->
|
|
goth_name = Keyword.get(opts, :goth_name, WhoNeedHelp.Goth)
|
|
|
|
case Goth.fetch(goth_name) do
|
|
{:ok, %{token: token}} -> {:ok, token}
|
|
{:error, reason} -> {:error, {:oauth, reason}}
|
|
end
|
|
end
|
|
catch
|
|
:exit, reason -> {:error, {:oauth, reason}}
|
|
end
|
|
|
|
defp endpoint(opts, project_id) do
|
|
case Keyword.get(opts, :endpoint) do
|
|
nil -> {:ok, "https://fcm.googleapis.com/v1/projects/#{project_id}/messages:send"}
|
|
endpoint when is_binary(endpoint) and endpoint != "" -> {:ok, endpoint}
|
|
_invalid -> {:error, {:invalid_configuration, :endpoint}}
|
|
end
|
|
end
|
|
|
|
defp fetch_binary(opts, key) do
|
|
case Keyword.get(opts, key) do
|
|
value when is_binary(value) and value != "" -> {:ok, value}
|
|
_invalid -> {:error, {:invalid_configuration, key}}
|
|
end
|
|
end
|
|
|
|
defp stringify_values(data) do
|
|
Map.new(data, fn {key, value} -> {to_string(key), stringify_value(value)} end)
|
|
end
|
|
|
|
defp stringify_value(value) when is_binary(value), do: value
|
|
defp stringify_value(value) when is_atom(value) or is_number(value), do: to_string(value)
|
|
defp stringify_value(value), do: Jason.encode!(value)
|
|
|
|
defp priority(kind) when kind in [:nearby_request, :request_accepted, :message_created],
|
|
do: "high"
|
|
|
|
defp priority(_kind), do: "normal"
|
|
|
|
defp sanitize(%{"error" => error}) when is_map(error),
|
|
do: Map.take(error, ["code", "status"])
|
|
|
|
defp sanitize(_body), do: nil
|
|
|
|
defp transport_reason(%Req.TransportError{reason: reason}), do: reason
|
|
defp transport_reason(%{__struct__: module}), do: module
|
|
end
|