README.md

[![Build Status](https://travis-ci.org/codedge-llc/pigeon.svg?branch=master)](https://travis-ci.org/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)
# Pigeon
HTTP2-compliant wrapper for sending iOS and Android push notifications.

## Installation
Add pigeon as a `mix.exs` dependency:

**Note: Pigeon's API will likely change until v1.0**
  ```elixir
  def deps do
    [{:pigeon, "~> 0.5.0"}]
  end
  ```
  
After running `mix deps.get`, configure `mix.exs` to start the application automatically.
  ```elixir
  def application do
    [applications: [:pigeon]]
  end
  ```
  
## GCM (Android)
### Usage
1. Set your environment variables.
  ```elixir
  config :pigeon, 
    gcm_key: "your_gcm_key_here"
  ```
  
2. Create a notification packet. 
  ```elixir
  data = %{ message: "your message" }
  n = Pigeon.GCM.Notification.new(data, "your device registration ID")
  ```
 
3. Send the packet.
  ```elixir
  Pigeon.GCM.push(n)
  ```
  
### Sending to Multiple Registration IDs
Pass in a list of registration IDs, as many as you want. IDs will automatically be chunked into sets of 1000 before sending the push (as per GCM guidelines).
  ```elixir
  data = %{ message: "your message" }
  n = Pigeon.GCM.Notification.new(data, ["first ID", "second ID"])
  ```


### Notification Struct
When using `Pigeon.GCM.Notification.new/2`, `message_id` and `updated_registration` will always be `nil`. These keys are set in the response callback. `registration_id` can either be a single string or a list of strings.
```elixir
%Pigeon.GCM.Notification{
    data: nil,
    message_id: nil,
    registration_id: nil,
    updated_registration_id: nil
}
```

## APNS (Apple iOS)
### Usage
1. Set your environment variables. See below for setting up your certificate and key.
  ```elixir
  config :pigeon, 
    apns_mode: :dev,
    apns_cert: "cert.pem",
    apns_key: "key_unencrypted.pem"
  ```

2. Create a notification packet. **Note: Your push topic is generally the app's bundle identifier.**
  ```elixir
  n = Pigeon.APNS.Notification.new("your message", "your device token", "your push topic")
  ```
  
  
3. Send the packet.
  ```elixir
  Pigeon.APNS.push(n)
  ```
  
### Generating Your Certificate and Key .pem
1. In Keychain Access, right-click your push certificate and select _"Export..."_
2. Export the certificate as `cert.p12`
3. Click the dropdown arrow next to the certificate, right-click the private key and select _"Export..."_
4. Export the private key as `key.p12`
5. From a shell, convert the certificate.
   ```
   openssl pkcs12 -clcerts -nokeys -out cert.pem -in cert.p12
   ```
   
6. Convert the key.
   ```
   openssl pkcs12 -nocerts -out key.pem -in key.p12
   ```

7. Remove the password from the key.
   ```
   openssl rsa -in key.pem -out key_unencrypted.pem
   ```
   
8. `cert.pem` and `key_unencrypted.pem` can now be used as the cert and key in `Pigeon.push`, respectively. Set them in your `config.exs`

### Notifications with Custom Data
Notifications can contain additional information for the `aps` key with a map passed as an optional 4th parameter (e.g. setting badge counters or defining custom sounds)
  ```elixir
  n = Pigeon.APNS.Notification.new("your message", "your device token", "your push topic", %{
    badge: 5,
    sound: "default"
  })
  ```
  
Or define custom payload data with an optional 5th parameter:
  ```elixir
  n = Pigeon.APNS.Notification.new("your message", "your device token", "your push topic", %{}, %{
    your-custom-key: %{
      custom-value: 500
    }
  })
  ```

## Handling Push Responses
### GCM
1. Pass an optional anonymous function as your second parameter. This function will get called on each registration ID assuming some of them were successful.
  ```elixir
  data = %{ message: "your message" }
  n = Pigeon.GCM.Notification.new(data, "device registration ID")
  Pigeon.GCM.push(n, fn(x) -> IO.inspect(x) end)
  ```
  
2. Reponses return a tuple of either `{:ok, notification}` or `{:error, reason, notification}`. You could handle responses like so:
  ```elixir
  on_response = fn(x) ->
    case x do
      {:ok, notification} ->
        # Push successful, check to see if the registration ID changed
        if !is_nil(notification.updated_registration_id) do
          # Update the registration ID in the database
        end
      {:error, :InvalidRegistration, notification} ->
        # Remove the bad ID from the database
      {:error, reason, notification} ->
        # Handle other errors
    end
  end
  
  data = %{ message: "your message" }
  n = Pigeon.GCM.Notification.new(data, "your device token")
  Pigeon.GCM.push(n, on_response)
  ```
  
Notification structs returned as `{:ok, notification}` will always contain exactly one registration ID for the `registration_id` key. 

For `{:error, reason, notification}` tuples, this key can be one or many IDs depending on the error. `:InvalidRegistration` will return exactly one, whereas `:AuthenticationError` and `:InternalServerError` will return up to 1000 IDs (and the callback called for each failed 1000-chunked request).
  
#### Error Responses
*Slightly modified from [GCM Server Reference](https://developers.google.com/cloud-messaging/http-server-ref#error-codes)*

|Reason                     |Description                  |
|---------------------------|-----------------------------|
|:MissingRegistration       |Missing Registration Token   |
|:InvalidRegistration       |Invalid Registration Token   |
|:NotRegistered             |Unregistered Device          |
|:InvalidPackageName        |Invalid Package Name         |
|:AuthenticationError       |Authentication Error         |
|:MismatchSenderId          |Mismatched Sender            |
|:InvalidJSON               |Invalid JSON                 |
|:MessageTooBig             |Message Too Big              |
|:InvalidDataKey            |Invalid Data Key             |
|:InvalidTtl                |Invalid Time to Live         |
|:Unavailable               |Timeout                      |
|:InternalServerError       |Internal Server Error        |
|:DeviceMessageRateExceeded |Message Rate Exceeded        |
|:TopicsMessageRateExceeded |Topics Message Rate Exceeded |
|:UnknownError              |Unknown Error                |

### APNS
1. Pass an optional anonymous function as your second parameter.
  ```elixir
  n = Pigeon.APNS.Notification.new("your message", "your device token", "your push topic")
  Pigeon.APNS.push(n, fn(x) -> IO.inspect(x) end)
  ```

2. Responses return a tuple of either `{:ok, notification}` or `{:error, reason, notification}`. You could handle responses like so:
  ```elixir
  on_response = fn(x) ->
    case x do
      {:ok, notification} ->
        Logger.debug "Push successful!"
      {:error, :BadDeviceToken, notification} ->
        Logger.error "Bad device token!"
      {:error, reason, notification} ->
        Logger.error "Some other error happened."
    end
  end

  n = Pigeon.APNS.Notification.new("your message", "your device token", "your push topic")
  Pigeon.APNS.push(n, on_response)
  ```

#### Error Responses
*Taken from [APNS Provider API](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/APNsProviderAPI.html#//apple_ref/doc/uid/TP40008194-CH101-SW3)*

|Reason                     |Description                   |
|---------------------------|------------------------------|
|:PayloadEmpty              |The message payload was empty.|
|:PayloadTooLarge           |The message payload was too large. The maximum payload size is 4096 bytes.|
|:BadTopic                  |The apns-topic was invalid.|
|:TopicDisallowed           |Pushing to this topic is not allowed.|
|:BadMessageId              |The apns-id value is bad.|
|:BadExpirationDate         |The apns-expiration value is bad.|
|:BadPriority               |The apns-priority value is bad.|
|:MissingDeviceToken        |The device token is not specified in the request :path. Verify that the :path header contains the device token.|
|:BadDeviceToken            |The specified device token was bad. Verify that the request contains a valid token and that the token matches the environment.|
|:DeviceTokenNotForTopic    |The device token does not match the specified topic.|
|:Unregistered              |The device token is inactive for the specified topic.|
|:DuplicateHeaders          |One or more headers were repeated.|
|:BadCertificateEnvironment |The client certificate was for the wrong environment.|
|:BadCertificate            |The certificate was bad.|
|:Forbidden                 |The specified action is not allowed.|
|:BadPath                   |The request contained a bad :path value.|
|:MethodNotAllowed          |The specified :method was not POST.|
|:TooManyRequests           |Too many requests were made consecutively to the same device token.|
|:IdleTimeout               |Idle time out.|
|:Shutdown                  |The server is shutting down.|
|:InternalServerError       |An internal server error occurred.|
|:ServiceUnavailable        |The service is unavailable.|
|:MissingTopic              |The apns-topic header of the request was not specified and was required. The apns-topic header is mandatory when the client is connected using a certificate that supports multiple topics.|