Skip to content

feat: bark notification #1699 - #1712

Merged
seefs001 merged 1 commit into
QuantumNous:alphafrom
seefs001:feature/bark
Sep 1, 2025
Merged

feat: bark notification #1699#1712
seefs001 merged 1 commit into
QuantumNous:alphafrom
seefs001:feature/bark

Conversation

@seefs001

@seefs001 seefs001 commented Sep 1, 2025

Copy link
Copy Markdown
Collaborator
image image

Summary by CodeRabbit

  • New Features
    • Added Bark as a notification channel for quota warnings.
    • Notification Settings now include a Bark option with a configurable Bark URL and in-form validation (must use HTTP/HTTPS).
    • UI provides examples and guidance for using template variables in the Bark URL.
    • Bark notifications deliver concise text content; existing Email/Webhook behavior remains unchanged.
    • Personal settings now load and save the Bark URL alongside other notification preferences.

@coderabbitai

coderabbitai Bot commented Sep 1, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds Bark as a notification channel across backend and frontend: new bark_url fields in requests and user settings; validation and persistence in user setting updates; quota notifications adapt content based on type; NotifyUser supports Bark via HTTP GET (worker or direct). UI exposes Bark option with URL input and validation.

Changes

Cohort / File(s) Summary
Backend DTOs & Types
dto/user_settings.go
Added NotifyTypeBark = "bark" and BarkUrl string json:"bark_url,omitempty" to UserSetting.
User Settings Controller
controller/user.go
Extended UpdateUserSettingRequest with BarkUrl. Validates Bark selection (non-empty, valid http/https URL). Persists settings.BarkUrl when type is Bark.
Quota Notification Logic
service/quota.go
checkAndSendQuotaNotify now formats notification content based on type: short/plain for Bark; existing HTML with top-up link for others. Constructs dto.NewNotify accordingly.
Notification Dispatch Service
service/user_notify.go
Added Bark handling in NotifyUser. Implemented sendBarkNotify to build content, substitute {{title}}/{{content}} into Bark URL, send GET via worker or direct HTTP, require 2xx response, set custom User-Agent.
Web UI: Personal Settings State
web/src/components/settings/PersonalSetting.jsx
Added barkUrl state, load from settings.bark_url, include bark_url in save payload.
Web UI: Notification Settings Card
web/src/components/settings/personal/cards/NotificationSettings.jsx
Added Bark option to method radios. Shows Bark URL input when selected, with http/https validation, helper text, and example/template docs link.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant FE as Web UI (Settings)
  participant API as Controller (UpdateUserSetting)
  participant S as User Service/Repo

  U->>FE: Select notify type = Bark, enter Bark URL
  FE->>API: PUT /user/settings { notify_type: bark, bark_url }
  API->>API: Validate notify_type and bark_url (non-empty, URL http/https)
  API->>S: SetSetting(BarkUrl), Update(user)
  S-->>API: OK
  API-->>FE: 200 OK
Loading
sequenceDiagram
  autonumber
  participant Q as Quota Checker
  participant NS as Notification Service
  participant Bark as Bark Endpoint

  Q->>NS: NotifyUser(user, notify{type?, values})
  alt type == bark
    NS->>NS: Build plain text content (no HTML)
    NS->>Bark: HTTP GET bark_url?title={{..}}&body={{..}}
    Bark-->>NS: 2xx
  else other types
    NS->>NS: Build HTML content with top-up link
    NS->>NS: Send via existing channel (email/webhook)
  end
Loading
sequenceDiagram
  autonumber
  participant NS as Notification Service
  participant W as Worker Gateway
  participant HTTP as Direct HTTP

  NS->>NS: sendBarkNotify(barkURL, data)
  alt Worker enabled
    NS->>W: DoWorkerRequest(GET, barkURL, UA header)
    W-->>NS: Response (must be 2xx)
  else
    NS->>HTTP: http.Client.Do(GET barkURL, UA header)
    HTTP-->>NS: Response (must be 2xx)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A whisker twitch, a gentle spark—
I hop and add a channel: Bark!
URLs packed with title, song,
Quotas pinged, not overly long.
Frontend fields and backend art—
A push, a whoosh, a bunny heart. 🐇📣

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/components/settings/PersonalSetting.jsx (1)

280-305: Avoid wiping saved Bark URL when switching types; trim before sending

Only include bark_url when warningType is 'bark', and trim whitespace to reduce validation noise.

Apply this diff:

   const saveNotificationSettings = async () => {
     try {
-      const res = await API.put('/api/user/setting', {
-        notify_type: notificationSettings.warningType,
-        quota_warning_threshold: parseFloat(
-          notificationSettings.warningThreshold,
-        ),
-        webhook_url: notificationSettings.webhookUrl,
-        webhook_secret: notificationSettings.webhookSecret,
-        notification_email: notificationSettings.notificationEmail,
-        bark_url: notificationSettings.barkUrl,
-        accept_unset_model_ratio_model:
-          notificationSettings.acceptUnsetModelRatioModel,
-        record_ip_log: notificationSettings.recordIpLog,
-      });
+      const payload = {
+        notify_type: notificationSettings.warningType,
+        quota_warning_threshold: parseFloat(notificationSettings.warningThreshold),
+        webhook_url: notificationSettings.webhookUrl,
+        webhook_secret: notificationSettings.webhookSecret,
+        notification_email: notificationSettings.notificationEmail,
+        accept_unset_model_ratio_model: notificationSettings.acceptUnsetModelRatioModel,
+        record_ip_log: notificationSettings.recordIpLog,
+      };
+      if (notificationSettings.warningType === 'bark') {
+        payload.bark_url = (notificationSettings.barkUrl || '').trim();
+      }
+      const res = await API.put('/api/user/setting', payload);
🧹 Nitpick comments (9)
dto/user_settings.go (1)

9-9: BarkUrl field addition looks good

No blocking issues. Consider length/scheme validation at the API boundary (controller already validates) and documenting expected template placeholders for downstream senders.

web/src/components/settings/personal/cards/NotificationSettings.jsx (1)

488-538: Harden Bark URL validation and fix i18n

  • Use a stricter pattern to reject whitespace.
  • Localize the “Bark 官方文档” link text.

Apply this diff:

-                      rules={[
+                      rules={[
                         {
                           required:
                             notificationSettings.warningType === 'bark',
                           message: t('请输入Bark推送URL'),
                         },
                         {
-                          pattern: /^https?:\/\/.+/,
+                          pattern: /^https?:\/\/\S+$/i,
                           message: t('Bark推送URL必须以http://或https://开头'),
                         },
                       ]}
                     />
@@
-                          <a 
+                          <a 
                             href='https://github.com/Finb/Bark' 
                             target='_blank' 
                             rel='noopener noreferrer'
                             className='text-blue-500 hover:text-blue-600 font-medium'
                           >
-                            Bark 官方文档
+                            {t('Bark 官方文档')}
                           </a>
service/user_notify.go (7)

57-65: Don't silently drop unknown notify types; fail fast.
Right now, an unsupported type returns nil. Prefer explicit error to avoid lost notifications.

 	case dto.NotifyTypeBark:
 		barkURL := userSetting.BarkUrl
 		if barkURL == "" {
 			common.SysLog(fmt.Sprintf("user %d has no bark url, skip sending bark", userId))
 			return nil
 		}
 		return sendBarkNotify(barkURL, data)
+	default:
+		return fmt.Errorf("unsupported notify type: %s", notifyType)
 	}
 	return nil

85-88: Use PathEscape for path placeholders (Bark commonly uses path segments).
QueryEscape uses '+' for spaces, which is correct for queries but not for path segments. PathEscape is safer here unless you guarantee placeholders are only in queries.

If the UI guarantees placeholders appear solely in query strings, keep QueryEscape; otherwise, prefer PathEscape:

-	finalURL := strings.ReplaceAll(barkURL, "{{title}}", url.QueryEscape(data.Title))
-	finalURL = strings.ReplaceAll(finalURL, "{{content}}", url.QueryEscape(content))
+	finalURL := strings.ReplaceAll(barkURL, "{{title}}", url.PathEscape(data.Title))
+	finalURL = strings.ReplaceAll(finalURL, "{{content}}", url.PathEscape(content))

105-108: Prefer error wrapping with %w for better traceability.
This keeps the original error for callers.

-		if err != nil {
-			return fmt.Errorf("failed to send bark request through worker: %v", err)
-		}
+		if err != nil {
+			return fmt.Errorf("failed to send bark request through worker: %w", err)
+		}
...
-		if err != nil {
-			return fmt.Errorf("failed to create bark request: %v", err)
-		}
+		if err != nil {
+			return fmt.Errorf("failed to create bark request: %w", err)
+		}
...
-		if err != nil {
-			return fmt.Errorf("failed to send bark request: %v", err)
-		}
+		if err != nil {
+			return fmt.Errorf("failed to send bark request: %w", err)
+		}

Also applies to: 118-120, 128-130


111-115: Include small response body on non-2xx to aid debugging.
Helps operators see Bark’s error message without extra logging.

-		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
-			return fmt.Errorf("bark request failed with status code: %d", resp.StatusCode)
-		}
+		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+			b, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
+			return fmt.Errorf("bark request failed via worker: status=%d body=%s", resp.StatusCode, string(b))
+		}

133-137: Mirror error-body capture on the direct-path too.
Consistency across both paths.

-		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
-			return fmt.Errorf("bark request failed with status code: %d", resp.StatusCode)
-		}
+		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+			b, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
+			return fmt.Errorf("bark request failed: status=%d body=%s", resp.StatusCode, string(b))
+		}

94-104: Consider enforcing worker-only egress for Bark.
Centralizing egress through the worker simplifies network policy and SSRF controls (cf. service/cf_worker.go). If feasible, return an error when worker is disabled instead of direct HTTP.

Would you like a follow-up PR to gate Bark behind worker-only with a feature flag?


68-76: Minor: placeholder replacement logic parity.
Email and Bark paths duplicate placeholder logic. Extract a small helper to format content once, reducing drift.

If you want, I can push a tiny helper like formatContent(tmpl string, values []interface{}) string and use it in both places.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5cfc133 and 247e029.

📒 Files selected for processing (6)
  • controller/user.go (4 hunks)
  • dto/user_settings.go (2 hunks)
  • service/quota.go (1 hunks)
  • service/user_notify.go (3 hunks)
  • web/src/components/settings/PersonalSetting.jsx (3 hunks)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
controller/user.go (1)
dto/user_settings.go (3)
  • NotifyTypeEmail (16-16)
  • NotifyTypeWebhook (17-17)
  • NotifyTypeBark (18-18)
service/quota.go (4)
dto/user_settings.go (3)
  • NotifyTypeEmail (16-16)
  • NotifyTypeBark (18-18)
  • UserSetting (3-13)
logger/logger.go (1)
  • FormatQuota (102-108)
service/user_notify.go (1)
  • NotifyUser (22-66)
dto/notify.go (2)
  • NewNotify (18-25)
  • NotifyTypeQuotaExceed (13-13)
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
web/src/components/settings/PersonalSetting.jsx (1)
  • notificationSettings (64-73)
service/user_notify.go (6)
dto/user_settings.go (1)
  • NotifyTypeBark (18-18)
common/sys_log.go (1)
  • SysLog (10-13)
dto/notify.go (2)
  • Notify (3-8)
  • ContentValueParam (10-10)
setting/system_setting.go (2)
  • EnableWorker (8-10)
  • WorkerValidKey (5-5)
service/cf_worker.go (2)
  • WorkerRequest (14-20)
  • DoWorkerRequest (23-43)
service/http_client.go (1)
  • GetHttpClient (27-29)
🔇 Additional comments (7)
dto/user_settings.go (1)

18-18: NotifyTypeBark constant added

LGTM. Keep notify type strings centralized here to prevent drift across layers.

web/src/components/settings/PersonalSetting.jsx (2)

64-73: State shape extended with barkUrl

Looks consistent with backend DTO naming and the child form’s expectations.


103-118: Populate barkUrl from settings

LGTM. Mirrors other fields and preserves empty default.

controller/user.go (3)

1100-1101: Carry Bark URL in request DTO

LGTM. Maps 1:1 with settings.


1116-1116: Notify type whitelist includes bark

Good addition. Keep this list in sync with dto constants.

Ensure no other code paths (e.g., admin settings) validate against an older list.


1219-1223: Unable to locate the SetSetting implementation in the repository; without knowing whether it merges missing fields or overwrites all settings, it's unsafe to assume BarkUrl will be preserved. Please verify that user.SetSetting(settings) merges non-zero fields from the existing record rather than replacing the entire settings blob—if it doesn’t, refactor to load the current UserSetting, update only the changed fields, and then save, so that BarkUrl (and other optional URLs) aren’t wiped when switching away from Bark.

web/src/components/settings/personal/cards/NotificationSettings.jsx (1)

350-351: Bark channel added to selector

LGTM. Order and value align with backend.

Comment thread controller/user.go
Comment on lines +1164 to +1189
// 如果是Bark类型,验证Bark URL
if req.QuotaWarningType == dto.NotifyTypeBark {
if req.BarkUrl == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "Bark推送URL不能为空",
})
return
}
// 验证URL格式
if _, err := url.ParseRequestURI(req.BarkUrl); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的Bark推送URL",
})
return
}
// 检查是否是HTTP或HTTPS
if !strings.HasPrefix(req.BarkUrl, "https://") && !strings.HasPrefix(req.BarkUrl, "http://") {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "Bark推送URL必须以http://或https://开头",
})
return
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

SSRF risk on user-controlled Bark URL; align webhook HTTPS policy

The backend will later perform an outbound request to Bark URL (see sendBarkNotify). Accepting arbitrary http/https endpoints without private-network safeguards enables SSRF to internal services. Also, the UI enforces HTTPS for webhook while the server currently accepts http; align server-side to the same policy.

Server-side mitigations to apply (in sendBarkNotify / common HTTP client):

  • Enforce timeouts and a redirect policy that rejects redirects to private/link-local/bare IPs.
  • Resolve DNS and block private/reserved CIDRs (RFC1918, 169.254.0.0/16, 127.0.0.0/8, ::1/128, fc00::/7, fe80::/10, metadata IPs like 169.254.169.254).
  • Optional: default-allowlist Bark hosts (e.g., api.day.app) with a config toggle for self-hosted servers.
  • Limit max URL length and response size.

Additionally, update webhook validation to require https to match the UI.

Proposed diffs:

  1. Tighten Bark/Webhook scheme checks here (keeps http for Bark, enforces https for webhook):
   if req.QuotaWarningType == dto.NotifyTypeWebhook {
@@
-    if _, err := url.ParseRequestURI(req.WebhookUrl); err != nil {
+    if _, err := url.ParseRequestURI(req.WebhookUrl); err != nil {
       ...
     }
+    if !strings.HasPrefix(strings.ToLower(req.WebhookUrl), "https://") {
+      c.JSON(http.StatusOK, gin.H{
+        "success": false,
+        "message": "Webhook地址必须以https://开头",
+      })
+      return
+    }
   }
@@
   if req.QuotaWarningType == dto.NotifyTypeBark {
-    if req.BarkUrl == "" {
+    if strings.TrimSpace(req.BarkUrl) == "" {
       ...
     }
-    if _, err := url.ParseRequestURI(req.BarkUrl); err != nil {
+    bark := strings.TrimSpace(req.BarkUrl)
+    if _, err := url.ParseRequestURI(bark); err != nil {
       ...
     }
-    if !strings.HasPrefix(req.BarkUrl, "https://") && !strings.HasPrefix(req.BarkUrl, "http://") {
+    if !strings.HasPrefix(strings.ToLower(bark), "https://") && !strings.HasPrefix(strings.ToLower(bark), "http://") {
       ...
     }
   }
  1. Preserve previously saved channel-specific fields when switching types (merge with existing settings):
-  // 构建设置
-  settings := dto.UserSetting{
-    NotifyType:            req.QuotaWarningType,
-    QuotaWarningThreshold: req.QuotaWarningThreshold,
-    AcceptUnsetRatioModel: req.AcceptUnsetModelRatioModel,
-    RecordIpLog:           req.RecordIpLog,
-  }
+  // 构建设置(在现有设置基础上更新,避免切换通知方式时丢失字段)
+  existing := user.GetSetting()
+  settings := existing
+  settings.NotifyType = req.QuotaWarningType
+  settings.QuotaWarningThreshold = req.QuotaWarningThreshold
+  settings.AcceptUnsetRatioModel = req.AcceptUnsetModelRatioModel
+  settings.RecordIpLog = req.RecordIpLog
@@
-  if req.QuotaWarningType == dto.NotifyTypeWebhook {
-    settings.WebhookUrl = req.WebhookUrl
-    if req.WebhookSecret != "" {
-      settings.WebhookSecret = req.WebhookSecret
-    }
-  }
+  if req.QuotaWarningType == dto.NotifyTypeWebhook {
+    settings.WebhookUrl = strings.TrimSpace(req.WebhookUrl)
+    if req.WebhookSecret != "" {
+      settings.WebhookSecret = req.WebhookSecret
+    }
+  }
@@
-  if req.QuotaWarningType == dto.NotifyTypeEmail && req.NotificationEmail != "" {
-    settings.NotificationEmail = req.NotificationEmail
-  }
+  if req.QuotaWarningType == dto.NotifyTypeEmail && req.NotificationEmail != "" {
+    settings.NotificationEmail = strings.TrimSpace(req.NotificationEmail)
+  }
@@
-  if req.QuotaWarningType == dto.NotifyTypeBark {
-    settings.BarkUrl = req.BarkUrl
-  }
+  if req.QuotaWarningType == dto.NotifyTypeBark {
+    settings.BarkUrl = strings.TrimSpace(req.BarkUrl)
+  }

Follow-up: implement SSRF-safe HTTP client in sendBarkNotify; I can provide a patch if you share that file.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
controller/user.go around lines 1164-1189: the Bark URL validation currently
allows http and lacks SSRF protections and doesn't match the UI HTTPS
requirement; change validation to require https (reject http URIs), enforce a
reasonable max URL length, and preserve existing channel-specific fields when
updating types (merge instead of overwrite). Then implement SSRF-safe outbound
logic in sendBarkNotify (or a shared HTTP client): use strict timeouts,
follow-a-limited-redirects but reject redirects that resolve to
private/link-local/bare IPs, resolve the target hostname to IPs and block
RFC1918, 169.254.0.0/16, 127.0.0.0/8, ::1/128, fc00::/7, fe80::/10 and metadata
addresses (e.g., 169.254.169.254), disallow direct IP literal targets unless on
an allowlist, provide an optional config allowlist of trusted Bark hosts (e.g.,
api.day.app), and cap response body size to a safe maximum; ensure errors return
clear messages to the caller.

Comment thread service/quota.go
Comment on lines +539 to +556
// 根据通知方式生成不同的内容格式
var content string
var values []interface{}

notifyType := userSetting.NotifyType
if notifyType == "" {
notifyType = dto.NotifyTypeEmail
}

if notifyType == dto.NotifyTypeBark {
// Bark推送使用简短文本,不支持HTML
content = "{{value}},剩余额度:{{value}},请及时充值"
values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)}
} else {
// 默认内容格式,适用于Email和Webhook
content = "{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。<br/>充值链接:<a href='{{value}}'>{{value}}</a>"
values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota), topUpLink, topUpLink}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Show remaining quota after this consumption, not pre-consume quota

Use (UserQuota - consumeQuota) and clamp at 0 to avoid negative display. Applies to Bark and Email/Webhook values.

Apply this diff:

-      prompt := "您的额度即将用尽"
+      prompt := "您的额度即将用尽"
       topUpLink := fmt.Sprintf("%s/topup", setting.ServerAddress)
@@
-      if notifyType == dto.NotifyTypeBark {
+      // 计算剩余额度
+      remain := relayInfo.UserQuota - consumeQuota
+      if remain < 0 {
+        remain = 0
+      }
+      if notifyType == dto.NotifyTypeBark {
         // Bark推送使用简短文本,不支持HTML
-        content = "{{value}},剩余额度:{{value}},请及时充值"
-        values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota)}
+        content = "{{value}},剩余额度:{{value}},请及时充值"
+        values = []interface{}{prompt, logger.FormatQuota(remain)}
       } else {
         // 默认内容格式,适用于Email和Webhook
         content = "{{value}},当前剩余额度为 {{value}},为了不影响您的使用,请及时充值。<br/>充值链接:<a href='{{value}}'>{{value}}</a>"
-        values = []interface{}{prompt, logger.FormatQuota(relayInfo.UserQuota), topUpLink, topUpLink}
+        values = []interface{}{prompt, logger.FormatQuota(remain), topUpLink, topUpLink}
       }
 
-      err := NotifyUser(relayInfo.UserId, relayInfo.UserEmail, relayInfo.UserSetting, dto.NewNotify(dto.NotifyTypeQuotaExceed, prompt, content, values))
+      err := NotifyUser(relayInfo.UserId, relayInfo.UserEmail, relayInfo.UserSetting, dto.NewNotify(dto.NotifyTypeQuotaExceed, prompt, content, values))

Also applies to: 558-558

🤖 Prompt for AI Agents
In service/quota.go around lines 539-556 (and also at line 558), the code shows
remaining quota using relayInfo.UserQuota (pre-consume) — change it to compute
remaining := relayInfo.UserQuota - consumeQuota, clamp remaining to a minimum of
0, and use that remaining value in both Bark and Email/Webhook values
formatting; update values slices so they pass remaining (formatted via
logger.FormatQuota) instead of relayInfo.UserQuota, and ensure any duplicate
topUpLink placements remain correct for the Email/Webhook branch.

Comment thread service/user_notify.go
Comment on lines 3 to 12
import (
"fmt"
"net/http"
"net/url"
"one-api/common"
"one-api/dto"
"one-api/model"
"one-api/setting"
"strings"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Harden against SSRF + improve diagnostics: add imports for validation and body capture.
To safely validate Bark URLs and include small error bodies in failures, add these imports.

 import (
 	"fmt"
+	"io"
+	"net"
 	"net/http"
 	"net/url"
 	"one-api/common"
 	"one-api/dto"
 	"one-api/model"
 	"one-api/setting"
 	"strings"
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import (
"fmt"
"net/http"
"net/url"
"one-api/common"
"one-api/dto"
"one-api/model"
"one-api/setting"
"strings"
)
import (
"fmt"
"io"
"net"
"net/http"
"net/url"
"one-api/common"
"one-api/dto"
"one-api/model"
"one-api/setting"
"strings"
)
🤖 Prompt for AI Agents
In service/user_notify.go around lines 3 to 12, the import block needs to be
hardened to support SSRF-safe URL validation and to capture small response
bodies for diagnostics; add imports for a validator and response/body dumping
(for example github.com/asaskevich/govalidator and net/http/httputil or io and
net/http/httputil) and reference them where you validate Bark URLs and include
small error bodies in logs so you can safely validate the URL before making
requests and dump a limited response body on failures.

Comment thread service/user_notify.go
Comment on lines +89 to +97
// 发送GET请求到Bark
var req *http.Request
var resp *http.Response
var err error

if setting.EnableWorker() {
// 使用worker发送请求
workerReq := &WorkerRequest{
URL: finalURL,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add SSRF guardrails on finalURL (scheme, absoluteness, and local addresses).
Since Bark URL is user-provided, validate before any outbound request.

 	// 发送GET请求到Bark
 	var req *http.Request
 	var resp *http.Response
 	var err error
 
+	// 基础 URL 校验(避免 SSRF)
+	u, perr := url.Parse(finalURL)
+	if perr != nil || !u.IsAbs() {
+		return fmt.Errorf("invalid bark url: %w", perr)
+	}
+	if u.Scheme != "https" && u.Scheme != "http" {
+		return fmt.Errorf("bark url must use http/https")
+	}
+	host := strings.ToLower(u.Hostname())
+	if host == "localhost" || host == "127.0.0.1" || host == "::1" {
+		return fmt.Errorf("bark url host not allowed: %s", host)
+	}
+	if ip := net.ParseIP(host); ip != nil && ip.IsPrivate() {
+		return fmt.Errorf("bark url private ip not allowed: %s", host)
+	}
+
 	if setting.EnableWorker() {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 发送GET请求到Bark
var req *http.Request
var resp *http.Response
var err error
if setting.EnableWorker() {
// 使用worker发送请求
workerReq := &WorkerRequest{
URL: finalURL,
// 发送GET请求到Bark
var req *http.Request
var resp *http.Response
var err error
// 基础 URL 校验(避免 SSRF)
u, perr := url.Parse(finalURL)
if perr != nil || !u.IsAbs() {
return fmt.Errorf("invalid bark url: %w", perr)
}
if u.Scheme != "https" && u.Scheme != "http" {
return fmt.Errorf("bark url must use http/https")
}
host := strings.ToLower(u.Hostname())
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
return fmt.Errorf("bark url host not allowed: %s", host)
}
if ip := net.ParseIP(host); ip != nil && ip.IsPrivate() {
return fmt.Errorf("bark url private ip not allowed: %s", host)
}
if setting.EnableWorker() {
// 使用worker发送请求
workerReq := &WorkerRequest{
URL: finalURL,
🤖 Prompt for AI Agents
In service/user_notify.go around lines 89–97, finalURL (user-provided) is used
directly; add SSRF guardrails by validating it before sending or enqueuing:
parse the URL and require an absolute URL with scheme http or https only, reject
non-absolute or disallowed schemes, normalize/percent-decode host, then resolve
the hostname to IP addresses (net.LookupIP) and reject any resolved IPs that are
loopback, link-local, private/reserved ranges, or multicast; also reject literal
IPv6 loopback/unique-local addresses and disallow localhost/127.0.0.0/8,
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16 etc.; perform this
validation before creating WorkerRequest or issuing http requests and return a
clear validation error when blocked.

Comment thread service/user_notify.go
Comment on lines +122 to +127
// 设置User-Agent
req.Header.Set("User-Agent", "OneAPI-Bark-Notify/1.0")

// 发送请求
client := GetHttpClient()
resp, err = client.Do(req)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify HTTP client timeouts.
Direct path relies on GetHttpClient(). Ensure it has sane timeouts; the worker path (http.Post inside DoWorkerRequest) may lack them.


🏁 Script executed:

#!/bin/bash
# Inspect http client configuration and worker request path.
rg -nP -C3 'http\.Client|httpClient|GetHttpClient\(\)' service/http_client.go service/cf_worker.go
rg -nP 'http\.Post\(' service/cf_worker.go -C2

Length of output: 2069


Enforce HTTP client timeouts

  • Always initialize httpClient.Timeout to a non-zero value in InitHttpClient() (e.g. default to common.RelayTimeout or a sensible constant when it’s zero).
  • Replace the unbounded http.Post in service/cf_worker.go:42 with a timeout-capable client (e.g. use GetHttpClient().Do or a Context-based request).
🤖 Prompt for AI Agents
In service/user_notify.go around lines 122-127 and service/cf_worker.go at line
42, the code uses an HTTP client without an enforced timeout and an unbounded
http.Post call; update InitHttpClient() to ensure the returned
httpClient.Timeout is initialized to a non-zero value (default to
common.RelayTimeout or a sensible constant when it’s zero) so all callers get a
timeout-enabled client, and replace the raw http.Post in service/cf_worker.go:42
with a timeout-capable request using GetHttpClient().Do (or create a context
with deadline/cancel and use http.NewRequestWithContext) so the outbound request
honors the configured timeout.

@seefs001
seefs001 merged commit e98ca00 into QuantumNous:alpha Sep 1, 2025
3 checks passed
x22x22 pushed a commit to x22x22/new-api that referenced this pull request Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant