feat: upgrade gcm to fcm (#231)

https://github.com/edganiukov/fcm

fix #230
This commit is contained in:
Bo-Yi Wu 2017-05-31 22:56:10 -05:00 committed by GitHub
parent c16500306f
commit 77bce18c9f
16 changed files with 531 additions and 895 deletions

View File

@ -38,12 +38,12 @@ A push notification micro server using [Gin](https://github.com/gin-gonic/gin) f
## Support Platform
* [APNS](https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/remotenotificationspg/Chapters/ApplePushService.html)
* [GCM](https://developer.android.com/google/gcm/index.html)
* [APNS](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html)
* [FCM](https://firebase.google.com/)
## Features
* Support [Google Cloud Message](https://developers.google.com/cloud-messaging/) using [go-gcm](https://github.com/google/go-gcm) library for Android.
* Support [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) using [fcm](https://github.com/edganiukov/fcm) library for Android.
* Support [HTTP/2](https://http2.github.io/) Apple Push Notification Service using [apns2](https://github.com/sideshow/apns2) library.
* Support [YAML](https://github.com/go-yaml/yaml) configuration.
* Support command line to send single Android or iOS notification.
@ -56,7 +56,7 @@ A push notification micro server using [Gin](https://github.com/gin-gonic/gin) f
* Support store app stat to memory, [Redis](http://redis.io/), [BoltDB](https://github.com/boltdb/bolt), [BuntDB](https://github.com/tidwall/buntdb) or [LevelDB](https://github.com/syndtr/goleveldb).
* Support `p12` or `pem` formtat of iOS certificate file.
* Support `/sys/stats` show response time, status code count, etc.
* Support for HTTP proxy to Google server (GCM).
* Support for HTTP proxy to Google server (FCM).
* Support retry send notification if server response is fail.
* Support expose [prometheus](https://prometheus.io/) metrics.
* Support install TLS certificates from [Let's Encrypt](https://letsencrypt.org/) automatically.
@ -75,7 +75,7 @@ core:
ssl: false
cert_path: "cert.pem"
key_path: "key.pem"
http_proxy: "" # only working for GCM server
http_proxy: "" # only working for FCM server
pid:
enabled: false
path: "gorush.pid"
@ -190,7 +190,7 @@ Server Options:
-m, --message <message> Notification message
-t, --token <token> Notification token
--title <title> Notification title
--proxy <proxy> Proxy URL (only for GCM)
--proxy <proxy> Proxy URL (only for FCM)
--pid <pid path> Process identifier path
iOS Options:
-i, --key <file> certificate key file path
@ -215,10 +215,10 @@ $ gorush -android -m="your message" -k="API Key" -t="Device token"
```
* `-m`: Notification message.
* `-k`: [Google Cloud Messaging](https://developers.google.com/cloud-messaging) api key
* `-k`: [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) api key
* `-t`: Device token.
* `--title`: Notification title.
* `--proxy`: Set http proxy url. (only working for GCM)
* `--proxy`: Set http proxy url. (only working for FCM)
### Send iOS notification
@ -430,10 +430,10 @@ Request body must has a notifications array. The following is a parameter table
| to | string | The value must be a registration token, notification key, or topic. | - | only Android |
| collapse_key | string | a key for collapsing notifications | - | only Android |
| delay_while_idle | bool | a flag for device idling | - | only Android |
| time_to_live | uint | expiration of message kept on GCM storage | - | only Android |
| time_to_live | uint | expiration of message kept on FCM storage | - | only Android |
| restricted_package_name | string | the package name of the application | - | only Android |
| dry_run | bool | allows developers to test a request without actually sending a message | - | only Android |
| notification | string array | payload of a GCM message | - | only Android. See the [detail](#android-notification-payload) |
| notification | string array | payload of a FCM message | - | only Android. See the [detail](#android-notification-payload) |
| expiration | int | expiration for notification | - | only iOS |
| apns_id | string | A canonical UUID that identifies the notification | - | only iOS |
| topic | string | topic of the remote notification | - | only iOS |
@ -457,7 +457,7 @@ Request body must has a notifications array. The following is a parameter table
| title-loc-args | array of strings | Variable string values to appear in place of the format specifiers in title-loc-key. | - | |
| title-loc-key | string | The key to a title string in the Localizable.strings file for the current localization. | - | |
See more detail about [APNs Remote Notification Payload](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html#//apple_ref/doc/uid/TP40008194-CH17-SW1).
See more detail about [APNs Remote Notification Payload](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html).
### Android notification payload
@ -472,7 +472,7 @@ See more detail about [APNs Remote Notification Payload](https://developer.apple
| title_loc_key | string | Indicates the key to the title string for localization. | - | |
| title_loc_args | string | Indicates the string value to replace format specifiers in title string for localization. | - | |
See more detail about [GCM server reference](https://developers.google.com/cloud-messaging/http-server-ref#send-downstream).
See more detail about [Firebase Cloud Messaging HTTP Protocol reference](https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream).
### iOS Example

View File

@ -8,7 +8,7 @@ core:
ssl: false
cert_path: "cert.pem"
key_path: "key.pem"
http_proxy: "" # only working for GCM server
http_proxy: "" # only working for FCM server
pid:
enabled: false
path: "gorush.pid"

View File

@ -10,7 +10,7 @@ import (
"sync"
"time"
"github.com/google/go-gcm"
"github.com/edganiukov/fcm"
apns "github.com/sideshow/apns2"
"github.com/sideshow/apns2/certificate"
"github.com/sideshow/apns2/payload"
@ -75,7 +75,7 @@ type PushNotification struct {
TimeToLive *uint `json:"time_to_live,omitempty"`
RestrictedPackageName string `json:"restricted_package_name,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
Notification gcm.Notification `json:"notification,omitempty"`
Notification fcm.Notification `json:"notification,omitempty"`
// iOS
Expiration int64 `json:"expiration,omitempty"`
@ -142,7 +142,7 @@ func CheckMessage(req PushNotification) error {
return nil
}
// SetProxy only working for GCM server.
// SetProxy only working for FCM server.
func SetProxy(proxy string) error {
proxyURL, err := url.ParseRequestURI(proxy)
@ -457,8 +457,8 @@ Retry:
// GetAndroidNotification use for define Android notification.
// HTTP Connection Server Reference for Android
// https://developers.google.com/cloud-messaging/http-server-ref
func GetAndroidNotification(req PushNotification) gcm.HttpMessage {
notification := gcm.HttpMessage{
func GetAndroidNotification(req PushNotification) *fcm.Message {
notification := &fcm.Message{
To: req.To,
CollapseKey: req.CollapseKey,
ContentAvailable: req.ContentAvailable,
@ -468,7 +468,7 @@ func GetAndroidNotification(req PushNotification) gcm.HttpMessage {
DryRun: req.DryRun,
}
notification.RegistrationIds = req.Tokens
notification.RegistrationIDs = req.Tokens
if len(req.Priority) > 0 && req.Priority == "high" {
notification.Priority = "high"
@ -530,11 +530,17 @@ Retry:
APIKey = req.APIKey
}
res, err := gcm.SendHttp(APIKey, notification)
client, err := fcm.NewClient(APIKey)
if err != nil {
// GCM server error
LogError.Error("GCM server error: " + err.Error())
// FCM server error
LogError.Error("FCM server error: " + err.Error())
return false
}
res, err := client.Send(notification)
if err != nil {
// FCM server error
LogError.Error("FCM server error: " + err.Error())
return false
}
@ -544,12 +550,12 @@ Retry:
var newTokens []string
for k, result := range res.Results {
if result.Error != "" {
if result.Error != nil {
isError = true
newTokens = append(newTokens, req.Tokens[k])
LogPush(FailedPush, req.Tokens[k], req, errors.New(result.Error))
LogPush(FailedPush, req.Tokens[k], req, result.Error)
if PushConf.Core.Sync {
req.AddLog(getLogPushEntry(FailedPush, req.Tokens[k], req, errors.New(result.Error)))
req.AddLog(getLogPushEntry(FailedPush, req.Tokens[k], req, result.Error))
}
continue
}

View File

@ -9,7 +9,7 @@ import (
"github.com/appleboy/gorush/config"
"github.com/buger/jsonparser"
"github.com/google/go-gcm"
"github.com/edganiukov/fcm"
"github.com/sideshow/apns2"
"github.com/stretchr/testify/assert"
)
@ -393,7 +393,7 @@ func TestAndroidNotificationStructure(t *testing.T) {
"a": "1",
"b": 2,
},
Notification: gcm.Notification{
Notification: fcm.Notification{
Color: test,
Tag: test,
},
@ -709,7 +709,7 @@ func TestAPNSClientProdHost(t *testing.T) {
assert.Equal(t, apns2.HostProduction, ApnsClient.Host)
}
func TestGCMMessage(t *testing.T) {
func TestFCMMessage(t *testing.T) {
var req PushNotification
var err error

21
vendor/github.com/edganiukov/fcm/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Eduard Ganiukov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

58
vendor/github.com/edganiukov/fcm/README.md generated vendored Normal file
View File

@ -0,0 +1,58 @@
# fcm
[![GoDoc](https://godoc.org/github.com/edganiukov/fcm?status.svg)](https://godoc.org/github.com/edganiukov/fcm)
[![Build Status](https://travis-ci.org/edganiukov/fcm.svg?branch=master)](https://travis-ci.org/edganiukov/fcm)
[![Go Report Card](https://goreportcard.com/badge/github.com/edganiukov/fcm)](https://goreportcard.com/report/github.com/edganiukov/fcm)
Golang client library for Firebase Cloud Messaging. Implemented only [HTTP client](https://firebase.google.com/docs/cloud-messaging/http-server-ref#downstream).
More information on [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/)
### Getting Started
-------------------
To install fcm, use `go get`:
```bash
go get github.com/edganiukov/fcm
```
or `govendor`:
```bash
govendor fetch github.com/edganiukov/fcm
```
or other tool for vendoring.
### Sample Usage
----------------
Here is a simple example illustrating how to use FCM library:
```go
package main
import (
"github.com/edganiukov/fcm"
)
func main() {
// Create the message to be sent.
msg := &fcm.Message{
Token: "sample_device_token",
Data: map[string]interface{}{
"foo": "bar",
}
}
// Create a FCM client to send the message.
client := fcm.NewClient("sample_api_key")
// Send the message and receive the response without retries.
response, err := client.Send(msg)
if err != nil {
/* ... */
}
/* ... */
}
```
#### TODO:
---------
- [ ] Retry only failed messages while multicast messaging.

133
vendor/github.com/edganiukov/fcm/client.go generated vendored Normal file
View File

@ -0,0 +1,133 @@
package fcm
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
)
const (
// DefaultEndpoint contains endpoint URL of FCM service.
DefaultEndpoint = "https://fcm.googleapis.com/fcm/send"
)
var (
// ErrInvalidAPIKey occurs if API key is not set.
ErrInvalidAPIKey = errors.New("client API Key is invalid")
)
// Client abstracts the interaction between the application server and the
// FCM server via HTTP protocol. The developer must obtain an API key from the
// Google APIs Console page and pass it to the `Client` so that it can
// perform authorized requests on the application server's behalf.
// To send a message to one or more devices use the Client's Send.
//
// If the `HTTP` field is nil, a zeroed http.Client will be allocated and used
// to send messages.
type Client struct {
apiKey string
client *http.Client
endpoint string
}
// NewClient creates new Firebase Cloud Messaging Client based on API key and
// with default endpoint and http client.
func NewClient(apiKey string, opts ...Option) (*Client, error) {
if apiKey == "" {
return nil, ErrInvalidAPIKey
}
c := &Client{
apiKey: apiKey,
endpoint: DefaultEndpoint,
client: &http.Client{},
}
for _, o := range opts {
if err := o(c); err != nil {
return nil, err
}
}
return c, nil
}
// Send sends a message to the FCM server without retrying in case of service
// unavailability. A non-nil error is returned if a non-recoverable error
// occurs (i.e. if the response status is not "200 OK").
func (c *Client) Send(msg *Message) (*Response, error) {
// validate
if err := msg.Validate(); err != nil {
return nil, err
}
// marshal message
data, err := json.Marshal(msg)
if err != nil {
return nil, err
}
return c.send(data)
}
// SendWithRetry sends a message to the FCM server with defined number of
// retrying in case of temporary error.
func (c *Client) SendWithRetry(msg *Message, retryAttempts int) (*Response, error) {
// validate
if err := msg.Validate(); err != nil {
return nil, err
}
// marshal message
data, err := json.Marshal(msg)
if err != nil {
return nil, err
}
resp := new(Response)
err = retry(func() error {
var err error
resp, err = c.send(data)
return err
}, retryAttempts)
if err != nil {
return nil, err
}
return resp, nil
}
// send sends a request.
func (c *Client) send(data []byte) (*Response, error) {
// create request
req, err := http.NewRequest("POST", c.endpoint, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
// add headers
req.Header.Add("Authorization", fmt.Sprintf("key=%s", c.apiKey))
req.Header.Add("Content-Type", "application/json")
// execute request
resp, err := c.client.Do(req)
if err != nil {
return nil, connectionError(err.Error())
}
defer resp.Body.Close()
// check response status
if resp.StatusCode != http.StatusOK {
if resp.StatusCode >= http.StatusInternalServerError {
return nil, serverError(fmt.Sprintf("%d error: %s", resp.StatusCode, resp.Status))
}
return nil, fmt.Errorf("%d error: %s", resp.StatusCode, resp.Status)
}
// build return
response := new(Response)
if err := json.NewDecoder(resp.Body).Decode(response); err != nil {
return nil, err
}
return response, nil
}

69
vendor/github.com/edganiukov/fcm/message.go generated vendored Normal file
View File

@ -0,0 +1,69 @@
package fcm
import (
"errors"
"strings"
)
var (
// ErrInvalidMessage occurs if push notitication message is nil.
ErrInvalidMessage = errors.New("message is invalid")
// ErrInvalidTarget occurs if message topic is empty.
ErrInvalidTarget = errors.New("topic is invalid or registration ids are not set")
// ErrToManyRegIDs occurs when registration ids more then 1000.
ErrToManyRegIDs = errors.New("too many registrations ids")
// ErrInvalidTimeToLive occurs if TimeToLive more then 2419200.
ErrInvalidTimeToLive = errors.New("messages time-to-live is invalid")
)
// Notification specifies the predefined, user-visible key-value pairs of the
// notification payload.
type Notification struct {
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
Icon string `json:"icon,omitempty"`
Sound string `json:"sound,omitempty"`
Badge string `json:"badge,omitempty"`
Tag string `json:"tag,omitempty"`
Color string `json:"color,omitempty"`
ClickAction string `json:"click_action,omitempty"`
BodyLocKey string `json:"body_loc_key,omitempty"`
BodyLocArgs string `json:"body_loc_args,omitempty"`
TitleLocKey string `json:"title_loc_key,omitempty"`
TitleLocArgs string `json:"title_loc_args,omitempty"`
}
// Message represents list of targets, options, and payload for HTTP JSON
// messages.
type Message struct {
To string `json:"to,omitempty"`
RegistrationIDs []string `json:"registration_ids,omitempty"`
Condition string `json:"condition,omitempty"`
CollapseKey string `json:"collapse_key,omitempty"`
Priority string `json:"priority,omitempty"`
ContentAvailable bool `json:"content_available,omitempty"`
DelayWhileIdle bool `json:"delay_while_idle,omitempty"`
TimeToLive *uint `json:"time_to_live,omitempty"`
DeliveryReceiptRequested bool `json:"delivery_receipt_requested,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
RestrictedPackageName string `json:"restricted_package_name,omitempty"`
Notification *Notification `json:"notification,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
}
// Validate returns an error if the message is not well-formed.
func (msg *Message) Validate() error {
if msg == nil {
return ErrInvalidMessage
}
// validate target identifier: `to` or `condition`, or `registration_ids`
opCnt := strings.Count(msg.Condition, "&&") + strings.Count(msg.Condition, "||")
if msg.To == "" && (msg.Condition == "" || opCnt > 2) && len(msg.RegistrationIDs) == 0 {
return ErrInvalidTarget
}
return nil
}

28
vendor/github.com/edganiukov/fcm/option.go generated vendored Normal file
View File

@ -0,0 +1,28 @@
package fcm
import (
"errors"
"net/http"
)
// Option configurates Client with defined option.
type Option func(*Client) error
// WithEndpoint returns Option to configure FCM Endpoint.
func WithEndpoint(endpoint string) Option {
return func(c *Client) error {
if endpoint == "" {
return errors.New("invalid endpoint")
}
c.endpoint = endpoint
return nil
}
}
// WithHTTPClient returns Option to configure HTTP Client.
func WithHTTPClient(httpClient *http.Client) Option {
return func(c *Client) error {
c.client = httpClient
return nil
}
}

147
vendor/github.com/edganiukov/fcm/response.go generated vendored Normal file
View File

@ -0,0 +1,147 @@
package fcm
import (
"encoding/json"
"errors"
)
var (
// ErrMissingRegistration occurs if registration token is not set.
ErrMissingRegistration = errors.New("missing registration token")
// ErrInvalidRegistration occurs if registration token is invalid.
ErrInvalidRegistration = errors.New("invalid registration token")
// ErrNotRegistered occurs when application was deleted from device and
// token is not registered in FCM.
ErrNotRegistered = errors.New("unregistered device")
// ErrInvalidPackageName occurs if package name in message is invalid.
ErrInvalidPackageName = errors.New("invalid package name")
// ErrMismatchSenderID occurs when application has a new registration token.
ErrMismatchSenderID = errors.New("mismatched sender id")
// ErrMessageTooBig occurs when message is too big.
ErrMessageTooBig = errors.New("message is too big")
// ErrInvalidDataKey occurs if data key is invalid.
ErrInvalidDataKey = errors.New("invalid data key")
// ErrInvalidTTL occurs when message has invalid TTL.
ErrInvalidTTL = errors.New("invalid time to live")
// ErrUnavailable occurs when FCM service is unavailable. It makes sense
// to retry after this error.
ErrUnavailable = connectionError("timeout")
// ErrInternalServerError is internal FCM error. It makes sense to retry
// after this error.
ErrInternalServerError = serverError("internal server error")
// ErrDeviceMessageRateExceeded occurs when client sent to many requests to
// the device.
ErrDeviceMessageRateExceeded = errors.New("device message rate exceeded")
// ErrTopicsMessageRateExceeded occurs when client sent to many requests to
// the topics.
ErrTopicsMessageRateExceeded = errors.New("topics message rate exceeded")
)
var (
errMap = map[string]error{
"MissingRegistration": ErrMissingRegistration,
"InvalidRegistration": ErrInvalidRegistration,
"NotRegistered": ErrNotRegistered,
"InvalidPackageName": ErrInvalidPackageName,
"MismatchSenderId": ErrMismatchSenderID,
"MessageTooBig": ErrMessageTooBig,
"InvalidDataKey": ErrInvalidDataKey,
"InvalidTtl": ErrInvalidTTL,
"Unavailable": ErrUnavailable,
"InternalServerError": ErrInternalServerError,
"DeviceMessageRateExceeded": ErrDeviceMessageRateExceeded,
"TopicsMessageRateExceeded": ErrTopicsMessageRateExceeded,
}
)
// connectionError represents connection errors such as timeout error, etc.
// Implements `net.Error` interface.
type connectionError string
func (err connectionError) Error() string {
return string(err)
}
func (err connectionError) Temporary() bool {
return true
}
func (err connectionError) Timeout() bool {
return true
}
// serverError represents internal server errors.
// Implements `net.Error` interface.
type serverError string
func (err serverError) Error() string {
return string(err)
}
func (serverError) Temporary() bool {
return true
}
func (serverError) Timeout() bool {
return false
}
// Response represents the FCM server's response to the application
// server's sent message.
type Response struct {
MulticastID int64 `json:"multicast_id"`
Success int `json:"success"`
Failure int `json:"failure"`
CanonicalIDs int `json:"canonical_ids"`
Results []Result `json:"results"`
}
// Result represents the status of a processed message.
type Result struct {
MessageID string `json:"message_id"`
RegistrationID string `json:"registration_id"`
Error error `json:"error"`
}
// UnmarshalJSON implements json.Unmarshaler interface.
func (r *Result) UnmarshalJSON(data []byte) error {
var result struct {
MessageID string `json:"message_id"`
RegistrationID string `json:"registration_id"`
Error string `json:"error"`
}
if err := json.Unmarshal(data, &result); err != nil {
return err
}
r.MessageID = result.MessageID
r.RegistrationID = result.RegistrationID
r.Error = errMap[result.Error]
return nil
}
// Unregistered checks if the device token is unregistered,
// according to response from FCM server. Useful to determine
// if app is uninstalled.
func (r Result) Unregistered() bool {
switch r.Error {
case ErrNotRegistered, ErrMismatchSenderID, ErrMissingRegistration, ErrInvalidRegistration:
return true
default:
return false
}
}

34
vendor/github.com/edganiukov/fcm/retry.go generated vendored Normal file
View File

@ -0,0 +1,34 @@
package fcm
import (
"net"
"time"
)
const (
minBackoff = 100 * time.Millisecond
maxBackoff = 1 * time.Minute
factor = 2.7
)
func retry(fn func() error, attempts int) error {
var attempt int
for {
err := fn()
if err == nil {
return nil
}
if tErr, ok := err.(net.Error); !ok || !tErr.Temporary() {
return err
}
attempt++
backoff := minBackoff * time.Duration(attempt*attempt)
if attempt > attempts || backoff > maxBackoff {
return err
}
time.Sleep(backoff)
}
}

View File

@ -1,24 +0,0 @@
Want to contribute? Great! First, read this page (including the small print at the end).
### Before you contribute
Before we can use your code, you must sign the
[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1)
(CLA), which you can do online. The CLA is necessary mainly because you own the
copyright to your changes, even after your contribution becomes part of our
codebase, so we need your permission to use and distribute your code. We also
need to be sure of various other things—for instance that you'll tell us if you
know that your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.
Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on.
### Code reviews
All submissions, including submissions by project members, require review. We
use Github pull requests for this purpose.
### The small print
Contributions made by corporations are covered by a different agreement than
the one above, the Software Grant and Corporate Contributor License Agreement.

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,28 +0,0 @@
# Project status #
![status: inactive](https://img.shields.io/badge/status-inactive-red.svg)
This project is no longer actively maintained, and remains here as an archive of this work.
For a replacement, check out [this actively maintained fork](https://github.com/kikinteractive/go-gcm) of the library.
GCM Library for Go
--
Provides the following functionality for Google Cloud Messaging:
1. Sending messages.
2. Listening to receiving messages.
Documentation: http://godoc.org/github.com/google/go-gcm
## Installation
$ go get github.com/google/go-gcm
## Status
This library is in Alpha. We will make an effort to support the library, but we reserve the right to make incompatible changes when necessary.
## Feedback
Please read CONTRIBUTING and raise issues here in Github.

View File

@ -1,606 +0,0 @@
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package gcm provides send and receive GCM functionality.
package gcm
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/jpillora/backoff"
"github.com/mattn/go-xmpp"
"github.com/pborman/uuid"
)
const (
CCSAck = "ack"
CCSNack = "nack"
CCSControl = "control"
CCSReceipt = "receipt"
httpAddress = "https://gcm-http.googleapis.com/gcm/send"
xmppHost = "gcm.googleapis.com"
xmppPort = "5235"
xmppAddress = xmppHost + ":" + xmppPort
// For ccs the min for exponential backoff has to be 1 sec
ccsMinBackoff = 1 * time.Second
)
var (
// DebugMode determines whether to have verbose logging.
DebugMode = false
// Default Min and Max delay for backoff.
DefaultMinBackoff = 1 * time.Second
DefaultMaxBackoff = 10 * time.Second
retryableErrors = map[string]bool{
"Unavailable": true,
"SERVICE_UNAVAILABLE": true,
"InternalServerError": true,
"INTERNAL_SERVER_ ERROR": true,
// TODO(silvano): should we backoff with the same strategy on
// DeviceMessageRateExceeded and TopicsMessageRateExceeded.
}
// Cache of xmpp clients.
xmppClients = struct {
sync.Mutex
m map[string]*xmppGcmClient
}{
m: make(map[string]*xmppGcmClient),
}
)
// Prints debug info if DebugMode is set.
func debug(m string, v interface{}) {
if DebugMode {
log.Printf(m+":%+v", v)
}
}
// A GCM Http message.
type HttpMessage struct {
To string `json:"to,omitempty"`
RegistrationIds []string `json:"registration_ids,omitempty"`
CollapseKey string `json:"collapse_key,omitempty"`
Priority string `json:"priority,omitempty"`
ContentAvailable bool `json:"content_available,omitempty"`
DelayWhileIdle bool `json:"delay_while_idle,omitempty"`
TimeToLive *uint `json:"time_to_live,omitempty"`
RestrictedPackageName string `json:"restricted_package_name,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
Data Data `json:"data,omitempty"`
Notification *Notification `json:"notification,omitempty"`
}
// A GCM Xmpp message.
type XmppMessage struct {
To string `json:"to,omitempty"`
MessageId string `json:"message_id"`
MessageType string `json:"message_type,omitempty"`
CollapseKey string `json:"collapse_key,omitempty"`
Priority string `json:"priority,omitempty"`
ContentAvailable bool `json:"content_available,omitempty"`
DelayWhileIdle bool `json:"delay_while_idle,omitempty"`
TimeToLive *uint `json:"time_to_live,omitempty"`
DeliveryReceiptRequested bool `json:"delivery_receipt_requested,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
Data Data `json:"data,omitempty"`
Notification *Notification `json:"notification,omitempty"`
}
// HttpResponse is the GCM connection server response to an HTTP downstream message request.
type HttpResponse struct {
MulticastId int `json:"multicast_id,omitempty"`
Success uint `json:"success,omitempty"`
Failure uint `json:"failure,omitempty"`
CanonicalIds uint `json:"canonical_ids,omitempty"`
Results []Result `json:"results,omitempty"`
MessageId int `json:"message_id,omitempty"`
Error string `json:"error,omitempty"`
}
// Result represents the status of a processed Http message.
type Result struct {
MessageId string `json:"message_id,omitempty"`
RegistrationId string `json:"registration_id,omitempty"`
Error string `json:"error,omitempty"`
}
// CcsMessage is an Xmpp message sent from CCS.
type CcsMessage struct {
From string `json:"from, omitempty"`
MessageId string `json:"message_id, omitempty"`
MessageType string `json:"message_type, omitempty"`
RegistrationId string `json:"registration_id,omitempty"`
Error string `json:"error,omitempty"`
ErrorDescription string `json:"error_description,omitempty"`
Category string `json:"category, omitempty"`
Data Data `json:"data,omitempty"`
ControlType string `json:"control_type,omitempty"`
}
// Used to compute results for multicast messages with retries.
type multicastResultsState map[string]*Result
// The data payload of a GCM message.
type Data map[string]interface{}
// The notification payload of a GCM message.
type Notification struct {
Title string `json:"title,omitempty"`
Body string `json:"body,omitempty"`
Icon string `json:"icon,omitempty"`
Sound string `json:"sound,omitempty"`
Badge string `json:"badge,omitempty"`
Tag string `json:"tag,omitempty"`
Color string `json:"color,omitempty"`
ClickAction string `json:"click_action,omitempty"`
BodyLocKey string `json:"body_loc_key,omitempty"`
BodyLocArgs string `json:"body_loc_args,omitempty"`
TitleLocArgs string `json:"title_loc_args,omitempty"`
TitleLocKey string `json:"title_loc_key,omitempty"`
}
// MessageHandler is the type for a function that handles a CCS message.
// The CCS message can be an upstream message (device to server) or a
// message from CCS (e.g. a delivery receipt).
type MessageHandler func(cm CcsMessage) error
// httpClient is an interface to stub the http client in tests.
type httpClient interface {
send(apiKey string, m HttpMessage) (*HttpResponse, error)
getRetryAfter() string
}
// httpGcmClient is a client for the Gcm Http Connection Server.
type httpGcmClient struct {
GcmURL string
HttpClient *http.Client
retryAfter string
}
// httpGcmClient implementation to send a message through GCM Http server.
func (c *httpGcmClient) send(apiKey string, m HttpMessage) (*HttpResponse, error) {
bs, err := json.Marshal(m)
if err != nil {
return nil, fmt.Errorf("error marshalling message>%v", err)
}
debug("sending", string(bs))
req, err := http.NewRequest("POST", c.GcmURL, bytes.NewReader(bs))
if err != nil {
return nil, fmt.Errorf("error creating request>%v", err)
}
req.Header.Add(http.CanonicalHeaderKey("Content-Type"), "application/json")
req.Header.Add(http.CanonicalHeaderKey("Authorization"), authHeader(apiKey))
httpResp, err := c.HttpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error sending request to HTTP connection server>%v", err)
}
gcmResp := &HttpResponse{}
body, err := ioutil.ReadAll(httpResp.Body)
defer httpResp.Body.Close()
if err != nil {
return gcmResp, fmt.Errorf("error reading http response body>%v", err)
}
debug("received body", string(body))
err = json.Unmarshal(body, &gcmResp)
if err != nil {
return gcmResp, fmt.Errorf("error unmarshaling json from body: %v", err)
}
// TODO(silvano): this is assuming that the header contains seconds instead of a date, need to check
c.retryAfter = httpResp.Header.Get(http.CanonicalHeaderKey("Retry-After"))
return gcmResp, nil
}
// Get the value of the retry after header if present.
func (c httpGcmClient) getRetryAfter() string {
return c.retryAfter
}
// xmppClient is an interface to stub the xmpp client in tests.
type xmppClient interface {
listen(h MessageHandler, stop chan<- bool) error
send(m XmppMessage) (int, error)
}
// xmppGcmClient is a client for the Gcm Xmpp Connection Server (CCS).
type xmppGcmClient struct {
sync.RWMutex
XmppClient xmpp.Client
messages struct {
sync.RWMutex
m map[string]*messageLogEntry
}
senderID string
isClosed bool
}
// An entry in the messages log, used to keep track of messages pending ack and
// retries for failed messages.
type messageLogEntry struct {
body *XmppMessage
backoff *exponentialBackoff
}
// Factory method for xmppGcmClient, to minimize the number of clients to one per sender id.
// TODO(silvano): this could be revised, taking into account that we cannot have more than 1000
// connections per senderId.
func newXmppGcmClient(senderID string, apiKey string) (*xmppGcmClient, error) {
xmppClients.Lock()
defer xmppClients.Unlock()
if xc, ok := xmppClients.m[senderID]; ok {
return xc, nil
}
nc, err := xmpp.NewClient(xmppAddress, xmppUser(senderID), apiKey, DebugMode)
if err != nil {
return nil, fmt.Errorf("error connecting client>%v", err)
}
xc := &xmppGcmClient{
XmppClient: *nc,
messages: struct {
sync.RWMutex
m map[string]*messageLogEntry
}{
m: make(map[string]*messageLogEntry),
},
senderID: senderID,
}
xmppClients.m[senderID] = xc
return xc, nil
}
// xmppGcmClient implementation of listening for messages from CCS; the messages can be
// acks or nacks for messages sent through XMPP, control messages, upstream messages.
func (c *xmppGcmClient) listen(h MessageHandler, stop <-chan bool) error {
if stop != nil {
go func() {
select {
case <-stop:
// Set isClosed to 0 so we don't trigger an error when returning.
c.Lock()
c.XmppClient.Close()
c.isClosed = true
c.Unlock()
// Remove client from cache.
xmppClients.Lock()
delete(xmppClients.m, c.senderID)
xmppClients.Unlock()
}
}()
}
for {
stanza, err := c.XmppClient.Recv()
if err != nil {
c.RLock()
defer c.RUnlock()
// If client is closed we can't return without error.
if c.isClosed {
return nil
}
// This is likely fatal, so return.
return fmt.Errorf("error on Recv>%v", err)
}
v, ok := stanza.(xmpp.Chat)
if !ok {
continue
}
switch v.Type {
case "":
cm := &CcsMessage{}
err = json.Unmarshal([]byte(v.Other[0]), cm)
if err != nil {
debug("Error unmarshaling ccs message: %v", err)
continue
}
switch cm.MessageType {
case CCSAck:
c.messages.Lock()
// ack for a sent message, delete it from log.
if _, ok := c.messages.m[cm.MessageId]; ok {
go h(*cm)
delete(c.messages.m, cm.MessageId)
}
c.messages.Unlock()
case CCSNack:
// nack for a sent message, retry if retryable error, bubble up otherwise.
if retryableErrors[cm.Error] {
c.retryMessage(*cm, h)
} else {
c.messages.Lock()
if _, ok := c.messages.m[cm.MessageId]; ok {
go h(*cm)
delete(c.messages.m, cm.MessageId)
}
c.messages.Unlock()
}
default:
debug("Unknown ccs message: %v", cm)
}
case "normal":
cm := &CcsMessage{}
err = json.Unmarshal([]byte(v.Other[0]), cm)
if err != nil {
debug("Error unmarshaling ccs message: %v", err)
continue
}
switch cm.MessageType {
case CCSControl:
// TODO(silvano): create a new connection, drop the old one 'after a while'
debug("control message! %v", cm)
case CCSReceipt:
debug("receipt! %v", cm)
// Receipt message: send ack and pass to listener.
origMessageID := strings.TrimPrefix(cm.MessageId, "dr2:")
ack := XmppMessage{To: cm.From, MessageId: origMessageID, MessageType: CCSAck}
c.send(ack)
go h(*cm)
default:
debug("uknown upstream message! %v", cm)
// Upstream message: send ack and pass to listener.
ack := XmppMessage{To: cm.From, MessageId: cm.MessageId, MessageType: CCSAck}
c.send(ack)
go h(*cm)
}
case "error":
debug("error response %v", v)
default:
debug("unknown message type %v", v)
}
}
}
//TODO(silvano): add flow control (max 100 pending messages at one time)
// xmppGcmClient implementation to send a message through Gcm Xmpp server (ccs).
func (c *xmppGcmClient) send(m XmppMessage) (string, int, error) {
if m.MessageId == "" {
m.MessageId = uuid.New()
}
c.messages.Lock()
if _, ok := c.messages.m[m.MessageId]; !ok {
b := newExponentialBackoff()
if b.b.Min < ccsMinBackoff {
b.setMin(ccsMinBackoff)
}
c.messages.m[m.MessageId] = &messageLogEntry{body: &m, backoff: b}
}
c.messages.Unlock()
stanza := `<message id=""><gcm xmlns="google:mobile:data">%v</gcm></message>`
body, err := json.Marshal(m)
if err != nil {
return m.MessageId, 0, fmt.Errorf("could not unmarshal body of xmpp message>%v", err)
}
bs := string(body)
debug("Sending XMPP: ", fmt.Sprintf(stanza, bs))
// Serialize wire access for thread safety.
c.Lock()
defer c.Unlock()
bytes, err := c.XmppClient.SendOrg(fmt.Sprintf(stanza, bs))
return m.MessageId, bytes, err
}
// Retry sending a message with exponential backoff; if over limit, bubble up the failed message.
func (c *xmppGcmClient) retryMessage(cm CcsMessage, h MessageHandler) {
c.messages.RLock()
defer c.messages.RUnlock()
if me, ok := c.messages.m[cm.MessageId]; ok {
if me.backoff.sendAnother() {
go func(m *messageLogEntry) {
m.backoff.wait()
c.send(*m.body)
}(me)
} else {
debug("Exponential backoff failed over limit for message: ", me)
go h(cm)
}
}
}
// Interface to stub the http client in tests.
type backoffProvider interface {
sendAnother() bool
setMin(min time.Duration)
wait()
}
// Implementation of backoff provider using exponential backoff.
type exponentialBackoff struct {
b backoff.Backoff
currentDelay time.Duration
}
// Factory method for exponential backoff, uses default values for Min and Max and
// adds Jitter.
func newExponentialBackoff() *exponentialBackoff {
b := &backoff.Backoff{
Min: DefaultMinBackoff,
Max: DefaultMaxBackoff,
Jitter: true,
}
return &exponentialBackoff{b: *b, currentDelay: b.Duration()}
}
// Returns true if not over the retries limit
func (eb exponentialBackoff) sendAnother() bool {
return eb.currentDelay <= eb.b.Max
}
// Set the minumim delay for backoff
func (eb *exponentialBackoff) setMin(min time.Duration) {
eb.b.Min = min
if (eb.currentDelay) < min {
eb.currentDelay = min
}
}
// Wait for the current value of backoff
func (eb exponentialBackoff) wait() {
time.Sleep(eb.currentDelay)
eb.currentDelay = eb.b.Duration()
}
// Send a message using the HTTP GCM connection server.
func SendHttp(apiKey string, m HttpMessage) (*HttpResponse, error) {
c := &httpGcmClient{httpAddress, &http.Client{}, "0"}
b := newExponentialBackoff()
return sendHttp(apiKey, m, c, b)
}
// sendHttp sends an http message using exponential backoff, handling multicast replies.
func sendHttp(apiKey string, m HttpMessage, c httpClient, b backoffProvider) (*HttpResponse, error) {
// TODO(silvano): check this with responses for topic/notification group
gcmResp := &HttpResponse{}
var multicastId int
targets, err := messageTargetAsStringsArray(m)
if err != nil {
return gcmResp, fmt.Errorf("error extracting target from message: %v", err)
}
// make a copy of the targets to keep track of results during retries
localTo := make([]string, len(targets))
copy(localTo, targets)
resultsState := &multicastResultsState{}
for b.sendAnother() {
gcmResp, err = c.send(apiKey, m)
if err != nil {
return gcmResp, fmt.Errorf("error sending request to GCM HTTP server: %v", err)
}
if len(gcmResp.Results) > 0 {
doRetry, toRetry, err := checkResults(gcmResp.Results, localTo, *resultsState)
multicastId = gcmResp.MulticastId
if err != nil {
return gcmResp, fmt.Errorf("error checking GCM results: %v", err)
}
if doRetry {
retryAfter, err := time.ParseDuration(c.getRetryAfter())
if err != nil {
b.setMin(retryAfter)
}
localTo = make([]string, len(toRetry))
copy(localTo, toRetry)
if m.RegistrationIds != nil {
m.RegistrationIds = toRetry
}
b.wait()
continue
} else {
break
}
} else {
break
}
}
// if it was multicast, reconstruct response in case there have been retries
if len(targets) > 1 {
gcmResp = buildRespForMulticast(targets, *resultsState, multicastId)
}
return gcmResp, nil
}
// Builds the final response for a multicast message, in case there have been retries for
// subsets of the original recipients.
func buildRespForMulticast(to []string, mrs multicastResultsState, mid int) *HttpResponse {
resp := &HttpResponse{}
resp.MulticastId = mid
resp.Results = make([]Result, len(to))
for i, regId := range to {
result, ok := mrs[regId]
if !ok {
continue
}
resp.Results[i] = *result
if result.MessageId != "" {
resp.Success++
} else if result.Error != "" {
resp.Failure++
}
if result.RegistrationId != "" {
resp.CanonicalIds++
}
}
return resp
}
// Transform the recipient in an array of strings if needed.
func messageTargetAsStringsArray(m HttpMessage) ([]string, error) {
if m.RegistrationIds != nil {
return m.RegistrationIds, nil
} else if m.To != "" {
target := []string{m.To}
return target, nil
}
target := []string{}
return target, fmt.Errorf("can't find any valid target field in message.")
}
// For a multicast send, determines which errors can be retried.
func checkResults(gcmResults []Result, recipients []string, resultsState multicastResultsState) (doRetry bool, toRetry []string, err error) {
doRetry = false
toRetry = []string{}
for i := 0; i < len(gcmResults); i++ {
result := gcmResults[i]
regId := recipients[i]
resultsState[regId] = &result
if result.Error != "" {
if retryableErrors[result.Error] {
toRetry = append(toRetry, regId)
if doRetry == false {
doRetry = true
}
}
}
}
return doRetry, toRetry, nil
}
// SendXmpp sends a message using the XMPP GCM connection server.
func SendXmpp(senderId, apiKey string, m XmppMessage) (string, int, error) {
c, err := newXmppGcmClient(senderId, apiKey)
if err != nil {
return "", 0, fmt.Errorf("error creating xmpp client>%v", err)
}
return c.send(m)
}
// Listen blocks and connects to GCM waiting for messages, calling the handler
// for CCS message that can be of interest to the listener: upstream messages, delivery receipt
// notifications, errors. An optional stop channel can be provided to
// stop listening.
func Listen(senderId, apiKey string, h MessageHandler, stop <-chan bool) error {
cl, err := newXmppGcmClient(senderId, apiKey)
if err != nil {
return fmt.Errorf("error creating xmpp client>%v", err)
}
return cl.listen(h, stop)
}
// authHeader generates an authorization header value for an api key.
func authHeader(apiKey string) string {
return fmt.Sprintf("key=%v", apiKey)
}
// xmppUser generates an xmpp username from a sender ID.
func xmppUser(senderId string) string {
return senderId + "@" + xmppHost
}

12
vendor/vendor.json vendored
View File

@ -68,6 +68,12 @@
"revision": "346938d642f2ec3594ed81d874461961cd0faa76",
"revisionTime": "2016-10-29T20:57:26Z"
},
{
"checksumSHA1": "Xqz4X1+t8MTsmX2guYh7RUj9ZvI=",
"path": "github.com/edganiukov/fcm",
"revision": "cbdb173263e8c5ed323ce370b57d1ee5f743b758",
"revisionTime": "2017-03-11T15:33:24Z"
},
{
"checksumSHA1": "rRYbBQkVsd/+EIryahsywNi34Lo=",
"path": "github.com/emirpasic/gods/containers",
@ -158,12 +164,6 @@
"revision": "553a641470496b2327abcac10b36396bd98e45c9",
"revisionTime": "2017-02-15T23:32:05Z"
},
{
"checksumSHA1": "pJ0oMI3iC3VIo5kTeibCezrKg/s=",
"path": "github.com/google/go-gcm",
"revision": "f387343038b10aec84c22f3809773a30630c12e8",
"revisionTime": "2017-02-14T17:04:21Z"
},
{
"checksumSHA1": "oqpLCsrXQdvns4RtmcO61+4BiQk=",
"path": "github.com/jpillora/backoff",