From a0d7c5b982460f892e5dc8f4b0d0b3e1f90ff19b Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 12 Dec 2022 19:39:21 +0800 Subject: [PATCH 1/6] Add Notifier: Dingtalk, Feishu --- config/config.go | 16 +++++++ config/config_test.go | 2 +- gobackup_test.yml | 19 ++++---- model/model.go | 15 ++++++- notifier/base.go | 91 +++++++++++++++++++++++++++++++++++++++ notifier/dingtalk.go | 34 +++++++++++++++ notifier/dingtalk_test.go | 19 ++++++++ notifier/feishu_test.go | 19 ++++++++ notifier/feisu.go | 34 +++++++++++++++ notifier/webhook.go | 68 +++++++++++++++++++++++++++++ 10 files changed, 306 insertions(+), 11 deletions(-) create mode 100644 notifier/base.go create mode 100644 notifier/dingtalk.go create mode 100644 notifier/dingtalk_test.go create mode 100644 notifier/feishu_test.go create mode 100644 notifier/feisu.go create mode 100644 notifier/webhook.go diff --git a/config/config.go b/config/config.go index 5dd4e116..d4e26060 100644 --- a/config/config.go +++ b/config/config.go @@ -58,6 +58,7 @@ type ModelConfig struct { Splitter *viper.Viper Databases map[string]SubConfig Storages map[string]SubConfig + Notifiers map[string]SubConfig Viper *viper.Viper } @@ -164,6 +165,8 @@ func loadModel(key string) (model ModelConfig) { logger.Fatalf("No storage found in model %s", model.Name) } + loadNotifiersConfig(&model) + return } @@ -220,6 +223,19 @@ func loadStoragesConfig(model *ModelConfig) { model.Storages = storageConfigs } +func loadNotifiersConfig(model *ModelConfig) { + subViper := model.Viper.Sub("notifiers") + model.Notifiers = map[string]SubConfig{} + for key := range model.Viper.GetStringMap("notifiers") { + dbViper := subViper.Sub(key) + model.Notifiers[key] = SubConfig{ + Name: key, + Type: dbViper.GetString("type"), + Viper: dbViper, + } + } +} + // GetModelConfigByName get model config by name func GetModelConfigByName(name string) (model *ModelConfig) { for _, m := range Models { diff --git a/config/config_test.go b/config/config_test.go index 4dad3a25..acf2d40a 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -12,7 +12,7 @@ func init() { func TestModelsLength(t *testing.T) { assert.Equal(t, Exist, true) - assert.Equal(t, len(Models), 5) + assert.Equal(t, len(Models), 4) } func TestModel(t *testing.T) { diff --git a/gobackup_test.yml b/gobackup_test.yml index 60c00ae1..27b8eee2 100644 --- a/gobackup_test.yml +++ b/gobackup_test.yml @@ -106,15 +106,18 @@ models: password: 123456 salt: false openssl: true + notifiers: + feishu: + type: feishu + url: https://open.feishu.cn/open-apis/bot/v2/hook/a8fb64fa-8f28-46f5-ba7a-c2d0a54a7871 + dingtalk: + type: dingtalk + url: https://oapi.dingtalk.com/robot/send?access_token=102f9b03e717078acf830a692e78a07092aca8b6a7fc3f5c2c048baf25de1370 storages: - azure: - type: azure - keep: 20 - account: my-storage-account - timeout: 300 - tenant_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - client_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - client_secret: xxxxxxxx + local: + type: local + keep: 10 + path: /Users/jason/Downloads/backup1 archive: includes: - /Users/jason/work/imageproxy diff --git a/model/model.go b/model/model.go index 2cf5afef..406aef3d 100644 --- a/model/model.go +++ b/model/model.go @@ -12,6 +12,7 @@ import ( "github.com/gobackup/gobackup/database" "github.com/gobackup/gobackup/encryptor" "github.com/gobackup/gobackup/logger" + "github.com/gobackup/gobackup/notifier" "github.com/gobackup/gobackup/splitter" "github.com/gobackup/gobackup/storage" ) @@ -22,9 +23,18 @@ type Model struct { } // Perform model -func (m Model) Perform() { +func (m Model) Perform() (err error) { logger := logger.Tag(fmt.Sprintf("Model: %s", m.Config.Name)) + defer func() { + if err != nil { + logger.Error(err) + notifier.Failure(m.Config, err.Error()) + } else { + notifier.Success(m.Config) + } + }() + logger.Info("WorkDir:", m.Config.DumpPath) defer func() { @@ -35,7 +45,7 @@ func (m Model) Perform() { m.cleanup() }() - err := database.Run(m.Config) + err = database.Run(m.Config) if err != nil { logger.Error(err) return @@ -73,6 +83,7 @@ func (m Model) Perform() { return } + return nil } // Cleanup model temp files diff --git a/notifier/base.go b/notifier/base.go new file mode 100644 index 00000000..4d7c25d0 --- /dev/null +++ b/notifier/base.go @@ -0,0 +1,91 @@ +package notifier + +import ( + "fmt" + "time" + + "github.com/gobackup/gobackup/config" + "github.com/gobackup/gobackup/logger" + "github.com/spf13/viper" +) + +type Base struct { + viper *viper.Viper + Name string + onSuccess bool + onFailure bool +} + +type Notifier interface { + notify(title, message string) error +} + +var ( + notifyTypeSuccess = 1 + notifyTypeFailure = 2 +) + +func newNotifier(name string, config config.SubConfig) (Notifier, *Base, error) { + base := Base{ + viper: config.Viper, + Name: name, + } + base.viper.SetDefault("on_success", true) + base.viper.SetDefault("on_failure", true) + + base.onSuccess = base.viper.GetBool("on_success") + base.onFailure = base.viper.GetBool("on_failure") + + switch config.Type { + case "webhook": + return &Webhook{Base: base}, &base, nil + case "feishu": + return NewFeishu(&base), &base, nil + case "dingtalk": + return NewDingtalk(&base), &base, nil + } + + return nil, nil, fmt.Errorf("Notifier: %s is not supported", name) +} + +func notify(model config.ModelConfig, title, message string, notifyType int) error { + logger := logger.Tag("Notifier") + + logger.Infof("Running %d Notifiers", len(model.Notifiers)) + for name, config := range model.Notifiers { + notifier, base, err := newNotifier(name, config) + if err != nil { + logger.Error(err) + continue + } + + if notifyType == notifyTypeSuccess { + if base.onSuccess { + if err := notifier.notify(title, message); err != nil { + logger.Error(err) + } + } + } else if notifyType == notifyTypeFailure { + if base.onFailure { + if err := notifier.notify(title, message); err != nil { + logger.Error(err) + } + } + } + } + + return nil +} + +func Success(model config.ModelConfig) error { + title := fmt.Sprintf("[GoBackup] Backup of %s completed successfully", model.Name) + message := fmt.Sprintf("Backup of %s completed successfully at %s", model.Name, time.Now().Local()) + return notify(model, title, message, notifyTypeSuccess) +} + +func Failure(model config.ModelConfig, reason string) error { + title := fmt.Sprintf("[GoBackup] Backup of %s failed", model.Name) + message := fmt.Sprintf("Backup of %s failed at %s:\n\n%s", model.Name, time.Now().Local(), reason) + + return notify(model, title, message, notifyTypeFailure) +} diff --git a/notifier/dingtalk.go b/notifier/dingtalk.go new file mode 100644 index 00000000..979673e0 --- /dev/null +++ b/notifier/dingtalk.go @@ -0,0 +1,34 @@ +package notifier + +import ( + "encoding/json" + "fmt" +) + +type dingtalkPayload struct { + MsgType string `json:"msgtype"` + Text dingtalkPayloadText `json:"text"` +} + +type dingtalkPayloadText struct { + Content string `json:"content"` +} + +func NewDingtalk(base *Base) *Webhook { + return &Webhook{ + Base: *base, + Service: "feishu", + method: "POST", + contentType: "application/json", + buildBody: func(title, message string) ([]byte, error) { + payload := dingtalkPayload{ + MsgType: "text", + Text: dingtalkPayloadText{ + Content: fmt.Sprintf("%s\n\n%s", title, message), + }, + } + + return json.Marshal(payload) + }, + } +} diff --git a/notifier/dingtalk_test.go b/notifier/dingtalk_test.go new file mode 100644 index 00000000..040c2cf4 --- /dev/null +++ b/notifier/dingtalk_test.go @@ -0,0 +1,19 @@ +package notifier + +import ( + "testing" + + "github.com/longbridgeapp/assert" +) + +func Test_Dingtalk(t *testing.T) { + base := &Base{} + + s := NewDingtalk(base) + assert.Equal(t, "POST", s.method) + assert.Equal(t, "application/json", s.contentType) + + body, err := s.buildBody("This is title", "This is body") + assert.NoError(t, err) + assert.Equal(t, `{"msgtype":"text","text":{"content":"This is title\n\nThis is body"}}`, string(body)) +} diff --git a/notifier/feishu_test.go b/notifier/feishu_test.go new file mode 100644 index 00000000..b1b59817 --- /dev/null +++ b/notifier/feishu_test.go @@ -0,0 +1,19 @@ +package notifier + +import ( + "testing" + + "github.com/longbridgeapp/assert" +) + +func Test_Feishu(t *testing.T) { + base := &Base{} + + s := NewFeishu(base) + assert.Equal(t, "POST", s.method) + assert.Equal(t, "application/json", s.contentType) + + body, err := s.buildBody("This is title", "This is body") + assert.NoError(t, err) + assert.Equal(t, `{"msg_type":"text","content":{"text":"This is title\n\nThis is body"}}`, string(body)) +} diff --git a/notifier/feisu.go b/notifier/feisu.go new file mode 100644 index 00000000..7c527564 --- /dev/null +++ b/notifier/feisu.go @@ -0,0 +1,34 @@ +package notifier + +import ( + "encoding/json" + "fmt" +) + +type feishuPayload struct { + MsgType string `json:"msg_type"` + Content feishuPayloadContent `json:"content"` +} + +type feishuPayloadContent struct { + Text string `json:"text"` +} + +func NewFeishu(base *Base) *Webhook { + return &Webhook{ + Base: *base, + Service: "feishu", + method: "POST", + contentType: "application/json", + buildBody: func(title, message string) ([]byte, error) { + payload := feishuPayload{ + MsgType: "text", + Content: feishuPayloadContent{ + Text: fmt.Sprintf("%s\n\n%s", title, message), + }, + } + + return json.Marshal(payload) + }, + } +} diff --git a/notifier/webhook.go b/notifier/webhook.go new file mode 100644 index 00000000..f046cbf9 --- /dev/null +++ b/notifier/webhook.go @@ -0,0 +1,68 @@ +package notifier + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/gobackup/gobackup/logger" +) + +type Webhook struct { + Base + + Service string + URL string + + method string + contentType string + buildBody func(title, message string) ([]byte, error) +} + +func (s *Webhook) getLogger() logger.Logger { + return logger.Tag(fmt.Sprintf("Notifier: %s", s.Service)) +} + +func (s *Webhook) notify(title string, message string) error { + logger := s.getLogger() + + s.viper.SetDefault("method", "POST") + + s.URL = s.viper.GetString("url") + + payload, err := s.buildBody(title, message) + if err != nil { + return err + } + + logger.Infof("Send message to %s...", s.URL) + req, err := http.NewRequest(s.method, s.URL, strings.NewReader(string(payload))) + if err != nil { + logger.Error(err) + return err + } + + req.Header.Set("Content-Type", s.contentType) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + logger.Error(err) + return err + } + defer resp.Body.Close() + + var body []byte + if resp.Body != nil { + body, err = io.ReadAll(resp.Body) + if err != nil { + return err + } + } + + logger.Debugf("Response body: %s", string(body)) + logger.Info("Notification sent.") + + return nil +} From 24f131e3e7e6a1644d0328627cd428c2e7a9c198 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 12 Dec 2022 19:54:09 +0800 Subject: [PATCH 2/6] Add Notifier: Discord --- gobackup_test.yml | 5 ++++- notifier/base.go | 10 ++++++---- notifier/discord.go | 26 ++++++++++++++++++++++++++ notifier/discord_test.go | 19 +++++++++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 notifier/discord.go create mode 100644 notifier/discord_test.go diff --git a/gobackup_test.yml b/gobackup_test.yml index 27b8eee2..0443cd46 100644 --- a/gobackup_test.yml +++ b/gobackup_test.yml @@ -112,7 +112,10 @@ models: url: https://open.feishu.cn/open-apis/bot/v2/hook/a8fb64fa-8f28-46f5-ba7a-c2d0a54a7871 dingtalk: type: dingtalk - url: https://oapi.dingtalk.com/robot/send?access_token=102f9b03e717078acf830a692e78a07092aca8b6a7fc3f5c2c048baf25de1370 + url: https://oapi.dingtalk.com/robot/send?access_token=102f9b03e717078acf830a692e78a07092aca8b6a7fc3f5c2c048baf25de1370r + discord: + type: discord + url: https://discordapp.com/api/webhooks/1051827238108135424/lWrAw9okY-6LCimnWlHn3pKMr-e3rr4fWn5rKgcjfn92n_RiZbRK9M7Kse-esKDBepV2 storages: local: type: local diff --git a/notifier/base.go b/notifier/base.go index 4d7c25d0..c54bb707 100644 --- a/notifier/base.go +++ b/notifier/base.go @@ -26,7 +26,7 @@ var ( ) func newNotifier(name string, config config.SubConfig) (Notifier, *Base, error) { - base := Base{ + base := &Base{ viper: config.Viper, Name: name, } @@ -38,11 +38,13 @@ func newNotifier(name string, config config.SubConfig) (Notifier, *Base, error) switch config.Type { case "webhook": - return &Webhook{Base: base}, &base, nil + return &Webhook{Base: *base}, base, nil case "feishu": - return NewFeishu(&base), &base, nil + return NewFeishu(base), base, nil case "dingtalk": - return NewDingtalk(&base), &base, nil + return NewDingtalk(base), base, nil + case "discord": + return NewDiscord(base), base, nil } return nil, nil, fmt.Errorf("Notifier: %s is not supported", name) diff --git a/notifier/discord.go b/notifier/discord.go new file mode 100644 index 00000000..87a8e74f --- /dev/null +++ b/notifier/discord.go @@ -0,0 +1,26 @@ +package notifier + +import ( + "encoding/json" + "fmt" +) + +type discordPayload struct { + Content string `json:"content"` +} + +func NewDiscord(base *Base) *Webhook { + return &Webhook{ + Base: *base, + Service: "feishu", + method: "POST", + contentType: "application/json", + buildBody: func(title, message string) ([]byte, error) { + payload := discordPayload{ + Content: fmt.Sprintf("%s\n\n%s", title, message), + } + + return json.Marshal(payload) + }, + } +} diff --git a/notifier/discord_test.go b/notifier/discord_test.go new file mode 100644 index 00000000..30b637bf --- /dev/null +++ b/notifier/discord_test.go @@ -0,0 +1,19 @@ +package notifier + +import ( + "testing" + + "github.com/longbridgeapp/assert" +) + +func Test_Discord(t *testing.T) { + base := &Base{} + + s := NewDiscord(base) + assert.Equal(t, "POST", s.method) + assert.Equal(t, "application/json", s.contentType) + + body, err := s.buildBody("This is title", "This is body") + assert.NoError(t, err) + assert.Equal(t, `{"content":"This is title\n\nThis is body"}`, string(body)) +} From c49c311e1210259584cf082d2a8c98556855f33b Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 12 Dec 2022 20:02:13 +0800 Subject: [PATCH 3/6] Add Notifier: Slack --- gobackup_test.yml | 3 +++ notifier/base.go | 2 ++ notifier/dingtalk.go | 2 +- notifier/dingtalk_test.go | 1 + notifier/discord.go | 2 +- notifier/discord_test.go | 1 + notifier/feishu_test.go | 1 + notifier/feisu.go | 2 +- notifier/slack.go | 26 ++++++++++++++++++++++++++ notifier/slack_test.go | 20 ++++++++++++++++++++ 10 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 notifier/slack.go create mode 100644 notifier/slack_test.go diff --git a/gobackup_test.yml b/gobackup_test.yml index 0443cd46..855a8558 100644 --- a/gobackup_test.yml +++ b/gobackup_test.yml @@ -116,6 +116,9 @@ models: discord: type: discord url: https://discordapp.com/api/webhooks/1051827238108135424/lWrAw9okY-6LCimnWlHn3pKMr-e3rr4fWn5rKgcjfn92n_RiZbRK9M7Kse-esKDBepV2 + slack: + type: slack + url: https://hooks.slack.com/services/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 storages: local: type: local diff --git a/notifier/base.go b/notifier/base.go index c54bb707..87188a0e 100644 --- a/notifier/base.go +++ b/notifier/base.go @@ -45,6 +45,8 @@ func newNotifier(name string, config config.SubConfig) (Notifier, *Base, error) return NewDingtalk(base), base, nil case "discord": return NewDiscord(base), base, nil + case "slack": + return NewSlack(base), base, nil } return nil, nil, fmt.Errorf("Notifier: %s is not supported", name) diff --git a/notifier/dingtalk.go b/notifier/dingtalk.go index 979673e0..0e2d8991 100644 --- a/notifier/dingtalk.go +++ b/notifier/dingtalk.go @@ -17,7 +17,7 @@ type dingtalkPayloadText struct { func NewDingtalk(base *Base) *Webhook { return &Webhook{ Base: *base, - Service: "feishu", + Service: "DingTalk", method: "POST", contentType: "application/json", buildBody: func(title, message string) ([]byte, error) { diff --git a/notifier/dingtalk_test.go b/notifier/dingtalk_test.go index 040c2cf4..919f9e38 100644 --- a/notifier/dingtalk_test.go +++ b/notifier/dingtalk_test.go @@ -10,6 +10,7 @@ func Test_Dingtalk(t *testing.T) { base := &Base{} s := NewDingtalk(base) + assert.Equal(t, "DingTalk", s.Service) assert.Equal(t, "POST", s.method) assert.Equal(t, "application/json", s.contentType) diff --git a/notifier/discord.go b/notifier/discord.go index 87a8e74f..56f60e1b 100644 --- a/notifier/discord.go +++ b/notifier/discord.go @@ -12,7 +12,7 @@ type discordPayload struct { func NewDiscord(base *Base) *Webhook { return &Webhook{ Base: *base, - Service: "feishu", + Service: "Discord", method: "POST", contentType: "application/json", buildBody: func(title, message string) ([]byte, error) { diff --git a/notifier/discord_test.go b/notifier/discord_test.go index 30b637bf..2cb1f5e1 100644 --- a/notifier/discord_test.go +++ b/notifier/discord_test.go @@ -10,6 +10,7 @@ func Test_Discord(t *testing.T) { base := &Base{} s := NewDiscord(base) + assert.Equal(t, "Discord", s.Service) assert.Equal(t, "POST", s.method) assert.Equal(t, "application/json", s.contentType) diff --git a/notifier/feishu_test.go b/notifier/feishu_test.go index b1b59817..4b6742ce 100644 --- a/notifier/feishu_test.go +++ b/notifier/feishu_test.go @@ -10,6 +10,7 @@ func Test_Feishu(t *testing.T) { base := &Base{} s := NewFeishu(base) + assert.Equal(t, "Feishu", s.Service) assert.Equal(t, "POST", s.method) assert.Equal(t, "application/json", s.contentType) diff --git a/notifier/feisu.go b/notifier/feisu.go index 7c527564..946bfa73 100644 --- a/notifier/feisu.go +++ b/notifier/feisu.go @@ -17,7 +17,7 @@ type feishuPayloadContent struct { func NewFeishu(base *Base) *Webhook { return &Webhook{ Base: *base, - Service: "feishu", + Service: "Feishu", method: "POST", contentType: "application/json", buildBody: func(title, message string) ([]byte, error) { diff --git a/notifier/slack.go b/notifier/slack.go new file mode 100644 index 00000000..53f1e5f9 --- /dev/null +++ b/notifier/slack.go @@ -0,0 +1,26 @@ +package notifier + +import ( + "encoding/json" + "fmt" +) + +type slackPayload struct { + Text string `json:"text"` +} + +func NewSlack(base *Base) *Webhook { + return &Webhook{ + Base: *base, + Service: "Slack", + method: "POST", + contentType: "application/json", + buildBody: func(title, message string) ([]byte, error) { + payload := slackPayload{ + Text: fmt.Sprintf("%s\n\n%s", title, message), + } + + return json.Marshal(payload) + }, + } +} diff --git a/notifier/slack_test.go b/notifier/slack_test.go new file mode 100644 index 00000000..3e51c188 --- /dev/null +++ b/notifier/slack_test.go @@ -0,0 +1,20 @@ +package notifier + +import ( + "testing" + + "github.com/longbridgeapp/assert" +) + +func Test_Slack(t *testing.T) { + base := &Base{} + + s := NewSlack(base) + assert.Equal(t, "Slack", s.Service) + assert.Equal(t, "POST", s.method) + assert.Equal(t, "application/json", s.contentType) + + body, err := s.buildBody("This is title", "This is body") + assert.NoError(t, err) + assert.Equal(t, `{"text":"This is title\n\nThis is body"}`, string(body)) +} From 0e6109e6ac16eee266ebfee5c407a382dce34345 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 12 Dec 2022 20:12:36 +0800 Subject: [PATCH 4/6] Improve model perform errors return. --- model/model.go | 6 ------ storage/base.go | 12 +++++------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/model/model.go b/model/model.go index 406aef3d..3e096a2d 100644 --- a/model/model.go +++ b/model/model.go @@ -47,39 +47,33 @@ func (m Model) Perform() (err error) { err = database.Run(m.Config) if err != nil { - logger.Error(err) return } if m.Config.Archive != nil { err = archive.Run(m.Config) if err != nil { - logger.Error(err) return } } archivePath, err := compressor.Run(m.Config) if err != nil { - logger.Error(err) return } archivePath, err = encryptor.Run(archivePath, m.Config) if err != nil { - logger.Error(err) return } archivePath, err = splitter.Run(archivePath, m.Config) if err != nil { - logger.Error(err) return } err = storage.Run(m.Config, archivePath) if err != nil { - logger.Error(err) return } diff --git a/storage/base.go b/storage/base.go index 63cf91e2..55294ab8 100644 --- a/storage/base.go +++ b/storage/base.go @@ -135,8 +135,8 @@ func runModel(model config.ModelConfig, archivePath string, storageConfig config // Run storage func Run(model config.ModelConfig, archivePath string) (err error) { - logger := logger.Tag("Storage") - var hasSuccess bool + var errors []error + n := len(model.Storages) for _, storageConfig := range model.Storages { err := runModel(model, archivePath, storageConfig) @@ -144,16 +144,14 @@ func Run(model config.ModelConfig, archivePath string) (err error) { if n == 1 { return err } else { - logger.Error(err) + errors = append(errors, err) continue } - } else { - hasSuccess = true } } - if !hasSuccess { - return fmt.Errorf("All storages are failed") + if len(errors) != 0 { + return fmt.Errorf("Storage errors: %v", errors) } return nil From 6449a0b695402b19feda5c39e5e3c8a893e81e01 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 12 Dec 2022 23:35:59 +0800 Subject: [PATCH 5/6] Add notifier: GitHub --- gobackup_test.yml | 10 ++++-- notifier/base.go | 2 ++ notifier/github.go | 78 +++++++++++++++++++++++++++++++++++++++++ notifier/github_test.go | 75 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 notifier/github.go create mode 100644 notifier/github_test.go diff --git a/gobackup_test.yml b/gobackup_test.yml index 855a8558..173e8576 100644 --- a/gobackup_test.yml +++ b/gobackup_test.yml @@ -109,16 +109,20 @@ models: notifiers: feishu: type: feishu - url: https://open.feishu.cn/open-apis/bot/v2/hook/a8fb64fa-8f28-46f5-ba7a-c2d0a54a7871 + url: https://open.feishu.cn/open-apis/bot/v2/hook/a8fb64fa-8f28-46f5-ba7a-c2d0a542a78711 dingtalk: type: dingtalk url: https://oapi.dingtalk.com/robot/send?access_token=102f9b03e717078acf830a692e78a07092aca8b6a7fc3f5c2c048baf25de1370r discord: type: discord - url: https://discordapp.com/api/webhooks/1051827238108135424/lWrAw9okY-6LCimnWlHn3pKMr-e3rr4fWn5rKgcjfn92n_RiZbRK9M7Kse-esKDBepV2 + url: https://discordapp.com/api/webhooks/1051827238108135424/lWrAw9okY-6LCimnWlHn3pKMr-e3rr4fWn5rKgcjfn92n_RiZbRK9M7Kse-esKDBepV21 slack: type: slack - url: https://hooks.slack.com/services/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 + url: https://hooks.slack.com/services/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o2 + github: + type: github + url: https://github.com/gobackup/gobackup/pull/111 + access_token: xxxxxxxxxxxx storages: local: type: local diff --git a/notifier/base.go b/notifier/base.go index 87188a0e..f3e962fe 100644 --- a/notifier/base.go +++ b/notifier/base.go @@ -47,6 +47,8 @@ func newNotifier(name string, config config.SubConfig) (Notifier, *Base, error) return NewDiscord(base), base, nil case "slack": return NewSlack(base), base, nil + case "github": + return NewGitHub(base), base, nil } return nil, nil, fmt.Errorf("Notifier: %s is not supported", name) diff --git a/notifier/github.go b/notifier/github.go new file mode 100644 index 00000000..60ab9d26 --- /dev/null +++ b/notifier/github.go @@ -0,0 +1,78 @@ +package notifier + +import ( + "encoding/json" + "fmt" + "regexp" +) + +type githubCommentPayload struct { + Body string `json:"body"` +} + +type githubIssue struct { + group string + repo string + issueId string +} + +var ( + githubIssueRe = regexp.MustCompile(`^http[s]?:\/\/github\.com\/([^\/]+)\/([^\/]+)\/(issues|pull)\/([^\/]+)`) +) + +func parseGitHubIssue(url string) (*githubIssue, error) { + if !githubIssueRe.MatchString(url) { + return nil, fmt.Errorf("invalid GitHub issue URL: %s", url) + } + + matches := githubIssueRe.FindAllStringSubmatch(url, -1) + + return &githubIssue{ + group: matches[0][1], + repo: matches[0][2], + issueId: matches[0][4], + }, nil +} + +func (issue githubIssue) commentURL() string { + return fmt.Sprintf("https://api.github.com/repos/%s/%s/issues/%s/comments", issue.group, issue.repo, issue.issueId) +} + +// type: github +// url: https://github.com/gobackup/gobackup/issues/111 +// access_token: xxxxxxxxxx +func NewGitHub(base *Base) *Webhook { + return &Webhook{ + Base: *base, + Service: "GitHub Comment", + method: "POST", + contentType: "application/json", + buildBody: func(title, message string) ([]byte, error) { + payload := githubCommentPayload{ + Body: fmt.Sprintf("%s\n\n%s", title, message), + } + + return json.Marshal(payload) + }, + buildWebhookURL: func(url string) (string, error) { + issue, err := parseGitHubIssue(url) + if err != nil { + return url, err + } + + return issue.commentURL(), nil + }, + buildHeaders: func() map[string]string { + return map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", base.viper.GetString("access_token")), + } + }, + checkResult: func(status int, body []byte) error { + if status == 200 || status == 201 { + return nil + } + + return fmt.Errorf(`status: %d, body: %s`, status, string(body)) + }, + } +} diff --git a/notifier/github_test.go b/notifier/github_test.go new file mode 100644 index 00000000..a70ec572 --- /dev/null +++ b/notifier/github_test.go @@ -0,0 +1,75 @@ +package notifier + +import ( + "fmt" + "testing" + + "github.com/longbridgeapp/assert" + "github.com/spf13/viper" +) + +func Test_parseGitHubIssue(t *testing.T) { + issue, err := parseGitHubIssue("https://github.com/huacnlee/gobackup/pull/111") + assert.NoError(t, err) + assert.Equal(t, "huacnlee", issue.group) + assert.Equal(t, "gobackup", issue.repo) + assert.Equal(t, "111", issue.issueId) + assert.Equal(t, "https://api.github.com/repos/huacnlee/gobackup/issues/111/comments", issue.commentURL()) + + issue, err = parseGitHubIssue("https://github.com/huacnlee/gobackup/issues/111") + assert.NoError(t, err) + assert.Equal(t, "huacnlee", issue.group) + assert.Equal(t, "gobackup", issue.repo) + assert.Equal(t, "111", issue.issueId) + assert.Equal(t, "https://api.github.com/repos/huacnlee/gobackup/issues/111/comments", issue.commentURL()) + + issue, err = parseGitHubIssue("http://github.com/huacnlee/gobackup/issues/111/foo/bar?foo=1") + assert.NoError(t, err) + assert.Equal(t, "huacnlee", issue.group) + assert.Equal(t, "gobackup", issue.repo) + assert.Equal(t, "111", issue.issueId) + + _, err = parseGitHubIssue("https://github.com/huacnlee") + assert.EqualError(t, err, "invalid GitHub issue URL: https://github.com/huacnlee") +} + +func Test_NewGitHub(t *testing.T) { + base := &Base{ + viper: viper.New(), + } + + s := NewGitHub(base) + s.viper.Set("access_token", "foo_bar_dar") + s.viper.Set("url", "https://github.com/huacnlee/gobackup/issues/111") + + assert.Equal(t, "GitHub Comment", s.Service) + assert.Equal(t, "POST", s.method) + assert.Equal(t, "application/json", s.contentType) + + body, err := s.buildBody("This is title", "This is body") + assert.NoError(t, err) + assert.Equal(t, `{"body":"This is title\n\nThis is body"}`, string(body)) + + url, err := s.buildWebhookURL("https://github.com/huacnlee") + assert.EqualError(t, err, "invalid GitHub issue URL: https://github.com/huacnlee") + assert.Equal(t, "https://github.com/huacnlee", url) + + url, err = s.buildWebhookURL("https://github.com/huacnlee/gobackup/issues/111") + assert.NoError(t, err) + assert.Equal(t, "https://api.github.com/repos/huacnlee/gobackup/issues/111/comments", url) + + headers := s.buildHeaders() + assert.Equal(t, "Bearer foo_bar_dar", headers["Authorization"]) + + respBody := `{"message":"Must have admin rights to Repository.","documentation_url":"https://docs.github.com/rest"}` + err = s.checkResult(404, []byte(respBody)) + assert.EqualError(t, err, fmt.Sprintf("status: %d, body: %s", 404, respBody)) + + respBody = `{}` + err = s.checkResult(200, []byte(respBody)) + assert.NoError(t, err) + + respBody = `{}` + err = s.checkResult(201, []byte(respBody)) + assert.NoError(t, err) +} From 3db8562f85590d363f2849742c710fcd6ff85f94 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 12 Dec 2022 23:36:42 +0800 Subject: [PATCH 6/6] Improve Notifiers for implementation result check. --- notifier/dingtalk.go | 18 +++++++++++++++ notifier/dingtalk_test.go | 8 +++++++ notifier/discord.go | 7 ++++++ notifier/feishu_test.go | 11 +++++++++ notifier/feisu.go | 7 ++++++ notifier/slack.go | 9 ++++++++ notifier/webhook.go | 47 ++++++++++++++++++++++++++++++++------- 7 files changed, 99 insertions(+), 8 deletions(-) diff --git a/notifier/dingtalk.go b/notifier/dingtalk.go index 0e2d8991..e5db7b8a 100644 --- a/notifier/dingtalk.go +++ b/notifier/dingtalk.go @@ -5,6 +5,11 @@ import ( "fmt" ) +type dingtalkResult struct { + Code int `json:"errcode"` + Message string `json:"errmsg"` +} + type dingtalkPayload struct { MsgType string `json:"msgtype"` Text dingtalkPayloadText `json:"text"` @@ -30,5 +35,18 @@ func NewDingtalk(base *Base) *Webhook { return json.Marshal(payload) }, + checkResult: func(status int, body []byte) error { + var result dingtalkResult + err := json.Unmarshal(body, &result) + if err != nil { + return err + } + + if result.Code != 0 || status != 200 { + return fmt.Errorf("status: %d, body: %s", status, string(body)) + } + + return nil + }, } } diff --git a/notifier/dingtalk_test.go b/notifier/dingtalk_test.go index 919f9e38..3abda3c6 100644 --- a/notifier/dingtalk_test.go +++ b/notifier/dingtalk_test.go @@ -1,6 +1,7 @@ package notifier import ( + "fmt" "testing" "github.com/longbridgeapp/assert" @@ -17,4 +18,11 @@ func Test_Dingtalk(t *testing.T) { body, err := s.buildBody("This is title", "This is body") assert.NoError(t, err) assert.Equal(t, `{"msgtype":"text","text":{"content":"This is title\n\nThis is body"}}`, string(body)) + + err = s.checkResult(200, []byte(`{"errcode":0,"errmsg":"ok"}`)) + assert.NoError(t, err) + + respBody := `{"errcode":300001,"errmsg":"Invalid token"}` + err = s.checkResult(403, []byte(respBody)) + assert.EqualError(t, err, fmt.Sprintf("status: %d, body: %s", 403, respBody)) } diff --git a/notifier/discord.go b/notifier/discord.go index 56f60e1b..794fe1bc 100644 --- a/notifier/discord.go +++ b/notifier/discord.go @@ -22,5 +22,12 @@ func NewDiscord(base *Base) *Webhook { return json.Marshal(payload) }, + checkResult: func(status int, body []byte) error { + if status != 200 { + return fmt.Errorf("status: %d, body: %s", status, string(body)) + } + + return nil + }, } } diff --git a/notifier/feishu_test.go b/notifier/feishu_test.go index 4b6742ce..de948e63 100644 --- a/notifier/feishu_test.go +++ b/notifier/feishu_test.go @@ -17,4 +17,15 @@ func Test_Feishu(t *testing.T) { body, err := s.buildBody("This is title", "This is body") assert.NoError(t, err) assert.Equal(t, `{"msg_type":"text","content":{"text":"This is title\n\nThis is body"}}`, string(body)) + + respBody := `{"StatusCode":0,"StatusMessage":"success"}` + err = s.checkResult(200, []byte(respBody)) + assert.NoError(t, err) + + respBody = `{"StatusCode":1000,"StatusMessage":"invalid token"}` + err = s.checkResult(403, []byte(respBody)) + assert.EqualError(t, err, "status: 403, body: "+respBody) + + err = s.checkResult(200, []byte(respBody)) + assert.EqualError(t, err, "status: 200, body: "+respBody) } diff --git a/notifier/feisu.go b/notifier/feisu.go index 946bfa73..bb8117d8 100644 --- a/notifier/feisu.go +++ b/notifier/feisu.go @@ -30,5 +30,12 @@ func NewFeishu(base *Base) *Webhook { return json.Marshal(payload) }, + checkResult: func(status int, body []byte) error { + if status != 200 { + return fmt.Errorf("status: %d, body: %s", status, string(body)) + } + + return nil + }, } } diff --git a/notifier/slack.go b/notifier/slack.go index 53f1e5f9..c633c590 100644 --- a/notifier/slack.go +++ b/notifier/slack.go @@ -9,6 +9,8 @@ type slackPayload struct { Text string `json:"text"` } +// type: slack +// url: https://hooks.slack.com/services/QC0UDT3L8/C58B274E5D0/pwFViFlRXon4WWxo30wevzyR func NewSlack(base *Base) *Webhook { return &Webhook{ Base: *base, @@ -22,5 +24,12 @@ func NewSlack(base *Base) *Webhook { return json.Marshal(payload) }, + checkResult: func(status int, body []byte) error { + if status != 200 { + return fmt.Errorf("status: %d, body: %s", status, string(body)) + } + + return nil + }, } } diff --git a/notifier/webhook.go b/notifier/webhook.go index f046cbf9..126dab22 100644 --- a/notifier/webhook.go +++ b/notifier/webhook.go @@ -13,31 +13,46 @@ type Webhook struct { Base Service string - URL string - method string - contentType string - buildBody func(title, message string) ([]byte, error) + method string + contentType string + buildBody func(title, message string) ([]byte, error) + buildWebhookURL func(url string) (string, error) + checkResult func(status int, responseBody []byte) error + buildHeaders func() map[string]string } func (s *Webhook) getLogger() logger.Logger { return logger.Tag(fmt.Sprintf("Notifier: %s", s.Service)) } +func (s *Webhook) webhookURL() (string, error) { + url := s.viper.GetString("url") + + if s.buildWebhookURL == nil { + return url, nil + } + + return s.buildWebhookURL(url) +} + func (s *Webhook) notify(title string, message string) error { logger := s.getLogger() s.viper.SetDefault("method", "POST") - s.URL = s.viper.GetString("url") + url, err := s.webhookURL() + if err != nil { + return err + } payload, err := s.buildBody(title, message) if err != nil { return err } - logger.Infof("Send message to %s...", s.URL) - req, err := http.NewRequest(s.method, s.URL, strings.NewReader(string(payload))) + logger.Infof("Send message to %s...", url) + req, err := http.NewRequest(s.method, url, strings.NewReader(string(payload))) if err != nil { logger.Error(err) return err @@ -45,6 +60,13 @@ func (s *Webhook) notify(title string, message string) error { req.Header.Set("Content-Type", s.contentType) + if s.buildHeaders != nil { + headers := s.buildHeaders() + for key, value := range headers { + req.Header.Set(key, value) + } + } + client := &http.Client{} resp, err := client.Do(req) if err != nil { @@ -61,7 +83,16 @@ func (s *Webhook) notify(title string, message string) error { } } - logger.Debugf("Response body: %s", string(body)) + if s.checkResult != nil { + err = s.checkResult(resp.StatusCode, body) + if err != nil { + logger.Error(err) + return nil + } + } else { + logger.Infof("Response body: %s", string(body)) + } + logger.Info("Notification sent.") return nil