mirror of
https://github.com/yusing/godoxy.git
synced 2025-07-28 14:43:16 +02:00
[BREAKING] changed notification config format, support multiple notification providers, support webhook and markdown style notification
This commit is contained in:
parent
fb9de4c4ad
commit
25eeabb9f9
12 changed files with 522 additions and 99 deletions
|
@ -187,14 +187,17 @@ func (cfg *Config) load() E.Error {
|
||||||
return errs.Error()
|
return errs.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) initNotification(notifCfgMap types.NotificationConfigMap) (err E.Error) {
|
func (cfg *Config) initNotification(notifCfg []types.NotificationConfig) (err E.Error) {
|
||||||
if len(notifCfgMap) == 0 {
|
if len(notifCfg) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
errs := E.NewBuilder("notification providers load errors")
|
errs := E.NewBuilder("notification providers load errors")
|
||||||
for name, notifCfg := range notifCfgMap {
|
for i, notifier := range notifCfg {
|
||||||
_, err := notif.RegisterProvider(cfg.task.Subtask(name), notifCfg)
|
_, err := notif.RegisterProvider(cfg.task.Subtask("notifier"), notifier)
|
||||||
errs.Add(err)
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
errs.Add(err.Subjectf("[%d]", i))
|
||||||
}
|
}
|
||||||
return errs.Error()
|
return errs.Error()
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,9 @@ type (
|
||||||
Providers struct {
|
Providers struct {
|
||||||
Files []string `json:"include" yaml:"include"`
|
Files []string `json:"include" yaml:"include"`
|
||||||
Docker map[string]string `json:"docker" yaml:"docker"`
|
Docker map[string]string `json:"docker" yaml:"docker"`
|
||||||
Notification NotificationConfigMap `json:"notification" yaml:"notification"`
|
Notification []NotificationConfig `json:"notification" yaml:"notification"`
|
||||||
}
|
}
|
||||||
|
NotificationConfig map[string]any
|
||||||
)
|
)
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
package types
|
|
||||||
|
|
||||||
import "github.com/yusing/go-proxy/internal/notif"
|
|
||||||
|
|
||||||
type NotificationConfigMap map[string]notif.ProviderConfig
|
|
23
internal/notif/color.go
Normal file
23
internal/notif/color.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package notif
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Color uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
Red Color = 0xff0000
|
||||||
|
Green Color = 0x00ff00
|
||||||
|
Blue Color = 0x0000ff
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c Color) HexString() string {
|
||||||
|
return fmt.Sprintf("#%x", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) DecString() string {
|
||||||
|
return fmt.Sprintf("%d", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) String() string {
|
||||||
|
return c.HexString()
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package notif
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/yusing/go-proxy/internal/config/types"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
"github.com/yusing/go-proxy/internal/task"
|
"github.com/yusing/go-proxy/internal/task"
|
||||||
|
@ -18,13 +19,19 @@ type (
|
||||||
}
|
}
|
||||||
LogMessage struct {
|
LogMessage struct {
|
||||||
Level zerolog.Level
|
Level zerolog.Level
|
||||||
Title, Message string
|
Title string
|
||||||
|
Extras map[string]any
|
||||||
|
Color Color
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var dispatcher *Dispatcher
|
var dispatcher *Dispatcher
|
||||||
|
|
||||||
var ErrUnknownNotifProvider = E.New("unknown notification provider")
|
var (
|
||||||
|
ErrMissingNotifProvider = E.New("missing notification provider")
|
||||||
|
ErrInvalidNotifProviderType = E.New("invalid notification provider type")
|
||||||
|
ErrUnknownNotifProvider = E.New("unknown notification provider")
|
||||||
|
)
|
||||||
|
|
||||||
const dispatchErr = "notification dispatch error"
|
const dispatchErr = "notification dispatch error"
|
||||||
|
|
||||||
|
@ -45,13 +52,19 @@ func GetDispatcher() *Dispatcher {
|
||||||
return dispatcher
|
return dispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterProvider(configSubTask task.Task, cfg ProviderConfig) (Provider, error) {
|
func RegisterProvider(configSubTask task.Task, cfg types.NotificationConfig) (Provider, E.Error) {
|
||||||
name := configSubTask.Name()
|
providerName, ok := cfg["provider"]
|
||||||
createFunc, ok := Providers[name]
|
if !ok {
|
||||||
|
return nil, ErrMissingNotifProvider
|
||||||
|
}
|
||||||
|
switch providerName := providerName.(type) {
|
||||||
|
case string:
|
||||||
|
delete(cfg, "provider")
|
||||||
|
createFunc, ok := Providers[providerName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, ErrUnknownNotifProvider.
|
return nil, ErrUnknownNotifProvider.
|
||||||
Subject(name).
|
Subject(providerName).
|
||||||
Withf(strutils.DoYouMean(utils.NearestField(name, Providers)))
|
Withf(strutils.DoYouMean(utils.NearestField(providerName, Providers)))
|
||||||
}
|
}
|
||||||
|
|
||||||
provider, err := createFunc(cfg)
|
provider, err := createFunc(cfg)
|
||||||
|
@ -62,6 +75,9 @@ func RegisterProvider(configSubTask task.Task, cfg ProviderConfig) (Provider, er
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return provider, err
|
return provider, err
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidNotifProviderType.Subjectf("%T", providerName)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (disp *Dispatcher) start() {
|
func (disp *Dispatcher) start() {
|
||||||
|
@ -83,14 +99,14 @@ func (disp *Dispatcher) dispatch(msg *LogMessage) {
|
||||||
|
|
||||||
errs := E.NewBuilder(dispatchErr)
|
errs := E.NewBuilder(dispatchErr)
|
||||||
disp.providers.RangeAllParallel(func(p Provider) {
|
disp.providers.RangeAllParallel(func(p Provider) {
|
||||||
if err := p.Send(task.Context(), msg); err != nil {
|
if err := notifyProvider(task.Context(), p, msg); err != nil {
|
||||||
errs.Add(E.PrependSubject(p.Name(), err))
|
errs.Add(E.PrependSubject(p.Name(), err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if errs.HasError() {
|
if errs.HasError() {
|
||||||
E.LogError(errs.About(), errs.Error())
|
E.LogError(errs.About(), errs.Error())
|
||||||
} else {
|
} else {
|
||||||
logging.Debug().Str("title", msg.Title).Str("message", msg.Message).Msgf("dispatched notif")
|
logging.Debug().Str("title", msg.Title).Msgf("dispatched notif")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,10 +124,6 @@ func (disp *Dispatcher) dispatch(msg *LogMessage) {
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func Notify(title, msg string) {
|
func Notify(msg *LogMessage) {
|
||||||
dispatcher.logCh <- &LogMessage{
|
dispatcher.logCh <- msg
|
||||||
Level: zerolog.InfoLevel,
|
|
||||||
Title: title,
|
|
||||||
Message: msg,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
36
internal/notif/format.go
Normal file
36
internal/notif/format.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package notif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func formatMarkdown(extras map[string]interface{}) string {
|
||||||
|
msg := bytes.NewBufferString("")
|
||||||
|
for k, v := range extras {
|
||||||
|
msg.WriteString("#### ")
|
||||||
|
msg.WriteString(k)
|
||||||
|
msg.WriteRune('\n')
|
||||||
|
msg.WriteString(fmt.Sprintf("%v", v))
|
||||||
|
msg.WriteRune('\n')
|
||||||
|
}
|
||||||
|
return msg.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDiscord(extras map[string]interface{}) (string, error) {
|
||||||
|
fieldsMap := make([]map[string]any, len(extras))
|
||||||
|
i := 0
|
||||||
|
for k, extra := range extras {
|
||||||
|
fieldsMap[i] = map[string]any{
|
||||||
|
"name": k,
|
||||||
|
"value": extra,
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
fields, err := json.Marshal(fieldsMap)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(fields), nil
|
||||||
|
}
|
|
@ -2,57 +2,53 @@ package notif
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/gotify/server/v2/model"
|
"github.com/gotify/server/v2/model"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
|
||||||
U "github.com/yusing/go-proxy/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
GotifyClient struct {
|
GotifyClient struct {
|
||||||
GotifyConfig
|
N string `json:"name" validate:"required"`
|
||||||
|
U string `json:"url" validate:"url"`
|
||||||
url *url.URL
|
Tok string `json:"token" validate:"required"`
|
||||||
http http.Client
|
|
||||||
}
|
}
|
||||||
GotifyConfig struct {
|
GotifyMessage model.MessageExternal
|
||||||
URL string `json:"url" yaml:"url"`
|
|
||||||
Token string `json:"token" yaml:"token"`
|
|
||||||
}
|
|
||||||
GotifyMessage model.Message
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const gotifyMsgEndpoint = "/message"
|
const gotifyMsgEndpoint = "/message"
|
||||||
|
|
||||||
func newGotifyClient(cfg map[string]any) (Provider, E.Error) {
|
// Name implements Provider.
|
||||||
client := new(GotifyClient)
|
|
||||||
err := U.Deserialize(cfg, &client.GotifyConfig)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
url, uErr := url.Parse(client.URL)
|
|
||||||
if uErr != nil {
|
|
||||||
return nil, E.Errorf("invalid gotify URL %s", client.URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
client.url = url
|
|
||||||
return client, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name implements NotifProvider.
|
|
||||||
func (client *GotifyClient) Name() string {
|
func (client *GotifyClient) Name() string {
|
||||||
return "gotify"
|
return client.N
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send implements NotifProvider.
|
// Method implements Provider.
|
||||||
func (client *GotifyClient) Send(ctx context.Context, logMsg *LogMessage) error {
|
func (client *GotifyClient) Method() string {
|
||||||
|
return http.MethodPost
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL implements Provider.
|
||||||
|
func (client *GotifyClient) URL() string {
|
||||||
|
return client.U + gotifyMsgEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token implements Provider.
|
||||||
|
func (client *GotifyClient) Token() string {
|
||||||
|
return client.Tok
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIMEType implements Provider.
|
||||||
|
func (client *GotifyClient) MIMEType() string {
|
||||||
|
return "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeBody implements Provider.
|
||||||
|
func (client *GotifyClient) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||||
var priority int
|
var priority int
|
||||||
|
|
||||||
switch logMsg.Level {
|
switch logMsg.Level {
|
||||||
|
@ -66,37 +62,29 @@ func (client *GotifyClient) Send(ctx context.Context, logMsg *LogMessage) error
|
||||||
|
|
||||||
msg := &GotifyMessage{
|
msg := &GotifyMessage{
|
||||||
Title: logMsg.Title,
|
Title: logMsg.Title,
|
||||||
Message: logMsg.Message,
|
Message: formatMarkdown(logMsg.Extras),
|
||||||
Priority: priority,
|
Priority: &priority,
|
||||||
|
Extras: map[string]interface{}{
|
||||||
|
"client::display": map[string]string{
|
||||||
|
"contentType": "text/markdown",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(msg)
|
data, err := json.Marshal(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, client.url.String()+gotifyMsgEndpoint, bytes.NewReader(data))
|
return bytes.NewReader(data), nil
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error creating request: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
// makeRespError implements Provider.
|
||||||
req.Header.Set("Authorization", "Bearer "+client.Token)
|
func (client *GotifyClient) makeRespError(resp *http.Response) error {
|
||||||
|
|
||||||
resp, err := client.http.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to send gotify message: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
var errm model.Error
|
var errm model.Error
|
||||||
err = json.NewDecoder(resp.Body).Decode(&errm)
|
err := json.NewDecoder(resp.Body).Decode(&errm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("gotify status %d, but failed to decode err response: %w", resp.StatusCode, err)
|
return fmt.Errorf(ProviderGotify+" status %d, but failed to decode err response: %w", resp.StatusCode, err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("gotify status %d %s: %s", resp.StatusCode, errm.Error, errm.ErrorDescription)
|
return fmt.Errorf(ProviderGotify+" status %d %s: %s", resp.StatusCode, errm.Error, errm.ErrorDescription)
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
52
internal/notif/gotify_test.go
Normal file
52
internal/notif/gotify_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package notif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGotifyValidation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
newGotify := Providers[ProviderGotify]
|
||||||
|
|
||||||
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newGotify(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"token": "token",
|
||||||
|
})
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing url", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newGotify(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"token": "token",
|
||||||
|
})
|
||||||
|
ExpectError(t, utils.ErrValidationError, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing token", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newGotify(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "https://example.com",
|
||||||
|
})
|
||||||
|
ExpectError(t, utils.ErrValidationError, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid url", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newGotify(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "example.com",
|
||||||
|
"token": "token",
|
||||||
|
})
|
||||||
|
ExpectError(t, utils.ErrValidationError, err)
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,19 +2,78 @@ package notif
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
|
gphttp "github.com/yusing/go-proxy/internal/net/http"
|
||||||
|
U "github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Provider interface {
|
Provider interface {
|
||||||
Name() string
|
Name() string
|
||||||
Send(ctx context.Context, logMsg *LogMessage) error
|
URL() string
|
||||||
|
Method() string
|
||||||
|
Token() string
|
||||||
|
MIMEType() string
|
||||||
|
MakeBody(logMsg *LogMessage) (io.Reader, error)
|
||||||
|
|
||||||
|
makeRespError(resp *http.Response) error
|
||||||
}
|
}
|
||||||
ProviderCreateFunc func(map[string]any) (Provider, E.Error)
|
ProviderCreateFunc func(map[string]any) (Provider, E.Error)
|
||||||
ProviderConfig map[string]any
|
ProviderConfig map[string]any
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProviderGotify = "gotify"
|
||||||
|
ProviderWebhook = "webhook"
|
||||||
|
)
|
||||||
|
|
||||||
var Providers = map[string]ProviderCreateFunc{
|
var Providers = map[string]ProviderCreateFunc{
|
||||||
"gotify": newGotifyClient,
|
ProviderGotify: newNotifProvider[*GotifyClient],
|
||||||
|
ProviderWebhook: newNotifProvider[*Webhook],
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNotifProvider[T Provider](cfg map[string]any) (Provider, E.Error) {
|
||||||
|
var client T
|
||||||
|
err := U.Deserialize(cfg, &client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err.Subject(client.Name())
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func notifyProvider(ctx context.Context, provider Provider, msg *LogMessage) error {
|
||||||
|
body, err := provider.MakeBody(msg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s error: %w", provider.Name(), err)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodPost,
|
||||||
|
provider.URL(),
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s error: %w", provider.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", provider.MIMEType())
|
||||||
|
if provider.Token() != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+provider.Token())
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s error: %w", provider.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if !gphttp.IsSuccess(resp.StatusCode) {
|
||||||
|
return provider.makeRespError(resp)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
9
internal/notif/templates/discord.json
Normal file
9
internal/notif/templates/discord.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"embeds": [
|
||||||
|
{
|
||||||
|
"title": $title,
|
||||||
|
"fields": $fields,
|
||||||
|
"color": "$color"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
133
internal/notif/webhook.go
Normal file
133
internal/notif/webhook.go
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
package notif
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Webhook struct {
|
||||||
|
N string `json:"name" validate:"required"`
|
||||||
|
U string `json:"url" validate:"url"`
|
||||||
|
Template string `json:"template" validate:"omitempty,oneof=discord"`
|
||||||
|
Payload string `json:"payload" validate:"jsonIfTemplateNotUsed"`
|
||||||
|
Tok string `json:"token"`
|
||||||
|
Meth string `json:"method" validate:"omitempty,oneof=GET POST PUT"`
|
||||||
|
MIMETyp string `json:"mime_type"`
|
||||||
|
ColorM string `json:"color_mode" validate:"omitempty,oneof=hex dec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed templates/discord.json
|
||||||
|
var discordPayload string
|
||||||
|
|
||||||
|
var webhookTemplates = map[string]string{
|
||||||
|
"discord": discordPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonIfTemplateNotUsed(fl validator.FieldLevel) bool {
|
||||||
|
template := fl.Parent().FieldByName("Template").String()
|
||||||
|
if template != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
payload := fl.Field().String()
|
||||||
|
return json.Valid([]byte(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
utils.Validator().RegisterValidation("jsonIfTemplateNotUsed", jsonIfTemplateNotUsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name implements Provider.
|
||||||
|
func (webhook *Webhook) Name() string {
|
||||||
|
return webhook.N
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method implements Provider.
|
||||||
|
func (webhook *Webhook) Method() string {
|
||||||
|
if webhook.Meth != "" {
|
||||||
|
return webhook.Meth
|
||||||
|
} else {
|
||||||
|
return http.MethodPost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL implements Provider.
|
||||||
|
func (webhook *Webhook) URL() string {
|
||||||
|
return webhook.U
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token implements Provider.
|
||||||
|
func (webhook *Webhook) Token() string {
|
||||||
|
return webhook.Tok
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIMEType implements Provider.
|
||||||
|
func (webhook *Webhook) MIMEType() string {
|
||||||
|
if webhook.MIMETyp != "" {
|
||||||
|
return webhook.MIMETyp
|
||||||
|
} else {
|
||||||
|
return "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Webhook *Webhook) ColorMode() string {
|
||||||
|
switch Webhook.Template {
|
||||||
|
case "discord":
|
||||||
|
return "dec"
|
||||||
|
default:
|
||||||
|
return Webhook.ColorM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeRespError implements Provider.
|
||||||
|
func (webhook *Webhook) makeRespError(resp *http.Response) error {
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("webhook status %d, failed to read body: %w", resp.StatusCode, err)
|
||||||
|
}
|
||||||
|
if len(body) > 0 {
|
||||||
|
return fmt.Errorf("webhook status %d: %s", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("webhook status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (webhook *Webhook) MakeBody(logMsg *LogMessage) (io.Reader, error) {
|
||||||
|
title, err := json.Marshal(logMsg.Title)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fields, err := formatDiscord(logMsg.Extras)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var color string
|
||||||
|
if webhook.ColorMode() == "hex" {
|
||||||
|
color = logMsg.Color.HexString()
|
||||||
|
} else {
|
||||||
|
color = logMsg.Color.DecString()
|
||||||
|
}
|
||||||
|
message, err := json.Marshal(formatMarkdown(logMsg.Extras))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
plTempl := strings.NewReplacer(
|
||||||
|
"$title", string(title),
|
||||||
|
"$message", string(message),
|
||||||
|
"$fields", string(fields),
|
||||||
|
"$color", color,
|
||||||
|
)
|
||||||
|
var pl string
|
||||||
|
if webhook.Template != "" {
|
||||||
|
pl = webhookTemplates[webhook.Template]
|
||||||
|
} else {
|
||||||
|
pl = webhook.Payload
|
||||||
|
}
|
||||||
|
pl = plTempl.Replace(pl)
|
||||||
|
return strings.NewReader(pl), nil
|
||||||
|
}
|
112
internal/notif/webhook_test.go
Normal file
112
internal/notif/webhook_test.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package notif
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWebhookValidation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
newWebhook := Providers[ProviderWebhook]
|
||||||
|
|
||||||
|
t.Run("valid", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newWebhook(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"payload": "{}",
|
||||||
|
})
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
})
|
||||||
|
t.Run("valid template", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newWebhook(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"template": "discord",
|
||||||
|
})
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing url", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newWebhook(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"payload": "{}",
|
||||||
|
})
|
||||||
|
ExpectError(t, utils.ErrValidationError, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing payload", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newWebhook(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "https://example.com",
|
||||||
|
})
|
||||||
|
ExpectError(t, utils.ErrValidationError, err)
|
||||||
|
})
|
||||||
|
t.Run("invalid url", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newWebhook(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "example.com",
|
||||||
|
"payload": "{}",
|
||||||
|
})
|
||||||
|
ExpectError(t, utils.ErrValidationError, err)
|
||||||
|
})
|
||||||
|
t.Run("invalid payload", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newWebhook(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"payload": "abcd",
|
||||||
|
})
|
||||||
|
ExpectError(t, utils.ErrValidationError, err)
|
||||||
|
})
|
||||||
|
t.Run("invalid method", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newWebhook(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"payload": "{}",
|
||||||
|
"method": "abcd",
|
||||||
|
})
|
||||||
|
ExpectError(t, utils.ErrValidationError, err)
|
||||||
|
})
|
||||||
|
t.Run("invalid template", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
_, err := newWebhook(map[string]any{
|
||||||
|
"name": "test",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"template": "abcd",
|
||||||
|
})
|
||||||
|
ExpectError(t, utils.ErrValidationError, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebhookBody(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var webhook Webhook
|
||||||
|
webhook.Payload = discordPayload
|
||||||
|
bodyReader, err := webhook.MakeBody(&LogMessage{
|
||||||
|
Title: "abc",
|
||||||
|
Extras: map[string]any{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
|
||||||
|
var body map[string][]map[string]any
|
||||||
|
err = json.NewDecoder(bodyReader).Decode(&body)
|
||||||
|
ExpectNoError(t, err)
|
||||||
|
|
||||||
|
ExpectEqual(t, body["embeds"][0]["title"], "abc")
|
||||||
|
fields := ExpectType[[]map[string]any](t, body["embeds"][0]["fields"])
|
||||||
|
ExpectEqual(t, fields[0]["name"], "foo")
|
||||||
|
ExpectEqual(t, fields[0]["value"], "bar")
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue