who_need_help/lib/who_need_help/push.ex

169 lines
5.1 KiB
Elixir

defmodule WhoNeedHelp.Push do
@moduledoc """
Durable product-event enqueueing for remote notifications.
Registered browser and Android devices use direct Web Push and FCM delivery.
An optional provider-neutral HTTP gateway remains available for deployments
that resolve a stable user recipient outside the application. Every remote
channel stays inactive until its complete provider configuration exists.
"""
alias WhoNeedHelp.Notifications
alias WhoNeedHelp.Notifications.{Notification, PushDevice}
alias WhoNeedHelp.Push.DeliveryWorker
@type notification :: %{
required(:idempotency_key) => String.t(),
required(:recipient) => String.t(),
required(:title) => String.t(),
required(:body) => String.t(),
optional(:data) => map()
}
@type receipt :: %{id: String.t(), duplicate: boolean()}
@type delivery_error :: :disabled | {:invalid_notification, atom()} | term()
@callback deliver(notification(), keyword()) ::
{:ok, receipt()} | {:error, delivery_error()}
def enabled?, do: Application.get_env(:who_need_help, :push_product_enabled, false) == true
def supervisor_children do
case Application.get_env(:who_need_help, :fcm_goth_source) do
nil -> []
source -> [{Goth, name: WhoNeedHelp.Goth, source: source}]
end
end
def enqueue_request_accepted(assignment_id, request_id, requester_id) do
Notifications.notify_user(requester_id, %{
kind: :request_accepted,
title: "A helper responded",
body: "Open Who Need Help to see the request update.",
path: "/requests/#{request_id}",
idempotency_key: "request-accepted:#{assignment_id}:#{requester_id}",
data: %{
"kind" => "request_accepted",
"assignment_id" => assignment_id,
"request_id" => request_id
}
})
end
def enqueue_message_created(message_id, assignment_id, request_id, recipient_id) do
Notifications.notify_user(recipient_id, %{
kind: :message_created,
title: "New message",
body: "Open Who Need Help to read the conversation.",
path: "/requests/#{request_id}#messages",
idempotency_key: "message-created:#{message_id}:#{recipient_id}",
data: %{
"kind" => "message_created",
"assignment_id" => assignment_id,
"message_id" => message_id,
"request_id" => request_id
}
})
end
def enqueue_lifecycle(kind, event_id, request_id, recipient_id, title, body, data \\ %{})
when is_atom(kind) do
Notifications.notify_user(recipient_id, %{
kind: kind,
title: title,
body: body,
path: "/requests/#{request_id}",
idempotency_key: "#{kind}:#{event_id}:#{recipient_id}",
data:
Map.merge(
%{"kind" => to_string(kind), "request_id" => request_id},
Map.new(data, fn {key, value} -> {to_string(key), value} end)
)
})
end
def enqueue(notification) do
if enabled?() do
notification
|> DeliveryWorker.new()
|> Oban.insert()
else
{:ok, :disabled}
end
end
def deliver(notification, opts \\ []) do
adapter =
Keyword.get_lazy(opts, :adapter, fn ->
Application.get_env(
:who_need_help,
:push_adapter,
WhoNeedHelp.Push.DisabledAdapter
)
end)
with :ok <- validate(notification) do
adapter.deliver(notification, Keyword.delete(opts, :adapter))
end
end
def delivery_options do
Application.get_env(:who_need_help, :push_delivery_options, [])
end
def gateway_enabled?, do: enabled?()
def deliver_device(%Notification{} = notification, %PushDevice{} = device, opts \\ []) do
adapter =
Keyword.get_lazy(opts, :adapter, fn ->
configured_device_adapter(device.provider)
end)
adapter.deliver(notification, device, Keyword.delete(opts, :adapter))
end
def configured_device_adapter(:web_push) do
Application.get_env(
:who_need_help,
:web_push_adapter,
WhoNeedHelp.Push.WebPushAdapter
)
end
def configured_device_adapter(:fcm) do
Application.get_env(:who_need_help, :fcm_adapter, WhoNeedHelp.Push.FCMAdapter)
end
def device_delivery_options(provider) do
Application.get_env(:who_need_help, :device_delivery_options, %{})
|> Map.get(provider, [])
end
defp validate(%{
idempotency_key: idempotency_key,
recipient: recipient,
title: title,
body: body
})
when is_binary(idempotency_key) and idempotency_key != "" and
is_binary(recipient) and recipient != "" and
is_binary(title) and title != "" and is_binary(body) and body != "" do
:ok
end
defp validate(notification) when not is_map(notification),
do: {:error, {:invalid_notification, :not_a_map}}
defp validate(notification) do
required = [:idempotency_key, :recipient, :title, :body]
case Enum.find(required, fn key ->
value = Map.get(notification, key)
not (is_binary(value) and value != "")
end) do
nil -> :ok
key -> {:error, {:invalid_notification, key}}
end
end
end