2017-09-15 15:57:34 +00:00
|
|
|
package fcm
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// fcm_server_url fcm server url
|
|
|
|
fcm_server_url = "https://fcm.googleapis.com/fcm/send"
|
|
|
|
// MAX_TTL the default ttl for a notification
|
|
|
|
MAX_TTL = 2419200
|
|
|
|
// Priority_HIGH notification priority
|
|
|
|
Priority_HIGH = "high"
|
|
|
|
// Priority_NORMAL notification priority
|
|
|
|
Priority_NORMAL = "normal"
|
|
|
|
// retry_after_header header name
|
|
|
|
retry_after_header = "Retry-After"
|
|
|
|
// error_key readable error caching !
|
|
|
|
error_key = "error"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// retreyableErrors whether the error is a retryable
|
|
|
|
retreyableErrors = map[string]bool{
|
|
|
|
"Unavailable": true,
|
|
|
|
"InternalServerError": true,
|
|
|
|
}
|
|
|
|
|
|
|
|
// fcmServerUrl for testing purposes
|
|
|
|
fcmServerUrl = fcm_server_url
|
|
|
|
)
|
|
|
|
|
|
|
|
// FcmClient stores the key and the Message (FcmMsg)
|
|
|
|
type FcmClient struct {
|
|
|
|
ApiKey string
|
|
|
|
Message FcmMsg
|
|
|
|
}
|
|
|
|
|
|
|
|
// FcmMsg represents fcm request message
|
|
|
|
type FcmMsg struct {
|
|
|
|
Data interface{} `json:"data,omitempty"`
|
|
|
|
To string `json:"to,omitempty"`
|
|
|
|
RegistrationIds []string `json:"registration_ids,omitempty"`
|
|
|
|
CollapseKey string `json:"collapse_key,omitempty"`
|
|
|
|
Priority string `json:"priority,omitempty"`
|
|
|
|
Notification NotificationPayload `json:"notification,omitempty"`
|
|
|
|
ContentAvailable bool `json:"content_available,omitempty"`
|
|
|
|
DelayWhileIdle bool `json:"delay_while_idle,omitempty"`
|
|
|
|
TimeToLive int `json:"time_to_live,omitempty"`
|
|
|
|
RestrictedPackageName string `json:"restricted_package_name,omitempty"`
|
|
|
|
DryRun bool `json:"dry_run,omitempty"`
|
|
|
|
Condition string `json:"condition,omitempty"`
|
2018-11-20 18:02:54 +00:00
|
|
|
MutableContent bool `json:"mutable_content,omitempty"`
|
2017-09-15 15:57:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// FcmMsg represents fcm response message - (tokens and topics)
|
|
|
|
type FcmResponseStatus struct {
|
|
|
|
Ok bool
|
|
|
|
StatusCode int
|
|
|
|
MulticastId int64 `json:"multicast_id"`
|
|
|
|
Success int `json:"success"`
|
|
|
|
Fail int `json:"failure"`
|
|
|
|
Canonical_ids int `json:"canonical_ids"`
|
|
|
|
Results []map[string]string `json:"results,omitempty"`
|
|
|
|
MsgId int64 `json:"message_id,omitempty"`
|
|
|
|
Err string `json:"error,omitempty"`
|
|
|
|
RetryAfter string
|
|
|
|
}
|
|
|
|
|
|
|
|
// NotificationPayload notification message payload
|
|
|
|
type NotificationPayload struct {
|
2018-01-25 13:08:43 +00:00
|
|
|
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"`
|
|
|
|
AndroidChannelID string `json:"android_channel_id,omitempty"`
|
2017-09-15 15:57:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewFcmClient init and create fcm client
|
|
|
|
func NewFcmClient(apiKey string) *FcmClient {
|
|
|
|
fcmc := new(FcmClient)
|
|
|
|
fcmc.ApiKey = apiKey
|
|
|
|
|
|
|
|
return fcmc
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewFcmTopicMsg sets the targeted token/topic and the data payload
|
|
|
|
func (this *FcmClient) NewFcmTopicMsg(to string, body map[string]string) *FcmClient {
|
|
|
|
|
|
|
|
this.NewFcmMsgTo(to, body)
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewFcmMsgTo sets the targeted token/topic and the data payload
|
|
|
|
func (this *FcmClient) NewFcmMsgTo(to string, body interface{}) *FcmClient {
|
|
|
|
this.Message.To = to
|
|
|
|
this.Message.Data = body
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetMsgData sets data payload
|
|
|
|
func (this *FcmClient) SetMsgData(body interface{}) *FcmClient {
|
|
|
|
|
|
|
|
this.Message.Data = body
|
|
|
|
|
|
|
|
return this
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewFcmRegIdsMsg gets a list of devices with data payload
|
|
|
|
func (this *FcmClient) NewFcmRegIdsMsg(list []string, body interface{}) *FcmClient {
|
|
|
|
this.newDevicesList(list)
|
|
|
|
this.Message.Data = body
|
|
|
|
|
|
|
|
return this
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// newDevicesList init the devices list
|
|
|
|
func (this *FcmClient) newDevicesList(list []string) *FcmClient {
|
|
|
|
this.Message.RegistrationIds = make([]string, len(list))
|
|
|
|
copy(this.Message.RegistrationIds, list)
|
|
|
|
|
|
|
|
return this
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// AppendDevices adds more devices/tokens to the Fcm request
|
|
|
|
func (this *FcmClient) AppendDevices(list []string) *FcmClient {
|
|
|
|
|
|
|
|
this.Message.RegistrationIds = append(this.Message.RegistrationIds, list...)
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// apiKeyHeader generates the value of the Authorization key
|
|
|
|
func (this *FcmClient) apiKeyHeader() string {
|
|
|
|
return fmt.Sprintf("key=%v", this.ApiKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
// sendOnce send a single request to fcm
|
|
|
|
func (this *FcmClient) sendOnce() (*FcmResponseStatus, error) {
|
|
|
|
|
|
|
|
fcmRespStatus := new(FcmResponseStatus)
|
|
|
|
|
|
|
|
jsonByte, err := this.Message.toJsonByte()
|
|
|
|
if err != nil {
|
|
|
|
return fcmRespStatus, err
|
|
|
|
}
|
|
|
|
|
|
|
|
request, err := http.NewRequest("POST", fcmServerUrl, bytes.NewBuffer(jsonByte))
|
|
|
|
request.Header.Set("Authorization", this.apiKeyHeader())
|
|
|
|
request.Header.Set("Content-Type", "application/json")
|
|
|
|
|
|
|
|
client := &http.Client{}
|
|
|
|
response, err := client.Do(request)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fcmRespStatus, err
|
|
|
|
}
|
|
|
|
defer response.Body.Close()
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
|
|
if err != nil {
|
|
|
|
return fcmRespStatus, err
|
|
|
|
}
|
|
|
|
|
2018-11-20 18:02:54 +00:00
|
|
|
fcmRespStatus.Err = string(body)
|
|
|
|
|
2017-09-15 15:57:34 +00:00
|
|
|
fcmRespStatus.StatusCode = response.StatusCode
|
|
|
|
|
|
|
|
fcmRespStatus.RetryAfter = response.Header.Get(retry_after_header)
|
|
|
|
|
|
|
|
if response.StatusCode != 200 {
|
|
|
|
return fcmRespStatus, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err = fcmRespStatus.parseStatusBody(body)
|
|
|
|
if err != nil {
|
|
|
|
return fcmRespStatus, err
|
|
|
|
}
|
|
|
|
fcmRespStatus.Ok = true
|
|
|
|
|
|
|
|
return fcmRespStatus, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Send to fcm
|
|
|
|
func (this *FcmClient) Send() (*FcmResponseStatus, error) {
|
|
|
|
return this.sendOnce()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// toJsonByte converts FcmMsg to a json byte
|
|
|
|
func (this *FcmMsg) toJsonByte() ([]byte, error) {
|
|
|
|
|
|
|
|
return json.Marshal(this)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// parseStatusBody parse FCM response body
|
|
|
|
func (this *FcmResponseStatus) parseStatusBody(body []byte) error {
|
|
|
|
|
|
|
|
if err := json.Unmarshal([]byte(body), &this); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetPriority Sets the priority of the message.
|
|
|
|
// Priority_HIGH or Priority_NORMAL
|
|
|
|
func (this *FcmClient) SetPriority(p string) *FcmClient {
|
|
|
|
|
|
|
|
if p == Priority_HIGH {
|
|
|
|
this.Message.Priority = Priority_HIGH
|
|
|
|
} else {
|
|
|
|
this.Message.Priority = Priority_NORMAL
|
|
|
|
}
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetCollapseKey This parameter identifies a group of messages
|
|
|
|
// (e.g., with collapse_key: "Updates Available") that can be collapsed,
|
|
|
|
// so that only the last message gets sent when delivery can be resumed.
|
|
|
|
// This is intended to avoid sending too many of the same messages when the
|
|
|
|
// device comes back online or becomes active (see delay_while_idle).
|
|
|
|
func (this *FcmClient) SetCollapseKey(val string) *FcmClient {
|
|
|
|
|
|
|
|
this.Message.CollapseKey = val
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetNotificationPayload sets the notification payload based on the specs
|
|
|
|
// https://firebase.google.com/docs/cloud-messaging/http-server-ref
|
|
|
|
func (this *FcmClient) SetNotificationPayload(payload *NotificationPayload) *FcmClient {
|
|
|
|
|
|
|
|
this.Message.Notification = *payload
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetContentAvailable On iOS, use this field to represent content-available
|
|
|
|
// in the APNS payload. When a notification or message is sent and this is set
|
|
|
|
// to true, an inactive client app is awoken. On Android, data messages wake
|
|
|
|
// the app by default. On Chrome, currently not supported.
|
|
|
|
func (this *FcmClient) SetContentAvailable(isContentAvailable bool) *FcmClient {
|
|
|
|
|
|
|
|
this.Message.ContentAvailable = isContentAvailable
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetDelayWhileIdle When this parameter is set to true, it indicates that
|
|
|
|
// the message should not be sent until the device becomes active.
|
|
|
|
// The default value is false.
|
|
|
|
func (this *FcmClient) SetDelayWhileIdle(isDelayWhileIdle bool) *FcmClient {
|
|
|
|
|
|
|
|
this.Message.DelayWhileIdle = isDelayWhileIdle
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetTimeToLive This parameter specifies how long (in seconds) the message
|
|
|
|
// should be kept in FCM storage if the device is offline. The maximum time
|
|
|
|
// to live supported is 4 weeks, and the default value is 4 weeks.
|
|
|
|
// For more information, see
|
|
|
|
// https://firebase.google.com/docs/cloud-messaging/concept-options#ttl
|
|
|
|
func (this *FcmClient) SetTimeToLive(ttl int) *FcmClient {
|
|
|
|
|
|
|
|
if ttl > MAX_TTL {
|
|
|
|
|
|
|
|
this.Message.TimeToLive = MAX_TTL
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
this.Message.TimeToLive = ttl
|
|
|
|
|
|
|
|
}
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetRestrictedPackageName This parameter specifies the package name of the
|
|
|
|
// application where the registration tokens must match in order to
|
|
|
|
// receive the message.
|
|
|
|
func (this *FcmClient) SetRestrictedPackageName(pkg string) *FcmClient {
|
|
|
|
|
|
|
|
this.Message.RestrictedPackageName = pkg
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetDryRun This parameter, when set to true, allows developers to test
|
|
|
|
// a request without actually sending a message.
|
|
|
|
// The default value is false
|
|
|
|
func (this *FcmClient) SetDryRun(drun bool) *FcmClient {
|
|
|
|
|
|
|
|
this.Message.DryRun = drun
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2018-11-20 18:02:54 +00:00
|
|
|
// SetMutableContent Currently for iOS 10+ devices only. On iOS,
|
|
|
|
// use this field to represent mutable-content in the APNs payload.
|
|
|
|
// When a notification is sent and this is set to true, the content
|
|
|
|
// of the notification can be modified before it is displayed,
|
|
|
|
// using a Notification Service app extension.
|
|
|
|
// This parameter will be ignored for Android and web.
|
|
|
|
func (this *FcmClient) SetMutableContent(mc bool) *FcmClient {
|
|
|
|
|
|
|
|
this.Message.MutableContent = mc
|
|
|
|
|
|
|
|
return this
|
|
|
|
}
|
|
|
|
|
2017-09-15 15:57:34 +00:00
|
|
|
// PrintResults prints the FcmResponseStatus results for fast using and debugging
|
|
|
|
func (this *FcmResponseStatus) PrintResults() {
|
|
|
|
fmt.Println("Status Code :", this.StatusCode)
|
|
|
|
fmt.Println("Success :", this.Success)
|
|
|
|
fmt.Println("Fail :", this.Fail)
|
|
|
|
fmt.Println("Canonical_ids :", this.Canonical_ids)
|
|
|
|
fmt.Println("Topic MsgId :", this.MsgId)
|
|
|
|
fmt.Println("Topic Err :", this.Err)
|
|
|
|
for i, val := range this.Results {
|
|
|
|
fmt.Printf("Result(%d)> \n", i)
|
|
|
|
for k, v := range val {
|
|
|
|
fmt.Println("\t", k, " : ", v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsTimeout check whether the response timeout based on http response status
|
|
|
|
// code and if any error is retryable
|
|
|
|
func (this *FcmResponseStatus) IsTimeout() bool {
|
|
|
|
if this.StatusCode >= 500 {
|
|
|
|
return true
|
|
|
|
} else if this.StatusCode == 200 {
|
|
|
|
for _, val := range this.Results {
|
|
|
|
for k, v := range val {
|
|
|
|
if k == error_key && retreyableErrors[v] == true {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetRetryAfterTime converts the retrey after response header
|
|
|
|
// to a time.Duration
|
|
|
|
func (this *FcmResponseStatus) GetRetryAfterTime() (t time.Duration, e error) {
|
|
|
|
t, e = time.ParseDuration(this.RetryAfter)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetCondition to set a logical expression of conditions that determine the message target
|
|
|
|
func (this *FcmClient) SetCondition(condition string) *FcmClient {
|
|
|
|
this.Message.Condition = condition
|
|
|
|
return this
|
|
|
|
}
|