refactor(notification): separate ios and android (#250)

This commit is contained in:
Bo-Yi Wu 2017-07-16 22:22:48 -05:00 committed by GitHub
parent 6a64b42ab0
commit 460b74d8a6
4 changed files with 422 additions and 405 deletions

View File

@ -2,18 +2,12 @@ package gorush
import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"time"
"github.com/appleboy/go-fcm"
apns "github.com/sideshow/apns2"
"github.com/sideshow/apns2/certificate"
"github.com/sideshow/apns2/payload"
)
// D provide string array
@ -182,402 +176,3 @@ func CheckPushConf() error {
return nil
}
// InitAPNSClient use for initialize APNs Client.
func InitAPNSClient() error {
if PushConf.Ios.Enabled {
var err error
ext := filepath.Ext(PushConf.Ios.KeyPath)
switch ext {
case ".p12":
CertificatePemIos, err = certificate.FromP12File(PushConf.Ios.KeyPath, PushConf.Ios.Password)
case ".pem":
CertificatePemIos, err = certificate.FromPemFile(PushConf.Ios.KeyPath, PushConf.Ios.Password)
default:
err = errors.New("wrong certificate key extension")
}
if err != nil {
LogError.Error("Cert Error:", err.Error())
return err
}
if PushConf.Ios.Production {
ApnsClient = apns.NewClient(CertificatePemIos).Production()
} else {
ApnsClient = apns.NewClient(CertificatePemIos).Development()
}
}
return nil
}
// InitWorkers for initialize all workers.
func InitWorkers(workerNum int64, queueNum int64) {
LogAccess.Debug("worker number is ", workerNum, ", queue number is ", queueNum)
QueueNotification = make(chan PushNotification, queueNum)
for i := int64(0); i < workerNum; i++ {
go startWorker()
}
}
func startWorker() {
for {
notification := <-QueueNotification
switch notification.Platform {
case PlatFormIos:
PushToIOS(notification)
case PlatFormAndroid:
PushToAndroid(notification)
}
}
}
// queueNotification add notification to queue list.
func queueNotification(req RequestPush) (int, []LogPushEntry) {
var count int
wg := sync.WaitGroup{}
newNotification := []PushNotification{}
for _, notification := range req.Notifications {
switch notification.Platform {
case PlatFormIos:
if !PushConf.Ios.Enabled {
continue
}
case PlatFormAndroid:
if !PushConf.Android.Enabled {
continue
}
}
newNotification = append(newNotification, notification)
}
log := make([]LogPushEntry, 0, count)
for _, notification := range newNotification {
if PushConf.Core.Sync {
notification.wg = &wg
notification.log = &log
notification.AddWaitCount()
}
QueueNotification <- notification
count += len(notification.Tokens)
}
if PushConf.Core.Sync {
wg.Wait()
}
StatStorage.AddTotalCount(int64(count))
return count, log
}
func iosAlertDictionary(payload *payload.Payload, req PushNotification) *payload.Payload {
// Alert dictionary
if len(req.Title) > 0 {
payload.AlertTitle(req.Title)
}
if len(req.Alert.Title) > 0 {
payload.AlertTitle(req.Alert.Title)
}
// Apple Watch & Safari display this string as part of the notification interface.
if len(req.Alert.Subtitle) > 0 {
payload.AlertSubtitle(req.Alert.Subtitle)
}
if len(req.Alert.TitleLocKey) > 0 {
payload.AlertTitleLocKey(req.Alert.TitleLocKey)
}
if len(req.Alert.LocArgs) > 0 {
payload.AlertLocArgs(req.Alert.LocArgs)
}
if len(req.Alert.TitleLocArgs) > 0 {
payload.AlertTitleLocArgs(req.Alert.TitleLocArgs)
}
if len(req.Alert.Body) > 0 {
payload.AlertBody(req.Alert.Body)
}
if len(req.Alert.LaunchImage) > 0 {
payload.AlertLaunchImage(req.Alert.LaunchImage)
}
if len(req.Alert.LocKey) > 0 {
payload.AlertLocKey(req.Alert.LocKey)
}
if len(req.Alert.Action) > 0 {
payload.AlertAction(req.Alert.Action)
}
if len(req.Alert.ActionLocKey) > 0 {
payload.AlertActionLocKey(req.Alert.ActionLocKey)
}
// General
if len(req.Category) > 0 {
payload.Category(req.Category)
}
return payload
}
// GetIOSNotification use for define iOS notification.
// The iOS Notification Payload
// ref: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html#//apple_ref/doc/uid/TP40008194-CH17-SW1
func GetIOSNotification(req PushNotification) *apns.Notification {
notification := &apns.Notification{
ApnsID: req.ApnsID,
Topic: req.Topic,
}
if req.Expiration > 0 {
notification.Expiration = time.Unix(req.Expiration, 0)
}
if len(req.Priority) > 0 && req.Priority == "normal" {
notification.Priority = apns.PriorityLow
}
payload := payload.NewPayload()
// add alert object if message length > 0
if len(req.Message) > 0 {
payload.Alert(req.Message)
}
// zero value for clear the badge on the app icon.
if req.Badge != nil && *req.Badge >= 0 {
payload.Badge(*req.Badge)
}
if req.MutableContent {
payload.MutableContent()
}
if len(req.Sound) > 0 {
payload.Sound(req.Sound)
}
if req.ContentAvailable {
payload.ContentAvailable()
}
if len(req.URLArgs) > 0 {
payload.URLArgs(req.URLArgs)
}
for k, v := range req.Data {
payload.Custom(k, v)
}
payload = iosAlertDictionary(payload, req)
notification.Payload = payload
return notification
}
// PushToIOS provide send notification to APNs server.
func PushToIOS(req PushNotification) bool {
LogAccess.Debug("Start push notification for iOS")
if PushConf.Core.Sync {
defer req.WaitDone()
}
var (
retryCount = 0
maxRetry = PushConf.Ios.MaxRetry
)
if req.Retry > 0 && req.Retry < maxRetry {
maxRetry = req.Retry
}
Retry:
var (
isError = false
newTokens []string
)
notification := GetIOSNotification(req)
for _, token := range req.Tokens {
notification.DeviceToken = token
// send ios notification
res, err := ApnsClient.Push(notification)
if err != nil {
// apns server error
LogPush(FailedPush, token, req, err)
if PushConf.Core.Sync {
req.AddLog(getLogPushEntry(FailedPush, token, req, err))
}
StatStorage.AddIosError(1)
newTokens = append(newTokens, token)
isError = true
continue
}
if res.StatusCode != 200 {
// error message:
// ref: https://github.com/sideshow/apns2/blob/master/response.go#L14-L65
LogPush(FailedPush, token, req, errors.New(res.Reason))
if PushConf.Core.Sync {
req.AddLog(getLogPushEntry(FailedPush, token, req, errors.New(res.Reason)))
}
StatStorage.AddIosError(1)
newTokens = append(newTokens, token)
isError = true
continue
}
if res.Sent() {
LogPush(SucceededPush, token, req, nil)
StatStorage.AddIosSuccess(1)
}
}
if isError == true && retryCount < maxRetry {
retryCount++
// resend fail token
req.Tokens = newTokens
goto Retry
}
return isError
}
// GetAndroidNotification use for define Android notification.
// HTTP Connection Server Reference for Android
// https://firebase.google.com/docs/cloud-messaging/http-server-ref
func GetAndroidNotification(req PushNotification) *fcm.Message {
notification := &fcm.Message{
To: req.To,
CollapseKey: req.CollapseKey,
ContentAvailable: req.ContentAvailable,
DelayWhileIdle: req.DelayWhileIdle,
TimeToLive: req.TimeToLive,
RestrictedPackageName: req.RestrictedPackageName,
DryRun: req.DryRun,
}
notification.RegistrationIDs = req.Tokens
if len(req.Priority) > 0 && req.Priority == "high" {
notification.Priority = "high"
}
// Add another field
if len(req.Data) > 0 {
notification.Data = make(map[string]interface{})
for k, v := range req.Data {
notification.Data[k] = v
}
}
notification.Notification = &req.Notification
// Set request message if body is empty
if len(req.Message) > 0 {
notification.Notification.Body = req.Message
}
if len(req.Title) > 0 {
notification.Notification.Title = req.Title
}
if len(req.Sound) > 0 {
notification.Notification.Sound = req.Sound
}
return notification
}
// PushToAndroid provide send notification to Android server.
func PushToAndroid(req PushNotification) bool {
LogAccess.Debug("Start push notification for Android")
if PushConf.Core.Sync {
defer req.WaitDone()
}
var (
APIKey string
retryCount = 0
maxRetry = PushConf.Android.MaxRetry
)
if req.Retry > 0 && req.Retry < maxRetry {
maxRetry = req.Retry
}
// check message
err := CheckMessage(req)
if err != nil {
LogError.Error("request error: " + err.Error())
return false
}
Retry:
var isError = false
notification := GetAndroidNotification(req)
if APIKey = PushConf.Android.APIKey; req.APIKey != "" {
APIKey = req.APIKey
}
client, err := fcm.NewClient(APIKey)
if err != nil {
// 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
}
LogAccess.Debug(fmt.Sprintf("Android Success count: %d, Failure count: %d", res.Success, res.Failure))
StatStorage.AddAndroidSuccess(int64(res.Success))
StatStorage.AddAndroidError(int64(res.Failure))
var newTokens []string
for k, result := range res.Results {
if result.Error != nil {
isError = true
newTokens = append(newTokens, req.Tokens[k])
LogPush(FailedPush, req.Tokens[k], req, result.Error)
if PushConf.Core.Sync {
req.AddLog(getLogPushEntry(FailedPush, req.Tokens[k], req, result.Error))
}
continue
}
LogPush(SucceededPush, req.Tokens[k], req, nil)
}
if isError == true && retryCount < maxRetry {
retryCount++
// resend fail token
req.Tokens = newTokens
goto Retry
}
return true
}

