chore: rename gorush to notify package (#609)
Signed-off-by: Bo-Yi Wu <appleboy.tw@gmail.com>
This commit is contained in:
53
notify/feedback.go
Normal file
53
notify/feedback.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/appleboy/gorush/logx"
|
||||
)
|
||||
|
||||
// DispatchFeedback sends a feedback to the configured gateway.
|
||||
func DispatchFeedback(log logx.LogPushEntry, url string, timeout int64) error {
|
||||
if url == "" {
|
||||
return errors.New("The url can't be empty")
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
|
||||
transport := &http.Transport{
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
}
|
||||
client := &http.Client{
|
||||
Timeout: time.Duration(timeout) * time.Second,
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
75
notify/feedback_test.go
Normal file
75
notify/feedback_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/appleboy/gorush/logx"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEmptyFeedbackURL(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
logEntry := logx.LogPushEntry{
|
||||
ID: "",
|
||||
Type: "",
|
||||
Platform: "",
|
||||
Token: "",
|
||||
Message: "",
|
||||
Error: "",
|
||||
}
|
||||
|
||||
err := DispatchFeedback(logEntry, cfg.Core.FeedbackURL, cfg.Core.FeedbackTimeout)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPErrorInFeedbackCall(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
cfg.Core.FeedbackURL = "http://test.example.com/api/"
|
||||
logEntry := logx.LogPushEntry{
|
||||
ID: "",
|
||||
Type: "",
|
||||
Platform: "",
|
||||
Token: "",
|
||||
Message: "",
|
||||
Error: "",
|
||||
}
|
||||
|
||||
err := DispatchFeedback(logEntry, cfg.Core.FeedbackURL, cfg.Core.FeedbackTimeout)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestSuccessfulFeedbackCall(t *testing.T) {
|
||||
// Mock http server
|
||||
httpMock := httptest.NewServer(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/dispatch" {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, err := w.Write([]byte(`{}`))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
defer httpMock.Close()
|
||||
|
||||
cfg, _ := config.LoadConf()
|
||||
cfg.Core.FeedbackURL = httpMock.URL
|
||||
logEntry := logx.LogPushEntry{
|
||||
ID: "",
|
||||
Type: "",
|
||||
Platform: "",
|
||||
Token: "",
|
||||
Message: "",
|
||||
Error: "",
|
||||
}
|
||||
|
||||
err := DispatchFeedback(logEntry, cfg.Core.FeedbackURL, cfg.Core.FeedbackTimeout)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
18
notify/global.go
Normal file
18
notify/global.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"github.com/appleboy/go-fcm"
|
||||
"github.com/msalihkarakasli/go-hms-push/push/core"
|
||||
"github.com/sideshow/apns2"
|
||||
)
|
||||
|
||||
var (
|
||||
// ApnsClient is apns client
|
||||
ApnsClient *apns2.Client
|
||||
// FCMClient is apns client
|
||||
FCMClient *fcm.Client
|
||||
// HMSClient is Huawei push client
|
||||
HMSClient *core.HMSClient
|
||||
// MaxConcurrentIOSPushes pool to limit the number of concurrent iOS pushes
|
||||
MaxConcurrentIOSPushes chan struct{}
|
||||
)
|
||||
18
notify/main_test.go
Normal file
18
notify/main_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/appleboy/gorush/status"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
cfg, _ := config.LoadConf()
|
||||
if err := status.InitAppStatus(cfg); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
m.Run()
|
||||
}
|
||||
271
notify/notification.go
Normal file
271
notify/notification.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/appleboy/gorush/core"
|
||||
"github.com/appleboy/gorush/logx"
|
||||
"github.com/appleboy/gorush/queue"
|
||||
|
||||
"github.com/appleboy/go-fcm"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/msalihkarakasli/go-hms-push/push/model"
|
||||
)
|
||||
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
// D provide string array
|
||||
type D map[string]interface{}
|
||||
|
||||
const (
|
||||
// ApnsPriorityLow will tell APNs to send the push message at a time that takes
|
||||
// into account power considerations for the device. Notifications with this
|
||||
// priority might be grouped and delivered in bursts. They are throttled, and
|
||||
// in some cases are not delivered.
|
||||
ApnsPriorityLow = 5
|
||||
|
||||
// ApnsPriorityHigh will tell APNs to send the push message immediately.
|
||||
// Notifications with this priority must trigger an alert, sound, or badge on
|
||||
// the target device. It is an error to use this priority for a push
|
||||
// notification that contains only the content-available key.
|
||||
ApnsPriorityHigh = 10
|
||||
)
|
||||
|
||||
// Alert is APNs payload
|
||||
type Alert struct {
|
||||
Action string `json:"action,omitempty"`
|
||||
ActionLocKey string `json:"action-loc-key,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
LaunchImage string `json:"launch-image,omitempty"`
|
||||
LocArgs []string `json:"loc-args,omitempty"`
|
||||
LocKey string `json:"loc-key,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Subtitle string `json:"subtitle,omitempty"`
|
||||
TitleLocArgs []string `json:"title-loc-args,omitempty"`
|
||||
TitleLocKey string `json:"title-loc-key,omitempty"`
|
||||
SummaryArg string `json:"summary-arg,omitempty"`
|
||||
SummaryArgCount int `json:"summary-arg-count,omitempty"`
|
||||
}
|
||||
|
||||
// RequestPush support multiple notification request.
|
||||
type RequestPush struct {
|
||||
Notifications []PushNotification `json:"notifications" binding:"required"`
|
||||
}
|
||||
|
||||
// PushNotification is single notification request
|
||||
type PushNotification struct {
|
||||
Wg *sync.WaitGroup
|
||||
Log *[]logx.LogPushEntry
|
||||
Cfg config.ConfYaml
|
||||
|
||||
// Common
|
||||
ID string `json:"notif_id,omitempty"`
|
||||
Tokens []string `json:"tokens" binding:"required"`
|
||||
Platform int `json:"platform" binding:"required"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
Priority string `json:"priority,omitempty"`
|
||||
ContentAvailable bool `json:"content_available,omitempty"`
|
||||
MutableContent bool `json:"mutable_content,omitempty"`
|
||||
Sound interface{} `json:"sound,omitempty"`
|
||||
Data D `json:"data,omitempty"`
|
||||
Retry int `json:"retry,omitempty"`
|
||||
|
||||
// Android
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
CollapseKey string `json:"collapse_key,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"`
|
||||
Condition string `json:"condition,omitempty"`
|
||||
Notification *fcm.Notification `json:"notification,omitempty"`
|
||||
|
||||
// Huawei
|
||||
AppID string `json:"app_id,omitempty"`
|
||||
AppSecret string `json:"app_secret,omitempty"`
|
||||
HuaweiNotification *model.AndroidNotification `json:"huawei_notification,omitempty"`
|
||||
HuaweiData string `json:"huawei_data,omitempty"`
|
||||
HuaweiCollapseKey int `json:"huawei_collapse_key,omitempty"`
|
||||
HuaweiTTL string `json:"huawei_ttl,omitempty"`
|
||||
BiTag string `json:"bi_tag,omitempty"`
|
||||
FastAppTarget int `json:"fast_app_target,omitempty"`
|
||||
|
||||
// iOS
|
||||
Expiration *int64 `json:"expiration,omitempty"`
|
||||
ApnsID string `json:"apns_id,omitempty"`
|
||||
CollapseID string `json:"collapse_id,omitempty"`
|
||||
Topic string `json:"topic,omitempty"`
|
||||
PushType string `json:"push_type,omitempty"`
|
||||
Badge *int `json:"badge,omitempty"`
|
||||
Category string `json:"category,omitempty"`
|
||||
ThreadID string `json:"thread-id,omitempty"`
|
||||
URLArgs []string `json:"url-args,omitempty"`
|
||||
Alert Alert `json:"alert,omitempty"`
|
||||
Production bool `json:"production,omitempty"`
|
||||
Development bool `json:"development,omitempty"`
|
||||
SoundName string `json:"name,omitempty"`
|
||||
SoundVolume float32 `json:"volume,omitempty"`
|
||||
Apns D `json:"apns,omitempty"`
|
||||
}
|
||||
|
||||
// WaitDone decrements the WaitGroup counter.
|
||||
func (p *PushNotification) WaitDone() {
|
||||
if p.Wg != nil {
|
||||
p.Wg.Done()
|
||||
}
|
||||
}
|
||||
|
||||
// AddWaitCount increments the WaitGroup counter.
|
||||
func (p *PushNotification) AddWaitCount() {
|
||||
if p.Wg != nil {
|
||||
p.Wg.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// AddLog record fail log of notification
|
||||
func (p *PushNotification) AddLog(log logx.LogPushEntry) {
|
||||
if p.Log != nil {
|
||||
*p.Log = append(*p.Log, log)
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes for queue message
|
||||
func (p *PushNotification) Bytes() []byte {
|
||||
b, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// IsTopic check if message format is topic for FCM
|
||||
// ref: https://firebase.google.com/docs/cloud-messaging/send-message#topic-http-post-request
|
||||
func (p *PushNotification) IsTopic() bool {
|
||||
if p.Platform == core.PlatFormAndroid {
|
||||
return p.To != "" && strings.HasPrefix(p.To, "/topics/") || p.Condition != ""
|
||||
}
|
||||
|
||||
if p.Platform == core.PlatFormHuawei {
|
||||
return p.Topic != "" || p.Condition != ""
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CheckMessage for check request message
|
||||
func CheckMessage(req PushNotification) error {
|
||||
var msg string
|
||||
|
||||
// ignore send topic mesaage from FCM
|
||||
if !req.IsTopic() && len(req.Tokens) == 0 && req.To == "" {
|
||||
msg = "the message must specify at least one registration ID"
|
||||
logx.LogAccess.Debug(msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
if len(req.Tokens) == core.PlatFormIos && req.Tokens[0] == "" {
|
||||
msg = "the token must not be empty"
|
||||
logx.LogAccess.Debug(msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
if req.Platform == core.PlatFormAndroid && len(req.Tokens) > 1000 {
|
||||
msg = "the message may specify at most 1000 registration IDs"
|
||||
logx.LogAccess.Debug(msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
if req.Platform == core.PlatFormHuawei && len(req.Tokens) > 500 {
|
||||
msg = "the message may specify at most 500 registration IDs for Huawei"
|
||||
logx.LogAccess.Debug(msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
// ref: https://firebase.google.com/docs/cloud-messaging/http-server-ref
|
||||
if req.Platform == core.PlatFormAndroid && req.TimeToLive != nil && *req.TimeToLive > uint(2419200) {
|
||||
msg = "the message's TimeToLive field must be an integer " +
|
||||
"between 0 and 2419200 (4 weeks)"
|
||||
logx.LogAccess.Debug(msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetProxy only working for FCM server.
|
||||
func SetProxy(proxy string) error {
|
||||
proxyURL, err := url.ParseRequestURI(proxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
http.DefaultTransport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
|
||||
logx.LogAccess.Debug("Set http proxy as " + proxy)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPushConf provide check your yml config.
|
||||
func CheckPushConf(cfg config.ConfYaml) error {
|
||||
if !cfg.Ios.Enabled && !cfg.Android.Enabled && !cfg.Huawei.Enabled {
|
||||
return errors.New("Please enable iOS, Android or Huawei config in yml config")
|
||||
}
|
||||
|
||||
if cfg.Ios.Enabled {
|
||||
if cfg.Ios.KeyPath == "" && cfg.Ios.KeyBase64 == "" {
|
||||
return errors.New("Missing iOS certificate key")
|
||||
}
|
||||
|
||||
// check certificate file exist
|
||||
if cfg.Ios.KeyPath != "" {
|
||||
if _, err := os.Stat(cfg.Ios.KeyPath); os.IsNotExist(err) {
|
||||
return errors.New("certificate file does not exist")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Android.Enabled {
|
||||
if cfg.Android.APIKey == "" {
|
||||
return errors.New("Missing Android API Key")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Huawei.Enabled {
|
||||
if cfg.Huawei.AppSecret == "" {
|
||||
return errors.New("Missing Huawei App Secret")
|
||||
}
|
||||
|
||||
if cfg.Huawei.AppID == "" {
|
||||
return errors.New("Missing Huawei App ID")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendNotification send notification
|
||||
func SendNotification(req queue.QueuedMessage) {
|
||||
v, _ := req.(*PushNotification)
|
||||
|
||||
defer func() {
|
||||
v.WaitDone()
|
||||
}()
|
||||
|
||||
switch v.Platform {
|
||||
case core.PlatFormIos:
|
||||
PushToIOS(*v)
|
||||
case core.PlatFormAndroid:
|
||||
PushToAndroid(*v)
|
||||
case core.PlatFormHuawei:
|
||||
PushToHuawei(*v)
|
||||
}
|
||||
}
|
||||
471
notify/notification_apns.go
Normal file
471
notify/notification_apns.go
Normal file
@@ -0,0 +1,471 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/appleboy/gorush/core"
|
||||
"github.com/appleboy/gorush/logx"
|
||||
"github.com/appleboy/gorush/status"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/sideshow/apns2"
|
||||
"github.com/sideshow/apns2/certificate"
|
||||
"github.com/sideshow/apns2/payload"
|
||||
"github.com/sideshow/apns2/token"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
idleConnTimeout = 90 * time.Second
|
||||
tlsDialTimeout = 20 * time.Second
|
||||
tcpKeepAlive = 60 * time.Second
|
||||
)
|
||||
|
||||
var doOnce sync.Once
|
||||
|
||||
// DialTLS is the default dial function for creating TLS connections for
|
||||
// non-proxied HTTPS requests.
|
||||
var DialTLS = func(cfg *tls.Config) func(network, addr string) (net.Conn, error) {
|
||||
return func(network, addr string) (net.Conn, error) {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: tlsDialTimeout,
|
||||
KeepAlive: tcpKeepAlive,
|
||||
}
|
||||
return tls.DialWithDialer(dialer, network, addr, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
// Sound sets the aps sound on the payload.
|
||||
type Sound struct {
|
||||
Critical int `json:"critical,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Volume float32 `json:"volume,omitempty"`
|
||||
}
|
||||
|
||||
// InitAPNSClient use for initialize APNs Client.
|
||||
func InitAPNSClient(cfg config.ConfYaml) error {
|
||||
if cfg.Ios.Enabled {
|
||||
var err error
|
||||
var authKey *ecdsa.PrivateKey
|
||||
var certificateKey tls.Certificate
|
||||
var ext string
|
||||
|
||||
if cfg.Ios.KeyPath != "" {
|
||||
ext = filepath.Ext(cfg.Ios.KeyPath)
|
||||
|
||||
switch ext {
|
||||
case ".p12":
|
||||
certificateKey, err = certificate.FromP12File(cfg.Ios.KeyPath, cfg.Ios.Password)
|
||||
case ".pem":
|
||||
certificateKey, err = certificate.FromPemFile(cfg.Ios.KeyPath, cfg.Ios.Password)
|
||||
case ".p8":
|
||||
authKey, err = token.AuthKeyFromFile(cfg.Ios.KeyPath)
|
||||
default:
|
||||
err = errors.New("wrong certificate key extension")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logx.LogError.Error("Cert Error:", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
} else if cfg.Ios.KeyBase64 != "" {
|
||||
ext = "." + cfg.Ios.KeyType
|
||||
key, err := base64.StdEncoding.DecodeString(cfg.Ios.KeyBase64)
|
||||
if err != nil {
|
||||
logx.LogError.Error("base64 decode error:", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
switch ext {
|
||||
case ".p12":
|
||||
certificateKey, err = certificate.FromP12Bytes(key, cfg.Ios.Password)
|
||||
case ".pem":
|
||||
certificateKey, err = certificate.FromPemBytes(key, cfg.Ios.Password)
|
||||
case ".p8":
|
||||
authKey, err = token.AuthKeyFromBytes(key)
|
||||
default:
|
||||
err = errors.New("wrong certificate key type")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logx.LogError.Error("Cert Error:", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ext == ".p8" {
|
||||
if cfg.Ios.KeyID == "" || cfg.Ios.TeamID == "" {
|
||||
msg := "You should provide ios.KeyID and ios.TeamID for P8 token"
|
||||
logx.LogError.Error(msg)
|
||||
return errors.New(msg)
|
||||
}
|
||||
token := &token.Token{
|
||||
AuthKey: authKey,
|
||||
// KeyID from developer account (Certificates, Identifiers & Profiles -> Keys)
|
||||
KeyID: cfg.Ios.KeyID,
|
||||
// TeamID from developer account (View Account -> Membership)
|
||||
TeamID: cfg.Ios.TeamID,
|
||||
}
|
||||
|
||||
ApnsClient, err = newApnsTokenClient(cfg, token)
|
||||
} else {
|
||||
ApnsClient, err = newApnsClient(cfg, certificateKey)
|
||||
}
|
||||
|
||||
if h2Transport, ok := ApnsClient.HTTPClient.Transport.(*http2.Transport); ok {
|
||||
configureHTTP2ConnHealthCheck(h2Transport)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logx.LogError.Error("Transport Error:", err.Error())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
doOnce.Do(func() {
|
||||
MaxConcurrentIOSPushes = make(chan struct{}, cfg.Ios.MaxConcurrentPushes)
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func newApnsClient(cfg config.ConfYaml, certificate tls.Certificate) (*apns2.Client, error) {
|
||||
var client *apns2.Client
|
||||
|
||||
if cfg.Ios.Production {
|
||||
client = apns2.NewClient(certificate).Production()
|
||||
} else {
|
||||
client = apns2.NewClient(certificate).Development()
|
||||
}
|
||||
|
||||
if cfg.Core.HTTPProxy == "" {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{certificate},
|
||||
}
|
||||
|
||||
if len(certificate.Certificate) > 0 {
|
||||
tlsConfig.BuildNameToCertificate()
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
DialTLS: DialTLS(tlsConfig),
|
||||
Proxy: http.DefaultTransport.(*http.Transport).Proxy,
|
||||
IdleConnTimeout: idleConnTimeout,
|
||||
}
|
||||
|
||||
h2Transport, err := http2.ConfigureTransports(transport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configureHTTP2ConnHealthCheck(h2Transport)
|
||||
|
||||
client.HTTPClient.Transport = transport
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func newApnsTokenClient(cfg config.ConfYaml, token *token.Token) (*apns2.Client, error) {
|
||||
var client *apns2.Client
|
||||
|
||||
if cfg.Ios.Production {
|
||||
client = apns2.NewTokenClient(token).Production()
|
||||
} else {
|
||||
client = apns2.NewTokenClient(token).Development()
|
||||
}
|
||||
|
||||
if cfg.Core.HTTPProxy == "" {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
transport := &http.Transport{
|
||||
DialTLS: DialTLS(nil),
|
||||
Proxy: http.DefaultTransport.(*http.Transport).Proxy,
|
||||
IdleConnTimeout: idleConnTimeout,
|
||||
}
|
||||
|
||||
h2Transport, err := http2.ConfigureTransports(transport)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configureHTTP2ConnHealthCheck(h2Transport)
|
||||
|
||||
client.HTTPClient.Transport = transport
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func configureHTTP2ConnHealthCheck(h2Transport *http2.Transport) {
|
||||
h2Transport.ReadIdleTimeout = 1 * time.Second
|
||||
h2Transport.PingTimeout = 1 * time.Second
|
||||
}
|
||||
|
||||
func iosAlertDictionary(payload *payload.Payload, req PushNotification) *payload.Payload {
|
||||
// Alert dictionary
|
||||
|
||||
if len(req.Title) > 0 {
|
||||
payload.AlertTitle(req.Title)
|
||||
}
|
||||
|
||||
if len(req.Message) > 0 && len(req.Title) > 0 {
|
||||
payload.AlertBody(req.Message)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if len(req.Alert.SummaryArg) > 0 {
|
||||
payload.AlertSummaryArg(req.Alert.SummaryArg)
|
||||
}
|
||||
|
||||
if req.Alert.SummaryArgCount > 0 {
|
||||
payload.AlertSummaryArgCount(req.Alert.SummaryArgCount)
|
||||
}
|
||||
|
||||
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) *apns2.Notification {
|
||||
notification := &apns2.Notification{
|
||||
ApnsID: req.ApnsID,
|
||||
Topic: req.Topic,
|
||||
CollapseID: req.CollapseID,
|
||||
}
|
||||
|
||||
if req.Expiration != nil {
|
||||
notification.Expiration = time.Unix(*req.Expiration, 0)
|
||||
}
|
||||
|
||||
if len(req.Priority) > 0 {
|
||||
if req.Priority == "normal" {
|
||||
notification.Priority = apns2.PriorityLow
|
||||
} else if req.Priority == "high" {
|
||||
notification.Priority = apns2.PriorityHigh
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.PushType) > 0 {
|
||||
notification.PushType = apns2.EPushType(req.PushType)
|
||||
}
|
||||
|
||||
payload := payload.NewPayload()
|
||||
|
||||
// add alert object if message length > 0 and title is empty
|
||||
if len(req.Message) > 0 && req.Title == "" {
|
||||
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()
|
||||
}
|
||||
|
||||
switch req.Sound.(type) {
|
||||
// from http request binding
|
||||
case map[string]interface{}:
|
||||
result := &Sound{}
|
||||
_ = mapstructure.Decode(req.Sound, &result)
|
||||
payload.Sound(result)
|
||||
// from http request binding for non critical alerts
|
||||
case string:
|
||||
payload.Sound(&req.Sound)
|
||||
case Sound:
|
||||
payload.Sound(&req.Sound)
|
||||
}
|
||||
|
||||
if len(req.SoundName) > 0 {
|
||||
payload.SoundName(req.SoundName)
|
||||
}
|
||||
|
||||
if req.SoundVolume > 0 {
|
||||
payload.SoundVolume(req.SoundVolume)
|
||||
}
|
||||
|
||||
if req.ContentAvailable {
|
||||
payload.ContentAvailable()
|
||||
}
|
||||
|
||||
if len(req.URLArgs) > 0 {
|
||||
payload.URLArgs(req.URLArgs)
|
||||
}
|
||||
|
||||
if len(req.ThreadID) > 0 {
|
||||
payload.ThreadID(req.ThreadID)
|
||||
}
|
||||
|
||||
for k, v := range req.Data {
|
||||
payload.Custom(k, v)
|
||||
}
|
||||
|
||||
payload = iosAlertDictionary(payload, req)
|
||||
|
||||
notification.Payload = payload
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
func getApnsClient(cfg config.ConfYaml, req PushNotification) (client *apns2.Client) {
|
||||
if req.Production {
|
||||
client = ApnsClient.Production()
|
||||
} else if req.Development {
|
||||
client = ApnsClient.Development()
|
||||
} else {
|
||||
if cfg.Ios.Production {
|
||||
client = ApnsClient.Production()
|
||||
} else {
|
||||
client = ApnsClient.Development()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PushToIOS provide send notification to APNs server.
|
||||
func PushToIOS(req PushNotification) {
|
||||
logx.LogAccess.Debug("Start push notification for iOS")
|
||||
|
||||
if req.Cfg.Core.Sync && !core.IsLocalQueue(core.Queue(req.Cfg.Queue.Engine)) {
|
||||
req.Cfg.Core.Sync = false
|
||||
}
|
||||
|
||||
var (
|
||||
retryCount = 0
|
||||
maxRetry = req.Cfg.Ios.MaxRetry
|
||||
)
|
||||
|
||||
if req.Retry > 0 && req.Retry < maxRetry {
|
||||
maxRetry = req.Retry
|
||||
}
|
||||
|
||||
Retry:
|
||||
var newTokens []string
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
client := getApnsClient(req.Cfg, req)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, token := range req.Tokens {
|
||||
// occupy push slot
|
||||
MaxConcurrentIOSPushes <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func(notification apns2.Notification, token string) {
|
||||
notification.DeviceToken = token
|
||||
|
||||
// send ios notification
|
||||
res, err := client.Push(¬ification)
|
||||
if err != nil || (res != nil && res.StatusCode != http.StatusOK) {
|
||||
if err == nil {
|
||||
// error message:
|
||||
// ref: https://github.com/sideshow/apns2/blob/master/response.go#L14-L65
|
||||
err = errors.New(res.Reason)
|
||||
}
|
||||
// apns server error
|
||||
logPush(req.Cfg, core.FailedPush, token, req, err)
|
||||
|
||||
if req.Cfg.Core.Sync {
|
||||
req.AddLog(createLogPushEntry(req.Cfg, core.FailedPush, token, req, err))
|
||||
} else if req.Cfg.Core.FeedbackURL != "" {
|
||||
go func(logger *logrus.Logger, log logx.LogPushEntry, url string, timeout int64) {
|
||||
err := DispatchFeedback(log, url, timeout)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}(logx.LogError, createLogPushEntry(req.Cfg, core.FailedPush, token, req, err), req.Cfg.Core.FeedbackURL, req.Cfg.Core.FeedbackTimeout)
|
||||
}
|
||||
|
||||
status.StatStorage.AddIosError(1)
|
||||
// We should retry only "retryable" statuses. More info about response:
|
||||
// https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/handling_notification_responses_from_apns
|
||||
if res != nil && res.StatusCode >= http.StatusInternalServerError {
|
||||
newTokens = append(newTokens, token)
|
||||
}
|
||||
}
|
||||
|
||||
if res != nil && res.Sent() {
|
||||
logPush(req.Cfg, core.SucceededPush, token, req, nil)
|
||||
status.StatStorage.AddIosSuccess(1)
|
||||
}
|
||||
|
||||
// free push slot
|
||||
<-MaxConcurrentIOSPushes
|
||||
wg.Done()
|
||||
}(*notification, token)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(newTokens) > 0 && retryCount < maxRetry {
|
||||
retryCount++
|
||||
|
||||
// resend fail token
|
||||
req.Tokens = newTokens
|
||||
goto Retry
|
||||
}
|
||||
}
|
||||
769
notify/notification_apns_test.go
Normal file
769
notify/notification_apns_test.go
Normal file
@@ -0,0 +1,769 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/appleboy/gorush/status"
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/sideshow/apns2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const certificateValidP12 = `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`
|
||||
|
||||
const certificateValidPEM = `QmFnIEF0dHJpYnV0ZXMKICAgIGxvY2FsS2V5SUQ6IDhDIDFBIDlGIDAwIDY2IEJEIDI0IDQyIEI5IDVEIDFFIEVCIEZFIDVFIDhCIENBIDA0IDNEIDczIDgzIAogICAgZnJpZW5kbHlOYW1lOiBBUE5TLzIgUHJpdmF0ZSBLZXkKc3ViamVjdD0vQz1OWi9TVD1XZWxsaW5ndG9uL0w9V2VsbGluZ3Rvbi9PPUludGVybmV0IFdpZGdpdHMgUHR5IEx0ZC9PVT05WkVINjJLUlZWL0NOPUFQTlMvMiBEZXZlbG9wbWVudCBJT1MgUHVzaCBTZXJ2aWNlczogY29tLnNpZGVzaG93LkFwbnMyCmlzc3Vlcj0vQz1OWi9TVD1XZWxsaW5ndG9uL0w9V2VsbGluZ3Rvbi9PPUFQTlMvMiBJbmMuL09VPUFQTlMvMiBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9ucy9DTj1BUE5TLzIgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkKLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQ2ekNDQXRNQ0FRSXdEUVlKS29aSWh2Y05BUUVMQlFBd2djTXhDekFKQmdOVkJBWVRBazVhTVJNd0VRWUQKVlFRSUV3cFhaV3hzYVc1bmRHOXVNUk13RVFZRFZRUUhFd3BYWld4c2FXNW5kRzl1TVJRd0VnWURWUVFLRXd0QgpVRTVUTHpJZ1NXNWpMakV0TUNzR0ExVUVDeE1rUVZCT1V5OHlJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnClVtVnNZWFJwYjI1ek1VVXdRd1lEVlFRREV6eEJVRTVUTHpJZ1YyOXliR1IzYVdSbElFUmxkbVZzYjNCbGNpQlMKWld4aGRHbHZibk1nUTJWeWRHbG1hV05oZEdsdmJpQkJkWFJvYjNKcGRIa3dIaGNOTVRZd01UQTRNRGd6TkRNdwpXaGNOTWpZd01UQTFNRGd6TkRNd1dqQ0JzakVMTUFrR0ExVUVCaE1DVGxveEV6QVJCZ05WQkFnVENsZGxiR3hwCmJtZDBiMjR4RXpBUkJnTlZCQWNUQ2xkbGJHeHBibWQwYjI0eElUQWZCZ05WQkFvVEdFbHVkR1Z5Ym1WMElGZHAKWkdkcGRITWdVSFI1SUV4MFpERVRNQkVHQTFVRUN4TUtPVnBGU0RZeVMxSldWakZCTUQ4R0ExVUVBeE00UVZCTwpVeTh5SUVSbGRtVnNiM0J0Wlc1MElFbFBVeUJRZFhOb0lGTmxjblpwWTJWek9pQmpiMjB1YzJsa1pYTm9iM2N1ClFYQnVjekl3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRFkwYzFUS0I1b1pQd1EKN3QxQ3dNSXJ2cUI2R0lVM3RQeTZSaGNrWlhUa09COFllQldKN1VLZkN6OEhHSEZWb21CUDBUNU9VYmVxUXpxVwpZSmJRelo4YTZaTXN6YkwwbE80WDkrKzNPaTUvVHRBd09VT0s4ck9GTjI1bTJLZnNheUhRWi80dldTdEsyRndtCjVhSmJHTGxwSC9iLzd6MUQ0dmhtTWdvQnVUMUl1eWhHaXlGeGxaOUV0VGxvRnZzcU0xRTVmWVpPU1pBQ3lYVGEKSzR2ZGdiUU1nVVZzSTcxNEZBZ0xUbEswVWVpUmttS20zcGRidGZWYnJ0aHpJK0lIWEtJdFVJeStGbjIwUFJNaApkU25henRTejd0Z0JXQ0l4MjJxdmNZb2dIV2lPZ1VZSU03NzJ6RTJ5OFVWT3I4RHNpUmxzT0hTQTdFSTRNSmNRCkcyRlVxMlovQWdNQkFBRXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBR3lmeU8ySE1nY2RlQmN6M2J0NUJJTFgKZjdSQTIvVW1WSXdjS1IxcW90VHNGK1BuQm1jSUxleU9RZ0RlOXRHVTVjUmM3OWtEdDNKUm1NWVJPRklNZ0ZSZgpXZjIydU9LdGhvN0dRUWFLdkcrYmtnTVZkWUZSbEJIbkYrS2VxS0g4MXFiOXArQ1Q0SXcwR2VoSUwxRGlqRkxSClZJQUlCWXB6NG9CUENJRTFJU1ZUK0ZnYWYzSkFoNTlrYlBiTnc5QUlEeGFCdFA4RXV6U1ROd2ZieG9HYkNvYlMKV2kxVThJc0N3UUZ0OHRNMW00WlhEMUNjWklyR2RyeWVBaFZrdktJSlJpVTVRWVdJMm5xWk4rSnFRdWNtOWFkMAptWU81bUprSW9iVWE0K1pKaENQS0VkbWdwRmJSR2swd1Z1YURNOUN2NlAyc3JzWUFqYU80eTNWUDBHdk5LUkk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KQmFnIEF0dHJpYnV0ZXMKICAgIGxvY2FsS2V5SUQ6IDhDIDFBIDlGIDAwIDY2IEJEIDI0IDQyIEI5IDVEIDFFIEVCIEZFIDVFIDhCIENBIDA0IDNEIDczIDgzIAogICAgZnJpZW5kbHlOYW1lOiBBUE5TLzIgUHJpdmF0ZSBLZXkKS2V5IEF0dHJpYnV0ZXM6IDxObyBBdHRyaWJ1dGVzPgotLS0tLUJFR0lOIFJTQSBQUklWQVRFIEtFWS0tLS0tCk1JSUVvd0lCQUFLQ0FRRUEyTkhOVXlnZWFHVDhFTzdkUXNEQ0s3NmdlaGlGTjdUOHVrWVhKR1YwNURnZkdIZ1YKaWUxQ253cy9CeGh4VmFKZ1Q5RStUbEczcWtNNmxtQ1cwTTJmR3VtVExNMnk5SlR1Ri9mdnR6b3VmMDdRTURsRAppdkt6aFRkdVp0aW43R3NoMEdmK0wxa3JTdGhjSnVXaVd4aTVhUi8yLys4OVErTDRaaklLQWJrOVNMc29Sb3NoCmNaV2ZSTFU1YUJiN0tqTlJPWDJHVGttUUFzbDAyaXVMM1lHMERJRkZiQ085ZUJRSUMwNVN0Rkhva1pKaXB0NlgKVzdYMVc2N1ljeVBpQjF5aUxWQ012aFo5dEQwVElYVXAyczdVcys3WUFWZ2lNZHRxcjNHS0lCMW9qb0ZHQ0RPKwo5c3hOc3ZGRlRxL0E3SWtaYkRoMGdPeENPRENYRUJ0aFZLdG1md0lEQVFBQkFvSUJBUUNXOFpDSStPQWFlMXRFCmlwWjlGMmJXUDNMSExYVG84RllWZENBK1ZXZUlUazNQb2lJVWtKbVYwYVdDVWhEc3RndG81ZG9EZWo1c0NUdXIKWHZqL3luYWVyTWVxSkZZV2tld2p3WmNnTHlBWnZ3dU8xdjdmcDlFMHgvOVRHRGZuampuUE5lYXVuZHhXMGNOdAp6T1kzbDBIVkhzeTlKcGUzUURjQUpvdnk0VHY1K2hGWTRrRHhVQkdzeWp2aFNjVmdLZzV0TGtKY2xtM3NPdS9MCkd5THFwd05JM09KQWRNSXVWRDROMkJaMWFPRWFwNm1wMnk4SWUwL1I0WVdjYVo1QTRQdzd4VVBsNlNYYzl1dWEKLzc4UVRFUnRQQzZlanlDQmlFMDVhOG0zUTNpdWQzWHRubHl3czJLd2hnQkFmRTZNNHpSL2YzT1FCN1pJWE1oeQpacG1aWnc1eEFvR0JBUFluODRJcmxJUWV0V1FmdlBkTTdLemdoNlVESEN1Z25sQ0RnaHdZcFJKR2k4aE1mdVpWCnhOSXJZQUp6TFlEUTAxbEZKUkpnV1hUY2JxejlOQnoxbmhnK2NOT3oxL0tZKzM4ZXVkZWU2RE5ZbXp0UDdqRFAKMmpuYVMrZHRqQzhoQVhPYm5GcUcrTmlsTURMTHU2YVJtckphSW1ialNyZnlMaUU2bXZKN3U4MW5Bb0dCQU9GOQpnOTN3WjBtTDFyazJzNVd3SEdUTlUvSGFPdG1XUzR6N2tBN2Y0UWFSdWIrTXdwcFptbURaUEhwaVpYN0JQY1p6CmlPUFFoK3huN0lxUkdvUVdCTHlrQlZ0OHpaRm9MWkpvQ1IzbjYzbGV4NUE0cC8wUHAxZ0ZaclIreFg4UFlWb3MKM3llZWlXeVBLc1hYTmMwczVRd0haY1g2V2I4RUhUaFRYR0NCZXRjcEFvR0FNZVFKQzlJUGFQUGNhZTJ3M0NMQQpPWTNNa0ZwZ0JFdXFxc0RzeHdzTHNmZVFiMGxwMHYrQlErTzhzdUpyVDVlRHJxMUFCVWgzK1NLUVlBbDEzWVMrCnhVVXFrdzM1YjljbjZpenRGOUhDV0YzV0lLQmpzNHI5UFFxTXBkeGpORTRwUUNoQytXb3YxNkVyY3JBdVdXVmIKaUZpU2JtNFUvOUZiSGlzRnFxMy9jM01DZ1lCK3Z6U3VQZ0Z3MzcrMG9FRFZ0UVpneXVHU29wNU56Q052ZmIvOQovRzNhYVhORmJuTzhtdjBoenpvbGVNV2dPRExuSis0Y1VBejNIM3RnY0N1OWJ6citaaHYwenZRbDlhOFlDbzZGClZ1V1BkVzByYmcxUE84dE91TXFBVG5ubzc5WkMvOUgzelM5bDdCdVkxVjJTbE5leXFUM1Z5T0ZGYzZTUkVwcHMKVEp1bDhRS0JnQXhuUUI4TUE3elBVTHUxY2x5YUpMZHRFZFJQa0tXTjdsS1lwdGMwZS9WSGZTc0t4c2VXa2ZxaQp6Z1haNTFrUVRyVDZaYjZIWVJmd0MxbU1YSFdSS1J5WWpBbkN4VmltNllRZCtLVlQ0OWlSRERBaUlGb01HQTRpCnZ2Y0lsbmVxT1paUERJb0tKNjBJak8vRFpIV2t3NW1MamFJclQrcVEzWEFHZEpBMTNoY20KLS0tLS1FTkQgUlNBIFBSSVZBVEUgS0VZLS0tLS0K`
|
||||
|
||||
const authkeyInvalidP8 = `TUlHSEFnRUFNQk1HQnlxR1NNNDlBZ0VHQ0NxR1NNNDlBd0VIQkcwd2F3SUJBUVFnRWJWemZQblpQeGZBeXhxRQpaVjA1bGFBb0pBbCsvNlh0Mk80bU9CNjExc09oUkFOQ0FBU2dGVEtqd0pBQVU5NWcrKy92ektXSGt6QVZtTk1JCnRCNXZUalpPT0l3bkViNzBNc1daRkl5VUZEMVA5R3dzdHo0K2FrSFg3dkk4Qkg2aEhtQm1mWlpaCg==`
|
||||
|
||||
const authkeyValidP8 = `LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ0ViVnpmUG5aUHhmQXl4cUUKWlYwNWxhQW9KQWwrLzZYdDJPNG1PQjYxMXNPaFJBTkNBQVNnRlRLandKQUFVOTVnKysvdnpLV0hrekFWbU5NSQp0QjV2VGpaT09Jd25FYjcwTXNXWkZJeVVGRDFQOUd3c3R6NCtha0hYN3ZJOEJINmhIbUJtZmVRbAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==`
|
||||
|
||||
func TestDisabledAndroidIosConf(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
cfg.Android.Enabled = false
|
||||
cfg.Huawei.Enabled = false
|
||||
|
||||
err := CheckPushConf(cfg)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Please enable iOS, Android or Huawei config in yml config", err.Error())
|
||||
}
|
||||
|
||||
func TestMissingIOSCertificate(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = ""
|
||||
cfg.Ios.KeyBase64 = ""
|
||||
err := CheckPushConf(cfg)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Missing iOS certificate key", err.Error())
|
||||
|
||||
cfg.Ios.KeyPath = "test.pem"
|
||||
err = CheckPushConf(cfg)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "certificate file does not exist", err.Error())
|
||||
}
|
||||
|
||||
func TestIOSNotificationStructure(t *testing.T) {
|
||||
var dat map[string]interface{}
|
||||
unix := time.Now().Unix()
|
||||
|
||||
test := "test"
|
||||
expectBadge := 0
|
||||
message := "Welcome notification Server"
|
||||
expiration := int64(time.Now().Unix())
|
||||
req := PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
Expiration: &expiration,
|
||||
Priority: "normal",
|
||||
Message: message,
|
||||
Badge: &expectBadge,
|
||||
Sound: Sound{
|
||||
Critical: 1,
|
||||
Name: test,
|
||||
Volume: 1.0,
|
||||
},
|
||||
ContentAvailable: true,
|
||||
Data: D{
|
||||
"key1": "test",
|
||||
"key2": 2,
|
||||
},
|
||||
Category: test,
|
||||
URLArgs: []string{"a", "b"},
|
||||
}
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
|
||||
dump, _ := json.Marshal(notification.Payload)
|
||||
data := []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
alert, _ := jsonparser.GetString(data, "aps", "alert")
|
||||
badge, _ := jsonparser.GetInt(data, "aps", "badge")
|
||||
soundName, _ := jsonparser.GetString(data, "aps", "sound", "name")
|
||||
soundCritical, _ := jsonparser.GetInt(data, "aps", "sound", "critical")
|
||||
soundVolume, _ := jsonparser.GetFloat(data, "aps", "sound", "volume")
|
||||
contentAvailable, _ := jsonparser.GetInt(data, "aps", "content-available")
|
||||
category, _ := jsonparser.GetString(data, "aps", "category")
|
||||
key1 := dat["key1"].(interface{})
|
||||
key2 := dat["key2"].(interface{})
|
||||
aps := dat["aps"].(map[string]interface{})
|
||||
urlArgs := aps["url-args"].([]interface{})
|
||||
|
||||
assert.Equal(t, test, notification.ApnsID)
|
||||
assert.Equal(t, test, notification.Topic)
|
||||
assert.Equal(t, unix, notification.Expiration.Unix())
|
||||
assert.Equal(t, ApnsPriorityLow, notification.Priority)
|
||||
assert.Equal(t, message, alert)
|
||||
assert.Equal(t, expectBadge, int(badge))
|
||||
assert.Equal(t, expectBadge, *req.Badge)
|
||||
assert.Equal(t, test, soundName)
|
||||
assert.Equal(t, 1.0, soundVolume)
|
||||
assert.Equal(t, int64(1), soundCritical)
|
||||
assert.Equal(t, 1, int(contentAvailable))
|
||||
assert.Equal(t, "test", key1)
|
||||
assert.Equal(t, 2, int(key2.(float64)))
|
||||
assert.Equal(t, test, category)
|
||||
assert.Contains(t, urlArgs, "a")
|
||||
assert.Contains(t, urlArgs, "b")
|
||||
}
|
||||
|
||||
func TestIOSSoundAndVolume(t *testing.T) {
|
||||
var dat map[string]interface{}
|
||||
|
||||
test := "test"
|
||||
message := "Welcome notification Server"
|
||||
req := PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
Priority: "normal",
|
||||
Message: message,
|
||||
Sound: Sound{
|
||||
Critical: 3,
|
||||
Name: test,
|
||||
Volume: 4.5,
|
||||
},
|
||||
}
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
|
||||
dump, _ := json.Marshal(notification.Payload)
|
||||
data := []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
alert, _ := jsonparser.GetString(data, "aps", "alert")
|
||||
soundName, _ := jsonparser.GetString(data, "aps", "sound", "name")
|
||||
soundCritical, _ := jsonparser.GetInt(data, "aps", "sound", "critical")
|
||||
soundVolume, _ := jsonparser.GetFloat(data, "aps", "sound", "volume")
|
||||
|
||||
assert.Equal(t, test, notification.ApnsID)
|
||||
assert.Equal(t, test, notification.Topic)
|
||||
assert.Equal(t, ApnsPriorityLow, notification.Priority)
|
||||
assert.Equal(t, message, alert)
|
||||
assert.Equal(t, test, soundName)
|
||||
assert.Equal(t, 4.5, soundVolume)
|
||||
assert.Equal(t, int64(3), soundCritical)
|
||||
|
||||
req.SoundName = "foobar"
|
||||
req.SoundVolume = 5.5
|
||||
notification = GetIOSNotification(req)
|
||||
dump, _ = json.Marshal(notification.Payload)
|
||||
data = []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
soundName, _ = jsonparser.GetString(data, "aps", "sound", "name")
|
||||
soundVolume, _ = jsonparser.GetFloat(data, "aps", "sound", "volume")
|
||||
soundCritical, _ = jsonparser.GetInt(data, "aps", "sound", "critical")
|
||||
assert.Equal(t, 5.5, soundVolume)
|
||||
assert.Equal(t, int64(1), soundCritical)
|
||||
assert.Equal(t, "foobar", soundName)
|
||||
|
||||
req = PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
Priority: "normal",
|
||||
Message: message,
|
||||
Sound: map[string]interface{}{
|
||||
"critical": 3,
|
||||
"name": "test",
|
||||
"volume": 4.5,
|
||||
},
|
||||
}
|
||||
|
||||
notification = GetIOSNotification(req)
|
||||
dump, _ = json.Marshal(notification.Payload)
|
||||
data = []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
soundName, _ = jsonparser.GetString(data, "aps", "sound", "name")
|
||||
soundVolume, _ = jsonparser.GetFloat(data, "aps", "sound", "volume")
|
||||
soundCritical, _ = jsonparser.GetInt(data, "aps", "sound", "critical")
|
||||
assert.Equal(t, 4.5, soundVolume)
|
||||
assert.Equal(t, int64(3), soundCritical)
|
||||
assert.Equal(t, "test", soundName)
|
||||
|
||||
req = PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
Priority: "normal",
|
||||
Message: message,
|
||||
Sound: "default",
|
||||
}
|
||||
|
||||
notification = GetIOSNotification(req)
|
||||
dump, _ = json.Marshal(notification.Payload)
|
||||
data = []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
soundName, _ = jsonparser.GetString(data, "aps", "sound")
|
||||
assert.Equal(t, "default", soundName)
|
||||
}
|
||||
|
||||
func TestIOSSummaryArg(t *testing.T) {
|
||||
var dat map[string]interface{}
|
||||
|
||||
test := "test"
|
||||
message := "Welcome notification Server"
|
||||
req := PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
Priority: "normal",
|
||||
Message: message,
|
||||
Alert: Alert{
|
||||
SummaryArg: "test",
|
||||
SummaryArgCount: 3,
|
||||
},
|
||||
}
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
|
||||
dump, _ := json.Marshal(notification.Payload)
|
||||
data := []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test, notification.ApnsID)
|
||||
assert.Equal(t, test, notification.Topic)
|
||||
assert.Equal(t, ApnsPriorityLow, notification.Priority)
|
||||
assert.Equal(t, "test", dat["aps"].(map[string]interface{})["alert"].(map[string]interface{})["summary-arg"])
|
||||
assert.Equal(t, float64(3), dat["aps"].(map[string]interface{})["alert"].(map[string]interface{})["summary-arg-count"])
|
||||
}
|
||||
|
||||
// Silent Notification which payload’s aps dictionary must not contain the alert, sound, or badge keys.
|
||||
// ref: https://goo.gl/m9xyqG
|
||||
func TestSendZeroValueForBadgeKey(t *testing.T) {
|
||||
var dat map[string]interface{}
|
||||
|
||||
test := "test"
|
||||
message := "Welcome notification Server"
|
||||
req := PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
Priority: "normal",
|
||||
Message: message,
|
||||
Sound: test,
|
||||
ContentAvailable: true,
|
||||
MutableContent: true,
|
||||
ThreadID: test,
|
||||
}
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
|
||||
dump, _ := json.Marshal(notification.Payload)
|
||||
data := []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
log.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
alert, _ := jsonparser.GetString(data, "aps", "alert")
|
||||
badge, _ := jsonparser.GetInt(data, "aps", "badge")
|
||||
sound, _ := jsonparser.GetString(data, "aps", "sound")
|
||||
threadID, _ := jsonparser.GetString(data, "aps", "thread-id")
|
||||
contentAvailable, _ := jsonparser.GetInt(data, "aps", "content-available")
|
||||
mutableContent, _ := jsonparser.GetInt(data, "aps", "mutable-content")
|
||||
|
||||
if req.Badge != nil {
|
||||
t.Errorf("req.Badge must be nil")
|
||||
}
|
||||
|
||||
assert.Equal(t, test, notification.ApnsID)
|
||||
assert.Equal(t, test, notification.Topic)
|
||||
assert.Equal(t, ApnsPriorityLow, notification.Priority)
|
||||
assert.Equal(t, message, alert)
|
||||
assert.Equal(t, 0, int(badge))
|
||||
assert.Equal(t, test, sound)
|
||||
assert.Equal(t, test, threadID)
|
||||
assert.Equal(t, 1, int(contentAvailable))
|
||||
assert.Equal(t, 1, int(mutableContent))
|
||||
|
||||
// Add Bage
|
||||
expectBadge := 10
|
||||
req.Badge = &expectBadge
|
||||
|
||||
notification = GetIOSNotification(req)
|
||||
|
||||
dump, _ = json.Marshal(notification.Payload)
|
||||
data = []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
log.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if req.Badge == nil {
|
||||
t.Errorf("req.Badge must be equal %d", *req.Badge)
|
||||
}
|
||||
|
||||
badge, _ = jsonparser.GetInt(data, "aps", "badge")
|
||||
assert.Equal(t, expectBadge, *req.Badge)
|
||||
assert.Equal(t, expectBadge, int(badge))
|
||||
}
|
||||
|
||||
// Silent Notification:
|
||||
// The payload’s aps dictionary must include the content-available key with a value of 1.
|
||||
// The payload’s aps dictionary must not contain the alert, sound, or badge keys.
|
||||
// ref: https://goo.gl/m9xyqG
|
||||
func TestCheckSilentNotification(t *testing.T) {
|
||||
var dat map[string]interface{}
|
||||
|
||||
test := "test"
|
||||
req := PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
CollapseID: test,
|
||||
Priority: "normal",
|
||||
ContentAvailable: true,
|
||||
}
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
|
||||
dump, _ := json.Marshal(notification.Payload)
|
||||
data := []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
log.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, test, notification.CollapseID)
|
||||
assert.Equal(t, test, notification.ApnsID)
|
||||
assert.Equal(t, test, notification.Topic)
|
||||
assert.Nil(t, dat["aps"].(map[string]interface{})["alert"])
|
||||
assert.Nil(t, dat["aps"].(map[string]interface{})["sound"])
|
||||
assert.Nil(t, dat["aps"].(map[string]interface{})["badge"])
|
||||
}
|
||||
|
||||
// URL: https://goo.gl/5xFo3C
|
||||
// Example 2
|
||||
// {
|
||||
// "aps" : {
|
||||
// "alert" : {
|
||||
// "title" : "Game Request",
|
||||
// "body" : "Bob wants to play poker",
|
||||
// "action-loc-key" : "PLAY"
|
||||
// },
|
||||
// "badge" : 5
|
||||
// },
|
||||
// "acme1" : "bar",
|
||||
// "acme2" : [ "bang", "whiz" ]
|
||||
// }
|
||||
func TestAlertStringExample2ForIos(t *testing.T) {
|
||||
var dat map[string]interface{}
|
||||
|
||||
test := "test"
|
||||
title := "Game Request"
|
||||
body := "Bob wants to play poker"
|
||||
actionLocKey := "PLAY"
|
||||
req := PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
Priority: "normal",
|
||||
Alert: Alert{
|
||||
Title: title,
|
||||
Body: body,
|
||||
ActionLocKey: actionLocKey,
|
||||
},
|
||||
}
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
|
||||
dump, _ := json.Marshal(notification.Payload)
|
||||
data := []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
log.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, title, dat["aps"].(map[string]interface{})["alert"].(map[string]interface{})["title"])
|
||||
assert.Equal(t, body, dat["aps"].(map[string]interface{})["alert"].(map[string]interface{})["body"])
|
||||
assert.Equal(t, actionLocKey, dat["aps"].(map[string]interface{})["alert"].(map[string]interface{})["action-loc-key"])
|
||||
}
|
||||
|
||||
// URL: https://goo.gl/5xFo3C
|
||||
// Example 3
|
||||
// {
|
||||
// "aps" : {
|
||||
// "alert" : "You got your emails.",
|
||||
// "badge" : 9,
|
||||
// "sound" : "bingbong.aiff"
|
||||
// },
|
||||
// "acme1" : "bar",
|
||||
// "acme2" : 42
|
||||
// }
|
||||
func TestAlertStringExample3ForIos(t *testing.T) {
|
||||
var dat map[string]interface{}
|
||||
|
||||
test := "test"
|
||||
badge := 9
|
||||
sound := "bingbong.aiff"
|
||||
req := PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
Priority: "normal",
|
||||
ContentAvailable: true,
|
||||
Message: test,
|
||||
Badge: &badge,
|
||||
Sound: sound,
|
||||
}
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
|
||||
dump, _ := json.Marshal(notification.Payload)
|
||||
data := []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
log.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, sound, dat["aps"].(map[string]interface{})["sound"])
|
||||
assert.Equal(t, float64(badge), dat["aps"].(map[string]interface{})["badge"].(float64))
|
||||
assert.Equal(t, test, dat["aps"].(map[string]interface{})["alert"])
|
||||
}
|
||||
|
||||
func TestMessageAndTitle(t *testing.T) {
|
||||
var dat map[string]interface{}
|
||||
|
||||
test := "test"
|
||||
message := "Welcome notification Server"
|
||||
title := "Welcome notification Server title"
|
||||
req := PushNotification{
|
||||
ApnsID: test,
|
||||
Topic: test,
|
||||
Priority: "normal",
|
||||
Message: message,
|
||||
Title: title,
|
||||
ContentAvailable: true,
|
||||
}
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
|
||||
dump, _ := json.Marshal(notification.Payload)
|
||||
data := []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
log.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
alert, _ := jsonparser.GetString(data, "aps", "alert")
|
||||
alertBody, _ := jsonparser.GetString(data, "aps", "alert", "body")
|
||||
alertTitle, _ := jsonparser.GetString(data, "aps", "alert", "title")
|
||||
|
||||
assert.Equal(t, test, notification.ApnsID)
|
||||
assert.Equal(t, ApnsPriorityLow, notification.Priority)
|
||||
assert.Equal(t, message, alertBody)
|
||||
assert.Equal(t, title, alertTitle)
|
||||
assert.NotEqual(t, message, alert)
|
||||
|
||||
// Add alert body
|
||||
messageOverride := "Welcome notification Server overridden"
|
||||
req.Alert.Body = messageOverride
|
||||
|
||||
notification = GetIOSNotification(req)
|
||||
|
||||
dump, _ = json.Marshal(notification.Payload)
|
||||
data = []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
log.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
alertBodyOverridden, _ := jsonparser.GetString(data, "aps", "alert", "body")
|
||||
alertTitle, _ = jsonparser.GetString(data, "aps", "alert", "title")
|
||||
assert.Equal(t, messageOverride, alertBodyOverridden)
|
||||
assert.NotEqual(t, message, alertBodyOverridden)
|
||||
assert.Equal(t, title, alertTitle)
|
||||
}
|
||||
|
||||
func TestIOSAlertNotificationStructure(t *testing.T) {
|
||||
var dat map[string]interface{}
|
||||
|
||||
test := "test"
|
||||
req := PushNotification{
|
||||
Message: "Welcome",
|
||||
Title: test,
|
||||
Alert: Alert{
|
||||
Action: test,
|
||||
ActionLocKey: test,
|
||||
Body: test,
|
||||
LaunchImage: test,
|
||||
LocArgs: []string{"a", "b"},
|
||||
LocKey: test,
|
||||
Subtitle: test,
|
||||
TitleLocArgs: []string{"a", "b"},
|
||||
TitleLocKey: test,
|
||||
},
|
||||
}
|
||||
|
||||
notification := GetIOSNotification(req)
|
||||
|
||||
dump, _ := json.Marshal(notification.Payload)
|
||||
data := []byte(string(dump))
|
||||
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
log.Println(err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
action, _ := jsonparser.GetString(data, "aps", "alert", "action")
|
||||
actionLocKey, _ := jsonparser.GetString(data, "aps", "alert", "action-loc-key")
|
||||
body, _ := jsonparser.GetString(data, "aps", "alert", "body")
|
||||
launchImage, _ := jsonparser.GetString(data, "aps", "alert", "launch-image")
|
||||
locKey, _ := jsonparser.GetString(data, "aps", "alert", "loc-key")
|
||||
title, _ := jsonparser.GetString(data, "aps", "alert", "title")
|
||||
subtitle, _ := jsonparser.GetString(data, "aps", "alert", "subtitle")
|
||||
titleLocKey, _ := jsonparser.GetString(data, "aps", "alert", "title-loc-key")
|
||||
aps := dat["aps"].(map[string]interface{})
|
||||
alert := aps["alert"].(map[string]interface{})
|
||||
titleLocArgs := alert["title-loc-args"].([]interface{})
|
||||
locArgs := alert["loc-args"].([]interface{})
|
||||
|
||||
assert.Equal(t, test, action)
|
||||
assert.Equal(t, test, actionLocKey)
|
||||
assert.Equal(t, test, body)
|
||||
assert.Equal(t, test, launchImage)
|
||||
assert.Equal(t, test, locKey)
|
||||
assert.Equal(t, test, title)
|
||||
assert.Equal(t, test, subtitle)
|
||||
assert.Equal(t, test, titleLocKey)
|
||||
assert.Contains(t, titleLocArgs, "a")
|
||||
assert.Contains(t, titleLocArgs, "b")
|
||||
assert.Contains(t, locArgs, "a")
|
||||
assert.Contains(t, locArgs, "b")
|
||||
}
|
||||
|
||||
func TestWrongIosCertificateExt(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = "test"
|
||||
err := InitAPNSClient(cfg)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "wrong certificate key extension", err.Error())
|
||||
|
||||
cfg.Ios.KeyPath = ""
|
||||
cfg.Ios.KeyBase64 = "abcd"
|
||||
cfg.Ios.KeyType = "abcd"
|
||||
err = InitAPNSClient(cfg)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "wrong certificate key type", err.Error())
|
||||
}
|
||||
|
||||
func TestAPNSClientDevHost(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = "../certificate/certificate-valid.p12"
|
||||
err := InitAPNSClient(cfg)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)
|
||||
|
||||
cfg.Ios.KeyPath = ""
|
||||
cfg.Ios.KeyBase64 = certificateValidP12
|
||||
cfg.Ios.KeyType = "p12"
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)
|
||||
}
|
||||
|
||||
func TestAPNSClientProdHost(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.Production = true
|
||||
cfg.Ios.KeyPath = "../certificate/certificate-valid.pem"
|
||||
err := InitAPNSClient(cfg)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, apns2.HostProduction, ApnsClient.Host)
|
||||
|
||||
cfg.Ios.KeyPath = ""
|
||||
cfg.Ios.KeyBase64 = certificateValidPEM
|
||||
cfg.Ios.KeyType = "pem"
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, apns2.HostProduction, ApnsClient.Host)
|
||||
}
|
||||
|
||||
func TestAPNSClientInvaildToken(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = "../certificate/authkey-invalid.p8"
|
||||
err := InitAPNSClient(cfg)
|
||||
assert.Error(t, err)
|
||||
|
||||
cfg.Ios.KeyPath = ""
|
||||
cfg.Ios.KeyBase64 = authkeyInvalidP8
|
||||
cfg.Ios.KeyType = "p8"
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.Error(t, err)
|
||||
|
||||
// empty key-id or team-id
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = "../certificate/authkey-valid.p8"
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.Error(t, err)
|
||||
|
||||
cfg.Ios.KeyID = "key-id"
|
||||
cfg.Ios.TeamID = ""
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.Error(t, err)
|
||||
|
||||
cfg.Ios.KeyID = ""
|
||||
cfg.Ios.TeamID = "team-id"
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAPNSClientVaildToken(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = "../certificate/authkey-valid.p8"
|
||||
cfg.Ios.KeyID = "key-id"
|
||||
cfg.Ios.TeamID = "team-id"
|
||||
err := InitAPNSClient(cfg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)
|
||||
|
||||
cfg.Ios.Production = true
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, apns2.HostProduction, ApnsClient.Host)
|
||||
|
||||
// test base64
|
||||
cfg.Ios.Production = false
|
||||
cfg.Ios.KeyPath = ""
|
||||
cfg.Ios.KeyBase64 = authkeyValidP8
|
||||
cfg.Ios.KeyType = "p8"
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)
|
||||
|
||||
cfg.Ios.Production = true
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, apns2.HostProduction, ApnsClient.Host)
|
||||
}
|
||||
|
||||
func TestAPNSClientUseProxy(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = "../certificate/certificate-valid.p12"
|
||||
cfg.Core.HTTPProxy = "http://127.0.0.1:8080"
|
||||
_ = SetProxy(cfg.Core.HTTPProxy)
|
||||
err := InitAPNSClient(cfg)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)
|
||||
|
||||
req, _ := http.NewRequest("GET", apns2.HostDevelopment, nil)
|
||||
actualProxyURL, err := ApnsClient.HTTPClient.Transport.(*http.Transport).Proxy(req)
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedProxyURL, _ := url.ParseRequestURI(cfg.Core.HTTPProxy)
|
||||
assert.Equal(t, expectedProxyURL, actualProxyURL)
|
||||
|
||||
cfg.Ios.KeyPath = "../certificate/authkey-valid.p8"
|
||||
cfg.Ios.TeamID = "example.team"
|
||||
cfg.Ios.KeyID = "example.key"
|
||||
err = InitAPNSClient(cfg)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, apns2.HostDevelopment, ApnsClient.Host)
|
||||
assert.NotNil(t, ApnsClient.Token)
|
||||
|
||||
req, _ = http.NewRequest("GET", apns2.HostDevelopment, nil)
|
||||
actualProxyURL, err = ApnsClient.HTTPClient.Transport.(*http.Transport).Proxy(req)
|
||||
assert.Nil(t, err)
|
||||
|
||||
expectedProxyURL, _ = url.ParseRequestURI(cfg.Core.HTTPProxy)
|
||||
assert.Equal(t, expectedProxyURL, actualProxyURL)
|
||||
|
||||
http.DefaultTransport.(*http.Transport).Proxy = nil
|
||||
}
|
||||
|
||||
func TestPushToIOS(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
MaxConcurrentIOSPushes = make(chan struct{}, cfg.Ios.MaxConcurrentPushes)
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = "../certificate/certificate-valid.pem"
|
||||
err := InitAPNSClient(cfg)
|
||||
assert.Nil(t, err)
|
||||
err = status.InitAppStatus(cfg)
|
||||
assert.Nil(t, err)
|
||||
|
||||
req := PushNotification{
|
||||
Cfg: cfg,
|
||||
Tokens: []string{"11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef7", "11aa01229f15f0f0c52029d8cf8cd0aeaf2365fe4cebc4af26cd6d76b7919ef1"},
|
||||
Platform: 1,
|
||||
Message: "Welcome",
|
||||
}
|
||||
|
||||
// send fail
|
||||
PushToIOS(req)
|
||||
}
|
||||
|
||||
func TestApnsHostFromRequest(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = "../certificate/certificate-valid.pem"
|
||||
err := InitAPNSClient(cfg)
|
||||
assert.Nil(t, err)
|
||||
err = status.InitAppStatus(cfg)
|
||||
assert.Nil(t, err)
|
||||
|
||||
req := PushNotification{
|
||||
Production: true,
|
||||
}
|
||||
client := getApnsClient(cfg, req)
|
||||
assert.Equal(t, apns2.HostProduction, client.Host)
|
||||
|
||||
req = PushNotification{
|
||||
Development: true,
|
||||
}
|
||||
client = getApnsClient(cfg, req)
|
||||
assert.Equal(t, apns2.HostDevelopment, client.Host)
|
||||
|
||||
req = PushNotification{}
|
||||
cfg.Ios.Production = true
|
||||
client = getApnsClient(cfg, req)
|
||||
assert.Equal(t, apns2.HostProduction, client.Host)
|
||||
|
||||
cfg.Ios.Production = false
|
||||
client = getApnsClient(cfg, req)
|
||||
assert.Equal(t, apns2.HostDevelopment, client.Host)
|
||||
}
|
||||
288
notify/notification_fcm.go
Normal file
288
notify/notification_fcm.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/appleboy/gorush/core"
|
||||
"github.com/appleboy/gorush/logx"
|
||||
"github.com/appleboy/gorush/status"
|
||||
|
||||
"github.com/appleboy/go-fcm"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// InitFCMClient use for initialize FCM Client.
|
||||
func InitFCMClient(cfg config.ConfYaml, key string) (*fcm.Client, error) {
|
||||
var err error
|
||||
|
||||
if key == "" && cfg.Android.APIKey == "" {
|
||||
return nil, errors.New("Missing Android API Key")
|
||||
}
|
||||
|
||||
if key != "" && key != cfg.Android.APIKey {
|
||||
return fcm.NewClient(key)
|
||||
}
|
||||
|
||||
if FCMClient == nil {
|
||||
FCMClient, err = fcm.NewClient(cfg.Android.APIKey)
|
||||
return FCMClient, err
|
||||
}
|
||||
|
||||
return FCMClient, nil
|
||||
}
|
||||
|
||||
// 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,
|
||||
Condition: req.Condition,
|
||||
CollapseKey: req.CollapseKey,
|
||||
ContentAvailable: req.ContentAvailable,
|
||||
MutableContent: req.MutableContent,
|
||||
DelayWhileIdle: req.DelayWhileIdle,
|
||||
TimeToLive: req.TimeToLive,
|
||||
RestrictedPackageName: req.RestrictedPackageName,
|
||||
DryRun: req.DryRun,
|
||||
}
|
||||
|
||||
if len(req.Tokens) > 0 {
|
||||
notification.RegistrationIDs = req.Tokens
|
||||
}
|
||||
|
||||
if req.Priority == "high" || req.Priority == "normal" {
|
||||
notification.Priority = req.Priority
|
||||
}
|
||||
|
||||
// Add another field
|
||||
if len(req.Data) > 0 {
|
||||
notification.Data = make(map[string]interface{})
|
||||
for k, v := range req.Data {
|
||||
notification.Data[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
n := &fcm.Notification{}
|
||||
isNotificationSet := false
|
||||
if req.Notification != nil {
|
||||
isNotificationSet = true
|
||||
n = req.Notification
|
||||
}
|
||||
|
||||
if len(req.Message) > 0 {
|
||||
isNotificationSet = true
|
||||
n.Body = req.Message
|
||||
}
|
||||
|
||||
if len(req.Title) > 0 {
|
||||
isNotificationSet = true
|
||||
n.Title = req.Title
|
||||
}
|
||||
|
||||
if len(req.Image) > 0 {
|
||||
isNotificationSet = true
|
||||
n.Image = req.Image
|
||||
}
|
||||
|
||||
if v, ok := req.Sound.(string); ok && len(v) > 0 {
|
||||
isNotificationSet = true
|
||||
n.Sound = v
|
||||
}
|
||||
|
||||
if isNotificationSet {
|
||||
notification.Notification = n
|
||||
}
|
||||
|
||||
// handle iOS apns in fcm
|
||||
|
||||
if len(req.Apns) > 0 {
|
||||
notification.Apns = req.Apns
|
||||
}
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
// PushToAndroid provide send notification to Android server.
|
||||
func PushToAndroid(req PushNotification) {
|
||||
logx.LogAccess.Debug("Start push notification for Android")
|
||||
|
||||
if req.Cfg.Core.Sync && !core.IsLocalQueue(core.Queue(req.Cfg.Queue.Engine)) {
|
||||
req.Cfg.Core.Sync = false
|
||||
}
|
||||
|
||||
var (
|
||||
client *fcm.Client
|
||||
retryCount = 0
|
||||
maxRetry = req.Cfg.Android.MaxRetry
|
||||
)
|
||||
|
||||
if req.Retry > 0 && req.Retry < maxRetry {
|
||||
maxRetry = req.Retry
|
||||
}
|
||||
|
||||
// check message
|
||||
err := CheckMessage(req)
|
||||
if err != nil {
|
||||
logx.LogError.Error("request error: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Retry:
|
||||
notification := GetAndroidNotification(req)
|
||||
|
||||
if req.APIKey != "" {
|
||||
client, err = InitFCMClient(req.Cfg, req.APIKey)
|
||||
} else {
|
||||
client, err = InitFCMClient(req.Cfg, req.Cfg.Android.APIKey)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// FCM server error
|
||||
logx.LogError.Error("FCM server error: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
res, err := client.Send(notification)
|
||||
if err != nil {
|
||||
// Send Message error
|
||||
logx.LogError.Error("FCM server send message error: " + err.Error())
|
||||
|
||||
if req.IsTopic() {
|
||||
if req.Cfg.Core.Sync {
|
||||
req.AddLog(createLogPushEntry(req.Cfg, core.FailedPush, req.To, req, err))
|
||||
} else if req.Cfg.Core.FeedbackURL != "" {
|
||||
go func(logger *logrus.Logger, log logx.LogPushEntry, url string, timeout int64) {
|
||||
err := DispatchFeedback(log, url, timeout)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}(logx.LogError, createLogPushEntry(req.Cfg, core.FailedPush, req.To, req, err), req.Cfg.Core.FeedbackURL, req.Cfg.Core.FeedbackTimeout)
|
||||
}
|
||||
status.StatStorage.AddAndroidError(1)
|
||||
} else {
|
||||
for _, token := range req.Tokens {
|
||||
if req.Cfg.Core.Sync {
|
||||
req.AddLog(createLogPushEntry(req.Cfg, core.FailedPush, token, req, err))
|
||||
} else if req.Cfg.Core.FeedbackURL != "" {
|
||||
go func(logger *logrus.Logger, log logx.LogPushEntry, url string, timeout int64) {
|
||||
err := DispatchFeedback(log, url, timeout)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}(logx.LogError, createLogPushEntry(req.Cfg, core.FailedPush, token, req, err), req.Cfg.Core.FeedbackURL, req.Cfg.Core.FeedbackTimeout)
|
||||
}
|
||||
}
|
||||
status.StatStorage.AddAndroidError(int64(len(req.Tokens)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !req.IsTopic() {
|
||||
logx.LogAccess.Debug(fmt.Sprintf("Android Success count: %d, Failure count: %d", res.Success, res.Failure))
|
||||
}
|
||||
|
||||
status.StatStorage.AddAndroidSuccess(int64(res.Success))
|
||||
status.StatStorage.AddAndroidError(int64(res.Failure))
|
||||
|
||||
var newTokens []string
|
||||
// result from Send messages to specific devices
|
||||
for k, result := range res.Results {
|
||||
to := ""
|
||||
if k < len(req.Tokens) {
|
||||
to = req.Tokens[k]
|
||||
} else {
|
||||
to = req.To
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
// We should retry only "retryable" statuses. More info about response:
|
||||
// https://firebase.google.com/docs/cloud-messaging/http-server-ref#downstream-http-messages-plain-text
|
||||
if !result.Unregistered() {
|
||||
newTokens = append(newTokens, to)
|
||||
}
|
||||
|
||||
logPush(req.Cfg, core.FailedPush, to, req, result.Error)
|
||||
if req.Cfg.Core.Sync {
|
||||
req.AddLog(createLogPushEntry(req.Cfg, core.FailedPush, to, req, result.Error))
|
||||
} else if req.Cfg.Core.FeedbackURL != "" {
|
||||
go func(logger *logrus.Logger, log logx.LogPushEntry, url string, timeout int64) {
|
||||
err := DispatchFeedback(log, url, timeout)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}(logx.LogError, createLogPushEntry(req.Cfg, core.FailedPush, to, req, result.Error), req.Cfg.Core.FeedbackURL, req.Cfg.Core.FeedbackTimeout)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
logPush(req.Cfg, core.SucceededPush, to, req, nil)
|
||||
}
|
||||
|
||||
// result from Send messages to topics
|
||||
if req.IsTopic() {
|
||||
to := ""
|
||||
if req.To != "" {
|
||||
to = req.To
|
||||
} else {
|
||||
to = req.Condition
|
||||
}
|
||||
logx.LogAccess.Debug("Send Topic Message: ", to)
|
||||
// Success
|
||||
if res.MessageID != 0 {
|
||||
logPush(req.Cfg, core.SucceededPush, to, req, nil)
|
||||
} else {
|
||||
// failure
|
||||
logPush(req.Cfg, core.FailedPush, to, req, res.Error)
|
||||
if req.Cfg.Core.Sync {
|
||||
req.AddLog(createLogPushEntry(req.Cfg, core.FailedPush, to, req, res.Error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Device Group HTTP Response
|
||||
if len(res.FailedRegistrationIDs) > 0 {
|
||||
newTokens = append(newTokens, res.FailedRegistrationIDs...)
|
||||
|
||||
logPush(req.Cfg, core.FailedPush, notification.To, req, errors.New("device group: partial success or all fails"))
|
||||
if req.Cfg.Core.Sync {
|
||||
req.AddLog(createLogPushEntry(req.Cfg, core.FailedPush, notification.To, req, errors.New("device group: partial success or all fails")))
|
||||
}
|
||||
}
|
||||
|
||||
if len(newTokens) > 0 && retryCount < maxRetry {
|
||||
retryCount++
|
||||
|
||||
// resend fail token
|
||||
req.Tokens = newTokens
|
||||
goto Retry
|
||||
}
|
||||
}
|
||||
|
||||
func createLogPushEntry(cfg config.ConfYaml, status, token string, req PushNotification, err error) logx.LogPushEntry {
|
||||
return logx.GetLogPushEntry(&logx.InputLog{
|
||||
ID: req.ID,
|
||||
Status: status,
|
||||
Token: token,
|
||||
Message: req.Message,
|
||||
Platform: req.Platform,
|
||||
Error: err,
|
||||
HideToken: cfg.Log.HideToken,
|
||||
Format: cfg.Log.Format,
|
||||
})
|
||||
}
|
||||
|
||||
func logPush(cfg config.ConfYaml, status, token string, req PushNotification, err error) {
|
||||
logx.LogPush(&logx.InputLog{
|
||||
ID: req.ID,
|
||||
Status: status,
|
||||
Token: token,
|
||||
Message: req.Message,
|
||||
Platform: req.Platform,
|
||||
Error: err,
|
||||
HideToken: cfg.Log.HideToken,
|
||||
Format: cfg.Log.Format,
|
||||
})
|
||||
}
|
||||
271
notify/notification_fcm_test.go
Normal file
271
notify/notification_fcm_test.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/appleboy/gorush/core"
|
||||
"github.com/appleboy/gorush/logx"
|
||||
|
||||
"github.com/appleboy/go-fcm"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMissingAndroidAPIKey(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Android.Enabled = true
|
||||
cfg.Android.APIKey = ""
|
||||
|
||||
err := CheckPushConf(cfg)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Missing Android API Key", err.Error())
|
||||
}
|
||||
|
||||
func TestMissingKeyForInitFCMClient(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
cfg.Android.APIKey = ""
|
||||
client, err := InitFCMClient(cfg, "")
|
||||
|
||||
assert.Nil(t, client)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Missing Android API Key", err.Error())
|
||||
}
|
||||
|
||||
func TestPushToAndroidWrongToken(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Android.Enabled = true
|
||||
cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY")
|
||||
|
||||
req := PushNotification{
|
||||
Cfg: cfg,
|
||||
Tokens: []string{"aaaaaa", "bbbbb"},
|
||||
Platform: core.PlatFormAndroid,
|
||||
Message: "Welcome",
|
||||
}
|
||||
|
||||
// Android Success count: 0, Failure count: 2
|
||||
PushToAndroid(req)
|
||||
}
|
||||
|
||||
func TestPushToAndroidRightTokenForJSONLog(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Android.Enabled = true
|
||||
cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY")
|
||||
// log for json
|
||||
cfg.Log.Format = "json"
|
||||
|
||||
androidToken := os.Getenv("ANDROID_TEST_TOKEN")
|
||||
|
||||
req := PushNotification{
|
||||
Cfg: cfg,
|
||||
Tokens: []string{androidToken},
|
||||
Platform: core.PlatFormAndroid,
|
||||
Message: "Welcome",
|
||||
}
|
||||
|
||||
PushToAndroid(req)
|
||||
}
|
||||
|
||||
func TestPushToAndroidRightTokenForStringLog(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Android.Enabled = true
|
||||
cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY")
|
||||
|
||||
androidToken := os.Getenv("ANDROID_TEST_TOKEN")
|
||||
|
||||
req := PushNotification{
|
||||
Cfg: cfg,
|
||||
Tokens: []string{androidToken},
|
||||
Platform: core.PlatFormAndroid,
|
||||
Message: "Welcome",
|
||||
}
|
||||
|
||||
PushToAndroid(req)
|
||||
}
|
||||
|
||||
func TestOverwriteAndroidAPIKey(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Core.Sync = true
|
||||
cfg.Android.Enabled = true
|
||||
cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY")
|
||||
|
||||
androidToken := os.Getenv("ANDROID_TEST_TOKEN")
|
||||
|
||||
req := PushNotification{
|
||||
Cfg: cfg,
|
||||
Tokens: []string{androidToken, "bbbbb"},
|
||||
Platform: core.PlatFormAndroid,
|
||||
Message: "Welcome",
|
||||
// overwrite android api key
|
||||
APIKey: "1234",
|
||||
|
||||
Log: &[]logx.LogPushEntry{},
|
||||
}
|
||||
|
||||
// FCM server error: 401 error: 401 Unauthorized (Wrong API Key)
|
||||
PushToAndroid(req)
|
||||
|
||||
assert.Len(t, *req.Log, 2)
|
||||
}
|
||||
|
||||
func TestFCMMessage(t *testing.T) {
|
||||
var req PushNotification
|
||||
var err error
|
||||
|
||||
// the message must specify at least one registration ID
|
||||
req = PushNotification{
|
||||
Message: "Test",
|
||||
Tokens: []string{},
|
||||
}
|
||||
|
||||
err = CheckMessage(req)
|
||||
assert.Error(t, err)
|
||||
|
||||
// the token must not be empty
|
||||
req = PushNotification{
|
||||
Message: "Test",
|
||||
Tokens: []string{""},
|
||||
}
|
||||
|
||||
err = CheckMessage(req)
|
||||
assert.Error(t, err)
|
||||
|
||||
// ignore check token length if send topic message
|
||||
req = PushNotification{
|
||||
Message: "Test",
|
||||
Platform: core.PlatFormAndroid,
|
||||
To: "/topics/foo-bar",
|
||||
}
|
||||
|
||||
err = CheckMessage(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// "condition": "'dogs' in topics || 'cats' in topics",
|
||||
req = PushNotification{
|
||||
Message: "Test",
|
||||
Platform: core.PlatFormAndroid,
|
||||
Condition: "'dogs' in topics || 'cats' in topics",
|
||||
}
|
||||
|
||||
err = CheckMessage(req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// the message may specify at most 1000 registration IDs
|
||||
req = PushNotification{
|
||||
Message: "Test",
|
||||
Platform: core.PlatFormAndroid,
|
||||
Tokens: make([]string, 1001),
|
||||
}
|
||||
|
||||
err = CheckMessage(req)
|
||||
assert.Error(t, err)
|
||||
|
||||
// the message's TimeToLive field must be an integer
|
||||
// between 0 and 2419200 (4 weeks)
|
||||
timeToLive := uint(2419201)
|
||||
req = PushNotification{
|
||||
Message: "Test",
|
||||
Platform: core.PlatFormAndroid,
|
||||
Tokens: []string{"XXXXXXXXX"},
|
||||
TimeToLive: &timeToLive,
|
||||
}
|
||||
|
||||
err = CheckMessage(req)
|
||||
assert.Error(t, err)
|
||||
|
||||
// Pass
|
||||
timeToLive = uint(86400)
|
||||
req = PushNotification{
|
||||
Message: "Test",
|
||||
Platform: core.PlatFormAndroid,
|
||||
Tokens: []string{"XXXXXXXXX"},
|
||||
TimeToLive: &timeToLive,
|
||||
}
|
||||
|
||||
err = CheckMessage(req)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCheckAndroidMessage(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Android.Enabled = true
|
||||
cfg.Android.APIKey = os.Getenv("ANDROID_API_KEY")
|
||||
|
||||
timeToLive := uint(2419201)
|
||||
req := PushNotification{
|
||||
Cfg: cfg,
|
||||
Tokens: []string{"aaaaaa", "bbbbb"},
|
||||
Platform: core.PlatFormAndroid,
|
||||
Message: "Welcome",
|
||||
TimeToLive: &timeToLive,
|
||||
}
|
||||
|
||||
PushToAndroid(req)
|
||||
}
|
||||
|
||||
func TestAndroidNotificationStructure(t *testing.T) {
|
||||
test := "test"
|
||||
timeToLive := uint(100)
|
||||
req := PushNotification{
|
||||
Tokens: []string{"a", "b"},
|
||||
Message: "Welcome",
|
||||
To: test,
|
||||
Priority: "high",
|
||||
CollapseKey: "1",
|
||||
ContentAvailable: true,
|
||||
DelayWhileIdle: true,
|
||||
TimeToLive: &timeToLive,
|
||||
RestrictedPackageName: test,
|
||||
DryRun: true,
|
||||
Title: test,
|
||||
Sound: test,
|
||||
Data: D{
|
||||
"a": "1",
|
||||
"b": 2,
|
||||
},
|
||||
Notification: &fcm.Notification{
|
||||
Color: test,
|
||||
Tag: test,
|
||||
Body: "",
|
||||
},
|
||||
}
|
||||
|
||||
notification := GetAndroidNotification(req)
|
||||
|
||||
assert.Equal(t, test, notification.To)
|
||||
assert.Equal(t, "high", notification.Priority)
|
||||
assert.Equal(t, "1", notification.CollapseKey)
|
||||
assert.True(t, notification.ContentAvailable)
|
||||
assert.True(t, notification.DelayWhileIdle)
|
||||
assert.Equal(t, uint(100), *notification.TimeToLive)
|
||||
assert.Equal(t, test, notification.RestrictedPackageName)
|
||||
assert.True(t, notification.DryRun)
|
||||
assert.Equal(t, test, notification.Notification.Title)
|
||||
assert.Equal(t, test, notification.Notification.Sound)
|
||||
assert.Equal(t, test, notification.Notification.Color)
|
||||
assert.Equal(t, test, notification.Notification.Tag)
|
||||
assert.Equal(t, "Welcome", notification.Notification.Body)
|
||||
assert.Equal(t, "1", notification.Data["a"])
|
||||
assert.Equal(t, 2, notification.Data["b"])
|
||||
|
||||
// test empty body
|
||||
req = PushNotification{
|
||||
Tokens: []string{"a", "b"},
|
||||
To: test,
|
||||
Notification: &fcm.Notification{
|
||||
Body: "",
|
||||
},
|
||||
}
|
||||
notification = GetAndroidNotification(req)
|
||||
|
||||
assert.Equal(t, test, notification.To)
|
||||
assert.Equal(t, "", notification.Notification.Body)
|
||||
}
|
||||
231
notify/notification_hms.go
Normal file
231
notify/notification_hms.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/appleboy/gorush/core"
|
||||
"github.com/appleboy/gorush/logx"
|
||||
"github.com/appleboy/gorush/status"
|
||||
|
||||
c "github.com/msalihkarakasli/go-hms-push/push/config"
|
||||
client "github.com/msalihkarakasli/go-hms-push/push/core"
|
||||
"github.com/msalihkarakasli/go-hms-push/push/model"
|
||||
)
|
||||
|
||||
var (
|
||||
pushError error
|
||||
pushClient *client.HMSClient
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// GetPushClient use for create HMS Push
|
||||
func GetPushClient(conf *c.Config) (*client.HMSClient, error) {
|
||||
once.Do(func() {
|
||||
client, err := client.NewHttpClient(conf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pushClient = client
|
||||
pushError = err
|
||||
})
|
||||
|
||||
return pushClient, pushError
|
||||
}
|
||||
|
||||
// InitHMSClient use for initialize HMS Client.
|
||||
func InitHMSClient(cfg config.ConfYaml, appSecret, appID string) (*client.HMSClient, error) {
|
||||
if appSecret == "" {
|
||||
return nil, errors.New("Missing Huawei App Secret")
|
||||
}
|
||||
|
||||
if appID == "" {
|
||||
return nil, errors.New("Missing Huawei App ID")
|
||||
}
|
||||
|
||||
conf := &c.Config{
|
||||
AppId: appID,
|
||||
AppSecret: appSecret,
|
||||
AuthUrl: "https://oauth-login.cloud.huawei.com/oauth2/v3/token",
|
||||
PushUrl: "https://push-api.cloud.huawei.com",
|
||||
}
|
||||
|
||||
if appSecret != cfg.Huawei.AppSecret || appID != cfg.Huawei.AppID {
|
||||
return GetPushClient(conf)
|
||||
}
|
||||
|
||||
if HMSClient == nil {
|
||||
return GetPushClient(conf)
|
||||
}
|
||||
|
||||
return HMSClient, nil
|
||||
}
|
||||
|
||||
// GetHuaweiNotification use for define HMS notification.
|
||||
// HTTP Connection Server Reference for HMS
|
||||
// https://developer.huawei.com/consumer/en/doc/development/HMS-References/push-sendapi
|
||||
func GetHuaweiNotification(req PushNotification) (*model.MessageRequest, error) {
|
||||
msgRequest := model.NewNotificationMsgRequest()
|
||||
|
||||
msgRequest.Message.Android = model.GetDefaultAndroid()
|
||||
|
||||
if len(req.Tokens) > 0 {
|
||||
msgRequest.Message.Token = req.Tokens
|
||||
}
|
||||
|
||||
if len(req.Topic) > 0 {
|
||||
msgRequest.Message.Topic = req.Topic
|
||||
}
|
||||
|
||||
if len(req.To) > 0 {
|
||||
msgRequest.Message.Topic = req.To
|
||||
}
|
||||
|
||||
if len(req.Condition) > 0 {
|
||||
msgRequest.Message.Condition = req.Condition
|
||||
}
|
||||
|
||||
if req.Priority == "high" {
|
||||
msgRequest.Message.Android.Urgency = "HIGH"
|
||||
}
|
||||
|
||||
// if req.HuaweiCollapseKey != nil {
|
||||
msgRequest.Message.Android.CollapseKey = req.HuaweiCollapseKey
|
||||
//}
|
||||
|
||||
if len(req.Category) > 0 {
|
||||
msgRequest.Message.Android.Category = req.Category
|
||||
}
|
||||
|
||||
if len(req.HuaweiTTL) > 0 {
|
||||
msgRequest.Message.Android.TTL = req.HuaweiTTL
|
||||
}
|
||||
|
||||
if len(req.BiTag) > 0 {
|
||||
msgRequest.Message.Android.BiTag = req.BiTag
|
||||
}
|
||||
|
||||
msgRequest.Message.Android.FastAppTarget = req.FastAppTarget
|
||||
|
||||
// Add data fields
|
||||
if len(req.HuaweiData) > 0 {
|
||||
msgRequest.Message.Data = req.HuaweiData
|
||||
} else {
|
||||
// Notification Message
|
||||
msgRequest.Message.Android.Notification = model.GetDefaultAndroidNotification()
|
||||
|
||||
n := msgRequest.Message.Android.Notification
|
||||
isNotificationSet := false
|
||||
|
||||
if req.HuaweiNotification != nil {
|
||||
isNotificationSet = true
|
||||
n = req.HuaweiNotification
|
||||
|
||||
if n.ClickAction == nil {
|
||||
n.ClickAction = model.GetDefaultClickAction()
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Message) > 0 {
|
||||
isNotificationSet = true
|
||||
n.Body = req.Message
|
||||
}
|
||||
|
||||
if len(req.Title) > 0 {
|
||||
isNotificationSet = true
|
||||
n.Title = req.Title
|
||||
}
|
||||
|
||||
if len(req.Image) > 0 {
|
||||
isNotificationSet = true
|
||||
n.Image = req.Image
|
||||
}
|
||||
|
||||
if v, ok := req.Sound.(string); ok && len(v) > 0 {
|
||||
isNotificationSet = true
|
||||
n.Sound = v
|
||||
} else {
|
||||
n.DefaultSound = true
|
||||
}
|
||||
|
||||
if isNotificationSet {
|
||||
msgRequest.Message.Android.Notification = n
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(msgRequest)
|
||||
if err != nil {
|
||||
logx.LogError.Error("Failed to marshal the default message! Error is " + err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logx.LogAccess.Debugf("Default message is %s", string(b))
|
||||
return msgRequest, nil
|
||||
}
|
||||
|
||||
// PushToHuawei provide send notification to Android server.
|
||||
func PushToHuawei(req PushNotification) bool {
|
||||
logx.LogAccess.Debug("Start push notification for Huawei")
|
||||
|
||||
if req.Cfg.Core.Sync && !core.IsLocalQueue(core.Queue(req.Cfg.Queue.Engine)) {
|
||||
req.Cfg.Core.Sync = false
|
||||
}
|
||||
|
||||
var (
|
||||
client *client.HMSClient
|
||||
retryCount = 0
|
||||
maxRetry = req.Cfg.Huawei.MaxRetry
|
||||
)
|
||||
|
||||
if req.Retry > 0 && req.Retry < maxRetry {
|
||||
maxRetry = req.Retry
|
||||
}
|
||||
|
||||
// check message
|
||||
err := CheckMessage(req)
|
||||
if err != nil {
|
||||
logx.LogError.Error("request error: " + err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
Retry:
|
||||
isError := false
|
||||
|
||||
notification, _ := GetHuaweiNotification(req)
|
||||
|
||||
client, err = InitHMSClient(req.Cfg, req.Cfg.Huawei.AppSecret, req.Cfg.Huawei.AppID)
|
||||
|
||||
if err != nil {
|
||||
// HMS server error
|
||||
logx.LogError.Error("HMS server error: " + err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
res, err := client.SendMessage(context.Background(), notification)
|
||||
if err != nil {
|
||||
// Send Message error
|
||||
logx.LogError.Error("HMS server send message error: " + err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
// Huawei Push Send API does not support exact results for each token
|
||||
if res.Code == "80000000" {
|
||||
status.StatStorage.AddHuaweiSuccess(int64(1))
|
||||
logx.LogAccess.Debug("Huwaei Send Notification is completed successfully!")
|
||||
} else {
|
||||
isError = true
|
||||
status.StatStorage.AddHuaweiError(int64(1))
|
||||
logx.LogAccess.Debug("Huawei Send Notification is failed! Code: " + res.Code)
|
||||
}
|
||||
|
||||
if isError && retryCount < maxRetry {
|
||||
retryCount++
|
||||
|
||||
// resend all tokens
|
||||
goto Retry
|
||||
}
|
||||
|
||||
return isError
|
||||
}
|
||||
50
notify/notification_hms_test.go
Normal file
50
notify/notification_hms_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMissingHuaweiAppSecret(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Huawei.Enabled = true
|
||||
cfg.Huawei.AppSecret = ""
|
||||
|
||||
err := CheckPushConf(cfg)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Missing Huawei App Secret", err.Error())
|
||||
}
|
||||
|
||||
func TestMissingHuaweiAppID(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Huawei.Enabled = true
|
||||
cfg.Huawei.AppID = ""
|
||||
|
||||
err := CheckPushConf(cfg)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Missing Huawei App ID", err.Error())
|
||||
}
|
||||
|
||||
func TestMissingAppSecretForInitHMSClient(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
client, err := InitHMSClient(cfg, "", "APP_SECRET")
|
||||
|
||||
assert.Nil(t, client)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Missing Huawei App Secret", err.Error())
|
||||
}
|
||||
|
||||
func TestMissingAppIDForInitHMSClient(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
client, err := InitHMSClient(cfg, "APP_ID", "")
|
||||
|
||||
assert.Nil(t, client)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Missing Huawei App ID", err.Error())
|
||||
}
|
||||
34
notify/notification_test.go
Normal file
34
notify/notification_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/appleboy/gorush/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCorrectConf(t *testing.T) {
|
||||
cfg, _ := config.LoadConf()
|
||||
|
||||
cfg.Android.Enabled = true
|
||||
cfg.Android.APIKey = "xxxxx"
|
||||
|
||||
cfg.Ios.Enabled = true
|
||||
cfg.Ios.KeyPath = "../certificate/certificate-valid.pem"
|
||||
|
||||
err := CheckPushConf(cfg)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSetProxyURL(t *testing.T) {
|
||||
err := SetProxy("87.236.233.92:8080")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "parse \"87.236.233.92:8080\": invalid URI for request", err.Error())
|
||||
|
||||
err = SetProxy("a.html")
|
||||
assert.Error(t, err)
|
||||
|
||||
err = SetProxy("http://87.236.233.92:8080")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user