From ce1eff473f69bd38f750764d807423a3a915fbaf Mon Sep 17 00:00:00 2001 From: Drew Ballance Date: Wed, 1 Feb 2023 09:19:10 -0600 Subject: [PATCH 01/37] fix: move token generation to agent (#1) --- lib/pigeon/apns/jwt_config.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pigeon/apns/jwt_config.ex b/lib/pigeon/apns/jwt_config.ex index aac10ef9..91549af9 100644 --- a/lib/pigeon/apns/jwt_config.ex +++ b/lib/pigeon/apns/jwt_config.ex @@ -78,11 +78,11 @@ defmodule Pigeon.APNS.JWTConfig do ...> ping_period: 300_000 ...> ) %Pigeon.APNS.JWTConfig{ - uri: "api.push.apple.com", - team_id: "DEF1234567", - key_identifier: "ABC1234567", + uri: "api.push.apple.com", + team_id: "DEF1234567", + key_identifier: "ABC1234567", key: File.read!("test/support/FakeAPNSAuthKey.p8"), - ping_period: 300000, + ping_period: 300000, port: 2197 } """ From ae6112658d499fd651da265f0130a7b5faaf1831 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Fri, 17 Mar 2023 14:46:50 -0700 Subject: [PATCH 02/37] chore: undo random formatting changes that aren't necessary --- lib/pigeon/apns/jwt_config.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pigeon/apns/jwt_config.ex b/lib/pigeon/apns/jwt_config.ex index 91549af9..aac10ef9 100644 --- a/lib/pigeon/apns/jwt_config.ex +++ b/lib/pigeon/apns/jwt_config.ex @@ -78,11 +78,11 @@ defmodule Pigeon.APNS.JWTConfig do ...> ping_period: 300_000 ...> ) %Pigeon.APNS.JWTConfig{ - uri: "api.push.apple.com", - team_id: "DEF1234567", - key_identifier: "ABC1234567", + uri: "api.push.apple.com", + team_id: "DEF1234567", + key_identifier: "ABC1234567", key: File.read!("test/support/FakeAPNSAuthKey.p8"), - ping_period: 300000, + ping_period: 300000, port: 2197 } """ From 757e948cd7992890b7cfe06dd5a83614b8ee076e Mon Sep 17 00:00:00 2001 From: John Hossler Date: Sun, 19 Mar 2023 21:52:30 -0700 Subject: [PATCH 03/37] feat: implement a pushy push notification adapter --- lib/pigeon/pushy.ex | 105 ++++++++++++++++++++ lib/pigeon/pushy/config.ex | 162 +++++++++++++++++++++++++++++++ lib/pigeon/pushy/error.ex | 22 +++++ lib/pigeon/pushy/notification.ex | 75 ++++++++++++++ 4 files changed, 364 insertions(+) create mode 100644 lib/pigeon/pushy.ex create mode 100644 lib/pigeon/pushy/config.ex create mode 100644 lib/pigeon/pushy/error.ex create mode 100644 lib/pigeon/pushy/notification.ex diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex new file mode 100644 index 00000000..84d1e770 --- /dev/null +++ b/lib/pigeon/pushy.ex @@ -0,0 +1,105 @@ +defmodule Pigeon.Pushy do + @moduledoc """ + `Pigeon.Adapter` for Pushy pushy notifications + """ + + defstruct config: nil, + queue: Pigeon.NotificationQueue.new(), + refresh_before: 5 * 60, + socket: nil + + @behaviour Pigeon.Adapter + + alias Pigeon.{Configurable, NotificationQueue} + alias Pigeon.Http2.{Client, Stream} + + @impl true + def init(opts) do + config = Pigeon.Pushy.Config.new(opts) + Configurable.validate!(config) + + state = %__MODULE__{config: config} + + with {:ok, socket} <- connect_socket(config) do + Configurable.schedule_ping(config) + {:ok, %{state | socket: socket}} + else + {:error, reason} -> {:stop, reason} + end + end + + @impl true + def handle_push(notification, state) do + %{config: config, queue: queue} = state + headers = Configurable.push_headers(config, notification, []) + payload = Configurable.push_payload(config, notification, []) + + Client.default().send_request(state.socket, headers, payload) + + new_q = NotificationQueue.add(queue, state.stream_id, notification) + + state = + state + |> inc_stream_id() + |> Map.put(:queue, new_q) + + {:noreply, state} + end + + @impl true + def handle_info(:ping, state) do + Client.default().send_ping(state.socket) + Configurable.schedule_ping(state.config) + + {:noreply, state} + end + + def handle_info({:closed, _}, %{config: config} = state) do + case connect_socket(config) do + {:ok, socket} -> + Configurable.schedule_ping(config) + + state = + state + |> reset_stream_id() + |> Map.put(:socket, socket) + + {:noreply, state} + + {:error, reason} -> + {:stop, reason} + end + end + + def handle_info(msg, state) do + case Client.default().handle_end_stream(msg, state) do + {:ok, %Stream{} = stream} -> process_end_stream(stream, state) + _else -> {:noreply, state} + end + end + + @doc false + def process_end_stream(%Stream{id: stream_id} = stream, state) do + %{queue: queue, config: config} = state + + case NotificationQueue.pop(queue, stream_id) do + {nil, new_queue} -> + # Do nothing if no queued item for stream + {:noreply, %{state | queue: new_queue}} + + {notif, new_queue} -> + Configurable.handle_end_stream(config, stream, notif) + {:noreply, %{state | queue: new_queue}} + end + end + + @doc false + def inc_stream_id(%{stream_id: stream_id} = state) do + %{state | stream_id: stream_id + 2} + end + + @doc false + def reset_stream_id(state) do + %{state | stream_id: 1} + end +end diff --git a/lib/pigeon/pushy/config.ex b/lib/pigeon/pushy/config.ex new file mode 100644 index 00000000..f14a5125 --- /dev/null +++ b/lib/pigeon/pushy/config.ex @@ -0,0 +1,162 @@ +defmodule Pigeon.Pushy.Config do + @moduledoc false + + defstruct key: nil, + port: 443, + uri: nil + + @typedoc ~S""" + Pushy configuration struct + + This struct should not be set directly. Instead, use `new/1` + with `t:config_opts/0`. + + ## Examples + + %Pigeon.Pushy.Config{ + key: "some-secret-key", + uri: "api.pushy.me", + port: 443 + } + """ + @type t :: %__MODULE__{ + key: binary | nil, + uri: binary | nil, + port: pos_integer + } + + @typedoc ~S""" + Options for configuring Pushy connections. + + ## Configuration Options + - `:key` - Pushy secrety key. + - `:uri` - Pushy server uri. + - `:port` - Push server port. Can be any value, but Pushy only accepts + `443` + """ + @type config_opts :: [ + key: binary, + uri: binary, + port: pos_integer + ] + + @doc false + def default_name, do: :pushy_default + + @doc ~S""" + Returns a new `Pushy.Config` with given `opts`. + + ## Examples + + iex> Pigeon.Pushy.Config.new( + ...> key: System.get_env("PUSHY_SECRET_KEY"), + ...> uri: "api.pushy.me", + ...> port: 443 + ...> ) + %Pigeon.Pushy.Config{ + key: System.get_env("PUSHY_SECRET_KEY") + port: 443, + uri: "api.pushy.me" + } + """ + def new(opts) when is_list(opts) do + %__MODULE__{ + key: opts |> Keyword.get(:key), + uri: Keyword.get(opts, :uri, 'api.pushy.me'), + port: Keyword.get(opts, :port, 443) + } + end +end + +defimpl Pigeon.Configurable, for: Pigeon.Pushy.Config do + @moduledoc false + + require Logger + + import Pigeon.Tasks, only: [process_on_response: 1] + + alias Pigeon.Encodable + alias Pigeon.Pushy.{Config, Error} + + @type sock :: {:sslsocket, any, pid | {any, any}} + + # Configurable Callbacks + + @spec connect(any) :: {:ok, sock} | {:error, String.t()} + def connect(%Config{uri: uri} = config) do + case connect_socket_options(config) do + {:ok, options} -> + Pigeon.Http2.Client.default().connect(uri, :https, options) + end + end + + def connect_socket_options(config) do + opts = + [ + {:active, :once}, + {:packet, :raw}, + {:reuseaddr, true}, + {:alpn_advertised_protocols, [<<"h2">>]}, + :binary + ] + |> add_port(config) + + {:ok, opts} + end + + def add_port(opts, %Config{port: 443}), do: opts + def add_port(opts, %Config{port: port}), do: [{:port, port} | opts] + + def push_headers( + %Config{key: key}, + _notification, + _opts + ) do + [ + {":method", "POST"}, + {":path", "/push/?api_key=#{key}"}, + {"content-type", "application/json"}, + {"accept", "application/json"} + ] + end + + def push_payload(_config, notification, _opts) do + Encodable.binary_payload(notification) + end + + def handle_end_stream(_config, %{error: nil} = stream, notif) do + stream.body + |> Pigeon.json_library().decode!() + |> case do + %{"name" => name} -> + notif + |> Map.put(:name, name) + |> Map.put(:response, :success) + |> process_on_response() + + %{"error" => error} -> + notif + |> Map.put(:error, error) + |> Map.put(:response, Error.parse(error)) + |> process_on_response() + end + end + + def schedule_ping(_config), do: :ok + + def close(_config) do + end + + def validate!(_config), do: :ok + + @doc false + def redact(config) when is_map(config) do + [:key] + |> Enum.reduce(config, fn key, acc -> + case Map.get(acc, key) do + val when is_map(val) -> Map.put(acc, key, "[FILTERED]") + _ -> acc + end + end) + end +end diff --git a/lib/pigeon/pushy/error.ex b/lib/pigeon/pushy/error.ex new file mode 100644 index 00000000..eb3dea76 --- /dev/null +++ b/lib/pigeon/pushy/error.ex @@ -0,0 +1,22 @@ +defmodule Pigeon.Pushy.Error do + @moduledoc false + + @doc false + @spec parse(map) :: atom() + def parse(error) do + error + |> Map.get("status") + |> parse_response() + end + + defp parse_response("NO_RECIPIENTS"), do: :no_recipients + defp parse_response("NO_APNS_AUTH"), do: :no_apns_auth + defp parse_response("PAYLOAD_LIMIT_EXCEEDED"), do: :payload_limit_exceeded + defp parse_response("INVALID_PARAM"), do: :invalid_param + defp parse_response("INVALID_API_KEY"), do: :invalid_api_key + defp parse_response("AUTH_LIMIT_EXCEEDED"), do: :auth_limit_exceeded + defp parse_response("ACCOUNT_SUSPENDED"), do: :account_suspended + defp parse_response("RATE_LIMIT_EXCEEDED"), do: :rate_limit_exceeded + defp parse_response("INTERNAL_SERVER_ERROR"), do: :internal_server_error + defp parse_response(_), do: :unknown_error +end diff --git a/lib/pigeon/pushy/notification.ex b/lib/pigeon/pushy/notification.ex new file mode 100644 index 00000000..fe877eb4 --- /dev/null +++ b/lib/pigeon/pushy/notification.ex @@ -0,0 +1,75 @@ +defmodule Pigeon.Pushy.Notification do + @moduledoc """ + Defines Pushy notification struct and convenience constructor functions. + """ + + defstruct __meta__: %Pigeon.Metadata{}, + to: "", + data: %{}, + time_to_live: nil, + content_available: nil, + mutable_content: nil, + notification: nil, + schedule: nil, + collapse_key: nil, + response: nil + + @type t :: %__MODULE__{ + __meta__: Pigeon.Metadata.t(), + to: String.t() | [String.t()], + data: map, + time_to_live: integer | nil, + content_available: boolean | nil, + mutable_content: boolean | nil, + notification: map | nil, + schedule: integer | nil, + collapse_key: String.t() | nil, + response: atom | nil + } + + @type error_response :: + :no_recipients + | :no_apns_auth + | :payload_limit_exceeded + | :invalid_param + | :invalid_api_key + | :auth_limit_exceeded + | :account_suspended + | :rate_limit_exceeded + | :internal_server_error + | :unknown_error +end + +defimpl Pigeon.Encodable, for: Pigeon.Pushy.Notification do + def binary_payload(notif) do + encode_requests(notif) + end + + @doc false + def encode_requests(notif) do + %{} + |> encode_to(notif.to) + |> encode_data(notif.data) + |> maybe_encode_attr("time_to_live", notif.time_to_live) + |> maybe_encode_attr("content_available", notif.content_available) + |> maybe_encode_attr("mutable_content", notif.mutable_content) + |> maybe_encode_attr("notification", notif.notification) + |> maybe_encode_attr("schedule", notif.schedule) + |> maybe_encode_attr("collapse_key", notif.collapse_key) + |> Pigeon.json_library().encode!() + end + + defp encode_to(map, value) do + Map.put(map, "to", value) + end + + defp encode_data(map, value) do + Map.put(map, "data", value) + end + + defp maybe_encode_attr(map, _key, nil), do: map + + defp maybe_encode_attr(map, key, val) do + Map.put(map, key, val) + end +end From bb265f93c66c7f8164febd5facd14561348b9488 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 09:30:46 -0700 Subject: [PATCH 04/37] fix: implement connect_socket method --- lib/pigeon/pushy.ex | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 84d1e770..6c6d2427 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -78,6 +78,17 @@ defmodule Pigeon.Pushy do end end + defp connect_socket(config), do: connect_socket(config, 0) + + defp connect_socket(_config, 3), do: {:error, :timeout} + + defp connect_socket(config, tries) do + case Configurable.connect(config) do + {:ok, socket} -> {:ok, socket} + {:error, _reason} -> connect_socket(config, tries + 1) + end + end + @doc false def process_end_stream(%Stream{id: stream_id} = stream, state) do %{queue: queue, config: config} = state From 2aa8eee1ec239e3688eac2ee2f9bcff8b9a87f7c Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 09:59:57 -0700 Subject: [PATCH 05/37] feat: add pushy notification constructor --- lib/pigeon/pushy/notification.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/pigeon/pushy/notification.ex b/lib/pigeon/pushy/notification.ex index fe877eb4..7682fb0f 100644 --- a/lib/pigeon/pushy/notification.ex +++ b/lib/pigeon/pushy/notification.ex @@ -38,6 +38,14 @@ defmodule Pigeon.Pushy.Notification do | :rate_limit_exceeded | :internal_server_error | :unknown_error + + @new(map, String.t() | [String.t()]) + def new(message, device_ids) do + %__MODULE__{ + to: device_ids, + data: message + } + end end defimpl Pigeon.Encodable, for: Pigeon.Pushy.Notification do From 4c3abe21387a8dd77486506277fb92c4dcfe7a35 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 10:07:34 -0700 Subject: [PATCH 06/37] fix: do @spec new instead of @new smh --- lib/pigeon/pushy/notification.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pigeon/pushy/notification.ex b/lib/pigeon/pushy/notification.ex index 7682fb0f..f73f1039 100644 --- a/lib/pigeon/pushy/notification.ex +++ b/lib/pigeon/pushy/notification.ex @@ -39,7 +39,7 @@ defmodule Pigeon.Pushy.Notification do | :internal_server_error | :unknown_error - @new(map, String.t() | [String.t()]) + @spec new(map, String.t() | [String.t()]) def new(message, device_ids) do %__MODULE__{ to: device_ids, From 917711f675385e9c6149254fc0ffb6818c6fad8c Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 10:09:30 -0700 Subject: [PATCH 07/37] fix: add return type to new spec --- lib/pigeon/pushy/notification.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pigeon/pushy/notification.ex b/lib/pigeon/pushy/notification.ex index f73f1039..cd4429a7 100644 --- a/lib/pigeon/pushy/notification.ex +++ b/lib/pigeon/pushy/notification.ex @@ -39,7 +39,7 @@ defmodule Pigeon.Pushy.Notification do | :internal_server_error | :unknown_error - @spec new(map, String.t() | [String.t()]) + @spec new(map, String.t() | [String.t()]) :: __MODULE__.t() def new(message, device_ids) do %__MODULE__{ to: device_ids, From b0f48c6fe8974cd3f5a768cc912149be639a697e Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 12:12:44 -0700 Subject: [PATCH 08/37] fix: update connect parameters and add docs link in mix --- lib/pigeon/pushy/config.ex | 3 +-- mix.exs | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pigeon/pushy/config.ex b/lib/pigeon/pushy/config.ex index f14a5125..a6d2b5c3 100644 --- a/lib/pigeon/pushy/config.ex +++ b/lib/pigeon/pushy/config.ex @@ -93,10 +93,9 @@ defimpl Pigeon.Configurable, for: Pigeon.Pushy.Config do def connect_socket_options(config) do opts = [ - {:active, :once}, + {:active, true}, {:packet, :raw}, {:reuseaddr, true}, - {:alpn_advertised_protocols, [<<"h2">>]}, :binary ] |> add_port(config) diff --git a/mix.exs b/mix.exs index 02888a32..00294f28 100644 --- a/mix.exs +++ b/mix.exs @@ -66,7 +66,8 @@ defmodule Pigeon.Mixfile do Pigeon.FCM.Notification, Pigeon.LegacyFCM, Pigeon.LegacyFCM.Notification - ] + ], + "Pushy": [Pigeon.Pushy, Pigeon.Pushy.Notification] ], main: "Pigeon" ] From fa4b59db026d364aa6bf957dfa04ac8ad8e4204e Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 12:16:31 -0700 Subject: [PATCH 09/37] debug: add error log so I can tell why connection to pushy isn't working --- lib/pigeon/pushy.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 6c6d2427..57cf525d 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -85,7 +85,9 @@ defmodule Pigeon.Pushy do defp connect_socket(config, tries) do case Configurable.connect(config) do {:ok, socket} -> {:ok, socket} - {:error, _reason} -> connect_socket(config, tries + 1) + {:error, reason} -> + Logger.error("Could not establish connection to push: #{IO.inspect(reason)}") + connect_socket(config, tries + 1) end end From addf2ae21493744b9b85fa86196c87f2eefe9e02 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 12:29:29 -0700 Subject: [PATCH 10/37] fix: require logger in pushy dispatcher --- lib/pigeon/pushy.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 57cf525d..408eccab 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -2,6 +2,7 @@ defmodule Pigeon.Pushy do @moduledoc """ `Pigeon.Adapter` for Pushy pushy notifications """ + require Logger defstruct config: nil, queue: Pigeon.NotificationQueue.new(), From f4c279c4d8000680d47d5bb5fb07172544604f97 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 12:48:55 -0700 Subject: [PATCH 11/37] fix: do a regular inspect of the error instead when connecting to pushy --- lib/pigeon/pushy.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 408eccab..cf015e8b 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -87,7 +87,7 @@ defmodule Pigeon.Pushy do case Configurable.connect(config) do {:ok, socket} -> {:ok, socket} {:error, reason} -> - Logger.error("Could not establish connection to push: #{IO.inspect(reason)}") + Logger.error("Could not establish connection to push: #{inspect(reason)}") connect_socket(config, tries + 1) end end From a39addeadcf61eda22dc85ae7be3bc8b9830f537 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 14:42:56 -0700 Subject: [PATCH 12/37] fix: adjust socket connection settings to attempt to resolve handshake failure --- lib/pigeon/pushy/config.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pigeon/pushy/config.ex b/lib/pigeon/pushy/config.ex index a6d2b5c3..eba26e22 100644 --- a/lib/pigeon/pushy/config.ex +++ b/lib/pigeon/pushy/config.ex @@ -93,7 +93,7 @@ defimpl Pigeon.Configurable, for: Pigeon.Pushy.Config do def connect_socket_options(config) do opts = [ - {:active, true}, + {:active, :once}, {:packet, :raw}, {:reuseaddr, true}, :binary @@ -103,7 +103,6 @@ defimpl Pigeon.Configurable, for: Pigeon.Pushy.Config do {:ok, opts} end - def add_port(opts, %Config{port: 443}), do: opts def add_port(opts, %Config{port: port}), do: [{:port, port} | opts] def push_headers( From ff3ed8a98b4b61170838e786ad04f29166e63958 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 15:41:43 -0700 Subject: [PATCH 13/37] refactor: don't use socket when connecting to pushy, just make http requests since ssl cert was throwing --- lib/pigeon/pushy.ex | 132 ++++++++++++++++--------------------- lib/pigeon/pushy/config.ex | 91 ------------------------- 2 files changed, 58 insertions(+), 165 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index cf015e8b..99e1fc3a 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -2,17 +2,14 @@ defmodule Pigeon.Pushy do @moduledoc """ `Pigeon.Adapter` for Pushy pushy notifications """ + import Pigeon.Tasks, only: [process_on_response: 1] require Logger - defstruct config: nil, - queue: Pigeon.NotificationQueue.new(), - refresh_before: 5 * 60, - socket: nil + alias Pigeon.Pushy.Error - @behaviour Pigeon.Adapter + defstruct config: nil - alias Pigeon.{Configurable, NotificationQueue} - alias Pigeon.Http2.{Client, Stream} + @behaviour Pigeon.Adapter @impl true def init(opts) do @@ -21,99 +18,86 @@ defmodule Pigeon.Pushy do state = %__MODULE__{config: config} - with {:ok, socket} <- connect_socket(config) do - Configurable.schedule_ping(config) - {:ok, %{state | socket: socket}} - else - {:error, reason} -> {:stop, reason} - end + {:ok, state} end @impl true def handle_push(notification, state) do - %{config: config, queue: queue} = state - headers = Configurable.push_headers(config, notification, []) - payload = Configurable.push_payload(config, notification, []) - - Client.default().send_request(state.socket, headers, payload) - - new_q = NotificationQueue.add(queue, state.stream_id, notification) - - state = - state - |> inc_stream_id() - |> Map.put(:queue, new_q) + %{config: config} = state + :ok = do_push(notification, state) {:noreply, state} end @impl true - def handle_info(:ping, state) do - Client.default().send_ping(state.socket) - Configurable.schedule_ping(state.config) - + def handle_info({_from, {:ok, %HTTPoison.Response{status_code: 200}}}, state) do {:noreply, state} end - def handle_info({:closed, _}, %{config: config} = state) do - case connect_socket(config) do - {:ok, socket} -> - Configurable.schedule_ping(config) + def handle_info(_msg, state) do + {:noreply, state} + end - state = - state - |> reset_stream_id() - |> Map.put(:socket, socket) + defp do_push(notification, state) do + encoded_notification = encode_payload(notification) - {:noreply, state} + response = fn notification -> + case HTTPoison.post(pushy_uri(state.config), notification, pushy_headers(state)) do + {:ok, %HTTPoison.Response{status_code: status, body: body}} -> + process_response(status, body, notification) - {:error, reason} -> - {:stop, reason} + {:error, %HTTPoison.Error{reason: :connect_timeout}} -> + notification + |> Map.put(:response, :timeout) + |> process_on_response() + end end - end - def handle_info(msg, state) do - case Client.default().handle_end_stream(msg, state) do - {:ok, %Stream{} = stream} -> process_end_stream(stream, state) - _else -> {:noreply, state} - end + Task.Supervisor.start_child(Pigeon.Tasks, fn -> response.(encoded_notification) end) + :ok end - defp connect_socket(config), do: connect_socket(config, 0) - - defp connect_socket(_config, 3), do: {:error, :timeout} + defp pushy_uri(%Pigeon.Pushy.Config{uri: base_uri, key: secret_key}) do + "https://#{base_uri}/pushy/?api_key=#{secret_key}" + end - defp connect_socket(config, tries) do - case Configurable.connect(config) do - {:ok, socket} -> {:ok, socket} - {:error, reason} -> - Logger.error("Could not establish connection to push: #{inspect(reason)}") - connect_socket(config, tries + 1) - end + def pushy_headers() do + [ + {"Content-Type", "application/json"}, + {"Accept", "application/json"} + ] end - @doc false - def process_end_stream(%Stream{id: stream_id} = stream, state) do - %{queue: queue, config: config} = state + defp process_response(200, body, notification), + do: handle_200_status(body, notification) - case NotificationQueue.pop(queue, stream_id) do - {nil, new_queue} -> - # Do nothing if no queued item for stream - {:noreply, %{state | queue: new_queue}} + defp process_response(status, body, notification), + do: handle_error_status_code(status, body, notification) - {notif, new_queue} -> - Configurable.handle_end_stream(config, stream, notif) - {:noreply, %{state | queue: new_queue}} - end - end + defp handle_200_status(body, notification) do + {:ok, json} = Pigeon.json_library().decode(body) - @doc false - def inc_stream_id(%{stream_id: stream_id} = state) do - %{state | stream_id: stream_id + 2} + notification + |> Error.parse(json) + |> process_on_response() end - @doc false - def reset_stream_id(state) do - %{state | stream_id: 1} + defp handle_error_status_code(status, body, notification) do + case Pigeon.json_library().decode(body) do + {:ok, %{"reason" => _reason} = result_json} -> + notification + |> Error.parse(result_json) + |> process_on_response() + + {:error, _} -> + notification + |> Map.put(:response, generic_error_reason(status)) + |> process_on_response() + end end + + defp generic_error_reason(400), do: :invalid_json + defp generic_error_reason(401), do: :authentication_error + defp generic_error_reason(500), do: :internal_server_error + defp generic_error_reason(_), do: :unknown_error end diff --git a/lib/pigeon/pushy/config.ex b/lib/pigeon/pushy/config.ex index eba26e22..2f7dc1cf 100644 --- a/lib/pigeon/pushy/config.ex +++ b/lib/pigeon/pushy/config.ex @@ -67,94 +67,3 @@ defmodule Pigeon.Pushy.Config do } end end - -defimpl Pigeon.Configurable, for: Pigeon.Pushy.Config do - @moduledoc false - - require Logger - - import Pigeon.Tasks, only: [process_on_response: 1] - - alias Pigeon.Encodable - alias Pigeon.Pushy.{Config, Error} - - @type sock :: {:sslsocket, any, pid | {any, any}} - - # Configurable Callbacks - - @spec connect(any) :: {:ok, sock} | {:error, String.t()} - def connect(%Config{uri: uri} = config) do - case connect_socket_options(config) do - {:ok, options} -> - Pigeon.Http2.Client.default().connect(uri, :https, options) - end - end - - def connect_socket_options(config) do - opts = - [ - {:active, :once}, - {:packet, :raw}, - {:reuseaddr, true}, - :binary - ] - |> add_port(config) - - {:ok, opts} - end - - def add_port(opts, %Config{port: port}), do: [{:port, port} | opts] - - def push_headers( - %Config{key: key}, - _notification, - _opts - ) do - [ - {":method", "POST"}, - {":path", "/push/?api_key=#{key}"}, - {"content-type", "application/json"}, - {"accept", "application/json"} - ] - end - - def push_payload(_config, notification, _opts) do - Encodable.binary_payload(notification) - end - - def handle_end_stream(_config, %{error: nil} = stream, notif) do - stream.body - |> Pigeon.json_library().decode!() - |> case do - %{"name" => name} -> - notif - |> Map.put(:name, name) - |> Map.put(:response, :success) - |> process_on_response() - - %{"error" => error} -> - notif - |> Map.put(:error, error) - |> Map.put(:response, Error.parse(error)) - |> process_on_response() - end - end - - def schedule_ping(_config), do: :ok - - def close(_config) do - end - - def validate!(_config), do: :ok - - @doc false - def redact(config) when is_map(config) do - [:key] - |> Enum.reduce(config, fn key, acc -> - case Map.get(acc, key) do - val when is_map(val) -> Map.put(acc, key, "[FILTERED]") - _ -> acc - end - end) - end -end From 3c89181f74728cde297b13057a415be0f99a7003 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 15:44:14 -0700 Subject: [PATCH 14/37] fix: correctly call pushy_headers and remove unnecessary match --- lib/pigeon/pushy.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 99e1fc3a..a76643b7 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -23,8 +23,6 @@ defmodule Pigeon.Pushy do @impl true def handle_push(notification, state) do - %{config: config} = state - :ok = do_push(notification, state) {:noreply, state} end @@ -42,7 +40,7 @@ defmodule Pigeon.Pushy do encoded_notification = encode_payload(notification) response = fn notification -> - case HTTPoison.post(pushy_uri(state.config), notification, pushy_headers(state)) do + case HTTPoison.post(pushy_uri(state.config), notification, pushy_headers()) do {:ok, %HTTPoison.Response{status_code: status, body: body}} -> process_response(status, body, notification) From d725ba5608b5dab85a5d89e970593e6b0297d83c Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 15:47:58 -0700 Subject: [PATCH 15/37] fix: use notification encode_requests to encode the payload --- lib/pigeon/pushy.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index a76643b7..29aa9c23 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -37,7 +37,7 @@ defmodule Pigeon.Pushy do end defp do_push(notification, state) do - encoded_notification = encode_payload(notification) + encoded_notification = Pigeon.Encodable.encode_requests(notification) response = fn notification -> case HTTPoison.post(pushy_uri(state.config), notification, pushy_headers()) do From 73cea75faa9b6b7df8c2279b298823dc1e34823e Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 15:52:15 -0700 Subject: [PATCH 16/37] fix: move over encoding logic so I can encode properly --- lib/pigeon/pushy.ex | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 29aa9c23..fd7f38d8 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -5,7 +5,7 @@ defmodule Pigeon.Pushy do import Pigeon.Tasks, only: [process_on_response: 1] require Logger - alias Pigeon.Pushy.Error + alias Pigeon.Pushy.{Error, Notification defstruct config: nil @@ -14,7 +14,6 @@ defmodule Pigeon.Pushy do @impl true def init(opts) do config = Pigeon.Pushy.Config.new(opts) - Configurable.validate!(config) state = %__MODULE__{config: config} @@ -37,7 +36,7 @@ defmodule Pigeon.Pushy do end defp do_push(notification, state) do - encoded_notification = Pigeon.Encodable.encode_requests(notification) + encoded_notification = encode_requests(notification) response = fn notification -> case HTTPoison.post(pushy_uri(state.config), notification, pushy_headers()) do @@ -66,6 +65,32 @@ defmodule Pigeon.Pushy do ] end + defp encode_requests(notif) do + message = + %{} + |> encode_target(notif.target) + |> maybe_encode_attr("android", notif.android) + |> maybe_encode_attr("apns", notif.apns) + |> maybe_encode_attr("data", notif.data) + |> maybe_encode_attr("fcm_options", notif.fcm_options) + |> maybe_encode_attr("notification", notif.notification) + |> maybe_encode_attr("webpush", notif.webpush) + + %{"message" => message} + |> maybe_encode_attr("validate_only", notif.validate_only) + |> Pigeon.json_library().encode!() + end + + defp encode_target(map, {type, value}) do + Map.put(map, to_string(type), value) + end + + defp maybe_encode_attr(map, _key, nil), do: map + + defp maybe_encode_attr(map, key, val) do + Map.put(map, key, val) + end + defp process_response(200, body, notification), do: handle_200_status(body, notification) From e42caab5326242b4d65ce214214edfccb82f91e0 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 15:55:41 -0700 Subject: [PATCH 17/37] fix: remove unmatched curly brace and use correct encode request impl --- lib/pigeon/pushy.ex | 31 ++++++++++++++++--------------- mix.exs | 2 +- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index fd7f38d8..b4f2e00c 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -5,7 +5,7 @@ defmodule Pigeon.Pushy do import Pigeon.Tasks, only: [process_on_response: 1] require Logger - alias Pigeon.Pushy.{Error, Notification + alias Pigeon.Pushy.Error defstruct config: nil @@ -66,23 +66,24 @@ defmodule Pigeon.Pushy do end defp encode_requests(notif) do - message = - %{} - |> encode_target(notif.target) - |> maybe_encode_attr("android", notif.android) - |> maybe_encode_attr("apns", notif.apns) - |> maybe_encode_attr("data", notif.data) - |> maybe_encode_attr("fcm_options", notif.fcm_options) - |> maybe_encode_attr("notification", notif.notification) - |> maybe_encode_attr("webpush", notif.webpush) - - %{"message" => message} - |> maybe_encode_attr("validate_only", notif.validate_only) + %{} + |> encode_to(notif.to) + |> encode_data(notif.data) + |> maybe_encode_attr("time_to_live", notif.time_to_live) + |> maybe_encode_attr("content_available", notif.content_available) + |> maybe_encode_attr("mutable_content", notif.mutable_content) + |> maybe_encode_attr("notification", notif.notification) + |> maybe_encode_attr("schedule", notif.schedule) + |> maybe_encode_attr("collapse_key", notif.collapse_key) |> Pigeon.json_library().encode!() end - defp encode_target(map, {type, value}) do - Map.put(map, to_string(type), value) + defp encode_to(map, value) do + Map.put(map, "to", value) + end + + defp encode_data(map, value) do + Map.put(map, "data", value) end defp maybe_encode_attr(map, _key, nil), do: map diff --git a/mix.exs b/mix.exs index 00294f28..bb832c20 100644 --- a/mix.exs +++ b/mix.exs @@ -67,7 +67,7 @@ defmodule Pigeon.Mixfile do Pigeon.LegacyFCM, Pigeon.LegacyFCM.Notification ], - "Pushy": [Pigeon.Pushy, Pigeon.Pushy.Notification] + Pushy: [Pigeon.Pushy, Pigeon.Pushy.Notification] ], main: "Pigeon" ] From 839ac03c3f550b68dccae24d540f87ef805399c3 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 15:58:25 -0700 Subject: [PATCH 18/37] fix: don't pass in more params than necessary to error.parse --- lib/pigeon/pushy.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index b4f2e00c..5d105fdb 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -102,15 +102,15 @@ defmodule Pigeon.Pushy do {:ok, json} = Pigeon.json_library().decode(body) notification - |> Error.parse(json) + |> Error.parse() |> process_on_response() end defp handle_error_status_code(status, body, notification) do case Pigeon.json_library().decode(body) do {:ok, %{"reason" => _reason} = result_json} -> - notification - |> Error.parse(result_json) + result_json + |> Error.parse() |> process_on_response() {:error, _} -> From c062ec27d6d2ad3afd14949fc604e091cb26e406 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 17:08:14 -0700 Subject: [PATCH 19/37] fix: correct the pushy path for pushing notifications --- lib/pigeon/pushy.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 5d105fdb..16f371c4 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -55,7 +55,7 @@ defmodule Pigeon.Pushy do end defp pushy_uri(%Pigeon.Pushy.Config{uri: base_uri, key: secret_key}) do - "https://#{base_uri}/pushy/?api_key=#{secret_key}" + "https://#{base_uri}/push/?api_key=#{secret_key}" end def pushy_headers() do From f7230b18c6b984e53c01a99d244a2967cfac6f7e Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 17:23:51 -0700 Subject: [PATCH 20/37] fix: remove error parse on good data, just return response from pushy --- lib/pigeon/pushy.ex | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 16f371c4..9be0ec13 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -36,10 +36,9 @@ defmodule Pigeon.Pushy do end defp do_push(notification, state) do - encoded_notification = encode_requests(notification) - response = fn notification -> - case HTTPoison.post(pushy_uri(state.config), notification, pushy_headers()) do + encoded_notification = encode_requests(notification) + case HTTPoison.post(pushy_uri(state.config), encoded_notification, pushy_headers()) do {:ok, %HTTPoison.Response{status_code: status, body: body}} -> process_response(status, body, notification) @@ -101,8 +100,7 @@ defmodule Pigeon.Pushy do defp handle_200_status(body, notification) do {:ok, json} = Pigeon.json_library().decode(body) - notification - |> Error.parse() + json |> process_on_response() end From a7d40d84e286752f41a22a2ce0ab4af93f899b07 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Mon, 20 Mar 2023 17:25:08 -0700 Subject: [PATCH 21/37] fix: pass notification around appropriately in do_push for pushy --- lib/pigeon/pushy.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 9be0ec13..08430a28 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -49,7 +49,7 @@ defmodule Pigeon.Pushy do end end - Task.Supervisor.start_child(Pigeon.Tasks, fn -> response.(encoded_notification) end) + Task.Supervisor.start_child(Pigeon.Tasks, fn -> response.(notification) end) :ok end From be63a72b8b7e5ac15909d5ce4c19c3b0050ce70a Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 21 Mar 2023 10:25:10 -0700 Subject: [PATCH 22/37] feat: add result parser to handle both error and success cases --- lib/pigeon/pushy.ex | 8 ++++---- lib/pigeon/pushy/error.ex | 6 +++--- lib/pigeon/pushy/result_parser.ex | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 lib/pigeon/pushy/result_parser.ex diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 08430a28..a33d0e8e 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -5,7 +5,7 @@ defmodule Pigeon.Pushy do import Pigeon.Tasks, only: [process_on_response: 1] require Logger - alias Pigeon.Pushy.Error + alias Pigeon.Pushy.{ResultParser} defstruct config: nil @@ -100,15 +100,15 @@ defmodule Pigeon.Pushy do defp handle_200_status(body, notification) do {:ok, json} = Pigeon.json_library().decode(body) - json + ResultParser.parse(notification, json) |> process_on_response() end defp handle_error_status_code(status, body, notification) do case Pigeon.json_library().decode(body) do {:ok, %{"reason" => _reason} = result_json} -> - result_json - |> Error.parse() + notification + |> ResultParser.parse(result_parser) |> process_on_response() {:error, _} -> diff --git a/lib/pigeon/pushy/error.ex b/lib/pigeon/pushy/error.ex index eb3dea76..807f0b0c 100644 --- a/lib/pigeon/pushy/error.ex +++ b/lib/pigeon/pushy/error.ex @@ -2,10 +2,10 @@ defmodule Pigeon.Pushy.Error do @moduledoc false @doc false - @spec parse(map) :: atom() - def parse(error) do + @spec parse(Pigeon.Pushy.Notification.t(), map) :: atom() + def parse(notification, error) do error - |> Map.get("status") + |> Map.get("code") |> parse_response() end diff --git a/lib/pigeon/pushy/result_parser.ex b/lib/pigeon/pushy/result_parser.ex new file mode 100644 index 00000000..685325e2 --- /dev/null +++ b/lib/pigeon/pushy/result_parser.ex @@ -0,0 +1,18 @@ +defmodule Pigeon.Pushy.ResultParser do + @moduledoc false + alias Pigeon.Pushy.Error + + def parse(notification, %{"id" => push_id, "success" => success_status, "info" => %{"devices" => num_devices, "failed" => failed_devices}}) do + notification + |> Map.put(:push_id, push_id) + |> Map.put(:success, success_status) + |> Map.put(:successful_device_count, num_devices) + |> Map.put(:failed, failed_device_ids) + end + + def parse(notification, response) do + notification + |> Error.parse(response) + end + +end From 1f8985b5b4a5a8899df51ae5efe3ed56f763091d Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 21 Mar 2023 10:28:01 -0700 Subject: [PATCH 23/37] fix: add additional notification fields to hold response info --- lib/pigeon/pushy/error.ex | 2 +- lib/pigeon/pushy/notification.ex | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/pigeon/pushy/error.ex b/lib/pigeon/pushy/error.ex index 807f0b0c..3dbab303 100644 --- a/lib/pigeon/pushy/error.ex +++ b/lib/pigeon/pushy/error.ex @@ -2,7 +2,7 @@ defmodule Pigeon.Pushy.Error do @moduledoc false @doc false - @spec parse(Pigeon.Pushy.Notification.t(), map) :: atom() + @spec parse(Pigeon.Pushy.Notification.t(), map) :: Pigeon.Pushy.Notification.error_response() def parse(notification, error) do error |> Map.get("code") diff --git a/lib/pigeon/pushy/notification.ex b/lib/pigeon/pushy/notification.ex index cd4429a7..f234ca98 100644 --- a/lib/pigeon/pushy/notification.ex +++ b/lib/pigeon/pushy/notification.ex @@ -12,7 +12,11 @@ defmodule Pigeon.Pushy.Notification do notification: nil, schedule: nil, collapse_key: nil, - response: nil + response: nil, + push_id: nil, + success: nil, + successful_device_count: nil, + failed: nil @type t :: %__MODULE__{ __meta__: Pigeon.Metadata.t(), @@ -24,7 +28,11 @@ defmodule Pigeon.Pushy.Notification do notification: map | nil, schedule: integer | nil, collapse_key: String.t() | nil, - response: atom | nil + response: atom | nil, + push_id: String.t() | nil, + success: boolean() | nil, + successful_device_count: integer() | nil, + failed: [String.t()] | nil } @type error_response :: From c9ec79a9c36396a6c54544d4fed9d7eb03ee15c1 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 21 Mar 2023 10:30:51 -0700 Subject: [PATCH 24/37] fix: address some invalid variable refs and remove unused param from error --- lib/pigeon/pushy/error.ex | 4 ++-- lib/pigeon/pushy/result_parser.ex | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/pigeon/pushy/error.ex b/lib/pigeon/pushy/error.ex index 3dbab303..6df015e0 100644 --- a/lib/pigeon/pushy/error.ex +++ b/lib/pigeon/pushy/error.ex @@ -2,8 +2,8 @@ defmodule Pigeon.Pushy.Error do @moduledoc false @doc false - @spec parse(Pigeon.Pushy.Notification.t(), map) :: Pigeon.Pushy.Notification.error_response() - def parse(notification, error) do + @spec parse(map) :: Pigeon.Pushy.Notification.error_response() + def parse(error) do error |> Map.get("code") |> parse_response() diff --git a/lib/pigeon/pushy/result_parser.ex b/lib/pigeon/pushy/result_parser.ex index 685325e2..559168f6 100644 --- a/lib/pigeon/pushy/result_parser.ex +++ b/lib/pigeon/pushy/result_parser.ex @@ -7,12 +7,11 @@ defmodule Pigeon.Pushy.ResultParser do |> Map.put(:push_id, push_id) |> Map.put(:success, success_status) |> Map.put(:successful_device_count, num_devices) - |> Map.put(:failed, failed_device_ids) + |> Map.put(:failed, failed_devices) end - def parse(notification, response) do - notification - |> Error.parse(response) + def parse(_notification, response = %{"code" => _}) do + Error.parse(response) end end From 0884f660b76f7df414ee173b158d492d580eb50f Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 21 Mar 2023 10:33:19 -0700 Subject: [PATCH 25/37] fix: pass the resulting json instead of some fictitious result parser to the result parser --- lib/pigeon/pushy.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index a33d0e8e..7acd1ada 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -106,9 +106,9 @@ defmodule Pigeon.Pushy do defp handle_error_status_code(status, body, notification) do case Pigeon.json_library().decode(body) do - {:ok, %{"reason" => _reason} = result_json} -> + {:ok, %{"error" => _reason} = result_json} -> notification - |> ResultParser.parse(result_parser) + |> ResultParser.parse(result_json) |> process_on_response() {:error, _} -> From 83bf249a22bf45c925c1d6555944584651e7eb8a Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 21 Mar 2023 10:46:03 -0700 Subject: [PATCH 26/37] fix: handle case where failed devices might not be included --- lib/pigeon/pushy.ex | 7 ++++++- lib/pigeon/pushy/result_parser.ex | 28 +++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 7acd1ada..76d53c29 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -38,7 +38,12 @@ defmodule Pigeon.Pushy do defp do_push(notification, state) do response = fn notification -> encoded_notification = encode_requests(notification) - case HTTPoison.post(pushy_uri(state.config), encoded_notification, pushy_headers()) do + + case HTTPoison.post( + pushy_uri(state.config), + encoded_notification, + pushy_headers() + ) do {:ok, %HTTPoison.Response{status_code: status, body: body}} -> process_response(status, body, notification) diff --git a/lib/pigeon/pushy/result_parser.ex b/lib/pigeon/pushy/result_parser.ex index 559168f6..df4ec9a7 100644 --- a/lib/pigeon/pushy/result_parser.ex +++ b/lib/pigeon/pushy/result_parser.ex @@ -2,16 +2,30 @@ defmodule Pigeon.Pushy.ResultParser do @moduledoc false alias Pigeon.Pushy.Error - def parse(notification, %{"id" => push_id, "success" => success_status, "info" => %{"devices" => num_devices, "failed" => failed_devices}}) do - notification - |> Map.put(:push_id, push_id) - |> Map.put(:success, success_status) - |> Map.put(:successful_device_count, num_devices) - |> Map.put(:failed, failed_devices) + def parse( + notification, + response = %{ + "id" => push_id, + "success" => success_status, + "info" => %{"devices" => num_devices} + } + ) do + notification = + notification + |> Map.put(:push_id, push_id) + |> Map.put(:success, success_status) + |> Map.put(:successful_device_count, num_devices) + |> Map.put(:failed, failed_devices) + + if match?(%{"info" => %{"failed" => _}}, response) do + notification + |> Map.put(:failed, response["info"]["failed"]) + else + notification + end end def parse(_notification, response = %{"code" => _}) do Error.parse(response) end - end From 29fba725e2c4b1afb00619f93ce2ccf33e96c34a Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 21 Mar 2023 10:47:06 -0700 Subject: [PATCH 27/37] fix: remove old reference to failed devices --- lib/pigeon/pushy/result_parser.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pigeon/pushy/result_parser.ex b/lib/pigeon/pushy/result_parser.ex index df4ec9a7..752039b2 100644 --- a/lib/pigeon/pushy/result_parser.ex +++ b/lib/pigeon/pushy/result_parser.ex @@ -15,7 +15,6 @@ defmodule Pigeon.Pushy.ResultParser do |> Map.put(:push_id, push_id) |> Map.put(:success, success_status) |> Map.put(:successful_device_count, num_devices) - |> Map.put(:failed, failed_devices) if match?(%{"info" => %{"failed" => _}}, response) do notification From 5b19e6d80c2f842df4dea9422ed97d629d6a1e36 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 21 Mar 2023 10:55:50 -0700 Subject: [PATCH 28/37] fix: parse errors into notification structs instead of just atom --- lib/pigeon/pushy/error.ex | 13 ++++++++----- lib/pigeon/pushy/result_parser.ex | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/pigeon/pushy/error.ex b/lib/pigeon/pushy/error.ex index 6df015e0..a08f66bc 100644 --- a/lib/pigeon/pushy/error.ex +++ b/lib/pigeon/pushy/error.ex @@ -2,11 +2,14 @@ defmodule Pigeon.Pushy.Error do @moduledoc false @doc false - @spec parse(map) :: Pigeon.Pushy.Notification.error_response() - def parse(error) do - error - |> Map.get("code") - |> parse_response() + @spec parse(Pigeon.Pushy.Notification.t(), map) :: Pigeon.Pushy.Notification.error_response() + def parse(notification, error) do + error_code = error + |> Map.get("code") + |> parse_response() + + notification + |> Map.put(:response, error_code) end defp parse_response("NO_RECIPIENTS"), do: :no_recipients diff --git a/lib/pigeon/pushy/result_parser.ex b/lib/pigeon/pushy/result_parser.ex index 752039b2..18644d41 100644 --- a/lib/pigeon/pushy/result_parser.ex +++ b/lib/pigeon/pushy/result_parser.ex @@ -24,7 +24,7 @@ defmodule Pigeon.Pushy.ResultParser do end end - def parse(_notification, response = %{"code" => _}) do - Error.parse(response) + def parse(notification, response = %{"code" => _}) do + Error.parse(notification, response) end end From bf903703fe1bb25949e8ced5854b323a14e5812e Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 21 Mar 2023 11:24:14 -0700 Subject: [PATCH 29/37] feat: add helper methods for filling in additional parameters for pushy notifications --- lib/pigeon/pushy/notification.ex | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/pigeon/pushy/notification.ex b/lib/pigeon/pushy/notification.ex index f234ca98..930a2b4f 100644 --- a/lib/pigeon/pushy/notification.ex +++ b/lib/pigeon/pushy/notification.ex @@ -54,6 +54,36 @@ defmodule Pigeon.Pushy.Notification do data: message } end + + @spec put_time_to_live(__MODULE__.t(), integer()) :: __MODULE__.t() + def put_time_to_live(notification, time_to_live) do + %{notification | time_to_live: time_to_live + end + + @spec put_content_available(__MODULE__.t(), boolean()) :: __MODULE__.t() + def put_content_available(notification, content_available) do + %{notification | content_available: content_available} + end + + @spec put_mutable_content(__MODULE__.t(), boolean()) :: __MODULE__.t() + def put_mutable_content(notification, mutable_content) do + %{notification | mutable_content: mutable_content} + end + + @spec put_notification(__MODULE__.t(), map) :: __MODULE__.t() + def put_notification(notification, notification_details) do + %{notification | notification: notification_details} + end + + @spec put_schedule(__MODULE__.t(), integer) :: __MODULE__.t() + def put_schedule(notification, schedule) do + %{notification | schedule: schedule} + end + + @spec put_collapse_key(__MODULE__.t(), String.t()) :: __MODULE__.t() + def put_collapse_key(notification, collapse_key) do + %{notification | collapse_key: collapse_key} + end end defimpl Pigeon.Encodable, for: Pigeon.Pushy.Notification do From 7af0b96a18e6b3db41875897694752409e092b81 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 21 Mar 2023 11:26:26 -0700 Subject: [PATCH 30/37] fix: add missing empty curly bracket and format files --- lib/pigeon/pushy/error.ex | 6 ++++-- lib/pigeon/pushy/notification.ex | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/pigeon/pushy/error.ex b/lib/pigeon/pushy/error.ex index a08f66bc..2903488f 100644 --- a/lib/pigeon/pushy/error.ex +++ b/lib/pigeon/pushy/error.ex @@ -2,9 +2,11 @@ defmodule Pigeon.Pushy.Error do @moduledoc false @doc false - @spec parse(Pigeon.Pushy.Notification.t(), map) :: Pigeon.Pushy.Notification.error_response() + @spec parse(Pigeon.Pushy.Notification.t(), map) :: + Pigeon.Pushy.Notification.error_response() def parse(notification, error) do - error_code = error + error_code = + error |> Map.get("code") |> parse_response() diff --git a/lib/pigeon/pushy/notification.ex b/lib/pigeon/pushy/notification.ex index 930a2b4f..e19a6bbe 100644 --- a/lib/pigeon/pushy/notification.ex +++ b/lib/pigeon/pushy/notification.ex @@ -57,7 +57,7 @@ defmodule Pigeon.Pushy.Notification do @spec put_time_to_live(__MODULE__.t(), integer()) :: __MODULE__.t() def put_time_to_live(notification, time_to_live) do - %{notification | time_to_live: time_to_live + %{notification | time_to_live: time_to_live} end @spec put_content_available(__MODULE__.t(), boolean()) :: __MODULE__.t() From 72c33cf6c7d4e2bc697643af549bca47cdb6ec89 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Wed, 29 Mar 2023 12:32:31 -0700 Subject: [PATCH 31/37] docs(pushy): add documentation for notification and pushy module --- lib/pigeon/pushy.ex | 106 ++++++++++++++++++- lib/pigeon/pushy/notification.ex | 163 +++++++++++++++++++++++------- lib/pigeon/pushy/result_parser.ex | 1 + 3 files changed, 234 insertions(+), 36 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 76d53c29..15ab1629 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -1,6 +1,110 @@ defmodule Pigeon.Pushy do @moduledoc """ - `Pigeon.Adapter` for Pushy pushy notifications + `Pigeon.Adapter` for Pushy push notifications. + + This adapter provides support for sending push notifications via the Pushy API. + It is designed to work with the `Pigeon` library and implements the `Pigeon.Adapter` behaviour. + + ## Example + + + Then, you can send a Pushy push notification like this: + + notif = Pigeon.Pushy.Notification.new(%{"message" => "Hello, world!"}, "device_token") + + Pigeon.push(notif) + + ## Configuration + + The following options can be set in the adapter configuration: + + * `:key` - (required) the API key for your Pushy account. + * `:base_uri` - (optional) the base URI for the Pushy API. Defaults to "api.pushy.me". + + ## Getting Started + + 1. Create a Pushy dispatcher. + + ``` + # lib/pushy.ex + defmodule YourApp.Pushy do + use Pigeon.Dispatcher, otp_app: :your_app + end + ``` + + 2. (Optional) Add configuration to your `config.exs`. + + To use this adapter, simply include it in your Pigeon configuration: + + config :your_app, YourApp.Pushy, + adapter: Pigeon.Pushy, + key: "pushy secret key" +en + + 3. Start your dispatcher on application boot. + + ``` + defmodule YourApp.Application do + @moduledoc false + + use Application + + @doc false + def start(_type, _args) do + children = [ + YourApp.Pushy + ] + opts = [strategy: :one_for_one, name: YourApp.Supervisor] + Supervisor.start_link(children, opts) + end + end + ``` + + 4. Create a notification. + + ``` + msg = %{ "body" => "your message" } + n = Pigeon.pushy.Notification.new(msg, "your device token") + ``` + + 5. Send the notification. + + ``` + YourApp.Pushy.push(n) + ``` + + ## Handling Push Responses + + 1. Pass an optional anonymous function as your second parameter. + + ``` + data = %{ "message" => "your message" } + n = Pigeon.Pushy.Notification.new(data, "device token") + YourApp.Pushy.push(n, on_response: fn(x) -> IO.inspect(x) end) + ``` + + 2. Responses return a notification with an updated `:response` key. + You could handle responses like so: + + ``` + on_response_handler = fn(x) -> + case x.response do + :success -> + # Push successful + :ok + :failure -> + # Retry or some other handling for x.failed (devices failed to send) + :timeout -> + # request didn't finish within expected time, server didn't respond + error -> + # Handle other errors + end + end + + data = %{ "message" => "your message" } + n = Pigeon.Pushy.Notification.new(data, "your device token") + Pigeon.Pushy.push(n, on_response: on_response_handler) + ``` """ import Pigeon.Tasks, only: [process_on_response: 1] require Logger diff --git a/lib/pigeon/pushy/notification.ex b/lib/pigeon/pushy/notification.ex index e19a6bbe..2b2f4e9f 100644 --- a/lib/pigeon/pushy/notification.ex +++ b/lib/pigeon/pushy/notification.ex @@ -1,6 +1,9 @@ defmodule Pigeon.Pushy.Notification do @moduledoc """ Defines Pushy notification struct and convenience constructor functions. + + For more information on Pushy notification requests, see + https://pushy.me/docs/api/send-notifications. """ defstruct __meta__: %Pigeon.Metadata{}, @@ -18,6 +21,27 @@ defmodule Pigeon.Pushy.Notification do successful_device_count: nil, failed: nil + @typedoc """ + Pushy notification + + ## Examples + %Pigeon.Pushy.Notification{ + __meta__: %Pigeon.Metadata{on_response: nil}, + to: "device token or topic", + data: %{"message" => "hello world"}, + time_to_live: nil, + content_available: nil, + mutable_content: nil, + notification: nil, + schedule: nil, + collapse_key: nil, + response: nil, # Set on push response + success: nil, # Set on push response + push_id: nil, # Set on push response + successful_device_count: nil, # Set on push response + failed: nil # Set on push response + } + """ @type t :: %__MODULE__{ __meta__: Pigeon.Metadata.t(), to: String.t() | [String.t()], @@ -28,13 +52,26 @@ defmodule Pigeon.Pushy.Notification do notification: map | nil, schedule: integer | nil, collapse_key: String.t() | nil, - response: atom | nil, + response: response, push_id: String.t() | nil, success: boolean() | nil, successful_device_count: integer() | nil, failed: [String.t()] | nil } + @typedoc """ + Pushy push response + + - nil - Push has not been sent yet + - `:success` - Push was successfully sent. + - `:failure` - If we don't receive an error code, but the response is not successful, + this message is returned. + - `t:Pigeon.Pushy.Error.error_response/0` - Push attempted + server responded with error. + - `:timeout` - Internal error. Push did not reach Pushy servers. + """ + @type response :: nil | :success | :failure | error_response | :timeout + @type error_response :: :no_recipients | :no_apns_auth @@ -47,6 +84,32 @@ defmodule Pigeon.Pushy.Notification do | :internal_server_error | :unknown_error + @doc """ + Returns a `Pushy.Notification` struct with the given message and destination. + + A destination could be either a single destination, or a list of them. A destination + is either a device token OR a topic (prefix topic names with '/topics/'). + + ## Examples + + iex> Pigeon.Pushy.Notification.new(%{"message" => "Hello, world!"}, "device token") + %Pigeon.APNS.Notification{ + __meta__: %Pigeon.Metadata{}, + to: "device token", + data: %{"message" => "Hello, world!"}, + time_to_live: nil, + content_available: nil, + mutable_content: nil, + notification: nil, + schedule: nil, + collapse_key: nil, + response: nil, + success: nil, + push_id: nil, + successful_device_count: nil, + failed: nil + } + """ @spec new(map, String.t() | [String.t()]) :: __MODULE__.t() def new(message, device_ids) do %__MODULE__{ @@ -55,67 +118,97 @@ defmodule Pigeon.Pushy.Notification do } end + @doc """ + Adds or updates the `data` field in the notification. + + ## Examples + + iex> Pigeon.Pushy.Notification.put_data(notification, %{"message" => "some message"}) + %Pigeon.Pushy.Notification{... data: %{"message" => "some message"} ...} + """ + @spec put_data(__MODULE__.t(), map()) :: __MODULE__.t() + def put_data(notification, data) do + %{notification | data: data} + end + + @doc """ + Adds or updates the `time_to_live` field in the notification. + + This is how long (in seconds) the push notification should be kept if the device is offline. + If unspecified, notifications will be kept for 30 days. The maximum value is 365 days. + + ## Examples + + iex> Pigeon.Pushy.Notification.put_time_to_live(notification, 3600) + %Pigeon.Pushy.Notification{... time_to_live: 3600 ...} + """ @spec put_time_to_live(__MODULE__.t(), integer()) :: __MODULE__.t() def put_time_to_live(notification, time_to_live) do %{notification | time_to_live: time_to_live} end + @doc """ + Adds or updates the `content_available` field in the notification. + + ## Examples + + iex> Pigeon.Pushy.Notification.put_content_available(notification, true) + %Pigeon.Pushy.Notification{... content_available: true ...} + """ @spec put_content_available(__MODULE__.t(), boolean()) :: __MODULE__.t() def put_content_available(notification, content_available) do %{notification | content_available: content_available} end + @doc """ + Adds or updates the `mutable_content` field in the notification. + + ## Examples + + iex> Pigeon.Pushy.Notification.put_mutable_content(notification, false) + %Pigeon.Pushy.Notification{... mutable_content: false ...} + """ @spec put_mutable_content(__MODULE__.t(), boolean()) :: __MODULE__.t() def put_mutable_content(notification, mutable_content) do %{notification | mutable_content: mutable_content} end + @doc """ + Adds or updates the `notification` field in the notification. + + ## Examples + + iex> Pigeon.Pushy.Notification.put_notification(notification, %{"title" => "New message", "body" => "Hello"}) + %Pigeon.Pushy.Notification{... notification: %{"title" => "New message", "body" => "Hello"} ...} + """ @spec put_notification(__MODULE__.t(), map) :: __MODULE__.t() def put_notification(notification, notification_details) do %{notification | notification: notification_details} end + @doc """ + Adds or updates the `schedule` field in the notification. + + ## Examples + + iex> Pigeon.Pushy.Notification.put_schedule(notification, 1648686700) + %Pigeon.Pushy.Notification{... schedule: 1648686700 ...} + """ @spec put_schedule(__MODULE__.t(), integer) :: __MODULE__.t() def put_schedule(notification, schedule) do %{notification | schedule: schedule} end + @doc """ + Adds or updates the `collapse_key` field in the notification. + + ## Examples + + iex> Pigeon.Pushy.Notification.put_collapse_key(notification, "new_message") + %Pigeon.Pushy.Notification{... collapse_key: "new_message" ...} + """ @spec put_collapse_key(__MODULE__.t(), String.t()) :: __MODULE__.t() def put_collapse_key(notification, collapse_key) do %{notification | collapse_key: collapse_key} end end - -defimpl Pigeon.Encodable, for: Pigeon.Pushy.Notification do - def binary_payload(notif) do - encode_requests(notif) - end - - @doc false - def encode_requests(notif) do - %{} - |> encode_to(notif.to) - |> encode_data(notif.data) - |> maybe_encode_attr("time_to_live", notif.time_to_live) - |> maybe_encode_attr("content_available", notif.content_available) - |> maybe_encode_attr("mutable_content", notif.mutable_content) - |> maybe_encode_attr("notification", notif.notification) - |> maybe_encode_attr("schedule", notif.schedule) - |> maybe_encode_attr("collapse_key", notif.collapse_key) - |> Pigeon.json_library().encode!() - end - - defp encode_to(map, value) do - Map.put(map, "to", value) - end - - defp encode_data(map, value) do - Map.put(map, "data", value) - end - - defp maybe_encode_attr(map, _key, nil), do: map - - defp maybe_encode_attr(map, key, val) do - Map.put(map, key, val) - end -end diff --git a/lib/pigeon/pushy/result_parser.ex b/lib/pigeon/pushy/result_parser.ex index 18644d41..44ccdec4 100644 --- a/lib/pigeon/pushy/result_parser.ex +++ b/lib/pigeon/pushy/result_parser.ex @@ -15,6 +15,7 @@ defmodule Pigeon.Pushy.ResultParser do |> Map.put(:push_id, push_id) |> Map.put(:success, success_status) |> Map.put(:successful_device_count, num_devices) + |> Map.put(:response, if success_status, do: :success, else: :failure) if match?(%{"info" => %{"failed" => _}}, response) do notification From a5cc9404eaa09cebd0eafdeeab4feb94949fd2d7 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Wed, 29 Mar 2023 14:19:04 -0700 Subject: [PATCH 32/37] refactor: change order of new to take destination as first parameter and data as an optional second parameter --- lib/pigeon/pushy/notification.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pigeon/pushy/notification.ex b/lib/pigeon/pushy/notification.ex index 2b2f4e9f..63ff799a 100644 --- a/lib/pigeon/pushy/notification.ex +++ b/lib/pigeon/pushy/notification.ex @@ -110,8 +110,10 @@ defmodule Pigeon.Pushy.Notification do failed: nil } """ - @spec new(map, String.t() | [String.t()]) :: __MODULE__.t() - def new(message, device_ids) do + @spec new(String.t() | [String.t()], map()) :: __MODULE__.t() + def new(device_ids, message \\ nil) + + def new(device_ids, message) do %__MODULE__{ to: device_ids, data: message From f46512f2ff0c51f401beaa0548df924f7734c0db Mon Sep 17 00:00:00 2001 From: John Hossler Date: Wed, 29 Mar 2023 14:20:01 -0700 Subject: [PATCH 33/37] test: implement basic pushy test implementation --- test/pigeon/pushy_test.exs | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 test/pigeon/pushy_test.exs diff --git a/test/pigeon/pushy_test.exs b/test/pigeon/pushy_test.exs new file mode 100644 index 00000000..42b44b88 --- /dev/null +++ b/test/pigeon/pushy_test.exs @@ -0,0 +1,67 @@ +defmodule Pigeon.PushyTest do + use ExUnit.Case + doctest Pigeon.Pushy, import: true + doctest Pigeon.Pushy.Config, import: true + + alias Pigeon.Pushy.Notification + + @invalid_key_msg ~r/^attempted to start without valid key or uri/ + + describe "init/1" do + test "initializes correctly if configured with key" do + opts = [uri: "api.pushy.me", key: "abc123"] + + expected = + {:ok, + %{ + config: %Pigeon.Pushy.Config{ + uri: "api.pushy.me", + key: "abc123" + } + }} + + assert Pigeon.Pushy.init(opts) == expected + end + + test "raises if configured with invalid uri" do + assert_raise(Pigeon.ConfigError, @invalid_key_msg, fn -> + opts = [uri: nil, key: "abc123"] + Pigeon.Pushy.init(opts) + end) + end + + test "raises if configured with invalid key" do + assert_raise(Pigeon.ConfigError, @invalid_key_msg, fn -> + opts = [ + uri: "api.pushy.me", + key: nil + ] + + Pigeon.Pushy.init(opts) + end) + end + end + + describe "handle_push/3" do + test "returns an error on pushing with a bad registration_id" do + token = "bad_token" + n = Notification.new(token, %{"message" => "example"}) + pid = self() + PigeonTest.Pushy.push(n, on_response: fn x -> send(pid, x) end) + + assert_receive(n = %Notification{}, 5000) + assert n.response == :invalid_registration_id + assert n.registration_id == reg_id + assert n.payload == %{"data" => %{"message" => "example"}} + end + + test "handles nil on_response" do + n = Notification.new("bad_token", %{"message" => "example"}) + PigeonTest.Pushy.push(n, on_response: nil) + end + end + + test "handle_info/2 handles random messages" do + assert Pigeon.ADM.handle_info("random", %{}) == {:noreply, %{}} + end +end From 9251f983ea31a25f83eeb03b2676fae4568b6f3f Mon Sep 17 00:00:00 2001 From: John Hossler Date: Wed, 29 Mar 2023 14:20:19 -0700 Subject: [PATCH 34/37] feat: ensure pushy config is validated --- lib/pigeon/pushy.ex | 2 ++ lib/pigeon/pushy/config.ex | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 15ab1629..8d55a9dc 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -119,6 +119,8 @@ en def init(opts) do config = Pigeon.Pushy.Config.new(opts) + Config.validate!(config) + state = %__MODULE__{config: config} {:ok, state} diff --git a/lib/pigeon/pushy/config.ex b/lib/pigeon/pushy/config.ex index 2f7dc1cf..ad04c27e 100644 --- a/lib/pigeon/pushy/config.ex +++ b/lib/pigeon/pushy/config.ex @@ -66,4 +66,39 @@ defmodule Pigeon.Pushy.Config do port: Keyword.get(opts, :port, 443) } end + + @doc ~S""" + Returns whether a given config has valid credentials. + + ## Examples + + iex> [] |> new() |> valid?() + false + """ + def valid?(config) do + valid_item?(config.uri) and valid_item?(config.key) + end + + defp valid_item?(item), do: is_binary(item) and String.length(item) > 0 + + @spec validate!(any) :: :ok | no_return + def validate!(config) do + if valid?(config) do + :ok + else + raise Pigeon.ConfigError, + reason: "attempted to start without valid key or uri", + config: redact(config) + end + end + + defp redact(config) do + [:key] + |> Enum.reduce(config, fn k, acc -> + case Map.get(acc, k) do + nil -> acc + _ -> Map.put(acc, k, "[FILTERED]") + end + end) + end end From 0de75f6f756505d8d96aa523a3e629a740a3d2bb Mon Sep 17 00:00:00 2001 From: Nathan Alderson Date: Thu, 29 Feb 2024 12:47:25 -0600 Subject: [PATCH 35/37] Fix compilation errors and warnings --- lib/pigeon/pushy.ex | 3 +-- lib/pigeon/pushy/result_parser.ex | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/pigeon/pushy.ex b/lib/pigeon/pushy.ex index 8d55a9dc..c2f3c60a 100644 --- a/lib/pigeon/pushy.ex +++ b/lib/pigeon/pushy.ex @@ -39,7 +39,6 @@ defmodule Pigeon.Pushy do config :your_app, YourApp.Pushy, adapter: Pigeon.Pushy, key: "pushy secret key" -en 3. Start your dispatcher on application boot. @@ -119,7 +118,7 @@ en def init(opts) do config = Pigeon.Pushy.Config.new(opts) - Config.validate!(config) + Pigeon.Pushy.Config.validate!(config) state = %__MODULE__{config: config} diff --git a/lib/pigeon/pushy/result_parser.ex b/lib/pigeon/pushy/result_parser.ex index 44ccdec4..46dc2444 100644 --- a/lib/pigeon/pushy/result_parser.ex +++ b/lib/pigeon/pushy/result_parser.ex @@ -15,7 +15,7 @@ defmodule Pigeon.Pushy.ResultParser do |> Map.put(:push_id, push_id) |> Map.put(:success, success_status) |> Map.put(:successful_device_count, num_devices) - |> Map.put(:response, if success_status, do: :success, else: :failure) + |> Map.put(:response, if(success_status, do: :success, else: :failure)) if match?(%{"info" => %{"failed" => _}}, response) do notification From fefcc3c10f948d947a6219e94012d1a7481bfc13 Mon Sep 17 00:00:00 2001 From: Nathan Alderson Date: Thu, 29 Feb 2024 13:07:10 -0600 Subject: [PATCH 36/37] Make the default uri a string, not a charlist --- lib/pigeon/pushy/config.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pigeon/pushy/config.ex b/lib/pigeon/pushy/config.ex index ad04c27e..d897b3cd 100644 --- a/lib/pigeon/pushy/config.ex +++ b/lib/pigeon/pushy/config.ex @@ -62,7 +62,7 @@ defmodule Pigeon.Pushy.Config do def new(opts) when is_list(opts) do %__MODULE__{ key: opts |> Keyword.get(:key), - uri: Keyword.get(opts, :uri, 'api.pushy.me'), + uri: Keyword.get(opts, :uri, "api.pushy.me"), port: Keyword.get(opts, :port, 443) } end From acb29b88be0c2f17392070b6130d1b321eb11fd4 Mon Sep 17 00:00:00 2001 From: John Hossler Date: Tue, 2 Apr 2024 15:29:00 -0700 Subject: [PATCH 37/37] fix: put variable on the right side of pattern match in parameter list --- lib/pigeon/pushy/result_parser.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pigeon/pushy/result_parser.ex b/lib/pigeon/pushy/result_parser.ex index 46dc2444..0e08e601 100644 --- a/lib/pigeon/pushy/result_parser.ex +++ b/lib/pigeon/pushy/result_parser.ex @@ -4,11 +4,11 @@ defmodule Pigeon.Pushy.ResultParser do def parse( notification, - response = %{ + %{ "id" => push_id, "success" => success_status, "info" => %{"devices" => num_devices} - } + } = response ) do notification = notification @@ -25,7 +25,7 @@ defmodule Pigeon.Pushy.ResultParser do end end - def parse(notification, response = %{"code" => _}) do + def parse(notification, %{"code" => _} = response) do Error.parse(notification, response) end end