227
gorush/notification_apns.go Normal file
View File

@ -0,0 +1,227 @@
package gorush
import (
"errors"
"path/filepath"
"time"
apns "github.com/sideshow/apns2"
"github.com/sideshow/apns2/certificate"
"github.com/sideshow/apns2/payload"
)
// InitAPNSClient use for initialize APNs Client.
func InitAPNSClient() error {
if PushConf.Ios.Enabled {
var err error
ext := filepath.Ext(PushConf.Ios.KeyPath)
switch ext {
case ".p12":
CertificatePemIos, err = certificate.FromP12File(PushConf.Ios.KeyPath, PushConf.Ios.Password)
case ".pem":
CertificatePemIos, err = certificate.FromPemFile(PushConf.Ios.KeyPath, PushConf.Ios.Password)
default:
err = errors.New("wrong certificate key extension")
}
if err != nil {
LogError.Error("Cert Error:", err.Error())
return err
}
if PushConf.Ios.Production {
ApnsClient = apns.NewClient(CertificatePemIos).Production()
} else {
ApnsClient = apns.NewClient(CertificatePemIos).Development()
}
}
return nil
}
func iosAlertDictionary(payload *payload.Payload, req PushNotification) *payload.Payload {
// Alert dictionary
if len(req.Title) > 0 {
payload.AlertTitle(req.Title)
}
if len(req.Alert.Title) > 0 {
payload.AlertTitle(req.Alert.Title)
}
// Apple Watch & Safari display this string as part of the notification interface.
if len(req.Alert.Subtitle) > 0 {
payload.AlertSubtitle(req.Alert.Subtitle)
}
if len(req.Alert.TitleLocKey) > 0 {
payload.AlertTitleLocKey(req.Alert.TitleLocKey)
}
if len(req.Alert.LocArgs) > 0 {
payload.AlertLocArgs(req.Alert.LocArgs)
}
if len(req.Alert.TitleLocArgs) > 0 {
payload.AlertTitleLocArgs(req.Alert.TitleLocArgs)
}
if len(req.Alert.Body) > 0 {
payload.AlertBody(req.Alert.Body)
}
if len(req.Alert.LaunchImage) > 0 {
payload.AlertLaunchImage(req.Alert.LaunchImage)
}
if len(req.Alert.LocKey) > 0 {
payload.AlertLocKey(req.Alert.LocKey)
}
if len(req.Alert.Action) > 0 {
payload.AlertAction(req.Alert.Action)
}
if len(req.Alert.ActionLocKey) > 0 {
payload.AlertActionLocKey(req.Alert.ActionLocKey)
}
// General
if len(req.Category) > 0 {
payload.Category(req.Category)
}
return payload
}
// GetIOSNotification use for define iOS notification.
// The iOS Notification Payload
// ref: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html#//apple_ref/doc/uid/TP40008194-CH17-SW1
func GetIOSNotification(req PushNotification) *apns.Notification {
notification := &apns.Notification{
ApnsID: req.ApnsID,
Topic: req.Topic,
}
if req.Expiration > 0 {
notification.Expiration = time.Unix(req.Expiration, 0)
}
if len(req.Priority) > 0 && req.Priority == "normal" {
notification.Priority = apns.PriorityLow
}
payload := payload.NewPayload()
// add alert object if message length > 0
if len(req.Message) > 0 {
payload.Alert(req.Message)
}
// zero value for clear the badge on the app icon.
if req.Badge != nil && *req.Badge >= 0 {
payload.Badge(*req.Badge)
}
if req.MutableContent {
payload.MutableContent()
}
if len(req.Sound) > 0 {
payload.Sound(req.Sound)
}
if req.ContentAvailable {
payload.ContentAvailable()
}
if len(req.URLArgs) > 0 {
payload.URLArgs(req.URLArgs)
}
for k, v := range req.Data {
payload.Custom(k, v)
}
payload = iosAlertDictionary(payload, req)
notification.Payload = payload
return notification
}
// PushToIOS provide send notification to APNs server.
func PushToIOS(req PushNotification) bool {
LogAccess.Debug("Start push notification for iOS")
if PushConf.Core.Sync {
defer req.WaitDone()
}
var (
retryCount = 0
maxRetry = PushConf.Ios.MaxRetry
)
if req.Retry > 0 && req.Retry < maxRetry {
maxRetry = req.Retry
}
Retry:
var (
isError = false
newTokens []string
)
notification := GetIOSNotification(req)
for _, token := range req.Tokens {
notification.DeviceToken = token
// send ios notification
res, err := ApnsClient.Push(notification)
if err != nil {
// apns server error
LogPush(FailedPush, token, req, err)
if PushConf.Core.Sync {
req.AddLog(getLogPushEntry(FailedPush, token, req, err))
}
StatStorage.AddIosError(1)
newTokens = append(newTokens, token)
isError = true
continue
}
if res.StatusCode != 200 {
// error message:
// ref: https://github.com/sideshow/apns2/blob/master/response.go#L14-L65
LogPush(FailedPush, token, req, errors.New(res.Reason))
if PushConf.Core.Sync {
req.AddLog(getLogPushEntry(FailedPush, token, req, errors.New(res.Reason)))
}
StatStorage.AddIosError(1)
newTokens = append(newTokens, token)
isError = true
continue
}
if res.Sent() {
LogPush(SucceededPush, token, req, nil)
StatStorage.AddIosSuccess(1)
}
}
if isError == true && retryCount < maxRetry {
retryCount++
// resend fail token
req.Tokens = newTokens
goto Retry
}
return isError
}

130
gorush/notification_fcm.go Normal file
View File

@ -0,0 +1,130 @@
package gorush
import (
"fmt"
"github.com/appleboy/go-fcm"
)
// GetAndroidNotification use for define Android notification.
// HTTP Connection Server Reference for Android
// https://firebase.google.com/docs/cloud-messaging/http-server-ref
func GetAndroidNotification(req PushNotification) *fcm.Message {
notification := &fcm.Message{
To: req.To,
CollapseKey: req.CollapseKey,
ContentAvailable: req.ContentAvailable,
DelayWhileIdle: req.DelayWhileIdle,
TimeToLive: req.TimeToLive,
RestrictedPackageName: req.RestrictedPackageName,
DryRun: req.DryRun,
}
notification.RegistrationIDs = req.Tokens
if len(req.Priority) > 0 && req.Priority == "high" {
notification.Priority = "high"
}
// Add another field
if len(req.Data) > 0 {
notification.Data = make(map[string]interface{})
for k, v := range req.Data {
notification.Data[k] = v
}
}
notification.Notification = &req.Notification
// Set request message if body is empty
if len(req.Message) > 0 {
notification.Notification.Body = req.Message
}
if len(req.Title) > 0 {
notification.Notification.Title = req.Title
}
if len(req.Sound) > 0 {
notification.Notification.Sound = req.Sound
}
return notification
}
// PushToAndroid provide send notification to Android server.
func PushToAndroid(req PushNotification) bool {
LogAccess.Debug("Start push notification for Android")
if PushConf.Core.Sync {
defer req.WaitDone()
}
var (
APIKey string
retryCount = 0
maxRetry = PushConf.Android.MaxRetry
)
if req.Retry > 0 && req.Retry < maxRetry {
maxRetry = req.Retry
}
// check message
err := CheckMessage(req)
if err != nil {
LogError.Error("request error: " + err.Error())
return false
}
Retry:
var isError = false
notification := GetAndroidNotification(req)
if APIKey = PushConf.Android.APIKey; req.APIKey != "" {
APIKey = req.APIKey
}
client, err := fcm.NewClient(APIKey)
if err != nil {
// 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
}
LogAccess.Debug(fmt.Sprintf("Android Success count: %d, Failure count: %d", res.Success, res.Failure))
StatStorage.AddAndroidSuccess(int64(res.Success))
StatStorage.AddAndroidError(int64(res.Failure))
var newTokens []string
for k, result := range res.Results {
if result.Error != nil {
isError = true
newTokens = append(newTokens, req.Tokens[k])
LogPush(FailedPush, req.Tokens[k], req, result.Error)
if PushConf.Core.Sync {
req.AddLog(getLogPushEntry(FailedPush, req.Tokens[k], req, result.Error))
}
continue
}
LogPush(SucceededPush, req.Tokens[k], req, nil)
}
if isError == true && retryCount < maxRetry {
retryCount++
// resend fail token
req.Tokens = newTokens
goto Retry
}
return true
}

65
gorush/worker.go Normal file
View File

@ -0,0 +1,65 @@
package gorush
import (
"sync"
)
// InitWorkers for initialize all workers.
func InitWorkers(workerNum int64, queueNum int64) {
LogAccess.Debug("worker number is ", workerNum, ", queue number is ", queueNum)
QueueNotification = make(chan PushNotification, queueNum)
for i := int64(0); i < workerNum; i++ {
go startWorker()
}
}
func startWorker() {
for {
notification := <-QueueNotification
switch notification.Platform {
case PlatFormIos:
PushToIOS(notification)
case PlatFormAndroid:
PushToAndroid(notification)
}
}
}
// queueNotification add notification to queue list.
func queueNotification(req RequestPush) (int, []LogPushEntry) {
var count int
wg := sync.WaitGroup{}
newNotification := []PushNotification{}
for _, notification := range req.Notifications {
switch notification.Platform {
case PlatFormIos:
if !PushConf.Ios.Enabled {
continue
}
case PlatFormAndroid:
if !PushConf.Android.Enabled {
continue
}
}
newNotification = append(newNotification, notification)
}
log := make([]LogPushEntry, 0, count)
for _, notification := range newNotification {
if PushConf.Core.Sync {
notification.wg = &wg
notification.log = &log
notification.AddWaitCount()
}
QueueNotification <- notification
count += len(notification.Tokens)
}
if PushConf.Core.Sync {
wg.Wait()
}
StatStorage.AddTotalCount(int64(count))
return count, log
}