Skip to content

Commit

Permalink
Update README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
rslota committed Feb 9, 2017
1 parent f1a38fb commit 2de6d39
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 32 deletions.
144 changes: 134 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,143 @@

[![Build Status](https://travis-ci.org/esl/MongoosePush.svg?branch=initial_implementation)](https://travis-ci.org/esl/MongoosePush) [![Coverage Status](https://coveralls.io/repos/github/esl/MongoosePush/badge.svg?branch=initial_implementation)](https://coveralls.io/github/esl/MongoosePush?branch=initial_implementation)

**TODO: Add description**
**MongoosePush** is simple (seriously) **REST** service written in **Elixir** providing ability to **send push
notification** to `FCM` (Firebase Cloud Messaging) and/or
`APNS` (Apple Push Notification Service) via their `HTTP/2` API.

## Installation
## Quick start

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `mongoose_push` to your list of dependencies in `mix.exs`:
### Docker

Soon :)

### Local build

#### Perquisites

* Elixir (http://elixir-lang.org/install.html)
* Rebar3 (just enter ```mix local.rebar```)

#### Build and run

Build step is really easy. Just type in root of the repository:
```bash
MIX_ENV=prod mix do deps.get, compile, release
```

After this step you may try to run the service via:
```bash
_build/prod/rel/mongoose_push/bin/mongoose_push console
```

Yeah, I know... It crashed. Running this service is fast and simple but unfortunately you can't have push notifications without properly configured `FCM` and/or `APNS` service. So, lets configure it!

## Configuration

The whole configuration is contained in file `config/{prod|dev|test}.exs` depending on with `MIX_ENV` you will be using. You should use `MIX_ENV=prod` for production installations and `MIX_ENV=dev` for you development. Anyway, lets take a look on `config/prod.exs`, part by part.

### REST API configuration

```elixir
config :maru, MongoosePush.Router,
versioning: [
using: :path
],
https: [
ip: {127, 0, 0, 1},
port: 8443,
keyfile: "priv/ssl/fake_key.pem",
certfile: "priv/ssl/fake_cert.pem",
otp_app: :mongoose_push
]
```

This part of configuration relates only to `REST` endpoints that `MongoosePush` exposes. Here you can set bind IP adress (option: `ip`), port and paths to you `HTTP` `TLS` certificates. You should ignore other options unless you know what you're doing or you're going to get to know by reading [maru's documentation](https://maru.readme.io/docs).

You may entirely skip the `maru` config entry to disable `REST` API and just use this project as `Elixir` library.

### FCM configuration
Lets take a look at sample `FCM` service configuration:
```elixir
def deps do
[{:mongoose_push, "~> 0.1.0"}]
end
config :mongoose_push, fcm: [
default: [
key: "fake_app_key",
pool_size: 5,
mode: :prod
]
]
```

The first level keyword list is a pool definition. You may have several named pools of different sizes and with different configurations. Currently the only reason you may want to do this is that, the `REST` client may switch between them by specifying matching `:mode` in their push request.

Each `FCM` pool may be configured by setting the following fields:
* **key** (*required*) - you `FCM` Application Key for using Googles API
* **pool_size** (*required*) - maximum number of used `HTTP/2` connections to google's service
* **mode** (*either `:prod` or `:dev`*) - pool's mode. `REST` client may select pool used to push his notification by specifying matching option in his request
* **endpoint** (*optional*) - URL override for `FCM` service. Useful mainly in tests

You may entirely skip the `FCM` config entry to disable `FCM` support.

### APNS configuration

Lets take a look at sample `APNS` service configuration:
```elixir
config :mongoose_push, apns: [
dev: [
cert: "priv/apns/dev_cert.pem",
key: "priv/apns/dev_key.pem",
mode: :dev,
use_2197: false,
pool_size: 5
],
prod: [
cert: "priv/apns/prod_cert.pem",
key: "priv/apns/prod_key.pem",
mode: :prod,
use_2197: false,
pool_size: 5
]
]
```
Analogically to `FCM` configuration, at top level we may specify named pools that have different configurations. For `APNS` this is specifically useful since Apple delivers different APS certificated for development and production use. As in `FCM`, `REST` client may select named pool by providing matching `:mode` in his `REST` request.

Each `APNS` pool may be configured by setting the following fields:
* **cert** (*required*) - relative path to `APNS` `PEM` certificate issued by Apple. This certificate have to be somewhere in `priv` directory
* **key** (*required*) - relative path to `PEM` private key for `APNS` certificate issued by Apple. This file have to be somewhere in `priv` directory
* **pool_size** (*required*) - maximum number of used `HTTP/2` connections to google's service
* **mode** (*either `:prod` or `:dev`*) - pool's mode. `REST` client may select pool used to push his notification by specifying matching option in his request
* **endpoint** (*optional*) - URL override for `APNS` service. Useful mainly in tests
* **use_2197** (*optional `true` or `false`*) - whether use alternative port for `APNS`: 2197

You may entirely skip the `APNS` config entry to disable `APNS` support.

## REST API

### Swagger

If for some reason you need `Swagger` spec for this `REST` service, after compiling and running this project with `MIX_ENV=dev`, there is swagger endpoint available at `REST` path `/swagger.json`

### Just tell me what to send already

There is only one endpoint at this moment:
* `POST /v1/notification/{device_id}`

As you can imagine, `{device_id}` should be replaced with device ID/Token generated by your push notification provider (`FCM` or `APNS`). The notification should be sent as `JSON` payload of this request. Minimal `JSON` request could be like this:

```json
{
"service": "apns",
"body": "notification's text body",
"title": "notification's title"
}
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/mongoose_push](https://hexdocs.pm/mongoose_push).
The full list of options contains the following:
* **service** (*required*, `apns` or `fcm`) - push notifications provider to be used for this notification
* **body** (*required*) - text body of notification
* **title** (*required*) - short title of notification
* **mode** (*optional*, `prod` (default) or `dev`) - allows for selecting named pool configured in `MongoosePush`
* **click_action** (*optional*) - for `FCM` its `activity` to run when notification is clicked. For `APNS` its `category` to invoke. Please refer to Android/iOS documentation for more details about this action
* **tag** (*optional*, `FCM` specific) - notifications aggregation key
* **badge** (*optional*, `APNS` specific) - unread notifications count
* **topic** (*optional*, `APNS` specific) - if APNS certificate configured in `MongoosePush` allows for multiple applications, this field selects the application. Please refer to `APNS` documentation for more datails
5 changes: 3 additions & 2 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ config :maru, MongoosePush.Router,
]

config :mongoose_push, fcm: [
prod: [
default: [
key: "fake_app_key",
pool_size: 5
pool_size: 5,
mode: :prod
]
]

Expand Down
3 changes: 2 additions & 1 deletion config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ config :maru, MongoosePush.Router,
config :mongoose_push, fcm: [
prod: [
key: "fake_app_key",
pool_size: 5
pool_size: 5,
mode: :prod
]
]

Expand Down
18 changes: 7 additions & 11 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,44 +13,40 @@ config :maru, MongoosePush.Router,
]

config :mongoose_push, fcm: [
prod: [
default: [
key: "fake_app_key",
endpoint: "localhost",
pool_size: 5
pool_size: 5,
mode: :prod
]
]

config :mongoose_push, apns: [
dev1: [
development_endpoint: "localhost",
production_endpoint: "localhost",
endpoint: "localhost",
cert: "priv/apns/dev_cert.pem",
key: "priv/apns/dev_key.pem",
mode: :dev,
use_2197: true,
pool_size: 1
],
prod1: [
development_endpoint: "localhost",
production_endpoint: "localhost",
endpoint: "localhost",
cert: "priv/apns/prod_cert.pem",
key: "priv/apns/prod_key.pem",
mode: :prod,
use_2197: true,
pool_size: 2
],
dev2: [
development_endpoint: "localhost",
production_endpoint: "localhost",
endpoint: "localhost",
cert: "priv/apns/dev_cert.pem",
key: "priv/apns/dev_key.pem",
mode: :dev,
use_2197: true,
pool_size: 3
],
prod2: [
development_endpoint: "localhost",
production_endpoint: "localhost",
endpoint: "localhost",
cert: "priv/apns/prod_cert.pem",
key: "priv/apns/prod_key.pem",
mode: :prod,
Expand Down
51 changes: 50 additions & 1 deletion lib/mongoose_push/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ defmodule MongoosePush.Application do
config = env(service)

config
|> Enum.group_by(&(elem(&1, 1)[:mode]), &(elem &1, 0))
|> Enum.group_by(&(mode(elem(&1, 1))), &(elem &1, 0))
|> Map.get(mode)
end

Expand All @@ -48,6 +48,8 @@ defmodule MongoosePush.Application do
defp fcm_workers(nil), do: []
defp fcm_workers(config) do
workers = Enum.map(config, fn({pool_name, pool_config}) ->
pool_config = translate_worker_config(:fcm, pool_config)

Enum.map(1..pool_size(:fcm, pool_name), fn(id) ->
worker_name = worker_name(:fcm, pool_name, id)
worker(Pigeon.GCMWorker, [worker_name, pool_config], [id: worker_name])
Expand All @@ -61,6 +63,8 @@ defmodule MongoosePush.Application do
defp apns_workers(nil), do: []
defp apns_workers(config) do
workers = Enum.map(config, fn({pool_name, pool_config}) ->
pool_config = translate_worker_config(:apns, pool_config)

Enum.map(1..pool_size(:apns, pool_name), fn(id) ->
worker_name = worker_name(:apns, pool_name, id)
worker_config = Pigeon.APNS.Config.config(worker_name, pool_config)
Expand All @@ -72,4 +76,49 @@ defmodule MongoosePush.Application do
|> List.flatten
end

defp translate_worker_config(:fcm, config) do
config
|> fix_priv_paths()
|> ensure_mode()
end

defp translate_worker_config(:apns, config) do
config
|> fix_priv_paths()
|> ensure_mode()
|> construct_apns_endpoint_options()
end

defp ensure_mode(config) do
case config[:mode] do
nil ->
Enum.into([mode: mode(config)], config)
_ ->
config
end
end

defp construct_apns_endpoint_options(config) do
new_key = case mode(config) do
:dev -> :development_endpoint
:prod -> :production_endpoint
end
Enum.into([{new_key, config[:endpoint]}], config)
end

defp fix_priv_paths(config) do
path_keys = [:cert, :key]
config
|> Enum.map(fn({key, value}) ->
case Enum.member?(path_keys, key) do
true ->
{key, Application.app_dir(:mongoose_push, value)}
false ->
{key, value}
end
end)
end

defp mode(config), do: config[:mode] || :prod

end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ defmodule MongoosePush.Mixfile do
{:poison, "~> 3.0"},
{:httpoison, "~> 0.10.0"},
{:maru_swagger, github: "elixir-maru/maru_swagger"},
{:distillery, "~> 1.0"},
# Below only :dev / :test deps
{:mock, "~> 0.2.0", only: :test},
{:excoveralls, "~> 0.6", only: :test},
Expand Down
11 changes: 10 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []},
%{"bbmustache": {:hex, :bbmustache, "1.0.1", "6f8504fa069e6e3595593260384c6c372065dd4ead557bf5b61a2b2bbb4d80f9", [:rebar], []},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []},
"certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []},
"conform": {:hex, :conform, "0.16.0", "f6d6b207c693f2f939af8edeab7c735c4fd48552c63c0a019a52b6d2d98a1ca4", [:mix], [{:neotoma, "~> 1.7.3", [hex: :neotoma, optional: false]}]},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, optional: false]}]},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], []},
"credo": {:hex, :credo, "0.6.1", "a941e2591bd2bd2055dc92b810c174650b40b8290459c89a835af9d59ac4a5f8", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]},
"dialyxir": {:hex, :dialyxir, "0.4.4", "e93ff4affc5f9e78b70dc7bec7b07da44ae1ed3fef38e7113568dd30ad7b01d3", [:mix], []},
"distillery": {:hex, :distillery, "1.1.0", "e9943bd29557e9c252a051d8ac4b47e597cd9bf2a74332b8628eab4954eb51d7", [:mix], []},
"earmark": {:hex, :earmark, "1.1.1", "433136b7f2e99cde88b745b3a0cfc3fbc81fe58b918a09b40fce7f00db4d8187", [:mix], []},
"erlware_commons": {:hex, :erlware_commons, "0.13.0", "b86d493f3c3e52acba16f0a78900c12525e9eb46db3450b0d25b419aa8221115", [:rebar], []},
"ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]},
"excoveralls": {:hex, :excoveralls, "0.6.2", "0e993d096f1fbb6e70a3daced5c89aac066bda6bce57829622aa2d1e2b338cfb", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]},
"exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]},
"exrm": {:hex, :exrm, "0.18.8", "8bc43a5da2cd54eb18fdffc6bb1608424f3897f97e816bcf752fb931c0ef1af4", [:mix], [{:conform, "~> 0.16.0", [hex: :conform, optional: false]}, {:relx, "~> 3.1.0", [hex: :relx, optional: false]}]},
"getopt": {:hex, :getopt, "0.8.2", "b17556db683000ba50370b16c0619df1337e7af7ecbf7d64fbf8d1d6bce3109b", [:rebar], []},
"hackney": {:hex, :hackney, "1.6.5", "8c025ee397ac94a184b0743c73b33b96465e85f90a02e210e86df6cbafaa5065", [:rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]},
"hpack": {:hex, :hpack, "1.0.2", "94f6e7214e3184d2ab9670a22c80c80b46651a17e35fd02e5c3c4d2ce2466d02", [:mix], []},
"httpoison": {:hex, :httpoison, "0.10.0", "4727b3a5e57e9a4ff168a3c2883e20f1208103a41bccc4754f15a9366f49b676", [:mix], [{:hackney, "~> 1.6.3", [hex: :hackney, optional: false]}]},
Expand All @@ -21,9 +27,12 @@
"mime": {:hex, :mime, "1.0.1", "05c393850524767d13a53627df71beeebb016205eb43bfbd92d14d24ec7a1b51", [:mix], []},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
"mock": {:hex, :mock, "0.2.1", "bfdba786903e77f9c18772dee472d020ceb8ef000783e737725a4c8f54ad28ec", [:mix], [{:meck, "~> 0.8.2", [hex: :meck, optional: false]}]},
"neotoma": {:hex, :neotoma, "1.7.3", "d8bd5404b73273989946e4f4f6d529e5c2088f5fa1ca790b4dbe81f4be408e61", [:rebar], []},
"pigeon": {:git, "https://github.com/rslota/pigeon.git", "6d1e4e3446e30822b6cede650c91275f7b15afdd", [tag: "6d1e4e3"]},
"plug": {:hex, :plug, "1.3.0", "6e2b01afc5db3fd011ca4a16efd9cb424528c157c30a44a0186bcc92c7b2e8f3", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, optional: true]}, {:mime, "~> 1.0", [hex: :mime, optional: false]}]},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []},
"providers": {:hex, :providers, "1.4.1", "e74176b29d4771544410af5022fe1306b100d0f57c9e0f3656bcff5962f87cd2", [:rebar], [{:getopt, "0.8.2", [hex: :getopt, optional: false]}]},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], []},
"relx": {:hex, :relx, "3.1.0", "71b7e00e83543c92512d785a268db78a970fe5d811171329623784daca7d23dc", [:rebar], [{:bbmustache, "1.0.1", [hex: :bbmustache, optional: false]}, {:erlware_commons, "0.13.0", [hex: :erlware_commons, optional: false]}, {:getopt, "0.8.2", [hex: :getopt, optional: false]}, {:providers, "1.4.1", [hex: :providers, optional: false]}]},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}}
12 changes: 6 additions & 6 deletions test/mongoose_push_application_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ defmodule MongoosePushApplicationTest do
test "pool groups" do
assert [:dev1, :dev2] == pools_by_mode(:apns, :dev)
assert [:prod1, :prod2] == pools_by_mode(:apns, :prod)
assert [:prod] == pools_by_mode(:fcm, :prod)
assert [:default] == pools_by_mode(:fcm, :default)
end

test "pool size" do
assert 1 == pool_size(:apns, :dev1)
assert 2 == pool_size(:apns, :prod1)
assert 3 == pool_size(:apns, :dev2)
assert 4 == pool_size(:apns, :prod2)
assert 5 == pool_size(:fcm, :prod)
assert 5 == pool_size(:fcm, :default)
end

test "pools online" do
Expand All @@ -34,16 +34,16 @@ defmodule MongoosePushApplicationTest do
assert Process.alive?(Process.whereis(worker_name(:apns, :dev2, 1)))
assert Process.alive?(Process.whereis(worker_name(:apns, :dev2, 3)))

assert Process.alive?(Process.whereis(worker_name(:fcm, :prod, 1)))
assert Process.alive?(Process.whereis(worker_name(:fcm, :prod, 5)))
assert Process.alive?(Process.whereis(worker_name(:fcm, :default, 1)))
assert Process.alive?(Process.whereis(worker_name(:fcm, :default, 5)))
end

test "pools have corrent size" do
assert nil == Process.whereis(worker_name(:apns, :dev1, 2))
assert nil == Process.whereis(worker_name(:apns, :prod1, 3))
assert nil == Process.whereis(worker_name(:apns, :dev2, 4))
assert nil == Process.whereis(worker_name(:apns, :prod2, 5))
assert nil == Process.whereis(worker_name(:fcm, :prod, 6))
assert nil == Process.whereis(worker_name(:fcm, :default, 6))
end

test "application starts and stops" do
Expand All @@ -57,7 +57,7 @@ defmodule MongoosePushApplicationTest do
assert nil == Process.whereis(worker_name(:apns, :prod1, 1))
assert nil == Process.whereis(worker_name(:apns, :dev2, 1))
assert nil == Process.whereis(worker_name(:apns, :prod2, 1))
assert nil == Process.whereis(worker_name(:fcm, :prod, 1))
assert nil == Process.whereis(worker_name(:fcm, :default, 1))
ok = Application.start(:mongoose_push, :temporary)
end
end

0 comments on commit 2de6d39

Please sign in to comment.