feat(notifications): implement webhook and email notification services for share events, enhancing user communication capabilities

This commit is contained in:
keven1024
2026-05-01 23:58:35 +08:00
parent bbf6ea561d
commit 4c6a18c063
6 changed files with 153 additions and 2 deletions

View File

@@ -6,9 +6,11 @@ require (
github.com/go-resty/resty/v2 v2.16.5 github.com/go-resty/resty/v2 v2.16.5
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/hibiken/asynq v0.26.0 github.com/hibiken/asynq v0.26.0
github.com/openai/openai-go/v3 v3.30.0
github.com/samber/lo v1.53.0 github.com/samber/lo v1.53.0
github.com/spf13/cast v1.10.0 github.com/spf13/cast v1.10.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/wneessen/go-mail v0.7.2
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
) )
@@ -16,7 +18,6 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/openai/openai-go/v3 v3.30.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/redis/go-redis/v9 v9.18.0 // indirect github.com/redis/go-redis/v9 v9.18.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
@@ -26,6 +27,7 @@ require (
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.15.0 // indirect

View File

@@ -10,6 +10,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -49,6 +51,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -59,6 +63,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=

View File

@@ -0,0 +1,98 @@
package services
import (
"fmt"
"pkg/i18n"
"pkg/models"
u "pkg/utils"
"strconv"
"strings"
"github.com/go-resty/resty/v2"
"github.com/samber/lo"
mail "github.com/wneessen/go-mail"
"go.uber.org/zap"
)
func SendWebhook(webhook models.NotifyWebhook) error {
method := strings.ToUpper(strings.TrimSpace(webhook.Method))
if method == "" {
method = "POST"
}
request := resty.New().R()
if strings.EqualFold(webhook.BodyType, "form-data") {
request.SetHeader("Content-Type", "application/x-www-form-urlencoded")
}
for key, value := range webhook.Headers {
request.SetHeader(key, value)
}
if !strings.EqualFold(webhook.BodyType, "none") && webhook.Body != "" {
request.SetBody(webhook.Body)
}
resp, err := request.Execute(method, webhook.URL)
if err != nil {
return err
}
if resp.StatusCode() >= 400 {
return fmt.Errorf("webhook %s returned status %d", webhook.URL, resp.StatusCode())
}
return nil
}
func SendEmail(to string, shareInfo *models.RedisShareInfo, ip string) error {
host := u.GetEnv("smtp.host")
if host == "" {
zap.L().Warn("smtp host is empty, skip share notify email", zap.String("to", to))
return nil
}
username := u.GetEnv("smtp.username")
password := u.GetEnv("smtp.password")
from := u.GetEnvWithDefault("smtp.from", username)
if from == "" {
return fmt.Errorf("smtp.from or smtp.username is required")
}
templateData := map[string]any{
"IP": ip,
"SiteURL": u.GetEnv("site.url"),
"ShareType": i18n.T(shareInfo.Locale, lo.Ternary(shareInfo.Type == models.ShareTypeText, "share_type_text", "share_type_file")),
"FileName": shareInfo.FileName,
}
subject := i18n.TWithData(shareInfo.Locale, "notify_email_subject", templateData)
body := i18n.TWithData(shareInfo.Locale, "notify_email_body", templateData)
message := mail.NewMsg()
if err := message.From(from); err != nil {
return err
}
if err := message.To(to); err != nil {
return err
}
message.Subject(subject)
message.SetBodyString(mail.TypeTextPlain, body)
port, err := strconv.Atoi(u.GetEnvWithDefault("smtp.port", "587"))
if err != nil {
return err
}
options := []mail.Option{
mail.WithPort(port),
}
if port == mail.DefaultPortSSL {
options = append(options, mail.WithSSL())
} else {
options = append(options, mail.WithTLSPortPolicy(mail.TLSMandatory))
}
if username != "" {
options = append(options, mail.WithUsername(username), mail.WithPassword(password), mail.WithSMTPAuth(mail.SMTPAuthAutoDiscover))
}
client, err := mail.NewClient(host, options...)
if err != nil {
return err
}
return client.DialAndSend(message)
}

View File

@@ -3,10 +3,13 @@ package tasks
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"pkg/models" "pkg/models"
u "pkg/utils" u "pkg/utils"
"worker/internal/services"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
"github.com/samber/lo" "github.com/samber/lo"
@@ -54,5 +57,36 @@ func RemoveShare(ctx context.Context, task *asynq.Task) error {
} }
func ShareNotify(ctx context.Context, task *asynq.Task) error { func ShareNotify(ctx context.Context, task *asynq.Task) error {
return nil var payload ShareNotifyTaskPayload
if err := json.Unmarshal(task.Payload(), &payload); err != nil {
return err
}
shareInfo, err := models.GetRedisShareInfo(payload.ShareId)
if err != nil || shareInfo == nil {
return err
}
var errs []error
successCount := 0
for _, webhook := range shareInfo.NotifyWebhooks {
if err := services.SendWebhook(webhook); err != nil {
errs = append(errs, err)
continue
}
successCount++
}
for _, email := range shareInfo.NotifyEmails {
if err := services.SendEmail(email, shareInfo, payload.IP); err != nil {
errs = append(errs, err)
continue
}
successCount++
}
if successCount > 0 || len(errs) == 0 {
return nil
}
return fmt.Errorf("all share notify targets failed: %w", errors.Join(errs...))
} }

View File

@@ -15,6 +15,11 @@ type RemoveFileTaskPayload struct {
BaseFileTaskPayload BaseFileTaskPayload
} }
type ShareNotifyTaskPayload struct {
ShareId string `json:"share_id"`
IP string `json:"ip"`
}
type CompressImageTaskPayload struct { type CompressImageTaskPayload struct {
BaseFileTaskPayload BaseFileTaskPayload
} }

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"log" "log"
"pkg/i18n"
"pkg/utils" "pkg/utils"
"worker/internal/tasks" "worker/internal/tasks"
"worker/middleware" "worker/middleware"
@@ -22,6 +23,10 @@ func main() {
defer logger.Sync() //nolint:errcheck defer logger.Sync() //nolint:errcheck
zap.ReplaceGlobals(logger) zap.ReplaceGlobals(logger)
if err := i18n.Init(); err != nil {
log.Fatalf("failed to init i18n: %v", err)
}
srv := asynq.NewServer( srv := asynq.NewServer(
utils.RedisURI2AsynqOpt(utils.GetEnv("redis.url")), utils.RedisURI2AsynqOpt(utils.GetEnv("redis.url")),
asynq.Config{Concurrency: cast.ToInt(utils.GetEnvWithDefault("worker.concurrency", "4"))}, asynq.Config{Concurrency: cast.ToInt(utils.GetEnvWithDefault("worker.concurrency", "4"))},
@@ -30,6 +35,7 @@ func main() {
mux := asynq.NewServeMux() mux := asynq.NewServeMux()
mux.Use(middleware.LoggerMiddleware) mux.Use(middleware.LoggerMiddleware)
mux.HandleFunc("share:remove", tasks.RemoveShare) mux.HandleFunc("share:remove", tasks.RemoveShare)
mux.HandleFunc("share:notify", tasks.ShareNotify)
mux.HandleFunc("file:remove", tasks.RemoveFile) mux.HandleFunc("file:remove", tasks.RemoveFile)
mux.HandleFunc("image:compress", tasks.CompressImage) mux.HandleFunc("image:compress", tasks.CompressImage)
mux.HandleFunc("image:convert", tasks.ConvertImage) mux.HandleFunc("image:convert", tasks.ConvertImage)