who_need_help/lib/who_need_help/push/fcm_adapter.ex

136 lines
4.2 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_error(body)}}
{:ok, %Req.Response{status: status, body: body}} ->
{:error, {:rejected, status, sanitize_error(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"
@doc false
def sanitize_error(%{"error" => error}) when is_map(error) do
fcm_error_code =
error
|> Map.get("details", [])
|> Enum.find_value(fn
%{
"@type" => "type.googleapis.com/google.firebase.fcm.v1.FcmError",
"errorCode" => error_code
}
when is_binary(error_code) ->
error_code
_detail ->
nil
end)
error
|> Map.take(["code", "status"])
|> maybe_put_fcm_error_code(fcm_error_code)
end
def sanitize_error(_body), do: nil
defp maybe_put_fcm_error_code(error, nil), do: error
defp maybe_put_fcm_error_code(error, fcm_error_code),
do: Map.put(error, "fcm_error_code", fcm_error_code)
defp transport_reason(%Req.TransportError{reason: reason}), do: reason
defp transport_reason(%{__struct__: module}), do: module
end