package service import ( "bytes" "fmt" "net/http" "net/url" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/relaykit/dto" "github.com/QuantumNous/new-api/setting/system_setting" ) // NotifyChannelWatchers sends a channel notice (a channel disabled or enabled // automatically, or a test of all channels finished) to every administrator // who receives channel notices. Until they choose, the root user receives them // and other administrators do not. render builds the subject and content in // the recipient's saved language; an empty language means DEFAULT_LANGUAGE. func NotifyChannelWatchers(notifyType string, render func(lang string) (subject string, content string)) { notifyAdmins(notifyType, func(role int, setting dto.UserSetting) bool { if setting.ChannelNotifyEnabled != nil { return *setting.ChannelNotifyEnabled } return role == common.RoleRootUser }, render) } // NotifyUpstreamModelUpdateWatchers sends the upstream model update notice to // every administrator who enabled it. render builds the subject and content in // the recipient's saved language; an empty language means DEFAULT_LANGUAGE. func NotifyUpstreamModelUpdateWatchers(render func(lang string) (subject string, content string)) { sentCount := notifyAdmins(dto.NotifyTypeChannelUpdate, func(_ int, setting dto.UserSetting) bool { return setting.UpstreamModelUpdateNotifyEnabled }, render) common.SysLog(common.LogText("upstream model update notifications sent: %d", sentCount)) } // notifyAdmins sends a notice to each enabled administrator for whom receives // returns true, and returns how many notices it sent. func notifyAdmins(notifyType string, receives func(role int, setting dto.UserSetting) bool, render func(lang string) (subject string, content string)) int { var users []model.User if err := model.DB. Select("id", "email", "role", "status", "setting"). Where("status = ? AND role >= ?", common.UserStatusEnabled, common.RoleAdminUser). Find(&users).Error; err != nil { common.SysLog(common.LogText("failed to query administrators to notify: %s", err.Error())) return 0 } sentCount := 0 for _, user := range users { userSetting := user.GetSetting() if !receives(user.Role, userSetting) { continue } subject, content := render(userSetting.Language) if err := NotifyUser(user.Id, user.Email, userSetting, dto.NewNotify(notifyType, subject, content, nil)); err != nil { common.SysLog(common.LogText("failed to send notice %s to user %d: %s", notifyType, user.Id, err.Error())) continue } sentCount++ } return sentCount } func NotifyUser(userId int, userEmail string, userSetting dto.UserSetting, data dto.Notify) error { notifyType := userSetting.NotifyType if notifyType == "" { notifyType = dto.NotifyTypeEmail } // Check notification limit canSend, err := CheckNotificationLimit(userId, data.Type) if err != nil { common.SysLog(common.LogText("failed to check notification limit: %s", err.Error())) return err } if !canSend { return fmt.Errorf("notification limit exceeded for user %d with type %s", userId, notifyType) } switch notifyType { case dto.NotifyTypeEmail: // 优先使用设置中的通知邮箱,如果为空则使用用户的默认邮箱 emailToUse := userSetting.NotificationEmail if emailToUse == "" { emailToUse = userEmail } if emailToUse == "" { common.SysLog(common.LogText("user %d has no email, skip sending email", userId)) return nil } return sendEmailNotify(emailToUse, data) case dto.NotifyTypeWebhook: webhookURLStr := userSetting.WebhookUrl if webhookURLStr == "" { common.SysLog(common.LogText("user %d has no webhook url, skip sending webhook", userId)) return nil } // 获取 webhook secret webhookSecret := userSetting.WebhookSecret return SendWebhookNotify(webhookURLStr, webhookSecret, data) case dto.NotifyTypeBark: barkURL := userSetting.BarkUrl if barkURL == "" { common.SysLog(common.LogText("user %d has no bark url, skip sending bark", userId)) return nil } return sendBarkNotify(barkURL, data) case dto.NotifyTypeGotify: gotifyUrl := userSetting.GotifyUrl gotifyToken := userSetting.GotifyToken if gotifyUrl == "" || gotifyToken == "" { common.SysLog(common.LogText("user %d has no gotify url or token, skip sending gotify", userId)) return nil } return sendGotifyNotify(gotifyUrl, gotifyToken, userSetting.GotifyPriority, data) } return nil } func sendEmailNotify(userEmail string, data dto.Notify) error { // make email content content := data.Content // 处理占位符 for _, value := range data.Values { content = strings.Replace(content, dto.ContentValueParam, fmt.Sprintf("%v", value), 1) } return common.SendEmail(data.Title, userEmail, content) } func sendBarkNotify(barkURL string, data dto.Notify) error { // 处理占位符 content := data.Content for _, value := range data.Values { content = strings.Replace(content, dto.ContentValueParam, fmt.Sprintf("%v", value), 1) } // 替换模板变量 finalURL := strings.ReplaceAll(barkURL, "{{title}}", url.QueryEscape(data.Title)) finalURL = strings.ReplaceAll(finalURL, "{{content}}", url.QueryEscape(content)) // 发送GET请求到Bark var req *http.Request var resp *http.Response var err error if system_setting.EnableWorker() { // 使用worker发送请求 workerReq := &WorkerRequest{ URL: finalURL, Key: system_setting.WorkerValidKey, Method: http.MethodGet, Headers: map[string]string{ "User-Agent": "OneAPI-Bark-Notify/1.0", }, } resp, err = DoWorkerRequest(workerReq) if err != nil { return fmt.Errorf("failed to send bark request through worker: %v", err) } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("bark request failed with status code: %d", resp.StatusCode) } } else { // SSRF防护:验证Bark URL(非Worker模式) if err := ValidateSSRFProtectedFetchURL(finalURL); err != nil { return fmt.Errorf("request reject: %v", err) } // 直接发送请求 req, err = http.NewRequest(http.MethodGet, finalURL, nil) if err != nil { return fmt.Errorf("failed to create bark request: %v", err) } // 设置User-Agent req.Header.Set("User-Agent", "OneAPI-Bark-Notify/1.0") // 发送请求 client := GetSSRFProtectedHTTPClient() resp, err = client.Do(req) if err != nil { return fmt.Errorf("failed to send bark request: %v", err) } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("bark request failed with status code: %d", resp.StatusCode) } } return nil } func sendGotifyNotify(gotifyUrl string, gotifyToken string, priority int, data dto.Notify) error { // 处理占位符 content := data.Content for _, value := range data.Values { content = strings.Replace(content, dto.ContentValueParam, fmt.Sprintf("%v", value), 1) } // 构建完整的 Gotify API URL // 确保 URL 以 /message 结尾 finalURL := strings.TrimSuffix(gotifyUrl, "/") + "/message?token=" + url.QueryEscape(gotifyToken) // Gotify优先级范围0-10,如果超出范围则使用默认值5 if priority < 0 || priority > 10 { priority = 5 } // 构建 JSON payload type GotifyMessage struct { Title string `json:"title"` Message string `json:"message"` Priority int `json:"priority"` } payload := GotifyMessage{ Title: data.Title, Message: content, Priority: priority, } // 序列化为 JSON payloadBytes, err := common.Marshal(payload) if err != nil { return fmt.Errorf("failed to marshal gotify payload: %v", err) } var req *http.Request var resp *http.Response if system_setting.EnableWorker() { // 使用worker发送请求 workerReq := &WorkerRequest{ URL: finalURL, Key: system_setting.WorkerValidKey, Method: http.MethodPost, Headers: map[string]string{ "Content-Type": "application/json; charset=utf-8", "User-Agent": "OneAPI-Gotify-Notify/1.0", }, Body: payloadBytes, } resp, err = DoWorkerRequest(workerReq) if err != nil { return fmt.Errorf("failed to send gotify request through worker: %v", err) } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("gotify request failed with status code: %d", resp.StatusCode) } } else { // SSRF防护:验证Gotify URL(非Worker模式) if err := ValidateSSRFProtectedFetchURL(finalURL); err != nil { return fmt.Errorf("request reject: %v", err) } // 直接发送请求 req, err = http.NewRequest(http.MethodPost, finalURL, bytes.NewBuffer(payloadBytes)) if err != nil { return fmt.Errorf("failed to create gotify request: %v", err) } // 设置请求头 req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("User-Agent", "NewAPI-Gotify-Notify/1.0") // 发送请求 client := GetSSRFProtectedHTTPClient() resp, err = client.Do(req) if err != nil { return fmt.Errorf("failed to send gotify request: %v", err) } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("gotify request failed with status code: %d", resp.StatusCode) } } return nil }