Skip to content

Commit

Permalink
feat: add async feedback hook option (#414)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
yacir authored and appleboy committed Sep 6, 2019
1 parent eab5710 commit 3812d35
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 16 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ core:
queue_num: 0 # default queue number is 8192
max_notification: 100
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"
ssl: false
cert_path: "cert.pem"
Expand Down Expand Up @@ -516,6 +517,7 @@ Request body must has a notifications array. The following is a parameter table

| name | type | description | required | note |
|-------------------------|--------------|---------------------------------------------------------------------------------------------------|----------|---------------------------------------------------------------|
| notif_id | string | A unique string that identifies the notification for async feedback | - | |
| tokens | string array | device tokens | o | |
| platform | int | platform(iOS,Android) | o | 1=iOS, 2=Android (Firebase) |
| message | string | message for notification | - | |
Expand All @@ -526,7 +528,7 @@ Request body must has a notifications array. The following is a parameter table
| data | string array | extensible partition | - | |
| retry | int | retry send notification if fail response from server. Value must be small than `max_retry` field. | - | |
| 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 |
| collapse_key | string | a key for collapsing notifications | - | only Android |
| delay_while_idle | bool | a flag for device idling | - | only Android |
Expand Down Expand Up @@ -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
core:
Expand Down
3 changes: 3 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ core:
queue_num: 0 # default queue number is 8192
max_notification: 100
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"
ssl: false
cert_path: "cert.pem"
Expand Down Expand Up @@ -114,6 +115,7 @@ type SectionCore struct {
CertBase64 string `yaml:"cert_base64"`
KeyBase64 string `yaml:"key_base64"`
HTTPProxy string `yaml:"http_proxy"`
FeedbackURL string `yaml:"feedback_hook_url"`
PID SectionPID `yaml:"pid"`
AutoTLS SectionAutoTLS `yaml:"auto_tls"`
}
Expand Down Expand Up @@ -256,6 +258,7 @@ func LoadConf(confPath string) (ConfYaml, error) {
conf.Core.QueueNum = int64(viper.GetInt("core.queue_num"))
conf.Core.Mode = viper.GetString("core.mode")
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.CertPath = viper.GetString("core.cert_path")
conf.Core.KeyPath = viper.GetString("core.key_path")
Expand Down
2 changes: 2 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func (suite *ConfigTestSuite) TestValidateConfDefault() {
assert.Equal(suite.T(), int64(8192), suite.ConfGorushDefault.Core.QueueNum)
assert.Equal(suite.T(), "release", suite.ConfGorushDefault.Core.Mode)
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(), "cert.pem", suite.ConfGorushDefault.Core.CertPath)
assert.Equal(suite.T(), "key.pem", suite.ConfGorushDefault.Core.KeyPath)
Expand Down Expand Up @@ -116,6 +117,7 @@ func (suite *ConfigTestSuite) TestValidateConf() {
assert.Equal(suite.T(), int64(8192), suite.ConfGorush.Core.QueueNum)
assert.Equal(suite.T(), "release", suite.ConfGorush.Core.Mode)
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(), "cert.pem", suite.ConfGorush.Core.CertPath)
assert.Equal(suite.T(), "key.pem", suite.ConfGorush.Core.KeyPath)
Expand Down
1 change: 1 addition & 0 deletions config/testdata/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ core:
queue_num: 0 # default queue number is 8192
max_notification: 100
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"
ssl: false
cert_path: "cert.pem"
Expand Down
38 changes: 38 additions & 0 deletions gorush/feedback.go
Original file line number Diff line number Diff line change
@@ -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
}
75 changes: 75 additions & 0 deletions gorush/feedback_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 2 additions & 0 deletions gorush/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type LogReq struct {

// LogPushEntry is push response log
type LogPushEntry struct {
ID string `json:"notif_id,omitempty"`
Type string `json:"type"`
Platform string `json:"platform"`
Token string `json:"token"`
Expand Down Expand Up @@ -211,6 +212,7 @@ func getLogPushEntry(status, token string, req PushNotification, errPush error)
}

return LogPushEntry{
ID: req.ID,
Type: status,
Platform: plat,
Token: token,
Expand Down
1 change: 1 addition & 0 deletions gorush/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type RequestPush struct {
// PushNotification is single notification request
type PushNotification struct {
// Common
ID string `json:"notif_id,omitempty"`
Tokens []string `json:"tokens" binding:"required"`
Platform int `json:"platform" binding:"required"`
Message string `json:"message,omitempty"`
Expand Down
28 changes: 15 additions & 13 deletions gorush/notification_apns.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/sideshow/apns2/certificate"
"github.com/sideshow/apns2/payload"
"github.com/sideshow/apns2/token"
"github.com/sirupsen/logrus"
)

// Sound sets the aps sound on the payload.
Expand Down Expand Up @@ -288,25 +289,26 @@ Retry:
// send ios 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
LogPush(FailedPush, token, req, err)

if PushConf.Core.Sync {
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)
newTokens = append(newTokens, token)
isError = true
Expand Down
10 changes: 9 additions & 1 deletion gorush/notification_fcm.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import (
"errors"
"fmt"

"github.com/appleboy/go-fcm"
fcm "github.com/appleboy/go-fcm"
"github.com/sirupsen/logrus"
)

// InitFCMClient use for initialize FCM Client.
Expand Down Expand Up @@ -149,6 +150,13 @@ Retry:
LogPush(FailedPush, to, req, result.Error)
if PushConf.Core.Sync {
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
}
Expand Down

0 comments on commit 3812d35

Please sign in to comment.