tusker_push/lib/tusker_push/apns.ex

146 lines
4.3 KiB
Elixir

defmodule TuskerPush.Apns do
require OpenTelemetry.Tracer
require Logger
@type environment() :: :development | :production
@spec send(environment(), String.t(), Map.t()) :: :ok | {:error, term()}
def send(apns_env, apns_device_token, payload) do
OpenTelemetry.Tracer.with_span :apns_push, %{attributes: [env: apns_env]} do
do_send(apns_env, apns_device_token, payload)
end
end
@spec do_send(environment(), String.t(), Map.t()) :: :ok | {:error, term()}
defp do_send(apns_env, apns_device_token, payload) do
with {:ok, body} <- Jason.encode(payload, pretty_print: false),
req <- make_request(apns_env, apns_device_token, body),
{:ok, resp} <- Finch.request(req, TuskerPush.Finch) do
handle_response(resp, apns_env, apns_device_token, payload)
else
{:error, %Finch.Error{reason: :connection_closed}} ->
Logger.warning("Apns Finch connection_closed, retrying in 1s")
OpenTelemetry.Tracer.add_event("Finch connection_closed", [])
Process.sleep(1000)
__MODULE__.send(apns_env, apns_device_token, payload)
{:error, %Finch.Error{reason: :disconnected}} ->
Logger.warning("Apns Finch disconnected, retrying in 1s")
OpenTelemetry.Tracer.add_event("Finch disconnected", [])
Process.sleep(1000)
__MODULE__.send(apns_env, apns_device_token, payload)
{:error, %Mint.TransportError{reason: :closed}} ->
Logger.warning("Apns Mint transport closed, retrying in 1s")
OpenTelemetry.Tracer.add_event("Mint transport closed", [])
Process.sleep(1000)
__MODULE__.send(apns_env, apns_device_token, payload)
{:error, reason} ->
{:error, reason}
end
end
@spec make_request(environment(), String.t(), binary()) :: Finch.Request.t()
defp make_request(apns_env, apns_device_token, body) do
bundle_id = Application.fetch_env!(:tusker_push, :apns)[:bundle_id]
headers = [
{"authorization", "bearer #{TuskerPush.Apns.Token.current()}"},
{"apns-push-type", "alert"},
{"apns-bundle-id", bundle_id},
{"apns-topic", bundle_id},
{"apns-expiration",
DateTime.utc_now() |> DateTime.add(1, :day) |> DateTime.to_unix() |> Integer.to_string()}
]
Finch.build(
:post,
"https://#{host(apns_env)}/3/device/#{apns_device_token}",
headers,
body
)
end
@spec handle_response(Finch.Response.t(), environment(), String.t(), Map.t()) ::
:ok | {:error, term()}
defp handle_response(resp, apns_env, apns_device_token, payload) do
maybe_log_unique_id(resp, apns_env)
if resp.status in 200..299 do
:ok
else
info =
case Jason.decode(resp.body) do
{:ok, data} ->
inspect(data)
{:error, _} ->
resp.body
end
case {resp.status, info} do
{403, %{reason: "ExpiredProviderToken"}} ->
Logger.warning("Expired provider token, retrying")
OpenTelemetry.Tracer.add_event("Expired provider token", [])
__MODULE__.send(apns_env, apns_device_token, payload)
{410, %{reason: "Unregistered"}} ->
Logger.warning("Device token unregistered")
OpenTelemetry.Tracer.set_status(:error, "Unregistered device token")
{:error, :device_token_unregistered}
_ ->
Logger.error("Received #{resp.status} with #{inspect(info)}")
OpenTelemetry.Tracer.set_attributes(
response_status: resp.status,
response_info: inspect(info)
)
OpenTelemetry.Tracer.set_status(:error, "Unexpected response")
{:error, "unexpected status #{resp.status}"}
end
end
end
@spec maybe_log_unique_id(Finch.Response.t(), :development | :production) :: :ok
defp maybe_log_unique_id(resp, :development) do
resp.headers
|> Enum.find(fn
{"apns-unique-id", _} -> true
_ -> false
end)
|> case do
{_, id} ->
Logger.debug("APNS unique id: #{id}")
OpenTelemetry.Tracer.add_event("APNS unique ID", unique_id: id)
_ ->
nil
end
:ok
end
defp maybe_log_unique_id(_resp, :production), do: :ok
@spec host(environment()) :: String.t()
def host(:development) do
"api.sandbox.push.apple.com"
end
def host(:production) do
"api.push.apple.com"
end
end