diff --git a/worker/go.mod b/worker/go.mod index 85cb2f7..19146cc 100644 --- a/worker/go.mod +++ b/worker/go.mod @@ -6,9 +6,11 @@ require ( github.com/go-resty/resty/v2 v2.16.5 github.com/google/uuid v1.6.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/spf13/cast v1.10.0 github.com/stretchr/testify v1.11.1 + github.com/wneessen/go-mail v0.7.2 go.uber.org/zap v1.27.1 ) @@ -16,7 +18,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // 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/redis/go-redis/v9 v9.18.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect @@ -26,6 +27,7 @@ require ( github.com/tidwall/sjson v1.2.5 // indirect go.uber.org/atomic 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/text v0.35.0 // indirect golang.org/x/time v0.15.0 // indirect diff --git a/worker/go.sum b/worker/go.sum index b3f5dcf..d656f64 100644 --- a/worker/go.sum +++ b/worker/go.sum @@ -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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 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/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 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/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= 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/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= diff --git a/worker/internal/services/notify.go b/worker/internal/services/notify.go new file mode 100644 index 0000000..376f4d0 --- /dev/null +++ b/worker/internal/services/notify.go @@ -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) +} diff --git a/worker/internal/tasks/share.go b/worker/internal/tasks/share.go index d618654..20c4a5d 100644 --- a/worker/internal/tasks/share.go +++ b/worker/internal/tasks/share.go @@ -3,10 +3,13 @@ package tasks import ( "context" "encoding/json" + "errors" + "fmt" "os" "path/filepath" "pkg/models" u "pkg/utils" + "worker/internal/services" "github.com/hibiken/asynq" "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 { - 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...)) } diff --git a/worker/internal/tasks/types.go b/worker/internal/tasks/types.go index 090ace7..611ef2e 100644 --- a/worker/internal/tasks/types.go +++ b/worker/internal/tasks/types.go @@ -15,6 +15,11 @@ type RemoveFileTaskPayload struct { BaseFileTaskPayload } +type ShareNotifyTaskPayload struct { + ShareId string `json:"share_id"` + IP string `json:"ip"` +} + type CompressImageTaskPayload struct { BaseFileTaskPayload } diff --git a/worker/main.go b/worker/main.go index e9335ea..0fc1da9 100644 --- a/worker/main.go +++ b/worker/main.go @@ -2,6 +2,7 @@ package main import ( "log" + "pkg/i18n" "pkg/utils" "worker/internal/tasks" "worker/middleware" @@ -22,6 +23,10 @@ func main() { defer logger.Sync() //nolint:errcheck zap.ReplaceGlobals(logger) + if err := i18n.Init(); err != nil { + log.Fatalf("failed to init i18n: %v", err) + } + srv := asynq.NewServer( utils.RedisURI2AsynqOpt(utils.GetEnv("redis.url")), asynq.Config{Concurrency: cast.ToInt(utils.GetEnvWithDefault("worker.concurrency", "4"))}, @@ -30,6 +35,7 @@ func main() { mux := asynq.NewServeMux() mux.Use(middleware.LoggerMiddleware) mux.HandleFunc("share:remove", tasks.RemoveShare) + mux.HandleFunc("share:notify", tasks.ShareNotify) mux.HandleFunc("file:remove", tasks.RemoveFile) mux.HandleFunc("image:compress", tasks.CompressImage) mux.HandleFunc("image:convert", tasks.ConvertImage)