From 460b74d8a6f0d36fec58327a00444b9164541074 Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Sun, 16 Jul 2017 22:22:48 -0500 Subject: [PATCH] refactor(notification): separate ios and android (#250) --- gorush/notification.go | 405 ------------------------------------ gorush/notification_apns.go | 227 ++++++++++++++++++++ gorush/notification_fcm.go | 130 ++++++++++++ gorush/worker.go | 65 ++++++ 4 files changed, 422 insertions(+), 405 deletions(-) create mode 100644 gorush/notification_apns.go create mode 100644 gorush/notification_fcm.go create mode 100644 gorush/worker.go diff --git a/gorush/notification.go b/gorush/notification.go index 021c0d1..a22f5cf 100644 --- a/gorush/notification.go +++ b/gorush/notification.go @@ -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 -} diff --git a/gorush/notification_apns.go b/gorush/notification_apns.go new file mode 100644 index 0000000..bb5d0fe --- /dev/null +++ b/gorush/notification_apns.go @@ -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 +} diff --git a/gorush/notification_fcm.go b/gorush/notification_fcm.go new file mode 100644 index 0000000..1946e61 --- /dev/null +++ b/gorush/notification_fcm.go @@ -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 +} diff --git a/gorush/worker.go b/gorush/worker.go new file mode 100644 index 0000000..2c7c031 --- /dev/null +++ b/gorush/worker.go @@ -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 +}