feat: add async feedback hook option (#414)
* feat: add async feedback hook option * remove unnecessary return values * review: check feedback's resp state * fix embedmd error * fix config test * add feedback tests * fix errcheck issues
This commit is contained in:
parent
eab5710100
commit
3812d357fd
19
README.md
19
README.md
|
@ -97,6 +97,7 @@ core:
|
||||||
queue_num: 0 # default queue number is 8192
|
queue_num: 0 # default queue number is 8192
|
||||||
max_notification: 100
|
max_notification: 100
|
||||||
sync: false # set true if you need get error message from fail push notification in API response.
|
sync: false # set true if you need get error message from fail push notification in API response.
|
||||||
|
feedback_hook_url: "" # set a hook url if you need get error message asynchronously from fail push notification in API response.
|
||||||
mode: "release"
|
mode: "release"
|
||||||
ssl: false
|
ssl: false
|
||||||
cert_path: "cert.pem"
|
cert_path: "cert.pem"
|
||||||
|
@ -516,6 +517,7 @@ Request body must has a notifications array. The following is a parameter table
|
||||||
|
|
||||||
| name | type | description | required | note |
|
| name | type | description | required | note |
|
||||||
|-------------------------|--------------|---------------------------------------------------------------------------------------------------|----------|---------------------------------------------------------------|
|
|-------------------------|--------------|---------------------------------------------------------------------------------------------------|----------|---------------------------------------------------------------|
|
||||||
|
| notif_id | string | A unique string that identifies the notification for async feedback | - | |
|
||||||
| tokens | string array | device tokens | o | |
|
| tokens | string array | device tokens | o | |
|
||||||
| platform | int | platform(iOS,Android) | o | 1=iOS, 2=Android (Firebase) |
|
| platform | int | platform(iOS,Android) | o | 1=iOS, 2=Android (Firebase) |
|
||||||
| message | string | message for notification | - | |
|
| message | string | message for notification | - | |
|
||||||
|
@ -526,7 +528,7 @@ Request body must has a notifications array. The following is a parameter table
|
||||||
| data | string array | extensible partition | - | |
|
| data | string array | extensible partition | - | |
|
||||||
| retry | int | retry send notification if fail response from server. Value must be small than `max_retry` field. | - | |
|
| retry | int | retry send notification if fail response from server. Value must be small than `max_retry` field. | - | |
|
||||||
| topic | string | send messages to topics | | |
|
| topic | string | send messages to topics | | |
|
||||||
| api_key | string | api key for firebase cloud message | - | only Android |
|
| api_key | string | api key for firebase cloud message | - | only Android |
|
||||||
| to | string | The value must be a registration token, notification key, or topic. | - | only Android |
|
| to | string | The value must be a registration token, notification key, or topic. | - | only Android |
|
||||||
| collapse_key | string | a key for collapsing notifications | - | only Android |
|
| collapse_key | string | a key for collapsing notifications | - | only Android |
|
||||||
| delay_while_idle | bool | a flag for device idling | - | only Android |
|
| delay_while_idle | bool | a flag for device idling | - | only Android |
|
||||||
|
@ -779,7 +781,20 @@ Success response:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
If you need error logs from sending fail notifications, please set `sync` as `true` on yaml config.
|
If you need error logs from sending fail notifications, please set a `feedback_hook_url`. The server with send the failing logs asynchronously to your API as `POST` requests.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
core:
|
||||||
|
port: "8088" # ignore this port number if auto_tls is enabled (listen 443).
|
||||||
|
worker_num: 0 # default worker number is runtime.NumCPU()
|
||||||
|
queue_num: 0 # default queue number is 8192
|
||||||
|
max_notification: 100
|
||||||
|
sync: false
|
||||||
|
- feedback_hook_url: ""
|
||||||
|
+ feedback_hook_url: "https://exemple.com/api/hook"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also switch to **sync** mode by setting the `sync` as `true` on yaml config.
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
core:
|
core:
|
||||||
|
|
|
@ -19,6 +19,7 @@ core:
|
||||||
queue_num: 0 # default queue number is 8192
|
queue_num: 0 # default queue number is 8192
|
||||||
max_notification: 100
|
max_notification: 100
|
||||||
sync: false # set true if you need get error message from fail push notification in API response.
|
sync: false # set true if you need get error message from fail push notification in API response.
|
||||||
|
feedback_hook_url: "" # set webhook url if you need get error message asynchronously from fail push notification in API response.
|
||||||
mode: "release"
|
mode: "release"
|
||||||
ssl: false
|
ssl: false
|
||||||
cert_path: "cert.pem"
|
cert_path: "cert.pem"
|
||||||
|
@ -114,6 +115,7 @@ type SectionCore struct {
|
||||||
CertBase64 string `yaml:"cert_base64"`
|
CertBase64 string `yaml:"cert_base64"`
|
||||||
KeyBase64 string `yaml:"key_base64"`
|
KeyBase64 string `yaml:"key_base64"`
|
||||||
HTTPProxy string `yaml:"http_proxy"`
|
HTTPProxy string `yaml:"http_proxy"`
|
||||||
|
FeedbackURL string `yaml:"feedback_hook_url"`
|
||||||
PID SectionPID `yaml:"pid"`
|
PID SectionPID `yaml:"pid"`
|
||||||
AutoTLS SectionAutoTLS `yaml:"auto_tls"`
|
AutoTLS SectionAutoTLS `yaml:"auto_tls"`
|
||||||
}
|
}
|
||||||
|
@ -256,6 +258,7 @@ func LoadConf(confPath string) (ConfYaml, error) {
|
||||||
conf.Core.QueueNum = int64(viper.GetInt("core.queue_num"))
|
conf.Core.QueueNum = int64(viper.GetInt("core.queue_num"))
|
||||||
conf.Core.Mode = viper.GetString("core.mode")
|
conf.Core.Mode = viper.GetString("core.mode")
|
||||||
conf.Core.Sync = viper.GetBool("core.sync")
|
conf.Core.Sync = viper.GetBool("core.sync")
|
||||||
|
conf.Core.FeedbackURL = viper.GetString("core.feedback_hook_url")
|
||||||
conf.Core.SSL = viper.GetBool("core.ssl")
|
conf.Core.SSL = viper.GetBool("core.ssl")
|
||||||
conf.Core.CertPath = viper.GetString("core.cert_path")
|
conf.Core.CertPath = viper.GetString("core.cert_path")
|
||||||
conf.Core.KeyPath = viper.GetString("core.key_path")
|
conf.Core.KeyPath = viper.GetString("core.key_path")
|
||||||
|
|
|
@ -44,6 +44,7 @@ func (suite *ConfigTestSuite) TestValidateConfDefault() {
|
||||||
assert.Equal(suite.T(), int64(8192), suite.ConfGorushDefault.Core.QueueNum)
|
assert.Equal(suite.T(), int64(8192), suite.ConfGorushDefault.Core.QueueNum)
|
||||||
assert.Equal(suite.T(), "release", suite.ConfGorushDefault.Core.Mode)
|
assert.Equal(suite.T(), "release", suite.ConfGorushDefault.Core.Mode)
|
||||||
assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.Sync)
|
assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.Sync)
|
||||||
|
assert.Equal(suite.T(), "", suite.ConfGorushDefault.Core.FeedbackURL)
|
||||||
assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.SSL)
|
assert.Equal(suite.T(), false, suite.ConfGorushDefault.Core.SSL)
|
||||||
assert.Equal(suite.T(), "cert.pem", suite.ConfGorushDefault.Core.CertPath)
|
assert.Equal(suite.T(), "cert.pem", suite.ConfGorushDefault.Core.CertPath)
|
||||||
assert.Equal(suite.T(), "key.pem", suite.ConfGorushDefault.Core.KeyPath)
|
assert.Equal(suite.T(), "key.pem", suite.ConfGorushDefault.Core.KeyPath)
|
||||||
|
@ -116,6 +117,7 @@ func (suite *ConfigTestSuite) TestValidateConf() {
|
||||||
assert.Equal(suite.T(), int64(8192), suite.ConfGorush.Core.QueueNum)
|
assert.Equal(suite.T(), int64(8192), suite.ConfGorush.Core.QueueNum)
|
||||||
assert.Equal(suite.T(), "release", suite.ConfGorush.Core.Mode)
|
assert.Equal(suite.T(), "release", suite.ConfGorush.Core.Mode)
|
||||||
assert.Equal(suite.T(), false, suite.ConfGorush.Core.Sync)
|
assert.Equal(suite.T(), false, suite.ConfGorush.Core.Sync)
|
||||||
|
assert.Equal(suite.T(), "", suite.ConfGorush.Core.FeedbackURL)
|
||||||
assert.Equal(suite.T(), false, suite.ConfGorush.Core.SSL)
|
assert.Equal(suite.T(), false, suite.ConfGorush.Core.SSL)
|
||||||
assert.Equal(suite.T(), "cert.pem", suite.ConfGorush.Core.CertPath)
|
assert.Equal(suite.T(), "cert.pem", suite.ConfGorush.Core.CertPath)
|
||||||
assert.Equal(suite.T(), "key.pem", suite.ConfGorush.Core.KeyPath)
|
assert.Equal(suite.T(), "key.pem", suite.ConfGorush.Core.KeyPath)
|
||||||
|
|
|
@ -6,6 +6,7 @@ core:
|
||||||
queue_num: 0 # default queue number is 8192
|
queue_num: 0 # default queue number is 8192
|
||||||
max_notification: 100
|
max_notification: 100
|
||||||
sync: false # set true if you need get error message from fail push notification in API response.
|
sync: false # set true if you need get error message from fail push notification in API response.
|
||||||
|
feedback_hook_url: "" # set a hook url if you need get error message asynchronously from fail push notification in API response.
|
||||||
mode: "release"
|
mode: "release"
|
||||||
ssl: false
|
ssl: false
|
||||||
cert_path: "cert.pem"
|
cert_path: "cert.pem"
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package gorush
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DispatchFeedback sends a feedback to the configured gateway.
|
||||||
|
func DispatchFeedback(log LogPushEntry, url string) error {
|
||||||
|
|
||||||
|
if url == "" {
|
||||||
|
return errors.New("The url can't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := json.Marshal(log)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(payload))
|
||||||
|
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
|
||||||
|
HTTPClient := &http.Client{}
|
||||||
|
resp, err := HTTPClient.Do(req)
|
||||||
|
|
||||||
|
if resp != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package gorush
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/appleboy/gorush/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEmptyFeedbackURL(t *testing.T) {
|
||||||
|
// PushConf, _ = config.LoadConf("")
|
||||||
|
logEntry := LogPushEntry{
|
||||||
|
ID: "",
|
||||||
|
Type: "",
|
||||||
|
Platform: "",
|
||||||
|
Token: "",
|
||||||
|
Message: "",
|
||||||
|
Error: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := DispatchFeedback(logEntry, PushConf.Core.FeedbackURL)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPErrorInFeedbackCall(t *testing.T) {
|
||||||
|
config, _ := config.LoadConf("")
|
||||||
|
config.Core.FeedbackURL = "http://test.example.com/api/"
|
||||||
|
logEntry := LogPushEntry{
|
||||||
|
ID: "",
|
||||||
|
Type: "",
|
||||||
|
Platform: "",
|
||||||
|
Token: "",
|
||||||
|
Message: "",
|
||||||
|
Error: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := DispatchFeedback(logEntry, config.Core.FeedbackURL)
|
||||||
|
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()
|
||||||
|
|
||||||
|
config, _ := config.LoadConf("")
|
||||||
|
config.Core.FeedbackURL = httpMock.URL
|
||||||
|
logEntry := LogPushEntry{
|
||||||
|
ID: "",
|
||||||
|
Type: "",
|
||||||
|
Platform: "",
|
||||||
|
Token: "",
|
||||||
|
Message: "",
|
||||||
|
Error: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := DispatchFeedback(logEntry, config.Core.FeedbackURL)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
}
|
|
@ -34,6 +34,7 @@ type LogReq struct {
|
||||||
|
|
||||||
// LogPushEntry is push response log
|
// LogPushEntry is push response log
|
||||||
type LogPushEntry struct {
|
type LogPushEntry struct {
|
||||||
|
ID string `json:"notif_id,omitempty"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
@ -211,6 +212,7 @@ func getLogPushEntry(status, token string, req PushNotification, errPush error)
|
||||||
}
|
}
|
||||||
|
|
||||||
return LogPushEntry{
|
return LogPushEntry{
|
||||||
|
ID: req.ID,
|
||||||
Type: status,
|
Type: status,
|
||||||
Platform: plat,
|
Platform: plat,
|
||||||
Token: token,
|
Token: token,
|
||||||
|
|
|
@ -52,6 +52,7 @@ type RequestPush struct {
|
||||||
// PushNotification is single notification request
|
// PushNotification is single notification request
|
||||||
type PushNotification struct {
|
type PushNotification struct {
|
||||||
// Common
|
// Common
|
||||||
|
ID string `json:"notif_id,omitempty"`
|
||||||
Tokens []string `json:"tokens" binding:"required"`
|
Tokens []string `json:"tokens" binding:"required"`
|
||||||
Platform int `json:"platform" binding:"required"`
|
Platform int `json:"platform" binding:"required"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/sideshow/apns2/certificate"
|
"github.com/sideshow/apns2/certificate"
|
||||||
"github.com/sideshow/apns2/payload"
|
"github.com/sideshow/apns2/payload"
|
||||||
"github.com/sideshow/apns2/token"
|
"github.com/sideshow/apns2/token"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Sound sets the aps sound on the payload.
|
// Sound sets the aps sound on the payload.
|
||||||
|
@ -288,25 +289,26 @@ Retry:
|
||||||
// send ios notification
|
// send ios notification
|
||||||
res, err := client.Push(notification)
|
res, err := client.Push(notification)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil || res.StatusCode != 200 {
|
||||||
|
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
|
// apns server error
|
||||||
LogPush(FailedPush, token, req, err)
|
LogPush(FailedPush, token, req, err)
|
||||||
|
|
||||||
if PushConf.Core.Sync {
|
if PushConf.Core.Sync {
|
||||||
req.AddLog(getLogPushEntry(FailedPush, token, req, err))
|
req.AddLog(getLogPushEntry(FailedPush, token, req, err))
|
||||||
|
} else if PushConf.Core.FeedbackURL != "" {
|
||||||
|
go func(logger *logrus.Logger, log LogPushEntry, url string) {
|
||||||
|
err := DispatchFeedback(log, url)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}(LogError, getLogPushEntry(FailedPush, token, req, err), PushConf.Core.FeedbackURL)
|
||||||
}
|
}
|
||||||
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)
|
StatStorage.AddIosError(1)
|
||||||
newTokens = append(newTokens, token)
|
newTokens = append(newTokens, token)
|
||||||
isError = true
|
isError = true
|
||||||
|
|
|
@ -4,7 +4,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/appleboy/go-fcm"
|
fcm "github.com/appleboy/go-fcm"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitFCMClient use for initialize FCM Client.
|
// InitFCMClient use for initialize FCM Client.
|
||||||
|
@ -149,6 +150,13 @@ Retry:
|
||||||
LogPush(FailedPush, to, req, result.Error)
|
LogPush(FailedPush, to, req, result.Error)
|
||||||
if PushConf.Core.Sync {
|
if PushConf.Core.Sync {
|
||||||
req.AddLog(getLogPushEntry(FailedPush, to, req, result.Error))
|
req.AddLog(getLogPushEntry(FailedPush, to, req, result.Error))
|
||||||
|
} else if PushConf.Core.FeedbackURL != "" {
|
||||||
|
go func(logger *logrus.Logger, log LogPushEntry, url string) {
|
||||||
|
err := DispatchFeedback(log, url)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}(LogError, getLogPushEntry(FailedPush, to, req, result.Error), PushConf.Core.FeedbackURL)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue