From 9721449c524e72d625b7d9cd30f740845ddd2ec6 Mon Sep 17 00:00:00 2001 From: Henry Popp Date: Fri, 25 May 2018 12:18:51 -0500 Subject: [PATCH] feat: v1.2.0 (#109) * chore: update to 1.6 style guide Might be some weird formatting I may have missed. Will need to double check before release. * feat: upgraded to kadabra v0.4.0 APNS.Config `:reconnect` option is now completely ignored. Key should be removed in whatever next major version. * feat: support for token based APNS authentication (#116) * Support for APNS JWT authentication tokens. Update docs to reflect changes. * Pigeon.ConfigError: Fixed moduledoc typo. * fix: APNS.CertConfig moved back to APNS.Config Required for backwards compatability. Config parsing is now handled in APNS.ConfigParser, which returns the appropriate struct for the given opts. `jwt_` prefix has been removed from the various attributes on JWTConfig. Still unsure whether or not to remove it for static Mix.Config options. TODO: Cleanup documentation and tests. * refactor: move shared config functionality back to APNS.Shared Necessary to clean up compile warnings. * test: tests added for APNS JWT configs Requires the following keys to be set: - APNS_JWT_KEY (file or plaintext) - APNS_JWT_KEY_IDENTIFIER - APNS_JWT_TEAM_ID TODO: update docs for config_opts * Alter APNS.Token storage key Tokens are now stored keyed on the JWTConfig key_identifier and team_id. * docs: update JWT documentation Minor fixes included in other parts of the docs. Also updated kadabra to v0.4.1-- I think unit tests run a little faster now? * ci: add secrets to travis * ci: global env instead of matrix * ci: fix env var? * ci: use JWT file instead * fix: elixir 1.4 compatability `use Agent` doesn't exist in 1.4 * fix: typo (#117) * chore: update version and changelog --- .formatter.exs | 4 + .gitignore | 3 + .travis.yml | 22 +-- CHANGELOG.md | 6 +- README.md | 20 ++- cert_key.tar.enc | Bin 7184 -> 0 bytes config/config.exs | 2 +- config/test.exs | 19 ++- docs/APNS Apple iOS.md | 16 +++ docs/Getting Started.md | 10 +- lib/pigeon.ex | 35 +++-- lib/pigeon/adm.ex | 49 ++++--- lib/pigeon/adm/config.ex | 13 +- lib/pigeon/adm/notification.ex | 51 ++++--- lib/pigeon/adm/result_parser.ex | 2 +- lib/pigeon/adm/worker.ex | 86 +++++++---- lib/pigeon/apns.ex | 45 +++--- lib/pigeon/apns/config.ex | 235 ++++++++++-------------------- lib/pigeon/apns/config_parser.ex | 79 ++++++++++ lib/pigeon/apns/error.ex | 79 +++++----- lib/pigeon/apns/jwt_config.ex | 239 +++++++++++++++++++++++++++++++ lib/pigeon/apns/notification.ex | 47 +++--- lib/pigeon/apns/shared.ex | 84 +++++++++++ lib/pigeon/apns/token.ex | 20 +++ lib/pigeon/configurable.ex | 2 +- lib/pigeon/connection.ex | 32 +++-- lib/pigeon/exceptions.ex | 10 ++ lib/pigeon/fcm.ex | 40 ++++-- lib/pigeon/fcm/config.ex | 55 +++---- lib/pigeon/fcm/notification.ex | 97 +++++++------ lib/pigeon/fcm/result_parser.ex | 53 +++---- lib/pigeon/http2/client.ex | 15 +- lib/pigeon/http2/kadabra.ex | 65 +++++---- lib/pigeon/http2/stream.ex | 10 +- lib/pigeon/tasks.ex | 1 + lib/pigeon/worker.ex | 17 ++- mix.exs | 8 +- mix.lock | 22 +-- secrets.tar.enc | Bin 0 -> 9744 bytes test/adm/notification_test.exs | 22 ++- test/apns/jwt_config_test.exs | 21 +++ test/apns/notification_test.exs | 33 +++-- test/apns_test.exs | 123 ++++++++++++---- test/fcm/result_parser_test.exs | 71 +++++---- test/fcm/worker_test.exs | 10 +- test/fcm_test.exs | 12 +- test/notification_test.exs | 9 ++ test/support/test_config.ex | 12 ++ 48 files changed, 1308 insertions(+), 598 deletions(-) create mode 100644 .formatter.exs delete mode 100644 cert_key.tar.enc create mode 100644 lib/pigeon/apns/config_parser.ex create mode 100644 lib/pigeon/apns/jwt_config.ex create mode 100644 lib/pigeon/apns/shared.ex create mode 100644 lib/pigeon/apns/token.ex create mode 100644 lib/pigeon/exceptions.ex create mode 100644 secrets.tar.enc create mode 100644 test/apns/jwt_config_test.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 00000000..4a7f4bb0 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,4 @@ +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + line_length: 80 +] diff --git a/.gitignore b/.gitignore index f3a4463c..8abc2786 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ erl_crash.dump *.ez *.pem +*.p8 +scratchpad.txt +.DS_Store .iex.exs diff --git a/.travis.yml b/.travis.yml index ceb9af78..bd2b2a47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,17 @@ language: elixir elixir: - - 1.4.5 - - 1.5.3 - - 1.6.0 +- 1.4.5 +- 1.5.3 +- 1.6.5 otp_release: - - 19.2 - - 20.0 +- 19.2 +- 20.0 before_install: -- openssl aes-256-cbc -K $encrypted_3033c569c247_key -iv - $encrypted_3033c569c247_iv -in cert_key.tar.enc -out - cert_key.tar -d -- tar xvf cert_key.tar +- openssl aes-256-cbc -K $encrypted_3033c569c247_key -iv $encrypted_3033c569c247_iv -in secrets.tar.enc -out secrets.tar -d +- tar xvf secrets.tar script: - - "MIX_ENV=test mix do deps.get, compile, coveralls.travis" +- MIX_ENV=test mix do deps.get, compile, coveralls.travis +env: + global: + - secure: kd6KF3eMgtaWIQbd/MG8m8L2cR3fqxmgRHWng4fqxRwPH45hP4IVLf2cQctGA9mm6nOV5wrNtys1fQzYPuXAKA/LAYJ/pc66ISdEjKjNpqxIGRCCJsB6lZft5XPgpAJTClwKGpFvEd7cLtz2ir4PBN0JjDyWtMTKHXarKzn7cOQNpRypwc5wWU1dmuqSsNNjr2hk3HUAwSNqB8W960Zqd39lqo0m+/tlq3L8LghcFlibn8XFNlzAmERpUpERkJZPbRplgx+ofigATGaC9Tzuc04WWLUthsPyBJkli2tYs/8Yd1tCjAYz90Tde0E1/Mzj6b4/LW4AGXrb4I5pKZrkkDw0gjqJ1Buu4U2d/TGZc+wU13BD5icpjq7zx8ZZ08N3XmQ6Y2ydd5p9MwCtUx5Dg2d9ybvdeCI1kj4HVcYAxqoBmf12od3fRZ+IwzbpUIvFn8R05wlvhA8nSLGloUceag3qUHJLD/tJOj9GRgR8ubw6hAQz7w7uWJhq3XSvHvdtYw2tfc9pGpjloA2gOhaStHrjJGFpzMFPITCEcQF1B7KraY/FPxMyvQL70l9RhQZ11F+56uwZ9oZFEQeyr5Tr6mPISyp1enz50HjwRrVy4pw5hOaTOCxA1dxrstWPhdKHOyYNtZDMFopn8yAIGzPE3z6UQ4bABqIlz8tpEEy13Eg= + - secure: f7LJkl/HyS/YghhADhVSNZjOsYkzuWXL9kqYAqjTxzB4g/2HPxkyjGjtlUHv82MvjvoIs7icDNjIveVf0ZP/is5t5T88HXfobYIW1H4hexXbN94oLJTaehdlkv0Fdi9Rfm7sBBH+tyd9HwS25R9MMSndRkxcnr78huFP8iWgRFFF4dDHyh8Dozzo+tHbrPPJtt8iJPz1Fs8U3nMQOdwHj8cA3fZXg65S7IzeavGAnkxtUDV22FgJd0PLD2i2RHAXAxjpmjLY3GIZDquSAO9DWJGElNRphg91nIT726vylm6IZ0JQ0H13hXbVfD4akYYb9ipL8CS/FzZexArtd1cg/nJKSz4G6AJyfByw0KqJwUgaPhvhlCeG8RDse4CH9dH7d8ODbUpxkcvh3Z31EUECY/2BC7Ma4ieLC4VAMBBgc21XxMPklt5z+wW8v2mblVudTSIRnXLL3QiiM2SHV0PLHyH7TPGiKU92J5HrhT3cKkrUMwcePLb6GUBKg6tSiF52DEiiGXWDWbj3rMEnwRbvAa5QkHNhVLwre1tsOt8nCX6cK3rY5fygR3W7lfzMQGMs/XzMdMaqFamJ0YRqUMh2msL0SdzltqFsXlS80JWS8U3zM1IlS448/qmhHzDjyV5mW/bd5iC7uDPQ/5rJfSaicfS1pNNp83Ufg7byNVaIbL4= diff --git a/CHANGELOG.md b/CHANGELOG.md index 5add718a..665be456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog +## v1.2.0 +* Support for APNS JWT configuration +* Bump `kadabra` dependency to `v0.4.2` + ## v1.1.6 * Relax `gen_stage` dependency to `~> 0.12` -* Bump `kadabra` dependency to 0.3.7 +* Bump `kadabra` dependency to `v0.3.7` ## v1.1.5 * Fix: relax `httpoison` dependency to allow `0.x` or `1.0` diff --git a/README.md b/README.md index 9b254f34..0f188185 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ Add pigeon and kadabra as `mix.exs` dependencies: ```elixir def deps do [ - {:pigeon, "~> 1.1.6"}, - {:kadabra, "~> 0.3.7"} + {:pigeon, "~> 1.2.0"}, + {:kadabra, "~> 0.4.2"} ] end ``` @@ -39,6 +39,22 @@ Add pigeon and kadabra as `mix.exs` dependencies: * Full-text string of the file contents * `{:my_app, "certs/cert.pem"}` (indicates path relative to the `priv` folder of the given application) + Alternatively, you can use token based authentication: + + ```elixir + config :pigeon, :apns, + apns_default: %{ + key: "AuthKey.p8", + key_identifier: "ABC1234567", + team_id: "DEF8901234", + mode: :dev + } + ``` + + * `:key` - Created and downloaded via your developer account. Like `:cert` this can be a file path, file contents string or tuple + * `:key_identifier` - The 10-character key identifier associated with `:key`, obtained from your developer account + * `:team_id` - Your 10-character Team ID, obtained from your developer account + 2. Create a notification packet. **Note: Your push topic is generally the app's bundle identifier.** ```elixir diff --git a/cert_key.tar.enc b/cert_key.tar.enc deleted file mode 100644 index 6653fac26364311f7d3420e7d03ad979610867f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7184 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 HTTP2-compliant wrapper for sending iOS and Android push notifications. [![Build Status](https://travis-ci.org/codedge-llc/pigeon.svg?branch=master)](https://travis-ci.org/codedge-llc/pigeon) -[![Coverage Status](https://coveralls.io/repos/github/codedge-llc/pigeon/badge.svg?branch=v1.1.0)](https://coveralls.io/github/codedge-llc/pigeon) -[![Hex.pm](http://img.shields.io/hexpm/v/pigeon.svg)](https://hex.pm/packages/pigeon) [![Hex.pm](http://img.shields.io/hexpm/dt/pigeon.svg)](https://hex.pm/packages/pigeon) -[![Deps Status](https://beta.hexfaktor.org/badge/all/github/codedge-llc/pigeon.svg)](https://beta.hexfaktor.org/github/codedge-llc/pigeon) +[![Coverage Status](https://coveralls.io/repos/github/codedge-llc/pigeon/badge.svg)](https://coveralls.io/github/codedge-llc/pigeon) +[![Hex.pm](http://img.shields.io/hexpm/v/pigeon.svg)](https://hex.pm/packages/pigeon) +[![Hex.pm](http://img.shields.io/hexpm/dt/pigeon.svg)](https://hex.pm/packages/pigeon) ## Installation @@ -12,8 +12,8 @@ Add pigeon and kadabra as `mix.exs` dependencies: ```elixir def deps do [ - {:pigeon, "~> 1.1.6"}, - {:kadabra, "~> 0.3.7"} + {:pigeon, "~> 1.2.0"}, + {:kadabra, "~> 0.4.2"} ] end ``` diff --git a/lib/pigeon.ex b/lib/pigeon.ex index cb663f20..88b651ba 100644 --- a/lib/pigeon.ex +++ b/lib/pigeon.ex @@ -19,11 +19,19 @@ defmodule Pigeon do end defp workers do - adm_workers() - ++ apns_workers() - ++ fcm_workers() - ++ env_workers() - ++ task_supervisors() + [ + adm_workers(), + apns_workers(), + fcm_workers(), + env_workers(), + apns_token_agent(), + task_supervisors() + ] + |> List.flatten() + end + + defp apns_token_agent do + [worker(APNS.Token, [%{}], restart: :permanent, shutdown: 5_000)] end defp task_supervisors do @@ -32,9 +40,11 @@ defmodule Pigeon do defp env_workers do case Application.get_env(:pigeon, :workers) do - nil -> [] + nil -> + [] + workers -> - Enum.map(workers, fn({mod, fun}) -> + Enum.map(workers, fn {mod, fun} -> config = apply(mod, fun, []) worker(config) end) @@ -44,6 +54,7 @@ defmodule Pigeon do defp worker(%ADM.Config{} = config) do worker(ADM.Worker, [config], id: config.name, restart: :temporary) end + defp worker(config) do worker(Pigeon.Worker, [config], id: config.name, restart: :temporary) end @@ -53,7 +64,7 @@ defmodule Pigeon do end defp apns_workers do - workers_for(:apns, &APNS.Config.new/1, Pigeon.Worker) + workers_for(:apns, &APNS.ConfigParser.parse/1, Pigeon.Worker) end defp fcm_workers do @@ -62,9 +73,11 @@ defmodule Pigeon do defp workers_for(name, config_fn, mod) do case Application.get_env(:pigeon, name) do - nil -> [] + nil -> + [] + workers -> - Enum.map(workers, fn({worker_name, _config}) -> + Enum.map(workers, fn {worker_name, _config} -> config = config_fn.(worker_name) worker(mod, [config], id: config.name, restart: :temporary) end) @@ -73,7 +86,7 @@ defmodule Pigeon do @doc false def start_connection(state) do - opts = [restart: :temporary, id: :erlang.make_ref] + opts = [restart: :temporary, id: :erlang.make_ref()] spec = worker(Pigeon.Connection, [state], opts) Supervisor.start_child(:pigeon, spec) end diff --git a/lib/pigeon/adm.ex b/lib/pigeon/adm.ex index d65e1915..3a8342ed 100644 --- a/lib/pigeon/adm.ex +++ b/lib/pigeon/adm.ex @@ -26,7 +26,7 @@ defmodule Pigeon.ADM do n = Pigeon.ADM.Notification.new("token", %{"message" => "test"}) Pigeon.ADM.push(n, on_response: handler) """ - @type on_response :: ((Notification.t) -> no_return) + @type on_response :: (Notification.t() -> no_return) @typedoc ~S""" Options for sending push notifications. @@ -36,12 +36,13 @@ defmodule Pigeon.ADM do See `t:on_response/0` """ @type push_opts :: [ - to: atom | pid | nil, - on_response: on_response | nil - ] + to: atom | pid | nil, + on_response: on_response | nil + ] - @type connection_response :: {:ok, pid} - | {:error, {:already_started, pid}} + @type connection_response :: + {:ok, pid} + | {:error, {:already_started, pid}} @default_timeout 5_000 @@ -87,28 +88,30 @@ defmodule Pigeon.ADM do iex> notif.response :timeout """ - @spec push(Notification.t | [Notification.t], Keyword.t) :: no_return + @spec push(Notification.t() | [Notification.t()], Keyword.t()) :: no_return def push(notifications, opts \\ []) + def push(notifications, opts) when is_list(notifications) do - worker_name = opts[:to] || Config.default_name + worker_name = opts[:to] || Config.default_name() if Keyword.has_key?(opts, :on_response) do cast_push(worker_name, notifications, opts[:on_response]) else notifications - |> Enum.map(& Task.async(fn -> sync_push(worker_name, &1) end)) + |> Enum.map(&Task.async(fn -> sync_push(worker_name, &1) end)) |> Task.yield_many(@default_timeout + 500) |> Enum.map(fn {task, response} -> - case response do - nil -> Task.shutdown(task, :brutal_kill) - {:ok, resp} -> resp - _error -> nil - end - end) + case response do + nil -> Task.shutdown(task, :brutal_kill) + {:ok, resp} -> resp + _error -> nil + end + end) end end + def push(notification, opts) do - worker_name = opts[:to] || Config.default_name + worker_name = opts[:to] || Config.default_name() if Keyword.has_key?(opts, :on_response) do cast_push(worker_name, notification, opts[:on_response]) @@ -117,17 +120,19 @@ defmodule Pigeon.ADM do end end - defp cast_push(worker_name, notifications, on_response) when is_list(notifications) do + defp cast_push(worker_name, notifications, on_response) + when is_list(notifications) do for n <- notifications, do: cast_push(worker_name, n, on_response) end + defp cast_push(worker_name, notification, on_response) do GenServer.cast(worker_name, {:push, :adm, notification, on_response}) end defp sync_push(worker_name, notification) do pid = self() - ref = :erlang.make_ref - on_response = fn(x) -> send pid, {ref, x} end + ref = :erlang.make_ref() + on_response = fn x -> send(pid, {ref, x}) end GenServer.cast(worker_name, {:push, :adm, notification, on_response}) @@ -148,17 +153,19 @@ defmodule Pigeon.ADM do iex> Process.alive?(pid) true """ - @spec start_connection(atom | Config.t | Keyword.t) :: connection_response + @spec start_connection(atom | Config.t() | Keyword.t()) :: connection_response def start_connection(name) when is_atom(name) do config = Config.new(name) Supervisor.start_child(:pigeon, worker(Worker, [config], id: name)) end + def start_connection(%Config{} = config) do Worker.start_link(config) end + def start_connection(opts) when is_list(opts) do opts - |> Config.new + |> Config.new() |> start_connection() end diff --git a/lib/pigeon/adm/config.ex b/lib/pigeon/adm/config.ex index 0adf32a5..36586446 100644 --- a/lib/pigeon/adm/config.ex +++ b/lib/pigeon/adm/config.ex @@ -6,10 +6,10 @@ defmodule Pigeon.ADM.Config do defstruct name: nil, client_id: nil, client_secret: nil @type t :: %__MODULE__{ - client_id: String.t | nil, - client_secret: String.t | nil, - name: atom | nil - } + client_id: String.t() | nil, + client_secret: String.t() | nil, + name: atom | nil + } def default_name, do: :adm_default @@ -26,7 +26,7 @@ defmodule Pigeon.ADM.Config do %Pigeon.ADM.Config{name: :test, client_id: "amzn.client.id", client_secret: "1234secret"} """ - @spec new(Keyword.t | atom) :: t + @spec new(Keyword.t() | atom) :: t def new(opts) when is_list(opts) do %__MODULE__{ name: opts[:name], @@ -34,9 +34,10 @@ defmodule Pigeon.ADM.Config do client_secret: opts[:client_secret] } end + def new(name) when is_atom(name) do Application.get_env(:pigeon, :adm)[name] - |> Enum.to_list + |> Enum.to_list() |> Keyword.put(:name, name) |> new() end diff --git a/lib/pigeon/adm/notification.ex b/lib/pigeon/adm/notification.ex index 075805d7..e5219d49 100644 --- a/lib/pigeon/adm/notification.ex +++ b/lib/pigeon/adm/notification.ex @@ -29,14 +29,14 @@ defmodule Pigeon.ADM.Notification do } """ @type t :: %__MODULE__{ - consolidation_key: String.t, - expires_after: integer, - md5: binary, - payload: %{}, - registration_id: String.t, - response: response, - updated_registration_id: String.t - } + consolidation_key: String.t(), + expires_after: integer, + md5: binary, + payload: %{}, + registration_id: String.t(), + response: response, + updated_registration_id: String.t() + } @typedoc ~S""" ADM push response @@ -52,16 +52,17 @@ defmodule Pigeon.ADM.Notification do @typedoc ~S""" ADM error responses """ - @type error_response :: :access_token_expired - | :invalid_registration_id - | :invalid_data - | :invalid_consolidation_key - | :invalid_expiration - | :invalid_checksum - | :invalid_type - | :max_rate_exceeded - | :message_too_large - | :unregistered + @type error_response :: + :access_token_expired + | :invalid_registration_id + | :invalid_data + | :invalid_consolidation_key + | :invalid_expiration + | :invalid_checksum + | :invalid_type + | :max_rate_exceeded + | :message_too_large + | :unregistered @doc ~S""" Creates `ADM.Notification` struct with device registration ID and optional data payload. @@ -97,7 +98,7 @@ defmodule Pigeon.ADM.Notification do updated_registration_id: nil } """ - @spec new(String.t, %{required(String.t) => term}) :: t + @spec new(String.t(), %{required(String.t()) => term}) :: t def new(registration_id, data \\ %{}) do %Pigeon.ADM.Notification{registration_id: registration_id} |> put_data(data) @@ -130,6 +131,7 @@ defmodule Pigeon.ADM.Notification do payload = notification.payload |> Map.put(key, value) + %{notification | payload: payload} end @@ -139,20 +141,23 @@ defmodule Pigeon.ADM.Notification do |> Enum.map(fn {key, value} -> {"#{key}", "#{value}"} end) |> Enum.into(%{}) end + def ensure_strings(_else), do: %{} @doc false - def calculate_md5(%{payload: %{"data" => data}} = notification) when is_map(data) do + def calculate_md5(%{payload: %{"data" => data}} = notification) + when is_map(data) do concat = data - |> Map.keys - |> Enum.sort + |> Map.keys() + |> Enum.sort() |> Enum.map(fn key -> "#{key}:#{data[key]}" end) |> Enum.join(",") - md5 = :md5 |> :crypto.hash(concat) |> Base.encode64 + md5 = :md5 |> :crypto.hash(concat) |> Base.encode64() %{notification | md5: md5} end + def calculate_md5(notification), do: notification end diff --git a/lib/pigeon/adm/result_parser.ex b/lib/pigeon/adm/result_parser.ex index 08e6912c..48d1165b 100644 --- a/lib/pigeon/adm/result_parser.ex +++ b/lib/pigeon/adm/result_parser.ex @@ -25,7 +25,7 @@ defmodule Pigeon.ADM.ResultParser do end def parse(notification, %{"reason" => error}, on_response) do - error = error |> Macro.underscore |> String.to_existing_atom + error = error |> Macro.underscore() |> String.to_existing_atom() n = %{notification | response: error} on_response.(n) end diff --git a/lib/pigeon/adm/worker.ex b/lib/pigeon/adm/worker.ex index fb46687d..133d6fd3 100644 --- a/lib/pigeon/adm/worker.ex +++ b/lib/pigeon/adm/worker.ex @@ -24,13 +24,14 @@ defmodule Pigeon.ADM.Worker do def init({:ok, config}), do: initialize_worker(config) def initialize_worker(config) do - {:ok, %{ - config: config, - access_token: nil, - access_token_refreshed_datetime_erl: {{0, 0, 0}, {0, 0, 0}}, - access_token_expiration_seconds: 0, - access_token_type: nil - }} + {:ok, + %{ + config: config, + access_token: nil, + access_token_refreshed_datetime_erl: {{0, 0, 0}, {0, 0, 0}}, + access_token_expiration_seconds: 0, + access_token_type: nil + }} end def handle_cast(:stop, state) do @@ -42,6 +43,7 @@ defmodule Pigeon.ADM.Worker do {:ok, state} -> :ok = do_push(notification, state, on_response) {:noreply, state} + {:error, reason} -> notification = %{notification | response: reason} process_on_response(on_response, notification) @@ -59,14 +61,17 @@ defmodule Pigeon.ADM.Worker do cond do is_nil(access_token) -> refresh_access_token(state) + access_token_expired?(access_ref_dt_erl, access_ref_exp_secs) -> refresh_access_token(state) + true -> {:ok, state} end end defp access_token_expired?(_refreshed_datetime_erl, 0), do: true + defp access_token_expired?(refreshed_datetime_erl, expiration_seconds) do seconds_since(refreshed_datetime_erl) >= expiration_seconds end @@ -74,23 +79,28 @@ defmodule Pigeon.ADM.Worker do defp seconds_since(datetime_erl) do gregorian_seconds = datetime_erl - |> :calendar.datetime_to_gregorian_seconds + |> :calendar.datetime_to_gregorian_seconds() now_gregorian_seconds = - :os.timestamp - |> :calendar.now_to_universal_time - |> :calendar.datetime_to_gregorian_seconds + :os.timestamp() + |> :calendar.now_to_universal_time() + |> :calendar.datetime_to_gregorian_seconds() now_gregorian_seconds - gregorian_seconds end defp refresh_access_token(state) do - post = HTTPoison.post(@token_refresh_uri, - token_refresh_body(state), - token_refresh_headers()) + post = + HTTPoison.post( + @token_refresh_uri, + token_refresh_body(state), + token_refresh_headers() + ) + case post do {:ok, %{status_code: 200, body: response_body}} -> {:ok, response_json} = Poison.decode(response_body) + %{ "access_token" => access_token, "expires_in" => expiration_seconds, @@ -98,29 +108,34 @@ defmodule Pigeon.ADM.Worker do "token_type" => token_type } = response_json - now_datetime_erl = :os.timestamp |> :calendar.now_to_universal_time + now_datetime_erl = :os.timestamp() |> :calendar.now_to_universal_time() - {:ok, %{state | access_token: access_token, - access_token_refreshed_datetime_erl: now_datetime_erl, - access_token_expiration_seconds: expiration_seconds, - access_token_type: token_type}} + {:ok, + %{ + state + | access_token: access_token, + access_token_refreshed_datetime_erl: now_datetime_erl, + access_token_expiration_seconds: expiration_seconds, + access_token_type: token_type + }} {:ok, %{body: response_body}} -> {:ok, response_json} = Poison.decode(response_body) - Logger.error "Refresh token response: #{inspect response_json}" + Logger.error("Refresh token response: #{inspect(response_json)}") {:error, response_json["reason"]} end end - defp token_refresh_body(%{config: %{client_id: client_id, - client_secret: client_secret}}) do + defp token_refresh_body(%{ + config: %{client_id: client_id, client_secret: client_secret} + }) do %{ "grant_type" => "client_credentials", "scope" => "messaging:push", "client_id" => client_id, "client_secret" => client_secret } - |> URI.encode_query + |> URI.encode_query() end defp token_refresh_headers do @@ -133,11 +148,12 @@ defmodule Pigeon.ADM.Worker do response = case on_response do nil -> - fn({reg_id, payload}) -> + fn {reg_id, payload} -> HTTPoison.post(adm_uri(reg_id), payload, adm_headers(state)) end + _ -> - fn({reg_id, payload}) -> + fn {reg_id, payload} -> {:ok, %HTTPoison.Response{status_code: status, body: body}} = HTTPoison.post(adm_uri(reg_id), payload, adm_headers(state)) @@ -145,6 +161,7 @@ defmodule Pigeon.ADM.Worker do process_response(status, body, notification, on_response) end end + Task.Supervisor.start_child(Pigeon.Tasks, fn -> response.(request) end) :ok end @@ -154,11 +171,13 @@ defmodule Pigeon.ADM.Worker do end defp adm_headers(%{access_token: access_token, access_token_type: token_type}) do - [{"Authorization", "#{token_type} #{access_token}"}, - {"Content-Type", "application/json"}, - {"X-Amzn-Type-Version", "com.amazon.device.messaging.ADMMessage@1.0"}, - {"Accept", "application/json"}, - {"X-Amzn-Accept-Type", "com.amazon.device.messaging.ADMSendResult@1.0"}] + [ + {"Authorization", "#{token_type} #{access_token}"}, + {"Content-Type", "application/json"}, + {"X-Amzn-Type-Version", "com.amazon.device.messaging.ADMMessage@1.0"}, + {"Accept", "application/json"}, + {"X-Amzn-Accept-Type", "com.amazon.device.messaging.ADMSendResult@1.0"} + ] end defp encode_payload(notification) do @@ -166,26 +185,30 @@ defmodule Pigeon.ADM.Worker do |> put_consolidation_key(notification.consolidation_key) |> put_expires_after(notification.expires_after) |> put_md5(notification.md5) - |> Poison.encode! + |> Poison.encode!() end defp put_consolidation_key(payload, nil), do: payload + defp put_consolidation_key(payload, consolidation_key) do payload |> Map.put("consolidationKey", consolidation_key) end defp put_expires_after(payload, nil), do: payload + defp put_expires_after(payload, expires_after) do payload |> Map.put("expiresAfter", expires_after) end defp put_md5(payload, nil), do: payload + defp put_md5(payload, md5) do payload |> Map.put("md5", md5) end defp process_response(200, body, notification, on_response), do: handle_200_status(body, notification, on_response) + defp process_response(status, body, notification, on_response), do: handle_error_status_code(status, body, notification, on_response) @@ -198,6 +221,7 @@ defmodule Pigeon.ADM.Worker do case Poison.decode(body) do {:ok, %{"reason" => _reason} = result_json} -> parse_result(notification, result_json, on_response) + {:error, _} -> n = %{notification | response: generic_error_reason(status)} process_on_response(on_response, n) diff --git a/lib/pigeon/apns.ex b/lib/pigeon/apns.ex index fbd4b9e9..7b8a0c31 100644 --- a/lib/pigeon/apns.ex +++ b/lib/pigeon/apns.ex @@ -12,7 +12,7 @@ defmodule Pigeon.APNS do @typedoc ~S""" Can be either a single notification or a list. """ - @type notification :: Notification.t | [Notification.t, ...] + @type notification :: Notification.t() | [Notification.t(), ...] @typedoc ~S""" Async callback for push notification response. @@ -33,7 +33,7 @@ defmodule Pigeon.APNS do n = Pigeon.APNS.Notification.new("msg", "device token", "push topic") Pigeon.APNS.push(n, on_response: handler) """ - @type on_response :: ((Notification.t) -> no_return) + @type on_response :: (Notification.t() -> no_return) @typedoc ~S""" Options for sending push notifications. @@ -43,9 +43,9 @@ defmodule Pigeon.APNS do See `t:on_response/0` """ @type push_opts :: [ - to: atom | pid | nil, - on_response: on_response | nil - ] + to: atom | pid | nil, + on_response: on_response | nil + ] @default_timeout 5_000 @@ -73,25 +73,27 @@ defmodule Pigeon.APNS do response: :bad_device_token, id: nil, payload: %{"aps" => %{"alert" => "msg"}}, topic: "topic"}] """ - @spec push(notification, push_opts) :: {:ok, term} | {:error, term, term} + @spec push(notification, push_opts) :: notification | :ok def push(notification, opts \\ []) + def push(notification, opts) when is_list(notification) do if Keyword.has_key?(opts, :on_response) do push(notification, opts[:on_response], opts) :ok else notification - |> Enum.map(& Task.async(fn -> sync_push(&1, opts) end)) + |> Enum.map(&Task.async(fn -> sync_push(&1, opts) end)) |> Task.yield_many(@default_timeout + 500) |> Enum.map(fn {task, response} -> - case response do - nil -> Task.shutdown(task, :brutal_kill) - {:ok, resp} -> resp - _error -> nil - end - end) + case response do + nil -> Task.shutdown(task, :brutal_kill) + {:ok, resp} -> resp + _error -> nil + end + end) end end + def push(notification, opts) do if Keyword.has_key?(opts, :on_response) do push(notification, opts[:on_response], opts) @@ -104,8 +106,9 @@ defmodule Pigeon.APNS do defp push(notification, on_response, opts) when is_list(notification) do for n <- notification, do: push(n, on_response, opts) end + defp push(notification, on_response, opts) do - worker_name = opts[:to] || Config.default_name + worker_name = opts[:to] || Config.default_name() Worker.send_push(worker_name, notification, on_response: on_response) end @@ -119,17 +122,19 @@ defmodule Pigeon.APNS do iex> Process.alive?(pid) true """ - @spec start_connection(atom | Config.t | Keyword.t) :: {:ok, pid} + @spec start_connection(atom | Config.t() | Keyword.t()) :: {:ok, pid} def start_connection(name) when is_atom(name) do config = Config.new(name) Supervisor.start_child(:pigeon, worker(Pigeon.Worker, [config], id: name)) end - def start_connection(%Config{} = config) do + + def start_connection(%_{} = config) do Worker.start_link(config) end + def start_connection(opts) when is_list(opts) do opts - |> Config.new + |> Config.new() |> start_connection() end @@ -151,10 +156,10 @@ defmodule Pigeon.APNS do defp sync_push(notification, opts) do pid = self() - ref = :erlang.make_ref - on_response = fn(x) -> send pid, {ref, x} end + ref = :erlang.make_ref() + on_response = fn x -> send(pid, {ref, x}) end - worker_name = opts[:to] || Config.default_name + worker_name = opts[:to] || Config.default_name() Worker.send_push(worker_name, notification, on_response: on_response) receive do diff --git a/lib/pigeon/apns/config.ex b/lib/pigeon/apns/config.ex index a2c20170..d8d8162e 100644 --- a/lib/pigeon/apns/config.ex +++ b/lib/pigeon/apns/config.ex @@ -1,6 +1,6 @@ defmodule Pigeon.APNS.Config do - @moduledoc """ - Configuration for APNS Workers + @moduledoc ~S""" + Configuration for APNS Workers using certificates. """ defstruct name: nil, @@ -14,7 +14,7 @@ defmodule Pigeon.APNS.Config do ping_period: 600_000 @typedoc ~S""" - APNS configuration struct + Certificate APNS configuration struct This struct should not be set directly. Instead use `new/1` with `t:config_opts/0`. @@ -34,20 +34,21 @@ defmodule Pigeon.APNS.Config do } """ @type t :: %__MODULE__{ - name: atom | nil, - reconnect: boolean, - cert: binary | nil, - certfile: binary | nil, - key: binary | nil, - keyfile: binary | nil, - uri: binary | nil, - port: pos_integer, - ping_period: pos_integer - } + name: atom | nil, + reconnect: boolean, + cert: binary | nil, + certfile: binary | nil, + key: binary | nil, + keyfile: binary | nil, + uri: binary | nil, + port: pos_integer, + ping_period: pos_integer + } @typedoc ~S""" - Options for configuring APNS connections. + Options for configuring certificate APNS connections. + ## Configuration Options - `:name` - Registered worker name. - `:mode` - If set to `:dev` or `:prod`, will set the appropriate `:uri` - `:cert` - Push certificate. Can be one of three options: @@ -62,20 +63,25 @@ defmodule Pigeon.APNS.Config do `443` and `2197` - `:ping_period` - Interval between server pings. Necessary to keep long running APNS connections alive. Defaults to 10 minutes. + + ## Deprecated Options + - `:reconnect` - No longer used as of `v1.2.0`. """ @type config_opts :: [ - name: atom | nil, - mode: :dev | :prod | nil, - cert: binary | {atom, binary}, - key: binary | {atom, binary}, - reconnect: boolean, - ping_period: pos_integer, - port: pos_integer, - uri: binary - ] - - @apns_production_api_uri "api.push.apple.com" - @apns_development_api_uri "api.development.push.apple.com" + name: atom | nil, + mode: :dev | :prod | nil, + cert: binary | {atom, binary}, + key: binary | {atom, binary}, + reconnect: boolean, + ping_period: pos_integer, + port: pos_integer, + uri: binary, + jwt_key: binary | {atom, binary}, + jwt_key_identifier: binary | nil, + jwt_team_id: binary | nil + ] + + alias Pigeon.APNS.ConfigParser @doc false def default_name, do: :apns_default @@ -83,7 +89,7 @@ defmodule Pigeon.APNS.Config do @doc ~S""" Returns a new `APNS.Config` with given `opts` or name. - If given an atom, returns the config specified in your `mix.exs`. + If given an atom, returns the config specified in your `config.exs`. ## Examples @@ -92,7 +98,6 @@ defmodule Pigeon.APNS.Config do ...> mode: :prod, ...> cert: "test_cert.pem", ...> key: "test_key.pem", - ...> reconnect: false, ...> port: 2197, ...> ping_period: 300_000 ...> ) @@ -101,181 +106,91 @@ defmodule Pigeon.APNS.Config do iex> config = Pigeon.APNS.Config.new(:apns_default) iex> %{config | certfile: nil, keyfile: nil} # Hide for testing - %Pigeon.APNS.Config{uri: "api.development.push.apple.com", - name: :apns_default, ping_period: 600_000, port: 443, reconnect: false} + iex> match? %_{uri: "api.development.push.apple.com", + ...> name: :apns_default, ping_period: 600_000, port: 443}, config + true """ - @spec new(atom | config_opts) :: t def new(opts) when is_list(opts) do %__MODULE__{ name: opts[:name], reconnect: Keyword.get(opts, :reconnect, false), - cert: cert(opts[:cert]), - certfile: file_path(opts[:cert]), - key: key(opts[:key]), - keyfile: file_path(opts[:key]), - uri: Keyword.get(opts, :uri, uri_for_mode(opts[:mode])), + cert: ConfigParser.cert(opts[:cert]), + certfile: ConfigParser.file_path(opts[:cert]), + key: ConfigParser.key(opts[:key]), + keyfile: ConfigParser.file_path(opts[:key]), + uri: Keyword.get(opts, :uri, ConfigParser.uri_for_mode(opts[:mode])), port: Keyword.get(opts, :port, 443), ping_period: Keyword.get(opts, :ping_period, 600_000) } end - def new(name) when is_atom(name) do - Application.get_env(:pigeon, :apns)[name] - |> Enum.to_list - |> Keyword.put(:name, name) - |> new() - end - - defp uri_for_mode(:dev), do: @apns_development_api_uri - defp uri_for_mode(:prod), do: @apns_production_api_uri - defp uri_for_mode(_else), do: nil - @doc false - def file_path(nil), do: nil - def file_path(path) when is_binary(path) do - if :filelib.is_file(path), do: Path.expand(path), else: nil - end - def file_path({app_name, path}) when is_atom(app_name), - do: Path.expand(path, :code.priv_dir(app_name)) - - @doc false - def cert({_app_name, _path}), do: nil - def cert(nil), do: nil - def cert(bin) do - case :public_key.pem_decode(bin) do - [{:Certificate, cert, _}] -> cert - _ -> nil - end - end - - @doc false - def key({_app_name, _path}), do: nil - def key(nil), do: nil - def key(bin) do - case :public_key.pem_decode(bin) do - [{:RSAPrivateKey, key, _}] -> {:RSAPrivateKey, key} - _ -> nil - end - end + def new(name) when is_atom(name), do: ConfigParser.parse(name) end defimpl Pigeon.Configurable, for: Pigeon.APNS.Config do @moduledoc false - import Pigeon.Tasks, only: [process_on_response: 2] - - alias Pigeon.APNS.{Config, Error} + alias Pigeon.APNS.Shared @type sock :: {:sslsocket, any, pid | {any, any}} # Configurable Callbacks - @spec worker_name(any) :: atom | nil - def worker_name(%Config{name: name}), do: name + defdelegate worker_name(any), to: Shared - @spec max_demand(any) :: non_neg_integer - def max_demand(_config), do: 1_000 + defdelegate max_demand(any), to: Shared - @spec connect(any) :: {:ok, sock} | {:error, String.t} - def connect(%Config{uri: uri} = config) do + @spec connect(any) :: {:ok, sock} | {:error, String.t()} + def connect(%{uri: uri} = config) do uri = to_charlist(uri) + case connect_socket_options(config) do {:ok, options} -> Pigeon.Http2.Client.default().connect(uri, :https, options) - error -> error - end - end - def push_headers(_config, notification, _opts) do - json = Poison.encode!(notification.payload) - - [ - {":method", "POST"}, - {":path", "/3/device/#{notification.device_token}"}, - {"content-length", "#{byte_size(json)}"} - ] - |> put_apns_id(notification) - |> put_apns_topic(notification) - end - - def push_payload(_config, notification, _opts) do - Poison.encode!(notification.payload) - end - - defp put_apns_id(headers, notification) do - case notification.id do - nil -> headers - id -> headers ++ [{"apns-id", id}] + error -> + error end end - defp put_apns_topic(headers, notification) do - case notification.topic do - nil -> headers - topic -> headers ++ [{"apns-topic", topic}] - end - end + defdelegate push_headers(config, notification, opts), to: Shared - def handle_end_stream(_config, - %{headers: headers, body: body, status: status}, - notification, - on_response) do - case status do - 200 -> - n = %{notification | id: get_apns_id(headers), response: :success} - process_on_response(on_response, n) - _error -> - reason = Error.parse(body) - Error.log(reason, notification) - notification = %{notification | response: reason} - process_on_response(on_response, notification) - end - end + defdelegate push_payload(config, notification, opts), to: Shared - def get_apns_id(headers) do - case Enum.find(headers, fn({key, _val}) -> key == "apns-id" end) do - {"apns-id", id} -> id - nil -> nil - end - end - - @spec schedule_ping(any) :: no_return - def schedule_ping(%Config{ping_period: ping}) do - Process.send_after(self(), :ping, ping) - end + defdelegate handle_end_stream(config, stream, notification, on_response), + to: Shared - def close(_config) do - end + defdelegate schedule_ping(any), to: Shared - # Everything Else + defdelegate close(config), to: Shared - def connect_socket_options(%Config{cert: nil, certfile: nil}) do + def connect_socket_options(%{cert: nil, certfile: nil}) do {:error, :invalid_config} end - def connect_socket_options(%Config{key: nil, keyfile: nil}) do + + def connect_socket_options(%{key: nil, keyfile: nil}) do {:error, :invalid_config} end + def connect_socket_options(config) do - options = [ - cert_option(config), - key_option(config), - {:password, ''}, - {:packet, 0}, - {:reuseaddr, true}, - {:active, true}, - {:reconnect, config.reconnect}, - :binary - ] - |> add_port(config) + options = + [ + cert_option(config), + key_option(config), + {:password, ''}, + {:packet, 0}, + {:reuseaddr, true}, + {:active, true}, + :binary + ] + |> Shared.add_port(config) {:ok, options} end - def cert_option(%Config{cert: cert, certfile: nil}), do: {:cert, cert} - def cert_option(%Config{cert: nil, certfile: file}), do: {:certfile, file} - - def key_option(%Config{key: key, keyfile: nil}), do: {:key, key} - def key_option(%Config{key: nil, keyfile: file}), do: {:keyfile, file} + def cert_option(%{cert: cert, certfile: nil}), do: {:cert, cert} + def cert_option(%{cert: nil, certfile: file}), do: {:certfile, file} - defp add_port(opts, %Config{port: 443}), do: opts - defp add_port(opts, %Config{port: port}), do: [{:port, port} | opts] + def key_option(%{key: key, keyfile: nil}), do: {:key, key} + def key_option(%{key: nil, keyfile: file}), do: {:keyfile, file} end diff --git a/lib/pigeon/apns/config_parser.ex b/lib/pigeon/apns/config_parser.ex new file mode 100644 index 00000000..5d2e78b4 --- /dev/null +++ b/lib/pigeon/apns/config_parser.ex @@ -0,0 +1,79 @@ +defmodule Pigeon.APNS.ConfigParser do + @moduledoc false + + alias Pigeon.APNS.{Config, JWTConfig} + + @type config_opts :: [ + name: atom | nil, + mode: :dev | :prod | nil, + cert: binary | {atom, binary}, + key: binary | {atom, binary}, + reconnect: boolean, + ping_period: pos_integer, + port: pos_integer, + uri: binary, + key_identifier: binary | nil, + team_id: binary | nil + ] + + @type config :: Config.t() | JWTConfig.t() + + @apns_production_api_uri "api.push.apple.com" + @apns_development_api_uri "api.development.push.apple.com" + + @spec parse(atom | config_opts) :: config | {:error, :invalid_config} + def parse(opts) when is_list(opts) do + case config_type(Enum.into(opts, %{})) do + :error -> raise "invalid apns configuration #{inspect(opts)}" + type -> type.new(opts) + end + end + + def parse(name) when is_atom(name) do + Application.get_env(:pigeon, :apns)[name] + |> Enum.to_list() + |> Keyword.put(:name, name) + |> parse() + end + + defp config_type(%{cert: _cert, key_identifier: _key_id}), do: :error + defp config_type(%{cert: _cert}), do: Config + defp config_type(%{key_identifier: _jwt_key}), do: JWTConfig + defp config_type(_else), do: :error + + @doc false + def file_path(nil), do: nil + + def file_path(path) when is_binary(path) do + if :filelib.is_file(path), do: Path.expand(path), else: nil + end + + def file_path({app_name, path}) when is_atom(app_name), + do: Path.expand(path, :code.priv_dir(app_name)) + + @doc false + def cert({_app_name, _path}), do: nil + def cert(nil), do: nil + + def cert(bin) do + case :public_key.pem_decode(bin) do + [{:Certificate, cert, _}] -> cert + _ -> nil + end + end + + @doc false + def key({_app_name, _path}), do: nil + def key(nil), do: nil + + def key(bin) do + case :public_key.pem_decode(bin) do + [{:RSAPrivateKey, key, _}] -> {:RSAPrivateKey, key} + _ -> nil + end + end + + def uri_for_mode(:dev), do: @apns_development_api_uri + def uri_for_mode(:prod), do: @apns_production_api_uri + def uri_for_mode(_else), do: nil +end diff --git a/lib/pigeon/apns/error.ex b/lib/pigeon/apns/error.ex index cdfe81fc..d4ef881c 100644 --- a/lib/pigeon/apns/error.ex +++ b/lib/pigeon/apns/error.ex @@ -7,79 +7,86 @@ defmodule Pigeon.APNS.Error do alias Pigeon.APNS.Notification - @type error_response :: :bad_collapse_id - | :bad_device_token - | :bad_expiration_date - | :bad_message_id - | :bad_priority - | :bad_topic - | :device_token_not_for_topic - | :duplicate_headers - | :idle_timeout - | :missing_device_token - | :missing_topic - | :payload_empty - | :topic_disallowed - | :bad_certificate - | :bad_certificate_environment - | :expired_provider_token - | :forbidden - | :invalid_provider_token - | :missing_provider_token - | :bad_path - | :method_not_allowed - | :unregistered - | :payload_too_large - | :too_many_provider_token_updates - | :too_many_requests - | :internal_server_error - | :service_unavailable - | :shutdown + @type error_response :: + :bad_collapse_id + | :bad_device_token + | :bad_expiration_date + | :bad_message_id + | :bad_priority + | :bad_topic + | :device_token_not_for_topic + | :duplicate_headers + | :idle_timeout + | :missing_device_token + | :missing_topic + | :payload_empty + | :topic_disallowed + | :bad_certificate + | :bad_certificate_environment + | :expired_provider_token + | :forbidden + | :invalid_provider_token + | :missing_provider_token + | :bad_path + | :method_not_allowed + | :unregistered + | :payload_too_large + | :too_many_provider_token_updates + | :too_many_requests + | :internal_server_error + | :service_unavailable + | :shutdown @doc false @spec parse(binary) :: error_response def parse(data) do {:ok, response} = Poison.decode(data) - response["reason"] |> Macro.underscore |> String.to_existing_atom + response["reason"] |> Macro.underscore() |> String.to_existing_atom() end @doc ~S""" If enabled, logs a notification and its error response. """ - @spec log(error_response, Notification.t) :: :ok + @spec log(error_response, Notification.t()) :: :ok def log(reason, notification) do - if Pigeon.debug_log? do + if Pigeon.debug_log?() do Logger.error("#{reason}: #{msg(reason)}\n#{inspect(notification)}") end end @doc false - @spec msg(error_response) :: String.t + @spec msg(error_response) :: String.t() # 400 def msg(:bad_collapse_id) do "The collapse identifier exceeds the maximum allowed size" end + def msg(:bad_device_token) do """ The specified device token was bad. Verify that the request contains a valid token and that the token matches the environment. """ end + def msg(:bad_expiration_date), do: "The apns-expiration value is bad." def msg(:bad_message_id), do: "The apns-id value is bad." def msg(:bad_priority), do: "The apns-priority value is bad." def msg(:bad_topic), do: "The apns-topic was invalid." + def msg(:device_token_not_for_topic) do "The device token does not match the specified topic." end + def msg(:duplicate_headers), do: "One or more headers were repeated." def msg(:idle_timeout), do: "Idle time out." + def msg(:missing_device_token) do """ The device token is not specified in the request :path. Verify that the :path header contains the device token. """ end + def msg(:missing_topic) do """ The apns-topic header of the request was not specified and was required. @@ -87,26 +94,32 @@ defmodule Pigeon.APNS.Error do certificate that supports multiple topics. """ end + def msg(:payload_empty), do: "The message payload was empty." def msg(:topic_disallowed), do: "Pushing to this topic is not allowed." # 403 def msg(:bad_certificate), do: "The certificate was bad." + def msg(:bad_certificate_environment) do "The client certificate was for the wrong environment." end + def msg(:expired_provider_token) do "The provider token is stale and a new token should be generated." end + def msg(:forbidden) do "The specified action is not allowed." end + def msg(:invalid_provider_token) do """ The provider token is not valid or the token signature could not be verified." """ end + def msg(:missing_provider_token) do """ No provider certificate was used to connect to APNs and Authorization @@ -132,10 +145,12 @@ defmodule Pigeon.APNS.Error do 4096 bytes. """ end + # 429 def msg(:too_many_provider_token_updates) do "The provider token is being updated too often." end + def msg(:too_many_requests) do "Too many requests were made consecutively to the same device token." end diff --git a/lib/pigeon/apns/jwt_config.ex b/lib/pigeon/apns/jwt_config.ex new file mode 100644 index 00000000..a3fff164 --- /dev/null +++ b/lib/pigeon/apns/jwt_config.ex @@ -0,0 +1,239 @@ +defmodule Pigeon.APNS.JWTConfig do + @moduledoc """ + Configuration for APNS Workers using JWT. + """ + + defstruct name: nil, + reconnect: true, + uri: nil, + port: 443, + ping_period: 600_000, + key: nil, + keyfile: nil, + key_identifier: nil, + team_id: nil + + alias Pigeon.APNS.{Config, ConfigParser} + + @typedoc ~S""" + JWT APNS configuration struct + + This struct should not be set directly. Instead use `new/1` + with `t:config_opts/0`. + + ## Examples + + %Pigeon.APNS.JWTConfig{ + name: :apns_default, + reconnect: true, + uri: "api.push.apple.com", + port: 443, + ping_period: 600_000, + key: nil, + keyfile: "key.p8", + key_identifier: "ABC1234567", + team_id: "DEF1234567" + } + """ + @type t :: %__MODULE__{ + name: atom | nil, + reconnect: boolean, + uri: binary | nil, + port: pos_integer, + ping_period: pos_integer, + key: binary | nil, + keyfile: binary | nil, + key_identifier: binary | nil, + team_id: binary | nil + } + + @typedoc ~S""" + Options for configuring JWT APNS connections. + + ## Configuration Options + - `:name` - Registered worker name. + - `:mode` - If set to `:dev` or `:prod`, will set the appropriate `:uri` + - `:key` - JWT private key. Can be one of three options: + - Static file path + - Full-text string of the file contents (useful for environment variables) + - `{:my_app, "keys/private_key.p8"}` (indicates path relative to the `priv` + folder of the given application) + - `:key_identifier` - A 10-character key identifier (kid) key, obtained from + your developer account + - `:team_id` - Your 10-character Team ID, obtained from your developer account + - `:uri` - Push server uri. If set, overrides uri defined by `:mode`. + Useful for test environments. + - `:port` - Push server port. Can be any value, but APNS only accepts + `443` and `2197` + - `:ping_period` - Interval between server pings. Necessary to keep long + running APNS connections alive. Defaults to 10 minutes. + + ## Deprecated Options + - `:reconnect` - No longer used as of `v1.2.0`. + """ + @type config_opts :: [ + name: atom | nil, + mode: :dev | :prod | nil, + key: binary | {atom, binary}, + key_identifier: binary | nil, + team_id: binary | nil, + reconnect: boolean, + ping_period: pos_integer, + port: pos_integer, + uri: binary + ] + + @doc ~S""" + Returns a new `APNS.JWTConfig` with given `opts` or name. + + If given an atom, returns the config specified in your `config.exs`. + + ## Examples + + iex> Pigeon.APNS.JWTConfig.new( + ...> name: :test, + ...> mode: :prod, + ...> key: "key.p8", + ...> key_identifier: "ABC1234567", + ...> team_id: "DEF1234567", + ...> port: 2197, + ...> ping_period: 300_000 + ...> ) + %Pigeon.APNS.JWTConfig{uri: "api.push.apple.com", name: :test, + team_id: "DEF1234567", key_identifier: "ABC1234567", key: "key.p8", + ping_period: 300000, port: 2197, reconnect: false} + + iex> config = Pigeon.APNS.JWTConfig.new(:apns_jwt_static) + iex> %{config | key: nil, key_identifier: nil, team_id: nil} # Hide for testing + iex> match? %_{uri: "api.development.push.apple.com", name: :apns_jwt_static, + ...> ping_period: 600_000, port: 443, reconnect: false}, config + true + """ + def new(opts) when is_list(opts) do + %__MODULE__{ + name: opts[:name], + reconnect: Keyword.get(opts, :reconnect, false), + uri: Keyword.get(opts, :uri, ConfigParser.uri_for_mode(opts[:mode])), + port: Keyword.get(opts, :port, 443), + ping_period: Keyword.get(opts, :ping_period, 600_000), + key: opts[:key], + keyfile: ConfigParser.file_path(opts[:key]), + key_identifier: Keyword.get(opts, :key_identifier), + team_id: Keyword.get(opts, :team_id) + } + end + + def new(name) when is_atom(name), do: ConfigParser.parse(name) +end + +defimpl Pigeon.Configurable, for: Pigeon.APNS.JWTConfig do + @moduledoc false + + alias Pigeon.APNS.{Config, JWTConfig, Notification, Shared} + + @type sock :: {:sslsocket, any, pid | {any, any}} + + # Seconds + @token_max_age 3_590 + + # Configurable Callbacks + + defdelegate worker_name(any), to: Shared + + defdelegate max_demand(any), to: Shared + + @spec connect(any) :: {:ok, sock} | {:error, String.t()} + def connect(%{uri: uri} = config) do + uri = to_charlist(uri) + + case connect_socket_options(config) do + {:ok, options} -> + Pigeon.Http2.Client.default().connect(uri, :https, options) + + error -> + error + end + end + + @spec push_headers(JWTConfig.t(), Notification.t(), Keyword.t()) :: + Shared.headers() + def push_headers(config, notification, opts) do + config + |> Shared.push_headers(notification, opts) + |> put_bearer_token(config) + end + + defdelegate push_payload(config, notification, opts), to: Shared + + defdelegate handle_end_stream(config, stream, notification, on_response), + to: Shared + + defdelegate schedule_ping(any), to: Shared + + defdelegate close(config), to: Shared + + def connect_socket_options(%{key: nil}) do + {:error, :invalid_config} + end + + def connect_socket_options(%{key: _jwt_key} = config) do + options = + [ + {:packet, 0}, + {:reuseaddr, true}, + {:active, true}, + :binary + ] + |> Shared.add_port(config) + + {:ok, options} + end + + @spec put_bearer_token(Config.headers(), JWTConfig.t()) :: Config.headers() + defp put_bearer_token(headers, %{key: nil}), do: headers + + defp put_bearer_token(headers, config) do + token_storage_key = config.key_identifier <> ":" <> config.team_id + {timestamp, saved_token} = Pigeon.APNS.Token.get(token_storage_key) + now = :os.system_time(:seconds) + + token = + case now - timestamp do + age when age < @token_max_age -> saved_token + _ -> generate_apns_jwt(config, token_storage_key) + end + + [{"authorization", "bearer " <> token} | headers] + end + + @spec generate_apns_jwt(JWTConfig.t(), String.t()) :: String.t() + defp generate_apns_jwt(config, token_storage_key) do + import Joken + + key = get_token_key(config) + + now = :os.system_time(:seconds) + + token = + token() + |> with_claims(%{"iss" => config.team_id, "iat" => now}) + |> with_header_arg("alg", "ES256") + |> with_header_arg("typ", "JWT") + |> with_header_arg("kid", config.key_identifier) + |> sign(es256(key)) + |> get_compact + + :ok = Pigeon.APNS.Token.update(token_storage_key, {now, token}) + + token + end + + @spec get_token_key(JWTConfig.t()) :: JOSE.JWK.t() + defp get_token_key(%JWTConfig{keyfile: nil} = config) do + JOSE.JWK.from_pem(config.key) + end + + defp get_token_key(%JWTConfig{keyfile: file}) do + JOSE.JWK.from_pem_file(file) + end +end diff --git a/lib/pigeon/apns/notification.ex b/lib/pigeon/apns/notification.ex index b418863a..4f467411 100644 --- a/lib/pigeon/apns/notification.ex +++ b/lib/pigeon/apns/notification.ex @@ -27,13 +27,13 @@ defmodule Pigeon.APNS.Notification do } """ @type t :: %__MODULE__{ - device_token: String.t | nil, - expiration: String.t | nil, - id: String.t | nil, - payload: %{String.t => String.t}, - response: response, - topic: String.t | nil - } + device_token: String.t() | nil, + expiration: String.t() | nil, + id: String.t() | nil, + payload: %{String.t() => String.t()}, + response: response, + topic: String.t() | nil + } @typedoc ~S""" APNS push response @@ -44,7 +44,7 @@ defmodule Pigeon.APNS.Notification do server responded with error - `:timeout` - Internal error. Push did not reach APNS servers """ - @type response :: nil | :success | Error.error_response | :timeout + @type response :: nil | :success | Error.error_response() | :timeout @doc """ Returns an `APNS.Notification` struct with given message, device token, and @@ -63,7 +63,7 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec new(String.t, String.t, String.t | nil) :: t + @spec new(String.t(), String.t(), String.t() | nil) :: t def new(msg, token, topic \\ nil) do %Notification{ device_token: token, @@ -89,7 +89,7 @@ defmodule Pigeon.APNS.Notification do topic: "topic" } """ - @spec new(String.t, String.t, String.t, String.t) :: t + @spec new(String.t(), String.t(), String.t(), String.t()) :: t def new(msg, token, topic, id) do %Notification{ device_token: token, @@ -115,8 +115,9 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec put_alert(t, String.t) :: t - def put_alert(notification, alert), do: update_payload(notification, "alert", alert) + @spec put_alert(t, String.t()) :: t + def put_alert(notification, alert), + do: update_payload(notification, "alert", alert) @doc """ Updates `"badge"` key in push payload. @@ -135,7 +136,8 @@ defmodule Pigeon.APNS.Notification do } """ @spec put_badge(t, integer) :: t - def put_badge(notification, badge), do: update_payload(notification, "badge", badge) + def put_badge(notification, badge), + do: update_payload(notification, "badge", badge) @doc """ Updates `"sound"` key in push payload. @@ -154,8 +156,9 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec put_sound(t, String.t) :: t - def put_sound(notification, sound), do: update_payload(notification, "sound", sound) + @spec put_sound(t, String.t()) :: t + def put_sound(notification, sound), + do: update_payload(notification, "sound", sound) @doc """ Sets `"content-available"` flag in push payload. @@ -175,7 +178,8 @@ defmodule Pigeon.APNS.Notification do } """ @spec put_content_available(t) :: t - def put_content_available(notification), do: update_payload(notification, "content-available", 1) + def put_content_available(notification), + do: update_payload(notification, "content-available", 1) @doc """ Updates `"category"` key in push payload. @@ -191,8 +195,9 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec put_category(t, String.t) :: t - def put_category(notification, category), do: update_payload(notification, "category", category) + @spec put_category(t, String.t()) :: t + def put_category(notification, category), + do: update_payload(notification, "category", category) @doc """ Sets `"mutable-content"` flag in push payload. @@ -211,13 +216,15 @@ defmodule Pigeon.APNS.Notification do } """ @spec put_mutable_content(t) :: t - def put_mutable_content(notification), do: update_payload(notification, "mutable-content", 1) + def put_mutable_content(notification), + do: update_payload(notification, "mutable-content", 1) defp update_payload(notification, key, value) do new_aps = notification.payload |> Map.get("aps") |> Map.put(key, value) + new_payload = notification.payload |> Map.put("aps", new_aps) %{notification | payload: new_payload} end @@ -237,7 +244,7 @@ defmodule Pigeon.APNS.Notification do topic: nil } """ - @spec put_custom(t, %{String.t => String.t}) :: t + @spec put_custom(t, %{String.t() => String.t()}) :: t def put_custom(notification, data) do new_payload = Map.merge(notification.payload, data) %{notification | payload: new_payload} diff --git a/lib/pigeon/apns/shared.ex b/lib/pigeon/apns/shared.ex new file mode 100644 index 00000000..2de5e7ba --- /dev/null +++ b/lib/pigeon/apns/shared.ex @@ -0,0 +1,84 @@ +defmodule Pigeon.APNS.Shared do + @moduledoc false + + import Pigeon.Tasks, only: [process_on_response: 2] + + alias Pigeon.APNS.{Config, Error, JWTConfig, Notification} + + @type config :: Config.t() | JWTConfig.t() + + @type headers :: [{binary(), any()}] + + @spec worker_name(any) :: atom | nil + def worker_name(%{name: name}), do: name + + @spec max_demand(any) :: non_neg_integer + def max_demand(_config), do: 1_000 + + @spec push_headers(Config.t(), Notification.t(), Keyword.t()) :: headers() + def push_headers(_config, notification, _opts) do + json = Poison.encode!(notification.payload) + + [ + {":method", "POST"}, + {":path", "/3/device/#{notification.device_token}"}, + {"content-length", "#{byte_size(json)}"} + ] + |> put_apns_id(notification) + |> put_apns_topic(notification) + end + + @spec push_payload(config, Notification.t(), Keyword.t()) :: + iodata | no_return + def push_payload(_config, notification, _opts) do + Poison.encode!(notification.payload) + end + + def handle_end_stream(_config, stream, notification, on_response) do + %{headers: headers, body: body, status: status} = stream + + case status do + 200 -> + n = %{notification | id: get_apns_id(headers), response: :success} + process_on_response(on_response, n) + + _error -> + reason = Error.parse(body) + Error.log(reason, notification) + notification = %{notification | response: reason} + process_on_response(on_response, notification) + end + end + + @spec schedule_ping(any) :: no_return + def schedule_ping(%{ping_period: ping}) do + Process.send_after(self(), :ping, ping) + end + + def close(_config) do + end + + def put_apns_id(headers, notification) do + case notification.id do + nil -> headers + id -> headers ++ [{"apns-id", id}] + end + end + + def put_apns_topic(headers, notification) do + case notification.topic do + nil -> headers + topic -> headers ++ [{"apns-topic", topic}] + end + end + + def get_apns_id(headers) do + case Enum.find(headers, fn {key, _val} -> key == "apns-id" end) do + {"apns-id", id} -> id + nil -> nil + end + end + + def add_port(opts, %{port: 443}), do: opts + def add_port(opts, %{port: port}), do: [{:port, port} | opts] +end diff --git a/lib/pigeon/apns/token.ex b/lib/pigeon/apns/token.ex new file mode 100644 index 00000000..cb02f8a0 --- /dev/null +++ b/lib/pigeon/apns/token.ex @@ -0,0 +1,20 @@ +defmodule Pigeon.APNS.Token do + @moduledoc false + + @type t :: {non_neg_integer(), binary() | nil} + + @spec start_link((() -> any())) :: Agent.on_start() + def start_link(_) do + Agent.start_link(fn -> %{} end, name: __MODULE__) + end + + @spec get(String.t()) :: t + def get(name) do + Agent.get(__MODULE__, &Map.get(&1, name, {0, nil})) + end + + @spec update(String.t(), t) :: :ok + def update(name, token) do + Agent.update(__MODULE__, &Map.put(&1, name, token)) + end +end diff --git a/lib/pigeon/configurable.ex b/lib/pigeon/configurable.ex index b7c04303..ce2cb30d 100644 --- a/lib/pigeon/configurable.ex +++ b/lib/pigeon/configurable.ex @@ -15,7 +15,7 @@ defprotocol Pigeon.Configurable do @spec worker_name(any) :: atom | nil def worker_name(config) - @spec connect(any) :: {:ok, sock} | {:error, String.t} + @spec connect(any) :: {:ok, sock} | {:error, String.t()} def connect(config) def push_headers(config, notification, opts) diff --git a/lib/pigeon/connection.ex b/lib/pigeon/connection.ex index 14baa1f0..f0f04adb 100644 --- a/lib/pigeon/connection.ex +++ b/lib/pigeon/connection.ex @@ -9,7 +9,7 @@ defmodule Pigeon.Connection do requested: 0, socket: nil, stream_id: 1, - queue: NotificationQueue.new + queue: NotificationQueue.new() use GenStage require Logger @@ -22,10 +22,12 @@ defmodule Pigeon.Connection do def handle_subscribe(:producer, _opts, from, state) do demand = Configurable.max_demand(state.config) GenStage.ask(from, demand) + state = state |> inc_requested(demand) |> Map.put(:from, from) + {:manual, state} end @@ -35,15 +37,19 @@ defmodule Pigeon.Connection do def init({config, from}) do state = %Connection{config: config, from: from} + case connect_socket(config, 0) do {:ok, socket} -> Configurable.schedule_ping(config) {:consumer, %{state | socket: socket}, subscribe_to: [from]} - {:error, reason} -> {:stop, reason} + + {:error, reason} -> + {:stop, reason} end end def connect_socket(_config, 3), do: {:error, :timeout} + def connect_socket(config, tries) do case Configurable.connect(config) do {:ok, socket} -> {:ok, socket} @@ -88,21 +94,24 @@ defmodule Pigeon.Connection do def handle_events(events, _from, state) do state = - Enum.reduce(events, state, fn({:push, notif, opts}, state) -> + Enum.reduce(events, state, fn {:push, notif, opts}, state -> send_push(state, notif, opts) end) {:noreply, [], state} end - def process_end_stream(%Stream{id: stream_id} = stream, - %{queue: queue, config: config} = state) do + 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, on_response}, new_queue} -> Configurable.handle_end_stream(config, stream, notif, on_response) + state = state |> inc_completed(1) @@ -111,6 +120,7 @@ defmodule Pigeon.Connection do total_requests = state.completed + state.requested max_demand = Configurable.max_demand(state.config) + state = if total_requests < @limit and state.requested < max_demand do to_ask = min(@limit - total_requests, max_demand - state.requested) @@ -123,6 +133,7 @@ defmodule Pigeon.Connection do if state.completed >= @limit do GenStage.cancel(state.from, :stream_id_exhausted) end + {:noreply, [], state} end end @@ -133,10 +144,13 @@ defmodule Pigeon.Connection do Client.default().send_request(state.socket, headers, payload) - new_q = NotificationQueue.add(queue, - state.stream_id, - notification, - opts[:on_response]) + new_q = + NotificationQueue.add( + queue, + state.stream_id, + notification, + opts[:on_response] + ) state |> inc_stream_id() diff --git a/lib/pigeon/exceptions.ex b/lib/pigeon/exceptions.ex new file mode 100644 index 00000000..9cb18172 --- /dev/null +++ b/lib/pigeon/exceptions.ex @@ -0,0 +1,10 @@ +defmodule Pigeon.ConfigError do + @moduledoc """ + This error represents configuration errors: for example, configuring + both `:cert` and `:jwt_key` for APNS. + """ + + defexception [:message] + + @type t :: %__MODULE__{message: binary} +end diff --git a/lib/pigeon/fcm.ex b/lib/pigeon/fcm.ex index 77add119..b46e854b 100644 --- a/lib/pigeon/fcm.ex +++ b/lib/pigeon/fcm.ex @@ -9,6 +9,11 @@ defmodule Pigeon.FCM do alias Pigeon.FCM.{Config, Notification} alias Pigeon.Worker + @typedoc ~S""" + Can be either a single notification or a list. + """ + @type notification :: Notification.t() | [Notification.t(), ...] + @typedoc ~S""" Async callback for push notification response. @@ -30,7 +35,7 @@ defmodule Pigeon.FCM do n = Pigeon.FCM.Notification.new("device token", %{}, %{"message" => "test"}) Pigeon.FCM.push(n, on_response: handler) """ - @type on_response :: ((Notification.t) -> no_return) + @type on_response :: (Notification.t() -> no_return) @typedoc ~S""" Options for sending push notifications. @@ -42,10 +47,10 @@ defmodule Pigeon.FCM do batches synchronously. """ @type push_opts :: [ - to: atom | pid | nil, - timeout: pos_integer | nil, - on_response: on_response | nil - ] + to: atom | pid | nil, + timeout: pos_integer | nil, + on_response: on_response | nil + ] @default_timeout 5_000 @default_worker :fcm_default @@ -82,21 +87,23 @@ defmodule Pigeon.FCM do [[invalid_registration: "regId", invalid_registration: "regId"], [invalid_registration: "regId", invalid_registration: "regId"]] """ - @spec push(Notification.t, Keyword.t) :: Notification.t - @spec push([Notification.t, ...], Keyword.t) :: [Notification.t, ...] + @spec push(notification, Keyword.t()) :: notification | :ok def push(notification, opts \\ []) + def push(notification, opts) when is_list(notification) do timeout = Keyword.get(opts, :timeout, @default_timeout) + if Keyword.has_key?(opts, :on_response) do for n <- notification, do: send_push(n, opts[:on_response], opts) :ok else notification - |> Enum.map(& Task.async(fn -> sync_push(&1, opts) end)) + |> Enum.map(&Task.async(fn -> sync_push(&1, opts) end)) |> Task.yield_many(timeout) |> Enum.map(&task_mapper(&1)) end end + def push(notification, opts) do if Keyword.has_key?(opts, :on_response) do send_push(notification, opts[:on_response], opts) @@ -113,11 +120,14 @@ defmodule Pigeon.FCM do end end - defp send_push(notifications, on_response, opts) when is_list(notifications) do + defp send_push(notifications, on_response, opts) + when is_list(notifications) do worker_name = opts[:to] || @default_worker + notifications - |> Enum.map(& cast_request(worker_name, &1, on_response, opts)) + |> Enum.map(&cast_request(worker_name, &1, on_response, opts)) end + defp send_push(notification, on_response, opts) do send_push([notification], on_response, opts) end @@ -129,10 +139,11 @@ defmodule Pigeon.FCM do defp sync_push(notification, opts) do timeout = Keyword.get(opts, :timeout, @default_timeout) - ref = :erlang.make_ref + ref = :erlang.make_ref() pid = self() - on_response = fn(x) -> send pid, {ref, x} end + on_response = fn x -> send(pid, {ref, x}) end send_push(notification, on_response, opts) + receive do {^ref, x} -> x after @@ -151,16 +162,19 @@ defmodule Pigeon.FCM do true """ def start_connection(opts \\ []) + def start_connection(name) when is_atom(name) do worker = worker(Pigeon.Worker, [Config.new(name)], id: name) Supervisor.start_child(:pigeon, worker) end + def start_connection(%Config{} = config) do Worker.start_link(config) end + def start_connection(opts) do opts - |> Config.new + |> Config.new() |> start_connection() end diff --git a/lib/pigeon/fcm/config.ex b/lib/pigeon/fcm/config.ex index 345e61e6..2449dcbf 100644 --- a/lib/pigeon/fcm/config.ex +++ b/lib/pigeon/fcm/config.ex @@ -7,11 +7,11 @@ defmodule Pigeon.FCM.Config do name: nil @type t :: %__MODULE__{ - key: binary, - name: term, - port: pos_integer, - uri: charlist, - } + key: binary, + name: term, + port: pos_integer, + uri: charlist + } @doc ~S""" Returns a new `FCM.Config` with given `opts`. @@ -35,9 +35,10 @@ defmodule Pigeon.FCM.Config do port: Keyword.get(opts, :port, 443) } end + def new(name) when is_atom(name) do Application.get_env(:pigeon, :fcm)[name] - |> Enum.to_list + |> Enum.to_list() |> Keyword.put(:name, name) |> new() end @@ -63,7 +64,7 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do @spec max_demand(any) :: non_neg_integer def max_demand(_config), do: 100 - @spec connect(any) :: {:ok, sock} | {:error, String.t} + @spec connect(any) :: {:ok, sock} | {:error, String.t()} def connect(%Config{uri: uri} = config) do case connect_socket_options(config) do {:ok, options} -> @@ -72,15 +73,15 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do end def connect_socket_options(config) do - opts = [ - {:active, :once}, - {:packet, :raw}, - {:reuseaddr, true}, - {:alpn_advertised_protocols, [<<"h2">>]}, - {:reconnect, false}, - :binary - ] - |> add_port(config) + opts = + [ + {:active, :once}, + {:packet, :raw}, + {:reuseaddr, true}, + {:alpn_advertised_protocols, [<<"h2">>]}, + :binary + ] + |> add_port(config) {:ok, opts} end @@ -102,14 +103,13 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do Encodable.binary_payload(notification) end - def handle_end_stream(_config, - %{body: body, status: status, error: nil}, - notif, - on_response) do - do_handle_end_stream(status, body, notif, on_response) + def handle_end_stream(_config, %{error: nil} = stream, notif, on_response) do + do_handle_end_stream(stream.status, stream.body, notif, on_response) end + def handle_end_stream(_config, %{error: _error}, _notif, nil), do: :ok - def handle_end_stream(_config, %{error: _error}, {_regids, notif}, on_response) do + + def handle_end_stream(_config, _stream, {_regids, notif}, on_response) do notif = %{notif | status: :unavailable} process_on_response(on_response, notif) end @@ -119,21 +119,25 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do notif = %{notif | status: :success} parse_result(notif.registration_id, result, on_response, notif) end + defp do_handle_end_stream(400, _body, notif, on_response) do log_error("400", "Malformed JSON") notif = %{notif | status: :malformed_json} process_on_response(on_response, notif) end + defp do_handle_end_stream(401, _body, notif, on_response) do log_error("401", "Unauthorized") notif = %{notif | status: :unauthorized} process_on_response(on_response, notif) end + defp do_handle_end_stream(500, _body, notif, on_response) do log_error("500", "Internal server error") notif = %{notif | status: :internal_server_error} process_on_response(on_response, notif) end + defp do_handle_end_stream(code, body, notif, on_response) do reason = parse_error(body) log_error(code, reason) @@ -156,14 +160,15 @@ defimpl Pigeon.Configurable, for: Pigeon.FCM.Config do def parse_error(data) do case Poison.decode(data) do {:ok, response} -> - response["reason"] |> Macro.underscore |> String.to_existing_atom + response["reason"] |> Macro.underscore() |> String.to_existing_atom() + error -> "Poison parse failed: #{inspect(error)}, body: #{inspect(data)}" - |> Logger.error + |> Logger.error() end end defp log_error(code, reason) do - if Pigeon.debug_log?, do: Logger.error("#{reason}: #{code}") + if Pigeon.debug_log?(), do: Logger.error("#{reason}: #{code}") end end diff --git a/lib/pigeon/fcm/notification.ex b/lib/pigeon/fcm/notification.ex index e6f69728..cd6f9c90 100644 --- a/lib/pigeon/fcm/notification.ex +++ b/lib/pigeon/fcm/notification.ex @@ -13,13 +13,13 @@ defmodule Pigeon.FCM.Notification do alias Pigeon.FCM.Notification @type t :: %__MODULE__{ - message_id: nil | String.t, - payload: %{}, - priority: :normal | :high, - registration_id: String.t | [String.t], - status: status | nil, - response: [] | [regid_response, ...] - } + message_id: nil | String.t(), + payload: %{}, + priority: :normal | :high, + registration_id: String.t() | [String.t()], + status: status | nil, + response: [] | [regid_response, ...] + } @typedoc ~S""" Status of FCM request @@ -33,12 +33,13 @@ defmodule Pigeon.FCM.Notification do to process the request - `:unavailable` - FCM server couldn't process the request in time """ - @type status :: :success - | :timeout - | :unauthorized - | :malformed_json - | :internal_server_error - | :unavailable + @type status :: + :success + | :timeout + | :unauthorized + | :malformed_json + | :internal_server_error + | :unavailable @typedoc ~S""" FCM push response for individual registration IDs @@ -49,22 +50,24 @@ defmodule Pigeon.FCM.Notification do - `{regid_error_response, "reg_id"}` - Push attempted but server responded with error """ - @type regid_response :: {:success, binary} - | {regid_error_response, binary} - | {:update, {binary, binary}} - - @type regid_error_response :: :device_essage_rate_exceeded - | :invalid_data_key - | :invalid_package_name - | :invalid_paramteres - | :invalid_registration - | :invalid_ttl - | :message_too_big - | :missing_registration - | :mismatch_sender_id - | :not_registered - | :topics_message_rate_exceeded - | :unavailable + @type regid_response :: + {:success, binary} + | {regid_error_response, binary} + | {:update, {binary, binary}} + + @type regid_error_response :: + :device_message_rate_exceeded + | :invalid_data_key + | :invalid_package_name + | :invalid_paramteres + | :invalid_registration + | :invalid_ttl + | :message_too_big + | :missing_registration + | :mismatch_sender_id + | :not_registered + | :topics_message_rate_exceeded + | :unavailable @chunk_size 1_000 @@ -108,21 +111,24 @@ defmodule Pigeon.FCM.Notification do 500 """ def new(registration_ids, notification \\ %{}, data \\ %{}) + def new(reg_id, notification, data) when is_binary(reg_id) do %Pigeon.FCM.Notification{registration_id: reg_id} |> put_notification(notification) |> put_data(data) end + def new(reg_ids, notification, data) when length(reg_ids) < 1001 do %Pigeon.FCM.Notification{registration_id: reg_ids} |> put_notification(notification) |> put_data(data) end + def new(reg_ids, notification, data) do reg_ids |> chunk(@chunk_size, @chunk_size, []) - |> Enum.map(& new(&1, notification, data)) - |> List.flatten + |> Enum.map(&new(&1, notification, data)) + |> List.flatten() end defp chunk(collection, chunk_size, step, padding) do @@ -158,7 +164,8 @@ defmodule Pigeon.FCM.Notification do registration_id: nil } """ - def put_notification(n, notification), do: update_payload(n, "notification", notification) + def put_notification(n, notification), + do: update_payload(n, "notification", notification) @doc """ Updates `"priority"` key. @@ -175,14 +182,17 @@ defmodule Pigeon.FCM.Notification do %Pigeon.FCM.Notification{priority: :normal} """ def put_priority(n, :normal), do: %{n | priority: :normal} - def put_priority(n, :high), do: %{n | priority: :high} - def put_priority(n, _), do: n + def put_priority(n, :high), do: %{n | priority: :high} + def put_priority(n, _), do: n + + defp update_payload(notification, _key, value) when value == %{}, + do: notification - defp update_payload(notification, _key, value) when value == %{}, do: notification defp update_payload(notification, key, value) do payload = notification.payload |> Map.put(key, value) + %{notification | payload: payload} end @@ -248,10 +258,10 @@ defmodule Pigeon.FCM.Notification do """ def remove?(%{response: response}) do response - |> Enum.filter(fn({k, _v}) -> + |> Enum.filter(fn {k, _v} -> k == :invalid_registration || k == :not_registered end) - |> Keyword.values + |> Keyword.values() end end @@ -261,17 +271,22 @@ defimpl Pigeon.Encodable, for: Pigeon.FCM.Notification do end @doc false - def encode_requests(%{registration_id: regid} = notification) when is_binary(regid) do + def encode_requests(%{registration_id: regid} = notification) + when is_binary(regid) do encode_requests(%{notification | registration_id: [regid]}) end - def encode_requests(%{registration_id: regid} = notification) when is_list(regid) do + + def encode_requests(%{registration_id: regid} = notification) + when is_list(regid) do regid |> recipient_attr() |> Map.merge(notification.payload) |> Map.put("priority", to_string(notification.priority)) - |> Poison.encode! + |> Poison.encode!() end defp recipient_attr([regid]), do: %{"to" => regid} - defp recipient_attr(regid) when is_list(regid), do: %{"registration_ids" => regid} + + defp recipient_attr(regid) when is_list(regid), + do: %{"registration_ids" => regid} end diff --git a/lib/pigeon/fcm/result_parser.ex b/lib/pigeon/fcm/result_parser.ex index 7005714f..3be23c25 100644 --- a/lib/pigeon/fcm/result_parser.ex +++ b/lib/pigeon/fcm/result_parser.ex @@ -3,8 +3,6 @@ defmodule Pigeon.FCM.ResultParser do import Pigeon.Tasks, only: [process_on_response: 2] - alias Pigeon.FCM.Notification - def parse([], [], on_response, notif) do process_on_response(on_response, notif) end @@ -13,36 +11,39 @@ defmodule Pigeon.FCM.ResultParser do parse([regid], results, on_response, notif) end - # Handle RegID updates - def parse([regid | reg_res], - [%{"message_id" => id, "registration_id" => new_regid} | rest], - on_response, - %Notification{response: resp} = notif) do + def parse([regid | reg_res], [result | rest_results], on_response, notif) do + updated_notif = + case result do + %{"message_id" => id, "registration_id" => new_regid} -> + notif + |> put_update(regid, new_regid) + |> Map.put(:message_id, id) + + %{"message_id" => id} -> + notif + |> put_success(regid) + |> Map.put(:message_id, id) + + %{"error" => error} -> + notif + |> put_error(regid, error) + end + + parse(reg_res, rest_results, on_response, updated_notif) + end + defp put_update(%{response: resp} = notif, regid, new_regid) do new_resp = [{:update, {regid, new_regid}} | resp] - notif = %{notif | message_id: id, response: new_resp} - parse(reg_res, rest, on_response, notif) + %{notif | response: new_resp} end - # Handle successful RegIDs, also parse `message_id` - def parse([regid | reg_res], - [%{"message_id" => id} | rest_results], - on_response, - %Notification{response: resp} = notif) do - + defp put_success(%{response: resp} = notif, regid) do new_resp = [{:success, regid} | resp] - n = %{notif | message_id: id, response: new_resp} - parse(reg_res, rest_results, on_response, n) + %{notif | response: new_resp} end - # Handle error RegIDs - def parse([regid | reg_res], - [%{"error" => error} | rest_results], - on_response, - %Notification{response: resp} = notif) do - - error = error |> Macro.underscore |> String.to_atom - n = %{notif | response: [{error, regid} | resp]} - parse(reg_res, rest_results, on_response, n) + defp put_error(%{response: resp} = notif, regid, error) do + error = error |> Macro.underscore() |> String.to_atom() + %{notif | response: [{error, regid} | resp]} end end diff --git a/lib/pigeon/http2/client.ex b/lib/pigeon/http2/client.ex index f395b59d..ef941f40 100644 --- a/lib/pigeon/http2/client.ex +++ b/lib/pigeon/http2/client.ex @@ -77,6 +77,8 @@ defmodule Pigeon.Http2.Client do config :pigeon, http2_client: Pigeon.YourCustomAdapter """ + @type uri :: charlist + @doc ~S""" Default http2 client to use. @@ -93,15 +95,14 @@ defmodule Pigeon.Http2.Client do @callback start() :: no_return - @callback connect(uri :: charlist, scheme :: :https, options :: Keyword.t) - :: {:ok, pid} - | {:error, any} + @callback connect(uri :: uri, scheme :: :https, options :: Keyword.t()) :: + {:ok, pid} | {:error, any} @callback send_ping(pid) :: :ok - @callback send_request(pid, headers :: Keyword.t, data :: String.t) :: :ok + @callback send_request(pid, headers :: Keyword.t(), data :: String.t()) :: :ok - @callback handle_end_stream(msg :: term, state :: term) - :: {:ok, %Pigeon.Http2.Stream{}} - | any + @callback handle_end_stream(msg :: term, state :: term) :: + {:ok, %Pigeon.Http2.Stream{}} + | any end diff --git a/lib/pigeon/http2/kadabra.ex b/lib/pigeon/http2/kadabra.ex index 6ffbd43e..1d3e867b 100644 --- a/lib/pigeon/http2/kadabra.ex +++ b/lib/pigeon/http2/kadabra.ex @@ -1,45 +1,50 @@ if Code.ensure_loaded?(Kadabra) do -defmodule Pigeon.Http2.Client.Kadabra do - @moduledoc false + defmodule Pigeon.Http2.Client.Kadabra do + @moduledoc false - @behaviour Pigeon.Http2.Client + @behaviour Pigeon.Http2.Client - def start do - Application.ensure_all_started(:kadabra) - end + def start do + Application.ensure_all_started(:kadabra) + end - def connect(uri, scheme, opts) do - Kadabra.open(uri, scheme, opts) - end + def connect(uri, scheme, opts) do + host = "#{scheme}://#{uri}" + Kadabra.open(host, ssl: opts) + end - def send_request(pid, headers, data) do - Kadabra.request(pid, headers, data) - end + def send_request(pid, headers, data) do + Kadabra.request(pid, headers: headers, body: data) + end - @doc ~S""" - send_ping/1 implementation + @doc ~S""" + send_ping/1 implementation - ## Examples + ## Examples - iex> {:ok, pid} = Kadabra.open('http2.golang.org', :https) - iex> Pigeon.Http2.Client.Kadabra.send_ping(pid) - :ok - """ - def send_ping(pid) do - Kadabra.ping(pid) - end + iex> {:ok, pid} = Kadabra.open("https://http2.golang.org") + iex> Pigeon.Http2.Client.Kadabra.send_ping(pid) + :ok + """ + def send_ping(pid) do + Kadabra.ping(pid) + end - def handle_end_stream({:end_stream, %{id: id, - status: status, - headers: headers, - body: body}}, _state) do - {:ok, %Pigeon.Http2.Stream{ + def handle_end_stream({:end_stream, stream}, _state) do + %{id: id, status: status, headers: headers, body: body} = stream + + pigeon_stream = %Pigeon.Http2.Stream{ id: id, status: status, headers: headers, body: body - }} + } + + {:ok, pigeon_stream} + end + + def handle_end_stream(msg, _state) do + msg + end end - def handle_end_stream(msg, _state), do: msg -end end diff --git a/lib/pigeon/http2/stream.ex b/lib/pigeon/http2/stream.ex index da082fe9..f4ee1f06 100644 --- a/lib/pigeon/http2/stream.ex +++ b/lib/pigeon/http2/stream.ex @@ -4,9 +4,9 @@ defmodule Pigeon.Http2.Stream do defstruct id: nil, status: nil, headers: nil, body: nil, error: nil @type t :: %__MODULE__{ - id: pos_integer, - headers: [{binary, binary}, ...], - body: binary, - error: term - } + id: pos_integer, + headers: [{binary, binary}, ...], + body: binary, + error: term + } end diff --git a/lib/pigeon/tasks.ex b/lib/pigeon/tasks.ex index eb3df219..ca19133f 100644 --- a/lib/pigeon/tasks.ex +++ b/lib/pigeon/tasks.ex @@ -2,6 +2,7 @@ defmodule Pigeon.Tasks do @moduledoc false def process_on_response(nil, _notif), do: :ok + def process_on_response(on_response, notif) do Task.Supervisor.start_child(Pigeon.Tasks, fn -> on_response.(notif) end) end diff --git a/lib/pigeon/worker.ex b/lib/pigeon/worker.ex index fae23bcd..f22089ef 100644 --- a/lib/pigeon/worker.ex +++ b/lib/pigeon/worker.ex @@ -4,7 +4,7 @@ defmodule Pigeon.Worker do defstruct config: nil, connections: 0, pending_demand: 0, - queue: :queue.new + queue: :queue.new() use GenStage @@ -12,10 +12,11 @@ defmodule Pigeon.Worker do require Logger - @type config :: APNS.Config.t | FCM.Config.t + @type config :: APNS.Config.t() | FCM.Config.t() - @type notification :: APNS.Notification.t - | FCM.Notification.t + @type notification :: + APNS.Notification.t() + | FCM.Notification.t() @spec start_link(config) :: {:ok, pid} def start_link(config) do @@ -30,7 +31,7 @@ defmodule Pigeon.Worker do GenStage.cast(pid, :stop) end - @spec send_push(atom | pid, notification, Keyword.t) :: :ok + @spec send_push(atom | pid, notification, Keyword.t()) :: :ok def send_push(name, notification, opts) do # Ensure connections are live before trying to push # Doesn't play nice if you try to do it all in one step @@ -50,8 +51,9 @@ defmodule Pigeon.Worker do def handle_call({:push, _notif, _opts} = msg, from, state) do GenStage.reply(from, :ok) + state - |> Map.update(:queue, :queue.new, &(:queue.in(msg, &1))) + |> Map.update(:queue, :queue.new(), &:queue.in(msg, &1)) |> dispatch_events([]) end @@ -89,11 +91,13 @@ defmodule Pigeon.Worker do Pigeon.start_connection({state.config, self()}) %{state | connections: state.connections + 1} end + defp ensure_connections(state), do: state defp dispatch_events(%{pending_demand: 0} = state, events) do {:noreply, Enum.reverse(events), state} end + defp dispatch_events(%{queue: queue} = state, events) do case :queue.out(queue) do {{:value, event}, queue} -> @@ -101,6 +105,7 @@ defmodule Pigeon.Worker do |> Map.put(:queue, queue) |> increment_demand(-1) |> dispatch_events([event | events]) + {:empty, _queue} -> {:noreply, Enum.reverse(events), state} end diff --git a/mix.exs b/mix.exs index b0831344..bbae4a1d 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Pigeon.Mixfile do use Mix.Project - @version "1.1.6" + @version "1.2.0" def project do [ @@ -33,7 +33,6 @@ defmodule Pigeon.Mixfile do "docs/APNS Apple iOS.md", "docs/FCM Android.md", "docs/ADM Amazon Android.md", - "docs/Migrating to v1-1-0.md", "CHANGELOG.md" ] ], @@ -53,9 +52,10 @@ defmodule Pigeon.Mixfile do {:poison, "~> 2.0 or ~> 3.0"}, {:httpoison, "~> 0.7 or ~> 1.0"}, {:gen_stage, "~> 0.12"}, - {:kadabra, "~> 0.3.7", optional: true}, + {:joken, "~> 1.5.0"}, + {:kadabra, "~> 0.4.2", optional: true}, {:earmark, "~> 1.0", only: :dev}, - {:ex_doc, "~> 0.17.0", only: :dev}, + {:ex_doc, "~> 0.18", only: :dev}, {:excoveralls, "~> 0.5", only: :test}, {:dialyxir, "~> 0.5", only: [:dev], runtime: false}, {:credo, "~> 0.8", only: [:dev, :test], runtime: false} diff --git a/mix.lock b/mix.lock index e0676337..c9bffaaa 100644 --- a/mix.lock +++ b/mix.lock @@ -1,23 +1,27 @@ %{ + "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [:rebar3], [], "hexpm"}, - "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, + "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "credo": {:hex, :credo, "0.9.2", "841d316612f568beb22ba310d816353dddf31c2d94aa488ae5a27bb53760d0bf", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, "dogma": {:hex, :dogma, "0.1.15", "5bceba9054b2b97a4adcb2ab4948ca9245e5258b883946e82d32f785340fd411", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, optional: false]}]}, - "earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.17.1", "39f777415e769992e6732d9589dc5846ea587f01412241f4a774664c746affbb", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.8.1", "0bbf67f22c7dbf7503981d21a5eef5db8bbc3cb86e70d3798e8c802c74fa5e27", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, + "excoveralls": {:hex, :excoveralls, "0.8.2", "b941a08a1842d7aa629e0bbc969186a4cefdd035bad9fe15d43aaaaaeb8fae36", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"}, "gen_stage": {:hex, :gen_stage, "0.13.1", "edff5bca9cab22c5d03a834062515e6a1aeeb7665fb44eddae086252e39c4378", [:mix], [], "hexpm"}, - "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hpack": {:hex, :hpack_erl, "0.2.3", "17670f83ff984ae6cd74b1c456edde906d27ff013740ee4d9efaa4f1bf999633", [:rebar3], [], "hexpm"}, - "httpoison": {:hex, :httpoison, "1.0.0", "1f02f827148d945d40b24f0b0a89afe40bfe037171a6cf70f2486976d86921cd", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "httpoison": {:hex, :httpoison, "1.1.1", "96ed7ab79f78a31081bb523eefec205fd2900a02cda6dbc2300e7a1226219566", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "joken": {:hex, :joken, "1.5.0", "42a0953e80bd933fc98a0874e156771f78bf0e92abe6c3a9c22feb6da28efb0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm"}, - "kadabra": {:hex, :kadabra, "0.3.7", "23032c7293922df3371d359e74de7b7abd6f6af64837551cc7e72c7bdfd1e123", [:mix], [{:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}, {:scribe, "~> 0.4", [hex: :scribe, repo: "hexpm", optional: true]}], "hexpm"}, + "kadabra": {:hex, :kadabra, "0.4.2", "e046b8596e3fc79301d612e0b605c82be6372908ee78d8a73344f0e8773d816f", [:mix], [{:gen_stage, "~> 0.13.1", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:hpack, "~> 0.2.3", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "pane": {:hex, :pane, "0.1.1", "4a9b46957a02991acbce012169ab7e8ecff74ad24886f94b142680062b10f167", [], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "scribe": {:hex, :scribe, "0.5.0", "1fe6dfffc4264a39633c1bd3fc9ce8377546974c40c7cd2a06194b2c2e91bba0", [], [{:pane, "~> 0.1", [hex: :pane, repo: "hexpm", optional: false]}], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, diff --git a/secrets.tar.enc b/secrets.tar.enc new file mode 100644 index 0000000000000000000000000000000000000000..85273d2cdb71d141a18eff156d88ac64a6aaccf9 GIT binary patch literal 9744 zcmV+rChys{8TSOfpZ)h$*O6=W&GV1oHlap_++IdqhDZwc4)Dd zs~3XuO&q!Icv$)SzQm?F-~Tp)HT5(BHjux9)N7KuxV+f5f0l=m7XS5p@$@fX#gVP4 zr-=G?h;^nNziekncvh)^YAbwOjeB=aD?+18&Z6$d#e2L;E|=m0r)f_{GUnHx87m`* zXYciyl3v2@Zu5e|f-!~1T{3TD{D~5GD7#YVX<&M>XkEfw)l|cYI z*uHPwvun2}7T!TkS<9LSi(#-wRl-7R=*q!u)xvU^CdKeoTuOV25di?Z3$sl+-_D*y+Dy>fgegx>CA|vn zL{s>Md!i<=aJY+PA#2lT^z4XhV=0As@cXpav7Xw*i1*a}S)gV7rbpbiJRJ#y20)|Y zHNQc(y7K73=&yFvoR5ACV+`Al>HOr=8?s1gDoSTuK*#HykedWUo}qCVe81xS)4fVf zJ)bDyVSIdV*nRYPzY`=)AP}knpcB;nIQKj!ytiLt6p0|nmvVraA~ko1J9hyQ#2*%W zXKZe@b88xrbZK5N*Qt+u?uN#$XZ9ZPcjk%O&HF!=`q+OlU;U}~xk|Cax~9Jz=|f>* zttViCJ3+{GRAQ2_w0}27B+Btg0Hjux@|t7e++uB!g|P#(=34G&s@#l~qmzf^!`G8a zwbDk-n&{A`YfA!S6%l#Nd7rzS;!25-4IUggvrD~(($C|U4bCQ#URG0U(FPF@VstsA zhf?Q5ujP%DUPZz!CrWJ{YNy&c(-NUsO3T$_5hY?~mx$JiBHG9(*3jRW#QZKonlik@ zicf1_^)sZmmx3l zF1OvUzgjJp(<_Y#F<=J3+cf)o%8}!DB%Z{hO$w(50{Y^+LUy zlV1us_Dfb>+*pnAUjZ)c)Q!5+QXb?j@D7jn*0i_IOE;uOmVqO2 z{b*fk2d&&13&hA)+LPncm&X3NQ--Ckb?G1Cp_BmfTxHF0YbSLL{7A8Jl+X0 z#PoMRxOCiy>_;t`~}7?3EJfj;%hwEHL?|8;kGEct79D_TFCk8~>E&u3Ke2j!oM zbURx9;=^zG%#CZO(>OYm(ktnwKH?>sg1{vLeE0aQu^SR9)a-|Daj?UlBbG!a*fc&y z&yP|ld37L6LbD!o{06pG&E66m4J1My+JNWRXV;5FJ-^J5877u-o)rzcyhZ6Hbs&Q- z6)@BnYyx+0(`711$hQFX5LF@*?3TTHa6vNwx3tY0TvtJbok6(gl=1POCE;QpXS1BV z`EH!Qi4cbi6$HBhb_3J3u05CC&-0|D?RJ;#_F4l;Od$Xs)i`cUK1@gZW}IT=F)Xz6tZpv&Xq(A?N}EZ&ni=eSfs@4{*3*GR+STlu;WE@LJ9ki6Xqc|i zd1#6CgCuoC2GcXwoj$b3D@L4p#x3bZW5D!DB zJ}^|9Xni=!J^O(|c+XDA<<jTfrq|Q-3`Mvz6=v z_L$Tb?sewm25PXc8)~S$uc?I%O)!FC)!cZsxl{Q_L&RiM;yVd$+kwUd!c1d%@Ei3J!X11W)n%CZ8xl;` z=D@&i|BA@;kX*7&%a%>`x&Rd644`F8S$*Y>dm)ip$^CzX{f*-F@XwqVaCp~b!)y*JZ=Nl z>-fHPQI&74*>Cm2xsoBgoqTV%H6I}EewgPxSJbXf@zfep4^9w9=%NFs^AXlRn-5TX z$Qkqhe7hj4XTc&|N3Fi>z~_!u<$c)%Cea6v>gv~vE;B^3M`}2SHF>&lw94yfp&g`r z^QM;Bw{n=(gm~SoxPVBqkGspn9 zVwoyLWrQp=rA0CULT<7bJo_np@J2`ClF$sK@DYMCmm0XdS*EwQ-}8fxy_L zk*2D=B|7oJE~E<);ZnE6MwEpewEywMg>0`ix{!A-v#F&bU7v1hhTlL3w10@6PiKS_ z9MDXr6)G3v`e}KT7WtkT8P-7Ul277YFU6P%+-9P*WhLsnUy@P)+W>7t-Jwb{oB|}t zH;eGaKh2P*!Q}cN&(?VWLeXrvVr|uwR{=c$sSz5}*9LvdFxWqf2fO3sB+{>)(3K;y z4|4axZ`@PFHVRj{s@bmx6b+`t599YPWF(7nvT{lI_#8k;VEkB4<|iio@zj*XSn7nV z(7ys1I>3^sxWzJzoqWU!w++T9E_9cj$`!3LlRMLFn=mIkQ4%k^53Z4pMv3XVXrHZ?x<}S5~jrZsO8#F z11P~$d+gP}5=(WifgVOJd}K?gCYLs+)u5UBwD3{YBCSEAxmcGRi#UEX?1 zm?%-%>bS2(TaFE4mfOH z@wuJ??HY<5R@NS|Y4WA%D@q+`=o&516Hod6Fdn6$k}ONHyzZt-@TULOfgk{>1?K~? zGhERur4kX5k_7H0U(voKB%xlQvfQRsXW!V;FQiugipl?W(vRol!c^@HN+qzOQ-tDaZS?coFX@jL!A7z zgN*!l4cI{+dG-|7k5uOvJ?YyrL}4XNQ$&~CW~?;+l6=txQR~2$GzAW3XPcIFE3_2w zd%Fqnd-CuyF@;?)st5B4S7XgujG`O^)Spe$vX3(M0Q!6pnimVqWVEtU4I;>AN?fxGsWt# zPbhSMeUTd=dtTW^_M{^llzR2LUv@1FdZjB68Y(jnsN12DX#FS&xjb2-8IA_&6(9+( zKoVa1KUS)Zj=M<+_y;;h^X41iF2$(3Dtvdc#?Xl!xuw})tAQHs$TKJ&r&Z_~j5S%8F2OUBB5be;E{QP)NOf5~z?yLftQilV1Et z{}Kc1CP+C8t;?!(3Br(!aN=6ro1}fxW|^`)1>;T- z`4)Ucd&8~+88c~{MRLVws5yv%kC;l$XmoT-JhIPb8w<4YQ-M=M{zRpLlOfLh5v3T3 z{lwEY_^IbboF$0{rFsWT_2i#q^~eyZ>Ll0wrupMo3L>zlcG3>O;(6Z~0T@RgyP6D9Sw*q57SnH*`sguksHvBMEw0DDY?4lUdg;1c484EC0a#?lux#|*7Ka7lNa;avrA z63^L(j&$28t;=G&+kkc#NLWzrx@|>A62q##UAHZtSqf|A3#xGsqYMY9{oC*3R=yRL zSMF!V_os3l3$V@Wqi%1vp9$RIGXuf%(+pTv7fx)EsmMY2$4OEmaeF}o%d8+=8g z1r|;5%88;g2bwnlnQ8eUWAAOeBD3>kAt6YPxwR)CD~&};rXA53go6^-^17~uYqjR; zo!sHFdU&ON0utWu{N|RE>Tz3=%H$sXS0cJ+*n|_14^7P*I6Jx0L=72;AQqR@89c{v zBv>P9-Mr@PpXmG;+pN!VB5w;2og4#(uO49}7Tm6c`@8rkxdPT>b7Vx`+~A{c{IM^_ z3o2xLixA7XcUK}5WU?^@2BGE3HZi>-0*9jIUJal`* zuHNIUDkzKuYXKB}(PSQUyTEr*f+e4mn8hZ}R{`b^VwFO|LS?gX+1Q>f3g@r^3_?^A zctSX0!wHF~Ua!E}D3~P6B=3hH>a~>vKHeG8>_PT2oY$@yVLlKCIhH&xUHzr? zc3gPMnQS;=DBU0BT>YwH%=p>&&wm` zkU>j5AlmcjegFC~e_;6v-%kYtrdB@9hLM+XAqIIgCd!nGK~li^tr0@4l_WzP36C^= zQ@M_fZk0WxXm3-gTYs{TlO*bg{2o1h`VmHydj6t5t$2%O8zraz+pJaQ;=(Dg}eyNt?eJm^rp~9#n>frsrHj7V8OAYf)|_;Mm~tl!B1_b0zm7xQO5kdzV@+ipmCw=u8(uQL5C;3>45ZV0{^9ta-^OzAs4U zaUipk0&)Cnan-RWOoKt>cMJ2lgus@(TsFnbORzZ!E@4K20Baw5Njwyi#jvUmq9B`< zGQ&hGMZNa}_qJ@=aCn^O=-2<>5x_ka{E7RoCRVCu6*!QRI#LP5 z_`#Ew3?U(A$v8^;iS4?@Hvsoqh?iy?ipZ0|EnuL=MW7u@oiRXE`PuRS!cW{mWqeo@ z%EDpvc#Z79kRVWb&R;Hg5JC-$n@vSp`{(~YH&E3wd+Sw0wFi`0O$>-}LTm;;ixx7ZN(tvUZhW3{Iu1AqI zg`tS;?fYBo&clCeur!14+ZgA7F^asN*Uzf(T`Gm}Ews@bXPN!Npkj#a>=}|h6P6WE ziRtTXs8k1O_Mu;JEtY6ll*&iK&6H0g%;O#E!}jN){BP=YV@qi6o{C;^zdyjtyI>Pa z4r}z$MyMNo+IkSxOidUc%vy4x2B8eB@*47?5AGGui{(P=AAld)UqxwLs1%3Qlbga} zA93f#y0As)qU%on)(*+DF=}sl-N~@~xcf4gTWR>6&Q0hhMi=Uccel=Cpcfn!&YjP{ zZVR8Vw3SUIY7Y@oVm~TP4yBCr*yU_i>HH3>+q33yL`B@Da>k&@2te=YtF>3)A$+!d z`5(>qIW6D`QywUFbuf)7Pu9m5d5r90b1N6u8Y|PItcm_9PVGKj=uORr+`K+LTgQ#! z`zRvtTX2>-h}h0!2u48TQ}Bx`-Sjj=4pPGY=RqW5ol)2}bUn-}Ndc6dyK}?U#*@F& z`b0cC@7DcN`?}>`_v^wG&Mv*lwKVxnb%h*#WYL>9vhDpY&=F{1L&JoL3xrjNP51GK z1~H{($>C#OkD=)Py@PYe)J`=dAR(pQg!!`16KS~@6d}9AEOj23WW9r$7w4lJ9 z0tRMPk)fbC>fhlIrf-qx$HO4h0io3bH<-`d*o?hR;6ClvEH2mYrEyMoA?a%I%!{id z5Q1QBjL~;whsiTFk+E=MCknr(>!TMiLl0lkUI#3 zL>Pv!p=Y6NdQP}A$e&t83IwyDXv7DnpXk-2;kmMSZI(#LbfimUacnp?QpB1mWO-Gl z)d->e{!zY0M-rWc$`B!`&Y&SyduYe7-gIY6VQt=%h`^l{eb5{AF`l=wa{5IWVbh)X zLQs5d!{+OMcQrz#1L0yN5OqqcF^MGMNyyi)m0y8&X*~D!4RhTpoPxN^pqc|EFMM=q zdYu>~r2W&`Z>wQoBzVm19HgHpyovZ-|KaW#MqsCU{HEIAXvwo}4SD+THK+&UV{$T# zE|jo(IsNv(MhDx=5zGVQvodlE3VT!J^i7&%Bl93mj>G&J3eSs%Yu_MnhnSd3EYHur zp=My{qUVol+*j=BzBtkM4Ve_YZshp{ou_k*Gv6AT;-9{zyG7BFSvx)&!kyxBho+oL zg4uuCik~cCj$qxINsi8`|1#73f1vdocSqplrazb=yQg!qi+B}>aw_A)MT)eSV{&?x zvf3_+&^EW8j!uv_w{)%v?1HVlQN-D4f@F%BNxor8=qQ~0$Q#$+w1Bn+zz57iRYqwI zKGk@P_L{OJlmJ4GWR_k_?E%Hn+`OI*3c~7ECU>Q`LCC5iPqJTfV4NwckD}9aD)_dj z>-d|q<@2C$q@O11)#l^%Hk@4XFvtRhnWB!hGP#tTvJH0A5#~z^qwBuEa4V|0fBq8!iD5?QJ+!){(}3CLik$qm_10 z@7*u!Ii;wN^&67KWv!iH&;oTuqnFjwXv;X#((1J7jZ65}T$_aSL%!flrn| zezA(xz@J)Vbe;+@H;}A(v=0nXXw}R4*#P^N#F64)O#q}j=Y;xE9$=nV?{q{wM^%Q~ zt#|us$^XOep@z|~`$H{ehEqqk;6OVr>V9FSjv7+DUq;{cqM1agxhzTOlcU&+R%a*dzprh$PcD!NAs#JO zsv-l`XZme+4%>*-pS6}XA%C7OdJgn{j-XCC@`3*- zg1HV;gbOyeQwZ17de$%W^u2WgGnA(q)K3S~vy_ooT`?wwaL6;W@z9AUehR9NrYGjk zel$3ZZc%7fTsTOpCTy-@qYQ?xLF`Per4iH~+*w}AcG)$9DlF2D>t&~+tLagn$@$z8 z7kj0Pj`Y?8K1o;%OYiNhi_75cos$ih0p9^@UZ!tHH>k9&Urie(?B5pzlaHY7X1Pf6 z{_Zo+4HU~~VX=Kp?3NN#X2po!F(k5n;+sZx|6@NtKW@j`6cnQt7emOwmDJI&`{ZlZ zIB}bpz-hY^EDNzU{?swXfsZ0Tc$PIeyS2YOv%RNAX>pOtV0ShS_-DA1k zM}X~i>I_PQ4jhpJYrmL<6O%9O!Z|W_UFWrpmJ)e1?j~U1!6(edwGP|jbXN>p@!6Pl z8u$I2-t@t-PQ|0eD@R&nOLC@2`P&Aor?iT^`5`fxsUrmzgS4ma;j=+QAmo2@71aZ# z%`yaR(6+~s|`Tfa&EyBbbQ_zSQjicsn^{Q1>~ zq}0Z5tdWKry}WS;GebCIQM6;(qUVgeVotkga= z!@DCgnfvjw^Bts8SZk>b4UA?_1yxTQ(OXkGfRT*J;{@XwT_My=CofLPrEJ)4KqXf+yme& zJG5%qvqMAWPmd>7gkICf9ZU6&hW)_Y01xg(3Aluez|(hLDb8v9_*pO}kTC+d>8Kpm zmu*4yVuFGGMTt#;Yg0%{c;@ti5fC)?7Ox+h2>=DaTNXXZv0Kh0X7`H@nl#E^I)-lXQ6Qc@D&0+ZfY$L*$rkej$7?Ejep2tqBA>RkPmT zQ0fAcfT_u{ol5@loS-+DJzg)7D^WY;XwKq8J)=qWrJ7we(ou1U`Cc)YAOBJ@Ixw{6 z!8Si;$Q(R^tVqs>u<#UU&$jdj@S0EhtR7e1GX1+~5s^%vu8;7NsRE(>Sfc{aQ80D0 zG6f2ILTWuk#DF2(Ry(w*t!MFV_8(6~|2w#!W}v+{t%juY5{RqmZ7(Y@fn}0qX5#4; z7TF(-{LD6?7$t#fD=#x{|Kmu2+ZIB+c`2=}3(V2=XuyhcVGS9531dP6 zv*A-GXacl5p0ulY9RUBckdvU_J(9_runDuXk!6mxvNp-#q*={}CwTH9UWYa16ox}g zucjK&E=vgJ@YE{rZFhJNp||Q%60W)#-UW~@HgbJE3;7p?wqu+IFLQJGxvzksgThri z=5 z@xfr}ycPVTU997|JjXhYDNfELNb^6|0mbyJsK!X5R>v|1v=!-}>w>S?r3r}d^!SW; z`u2z-0F}4T#zWm%1ZdV&Er7dfzWCo`y_Ulx5&OI;TtemP5Hh51=n#@z4CR<~fc6N| zW$1J51<2nT>0;g_2seVL5DsRnJ{ zSuf{QF|Wu*d5o#Q>whCU-DZ;K3VZ8hYB$YD=DQVHJ|#ut8jFb~p#LG&0(dhQu+Ew% z;v+-oPY?>j<6G9h-CjF)rYleC$%P2h%Lvk(?#Zwn&XxHg=1+o>QvdO5bU;)6j>McN zP*YXcgttNGlk|@ghp+oifwe)Q|s|1Py8 zi6LquDVf#_@E+4&Bd@y3N=I+MAG7DV;1Ep`Zl|Z3g8;vW(IP%3++)gaL>Cx^{5Rc| zx)*+NdnVUW;$$#6=NdpJ(2%0bQ5|92{*`7n{Qi1{w>cfKSMN^$xK4MfAp6tQ)uQaT zgU$+YTd+{%8pB!`W)W0|QH0^i>0j1a^vv5R9!e49ocM>O@4|#a86tg3TH-c<<9lv> z@WPCZqUk9d%gUu6@Z{jFW9Q?%Qz)3l^)1iPEfWK*#C!^DR)4^`kf?#E3e@C!-s{wQ z=?~NTwWV=NV!~dAWnWE$SOzxiq<1RYbYNNH@i!_PUt6E4{r<_f<>6__|ng$BX?GBlu(BpEPe46cVJEJ^4Xa zN4S99D}w(ONDuq?4ve#bx66$c5iF-~TMWjulqw$FSB1@@uJ%vJvzzfi4%I{bX5g$a z4K2SMD&_E`H>Hlgz(M?+LX1{)O%?Cmw-EzpKPE13+^o>nn6zPcc$``e9sB5}DML|H zg#r99?(~a4&z#Bk;$J6D`$gd(6A#9D%M!m6$iLQVKQ zspzZn&nm~7qIgk`L)|Q5P3vC?*53u$EBZD9sSQW06}d7#d_m5pd?`PV)!oC<+a#G} zQV?ONY6_5`jp%#4Bpj=BjpXV^kIN6q?p#&!Y;Vp{(cA8YlP~Wilowa-IOM%Hhr-F` z;-hV`t3S)QoVjC(a;qck3V%ws^KEC`Hh z{Uj)8g)XBe67|T%Hy%f!%#%xkW1XpnxP<=ens}crPu~q-n;>@lr_LnTBw=Mzs#h?F8r6P_LLwB`RJVvM*gB_mwvteBpI{FeqLsmn!8;~YP4EQNaKBzJR!p#T6WQ8o#))=_qZ(;`0a}*kVpd_ z_`;tU@w*ch){uYrbcRe2&(m*`M?RT?|E*ZgTAS<0RND-(fVhA`PM^}_-Y7a&(_3F?guVX%Ojna{w6<$Ew6QyDPPyTGbc?(lEIl?7*2 z={tNvhi=x+r6vgI8H4K{)3-pU4;4D(7LC!Nv069pSlC5U_*$8p>bnd4+1lWaYFh8qU9`a7ebe+&*#nMjtkgWy1a=F$yCQ z_3r`X>S>D}f)Z{qg;17-Xugt}?%(M>dG?H9J1uajV-H$SY5wGIVKS>DVvN$zH_4ZV z_;YEh;9 %{}}, updated_registration_id: nil, consolidation_key: nil, - expires_after: 604800, + expires_after: 604_800, md5: "1B2M2Y8AsgTpgAmY7PhCfg==" } - assert expected_result == Pigeon.ADM.Notification.new(test_registration_id()) + + assert expected_result == + Pigeon.ADM.Notification.new(test_registration_id()) end test "new/2" do expected_result = %Pigeon.ADM.Notification{ registration_id: test_registration_id(), - payload: %{"data" => %{ "message" => "your message" }}, + payload: %{"data" => %{"message" => "your message"}}, updated_registration_id: nil, consolidation_key: nil, - expires_after: 604800, + expires_after: 604_800, md5: "qzF+HgArKZjJrpfcTbiFxg==" } - assert expected_result == Pigeon.ADM.Notification.new(test_registration_id(), test_data()) + + assert expected_result == + Pigeon.ADM.Notification.new(test_registration_id(), test_data()) end describe "calculate_md5/1" do test "puts md5 hash if a valid data payload" do n = %Pigeon.ADM.Notification{ - payload: %{"data" => %{ message: "your message", hi: "bye" }} + payload: %{"data" => %{message: "your message", hi: "bye"}} } + result_n = Pigeon.ADM.Notification.calculate_md5(n) assert "w2qyl/pbK7HVl9zfzu7Nww==" == result_n.md5 end @@ -42,6 +47,7 @@ defmodule Pigeon.ADMNotificationTest do n = %Pigeon.ADM.Notification{ payload: :bad } + result_n = Pigeon.ADM.Notification.calculate_md5(n) refute result_n.md5 end @@ -54,6 +60,7 @@ defmodule Pigeon.ADMNotificationTest do "something" => 123, 456 => true } + n = Pigeon.ADM.Notification.new(test_registration_id(), data) expected_result = %{ @@ -62,6 +69,7 @@ defmodule Pigeon.ADMNotificationTest do "something" => "123", "456" => "true" } + assert expected_result == n.payload["data"] end end diff --git a/test/apns/jwt_config_test.exs b/test/apns/jwt_config_test.exs new file mode 100644 index 00000000..4999b2ad --- /dev/null +++ b/test/apns/jwt_config_test.exs @@ -0,0 +1,21 @@ +defmodule Pigeon.APNS.JWTConfigTest do + use ExUnit.Case + doctest Pigeon.APNS.JWTConfig, import: true + + def test_message(msg), + do: "#{DateTime.to_string(DateTime.utc_now())} - #{msg}" + + def test_topic, do: Application.get_env(:pigeon, :test)[:apns_topic] + def test_token, do: Application.get_env(:pigeon, :test)[:valid_apns_token] + + test "sends a push with valid config" do + n = + Pigeon.APNS.Notification.new( + test_message("push/1 with jwt"), + test_token(), + test_topic() + ) + + assert Pigeon.APNS.push(n, to: :apns_jwt_dynamic).response == :success + end +end diff --git a/test/apns/notification_test.exs b/test/apns/notification_test.exs index beeb8ab6..73cbaba2 100644 --- a/test/apns/notification_test.exs +++ b/test/apns/notification_test.exs @@ -12,8 +12,12 @@ defmodule Pigeon.APNS.NotificationTest do payload: %{"aps" => %{"alert" => test_msg()}}, expiration: nil } - assert Pigeon.APNS.Notification.new(test_msg(), test_device_token(), test_topic()) == - expected_result + + assert Pigeon.APNS.Notification.new( + test_msg(), + test_device_token(), + test_topic() + ) == expected_result end test "put_alert" do @@ -31,6 +35,7 @@ defmodule Pigeon.APNS.NotificationTest do test "put_badge" do badge = 5 + n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) @@ -41,6 +46,7 @@ defmodule Pigeon.APNS.NotificationTest do test "put_sound" do sound = "default" + n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) @@ -53,38 +59,49 @@ defmodule Pigeon.APNS.NotificationTest do n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) - |> Pigeon.APNS.Notification.put_content_available + |> Pigeon.APNS.Notification.put_content_available() - assert n.payload == %{"aps" => %{"alert" => test_msg(), "content-available" => 1}} + assert n.payload == %{ + "aps" => %{"alert" => test_msg(), "content-available" => 1} + } end test "put_category" do category = "test-category" + n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) |> Pigeon.APNS.Notification.put_category(category) - assert n.payload == %{"aps" => %{"alert" => test_msg(), "category" => category}} + assert n.payload == %{ + "aps" => %{"alert" => test_msg(), "category" => category} + } end test "put_mutable_content" do n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) - |> Pigeon.APNS.Notification.put_mutable_content + |> Pigeon.APNS.Notification.put_mutable_content() - assert n.payload == %{"aps" => %{"alert" => test_msg(), "mutable-content" => 1}} + assert n.payload == %{ + "aps" => %{"alert" => test_msg(), "mutable-content" => 1} + } end test "put_custom" do custom = %{"custom-key" => %{"custom-value" => 500}} + n = test_msg() |> Pigeon.APNS.Notification.new(test_device_token(), test_topic()) |> Pigeon.APNS.Notification.put_custom(custom) assert n.payload == - %{"aps" => %{"alert" => test_msg()}, "custom-key" => %{"custom-value" => 500}} + %{ + "aps" => %{"alert" => test_msg()}, + "custom-key" => %{"custom-value" => 500} + } end end diff --git a/test/apns_test.exs b/test/apns_test.exs index ca110533..bb139691 100644 --- a/test/apns_test.exs +++ b/test/apns_test.exs @@ -3,10 +3,15 @@ defmodule Pigeon.APNSTest do doctest Pigeon.APNS doctest Pigeon.APNS.Notification - def test_message(msg), do: "#{DateTime.to_string(DateTime.utc_now())} - #{msg}" + def test_message(msg), + do: "#{DateTime.to_string(DateTime.utc_now())} - #{msg}" + def test_topic, do: Application.get_env(:pigeon, :test)[:apns_topic] def test_token, do: Application.get_env(:pigeon, :test)[:valid_apns_token] - def bad_token, do: "00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0" + + def bad_token, + do: "00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0" + def bad_id, do: "123e4567-e89b-12d3-a456-42665544000" describe "start_connection/1" do @@ -14,6 +19,10 @@ defmodule Pigeon.APNSTest do opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev ] @@ -32,6 +41,10 @@ defmodule Pigeon.APNSTest do opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev, ping_period: 30_000 ] @@ -44,20 +57,49 @@ defmodule Pigeon.APNSTest do describe "push/1" do test "returns notification with :success on successful push" do - n = Pigeon.APNS.Notification.new(test_message("push/1"), test_token(), test_topic()) + n = + Pigeon.APNS.Notification.new( + test_message("push/1"), + test_token(), + test_topic() + ) + assert Pigeon.APNS.push(n).response == :success end test "returns notification with response error on unsuccessful push" do - n = Pigeon.APNS.Notification.new(test_message("push/1"), "bad_token", test_topic()) + n = + Pigeon.APNS.Notification.new( + test_message("push/1"), + "bad_token", + test_topic() + ) + assert Pigeon.APNS.push(n).response == :bad_device_token end test "returns list for multiple notifications" do - n = Pigeon.APNS.Notification.new(test_message("push/1"), test_token(), test_topic()) - bad_n = Pigeon.APNS.Notification.new(test_message("push/1"), "asdf1234", test_topic()) + n = + Pigeon.APNS.Notification.new( + test_message("push/1"), + test_token(), + test_topic() + ) + + bad_n = + Pigeon.APNS.Notification.new( + test_message("push/1"), + "asdf1234", + test_topic() + ) + actual = Pigeon.APNS.push([n, n, bad_n]) - assert Enum.map(actual, & &1.response) == [:success, :success, :bad_device_token] + + assert Enum.map(actual, & &1.response) == [ + :success, + :success, + :bad_device_token + ] end end @@ -66,48 +108,60 @@ defmodule Pigeon.APNSTest do n = Pigeon.APNS.Notification.new( test_message("push/1, custom worker"), - test_token(), test_topic() + test_token(), + test_topic() ) - #Pigeon.APNS.stop_connection(:apns_default) + # Pigeon.APNS.stop_connection(:apns_default) opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev ] + {:ok, worker_pid} = Pigeon.APNS.start_connection(opts) assert Pigeon.APNS.push(n, to: worker_pid).response == :success - #Pigeon.APNS.start_connection(:apns_default) + # Pigeon.APNS.start_connection(:apns_default) end test "pushes to worker's atom name" do n = Pigeon.APNS.Notification.new( test_message("push/1, custom worker"), - test_token(), test_topic() + test_token(), + test_topic() ) - #Pigeon.APNS.stop_connection(:default) + # Pigeon.APNS.stop_connection(:default) opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev, name: :custom ] + {:ok, _worker_pid} = Pigeon.APNS.start_connection(opts) assert Pigeon.APNS.push(n, to: :custom).response == :success - #Pigeon.APNS.start_connection(:apns_default) + # Pigeon.APNS.start_connection(:apns_default) end end describe "push/2 with :on_response" do test "returns :success response on successful push" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end n = "push/2 :ok" @@ -121,7 +175,8 @@ defmodule Pigeon.APNSTest do test "returns :bad_message_id response if apns-id is invalid" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end + n = "push/2 :bad_message_id" |> test_message() @@ -129,12 +184,16 @@ defmodule Pigeon.APNSTest do assert Pigeon.APNS.push(n, on_response: on_response) == :ok - assert_receive(%Pigeon.APNS.Notification{response: :bad_message_id}, 5_000) + assert_receive( + %Pigeon.APNS.Notification{response: :bad_message_id}, + 5_000 + ) end test "returns :bad_device_token if token is invalid" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end + n = "push/2 :bad_device_token" |> test_message() @@ -142,12 +201,16 @@ defmodule Pigeon.APNSTest do assert Pigeon.APNS.push(n, on_response: on_response) == :ok - assert_receive(%Pigeon.APNS.Notification{response: :bad_device_token}, 5_000) + assert_receive( + %Pigeon.APNS.Notification{response: :bad_device_token}, + 5_000 + ) end test "returns :missing_topic reponse on missing topic for certs supporting mult topics" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end + n = "push/2 :missing_topic" |> test_message() @@ -162,7 +225,7 @@ defmodule Pigeon.APNSTest do describe "push/1 with :on_response to custom worker" do test "sends to pid if specified" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end n = "push/2 :ok, custom worker" @@ -170,14 +233,21 @@ defmodule Pigeon.APNSTest do |> Pigeon.APNS.Notification.new(test_token(), test_topic()) Pigeon.APNS.stop_connection(:default) + opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev ] + {:ok, worker_pid} = Pigeon.APNS.start_connection(opts) - assert Pigeon.APNS.push(n, on_response: on_response, to: worker_pid) == :ok + assert Pigeon.APNS.push(n, on_response: on_response, to: worker_pid) == + :ok assert_receive(%Pigeon.APNS.Notification{response: :success}, 5_000) @@ -186,27 +256,32 @@ defmodule Pigeon.APNSTest do test "sends to worker's atom name if specified" do pid = self() - on_response = fn(x) -> send pid, x end + on_response = fn x -> send(pid, x) end n = "push/2 :ok, custom worker" |> test_message() |> Pigeon.APNS.Notification.new(test_token(), test_topic()) - #Pigeon.APNS.stop_connection(:default) + # Pigeon.APNS.stop_connection(:default) opts = [ cert: Application.get_env(:pigeon, :test)[:apns_cert], key: Application.get_env(:pigeon, :test)[:apns_key], + jwt_key: Application.get_env(:pigeon, :test)[:apns_jwt_key], + jwt_key_identifier: + Application.get_env(:pigeon, :test)[:apns_jwt_key_identifier], + jwt_team_id: Application.get_env(:pigeon, :test)[:apns_jwt_team_id], mode: :dev, name: :custom ] + {:ok, _worker_pid} = Pigeon.APNS.start_connection(opts) assert Pigeon.APNS.push(n, on_response: on_response, to: :custom) == :ok assert_receive(%Pigeon.APNS.Notification{response: :success}, 5_000) - #Pigeon.APNS.start_connection(:apns_default) + # Pigeon.APNS.start_connection(:apns_default) end end end diff --git a/test/fcm/result_parser_test.exs b/test/fcm/result_parser_test.exs index 14c92080..93151c70 100644 --- a/test/fcm/result_parser_test.exs +++ b/test/fcm/result_parser_test.exs @@ -10,28 +10,33 @@ defmodule Pigeon.FCM.ResultParserTest do test "parse_result with success" do expected = [success: "regid"] - ResultParser.parse(["regid"], - [%{"message_id" => "1:0408"}], - & assert_response(&1, expected), - %Notification{}) + ResultParser.parse( + ["regid"], + [%{"message_id" => "1:0408"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with single non-list regid" do expected = [success: "regid"] - ResultParser.parse("regid", - [%{"message_id" => "1:0408"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + "regid", + [%{"message_id" => "1:0408"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with success and new registration_id" do pid = self() - resp = fn resp -> send pid, resp end + resp = fn resp -> send(pid, resp) end ResultParser.parse( ["regid"], [%{"message_id" => "1:2342", "registration_id" => "32"}], - & resp.(&1), + &resp.(&1), %Notification{} ) @@ -40,39 +45,51 @@ defmodule Pigeon.FCM.ResultParserTest do assert notif.response == [update: {"regid", "32"}] assert notif.message_id == "1:2342" after - 5_000 -> flunk "No response received." + 5_000 -> flunk("No response received.") end end test "parse_result with error Unavailable" do expected = [unavailable: "regid"] - ResultParser.parse(["regid"], - [%{"error" => "Unavailable"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + ["regid"], + [%{"error" => "Unavailable"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with error NotRegistered" do expected = [not_registered: "regid"] - ResultParser.parse(["regid"], - [%{"error" => "NotRegistered"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + ["regid"], + [%{"error" => "NotRegistered"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with error InvalidRegistration" do expected = [invalid_registration: "regid"] - ResultParser.parse(["regid"], - [%{"error" => "InvalidRegistration"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + ["regid"], + [%{"error" => "InvalidRegistration"}], + &assert_response(&1, expected), + %Notification{} + ) end test "parse_result with custom error" do expected = [custom_error: "regid"] - ResultParser.parse(["regid"], - [%{"error" => "CustomError"}], - & assert_response(&1, expected), - %Notification{}) + + ResultParser.parse( + ["regid"], + [%{"error" => "CustomError"}], + &assert_response(&1, expected), + %Notification{} + ) end end diff --git a/test/fcm/worker_test.exs b/test/fcm/worker_test.exs index 46ff4375..7f6ec261 100644 --- a/test/fcm/worker_test.exs +++ b/test/fcm/worker_test.exs @@ -12,15 +12,17 @@ defmodule Pigeon.FCM.WorkerTest do pid |> :sys.get_state() |> Map.get(:consumers) - |> Map.values - |> List.first + |> Map.values() + |> List.first() + conn_pid end defp send_push(pid, count) do n = FCM.Notification.new(valid_fcm_reg_id(), %{}, %{"message" => "Test"}) + 1..count - |> Enum.each(fn(_x) -> + |> Enum.each(fn _x -> assert _notif = Pigeon.FCM.push(n, to: pid) end) end @@ -33,6 +35,7 @@ defmodule Pigeon.FCM.WorkerTest do opts = [ key: Application.get_env(:pigeon, :test)[:fcm_key] ] + {:ok, pid} = FCM.start_connection(opts) send_push(pid, 3) @@ -56,6 +59,7 @@ defmodule Pigeon.FCM.WorkerTest do opts = [ key: Application.get_env(:pigeon, :test)[:fcm_key] ] + {:ok, pid} = FCM.start_connection(opts) send_push(pid, 1) diff --git a/test/fcm_test.exs b/test/fcm_test.exs index a16022bc..7e2f8b0c 100644 --- a/test/fcm_test.exs +++ b/test/fcm_test.exs @@ -15,9 +15,11 @@ defmodule Pigeon.FCMTest do describe "start_connection/1" do test "starts conneciton with opts keyword list" do fcm_key = Application.get_env(:pigeon, :test)[:fcm_key] + opts = [ key: fcm_key ] + {:ok, pid} = Pigeon.FCM.start_connection(opts) assert is_pid(pid) @@ -36,6 +38,7 @@ defmodule Pigeon.FCMTest do opts = [ key: Application.get_env(:pigeon, :test)[:fcm_key] ] + {:ok, worker_pid} = Pigeon.FCM.start_connection(opts) expected = [success: valid_fcm_reg_id()] @@ -52,6 +55,7 @@ defmodule Pigeon.FCMTest do key: Application.get_env(:pigeon, :test)[:fcm_key], name: :custom ] + {:ok, _worker_pid} = Pigeon.FCM.start_connection(opts) expected = [success: valid_fcm_reg_id()] @@ -64,7 +68,7 @@ defmodule Pigeon.FCMTest do notification = valid_fcm_reg_id() |> Notification.new(%{}, @data) - |> Pigeon.FCM.push + |> Pigeon.FCM.push() expected = [success: valid_fcm_reg_id()] assert notification.response == expected @@ -76,14 +80,14 @@ defmodule Pigeon.FCMTest do |> Notification.new(%{}, @data) |> Pigeon.FCM.push(key: "explicit") - assert notif.status == :unauthorized + assert notif.status == :unauthorized end test "successfully sends a valid push with callback" do reg_id = valid_fcm_reg_id() n = Notification.new(reg_id, %{}, @data) pid = self() - FCM.push(n, on_response: fn(x) -> send pid, x end) + FCM.push(n, on_response: fn x -> send(pid, x) end) assert_receive(n = %Notification{response: regids}, 5000) assert n.status == :success @@ -94,7 +98,7 @@ defmodule Pigeon.FCMTest do reg_id = "bad_reg_id" n = Notification.new(reg_id, %{}, @data) pid = self() - Pigeon.FCM.push(n, on_response: fn(x) -> send pid, x end) + Pigeon.FCM.push(n, on_response: fn x -> send(pid, x) end) assert_receive(n = %Notification{}, 5000) assert n.status == :success diff --git a/test/notification_test.exs b/test/notification_test.exs index 395f77d0..43eeee54 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -8,6 +8,7 @@ defmodule Pigeon.NotificationTest do registration_id: @reg_id, payload: %{} } + assert Pigeon.FCM.Notification.new(@reg_id) == expected_result end @@ -18,6 +19,7 @@ defmodule Pigeon.NotificationTest do registration_id: reg_ids, payload: %{} } + assert Pigeon.FCM.Notification.new(reg_ids) == expected_result end @@ -27,10 +29,12 @@ defmodule Pigeon.NotificationTest do "title" => "Test Push", "icon" => "icon" } + expected_result = %Pigeon.FCM.Notification{ registration_id: @reg_id, payload: %{"notification" => n} } + assert Pigeon.FCM.Notification.new(@reg_id, n) == expected_result end @@ -38,10 +42,12 @@ defmodule Pigeon.NotificationTest do data = %{ "message" => "test" } + expected_result = %Pigeon.FCM.Notification{ registration_id: @reg_id, payload: %{"data" => data} } + assert Pigeon.FCM.Notification.new(@reg_id, %{}, data) == expected_result end @@ -51,13 +57,16 @@ defmodule Pigeon.NotificationTest do "title" => "Test Push", "icon" => "icon" } + data = %{ "message" => "test" } + expected_result = %Pigeon.FCM.Notification{ registration_id: @reg_id, payload: %{"notification" => n, "data" => data} } + assert Pigeon.FCM.Notification.new(@reg_id, n, data) == expected_result end end diff --git a/test/support/test_config.ex b/test/support/test_config.ex index 41803acf..5bf39bbc 100644 --- a/test/support/test_config.ex +++ b/test/support/test_config.ex @@ -1,4 +1,6 @@ defmodule Pigeon.TestConfig do + @moduledoc false + alias Pigeon.{APNS, FCM, ADM} def apns_dynamic do @@ -10,6 +12,16 @@ defmodule Pigeon.TestConfig do ) end + def apns_jwt_dynamic do + APNS.JWTConfig.new( + name: :apns_jwt_dynamic, + key: "AuthKey.p8", + key_identifier: System.get_env("APNS_JWT_KEY_IDENTIFIER"), + team_id: System.get_env("APNS_JWT_TEAM_ID"), + mode: :dev + ) + end + def fcm_dynamic do FCM.Config.new( name: :fcm_dynamic,