Skip to content

新增 Webhook 通知渠道#1799

Open
gongfuture wants to merge 11 commits intoPBH-BTN:devfrom
gongfuture:feat/notification/webhook
Open

新增 Webhook 通知渠道#1799
gongfuture wants to merge 11 commits intoPBH-BTN:devfrom
gongfuture:feat/notification/webhook

Conversation

@gongfuture
Copy link
Copy Markdown
Contributor

@gongfuture gongfuture commented Apr 27, 2026

增加 Webhook 用于没有需要的通知渠道情况下用户自定义

支持 Post/Get,支持json/plaintext
支持自定义消息模板,可用变量 {title} {content} {level} {date} {time} {datetime} {channelName}

截图
json post
ptn_webhook_post_json
ntfy_webhook_post_json

plaintext post
ptn_webhook_post_plaintext
ntfy_webhook_post_plaintext

get (这边懒得写参数了,所以只要一个触发)
ptn_webhook_get
ntfy_webhook_get

截图完成之后跳了一下网页排布和说明,如下
变量tip

Summary by CodeRabbit

发布说明

  • 新功能

    • 新增 Webhook 推送通道:支持目标 URL、HTTP 方法(GET/POST)、内容类型(JSON/文本)、消息模板变量替换与自定义请求头。
    • 在设置界面新增 Webhook 配置表单;推送类型选择与通知卡片显示 Webhook 图标与配色。
    • 添加英文、简体中文与繁体中文本地化文案。
  • 杂项

    • 贡献者名单中新增一位贡献者。

@gongfuture gongfuture requested review from a team as code owners April 27, 2026 14:32
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Walkthrough

添加对 Webhook 推送的端到端支持:后端新增 WebhookPushProvider 并在 PushManagerImpl 注册;前端扩展类型、模型、表单、卡片显示与多语言文案;并更新贡献者名单。

Changes

Cohort / File(s) Summary
后端:Push 管理与实现
src/main/java/com/ghostchu/peerbanhelper/util/push/PushManagerImpl.java, src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java
新增 WebhookPushProvider(url、method、contentType、bodyTemplate、headers),实现 JSON/YAML 的加载与保存、模板渲染、请求构建与执行;在工厂中注册 webhook 类型并对未知类型显式设为 null
前端:API 模型
webui/src/api/model/push.ts
添加 PushType.WebhookWebhookMethodWebhookContentTypeWebhookConfig,并将 PushConfig 联合类型扩展以支持 webhook 配置。
前端:设置界面与表单
webui/src/views/settings/components/config/components/push/editPush.vue, webui/src/views/settings/components/config/components/push/forms/webhookForm.vue, webui/src/views/settings/components/config/components/push/pushCard.vue
在编辑界面加入 Webhook 类型选项并映射到新表单;新增 webhookForm.vue(URL、方法、内容类型、模板、头部编辑与行管理逻辑);扩展 push 卡片的颜色与头像映射。
前端:多语言文案
webui/src/views/settings/components/config/locale/en-US.ts, webui/src/views/settings/components/config/locale/zh-CN.ts, webui/src/views/settings/components/config/locale/zh-TW.ts
为 EN/简体/繁体 添加 Webhook 推送类型显示名及其表单字段、占位符、可用变量说明与提示文案。
资源文件
src/main/resources/assets/credit.txt
在贡献者列表中新增 gongfuture 条目。

Sequence Diagram(s)

sequenceDiagram
    participant User as rgba(66,133,244,0.5) User/WebUI
    participant App as rgba(52,168,83,0.5) PeerBanHelper App
    participant WebhookProvider as rgba(255,167,38,0.5) WebhookPushProvider
    participant HTTP as rgba(156,39,176,0.5) Target Webhook Server

    User->>App: 配置/保存 Webhook (url, method, contentType, template, headers)
    App->>WebhookProvider: 初始化/加载 WebhookPushProvider

    Note over App,WebhookProvider: 触发推送事件
    App->>WebhookProvider: push(title, content)
    WebhookProvider->>WebhookProvider: 验证 URL、规范化 method/contentType
    WebhookProvider->>WebhookProvider: 渲染模板变量(转义/编码)并构建请求体/URL
    WebhookProvider->>WebhookProvider: 应用 headers 与 Content-Type
    WebhookProvider->>HTTP: 发送 HTTP 请求
    HTTP-->>WebhookProvider: 返回 HTTP 响应

    alt HTTP 成功 (2xx-3xx)
        WebhookProvider-->>App: 返回成功
        App-->>User: 推送成功
    else HTTP 失败 (4xx-5xx)
        WebhookProvider-->>App: 抛出异常(含响应体)
        App-->>User: 推送失败
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Gaojianli
  • Ghost-chu
  • Kaffu-Chino
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题'新增 Webhook 通知渠道'清晰准确地概括了主要变更——添加Webhook推送通知渠道功能。标题与整个PR变更集高度相关且具体明确。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added enhancement 功能增强 java Pull requests that update Java code javascript Pull requests that update Javascript code backend This is a backend related webui This is a webui related labels Apr 27, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new Webhook push provider, enabling notifications to be sent to custom endpoints via GET or POST requests with configurable body templates and headers. The changes include the backend implementation for request execution and template rendering, alongside a frontend configuration interface with multi-language support. Review feedback suggests extending template rendering to the URL for dynamic GET parameters, removing redundant null checks for OkHttp response bodies, and implementing JSON escaping within templates to prevent malformed payloads when special characters are present.

Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (3)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java (1)

189-202: DateTimeFormatter 可缓存为静态常量。

每次推送都会通过 DateTimeFormatter.ofPattern(...) 重新构造三个实例,对热路径而言完全可避免。DateTimeFormatter 是线程安全的,建议提取为类级常量。

♻️ 建议
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
+    private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@
-        OffsetDateTime now = OffsetDateTime.now();
-        String date = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
-        String time = now.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
-        String datetime = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+        OffsetDateTime now = OffsetDateTime.now();
+        String date = now.format(DATE_FORMATTER);
+        String time = now.format(TIME_FORMATTER);
+        String datetime = now.format(DATETIME_FORMATTER);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
around lines 189 - 202, renderTemplate currently constructs three
DateTimeFormatter instances on every call; extract them as reusable class-level
constants (e.g. private static final DateTimeFormatter DATE_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd"), TIME_FMT =
DateTimeFormatter.ofPattern("HH:mm:ss"), DATETIME_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) and replace the inline
ofPattern(...) calls in renderTemplate with these constants (class
WebhookPushProvider, method renderTemplate) to avoid repeated allocation and
leverage DateTimeFormatter's thread-safety.
webui/src/views/settings/components/config/components/push/forms/webhookForm.vue (2)

142-153: JSON.stringify 比较对键序敏感,可能导致不必要的行重建。

JSON.stringify(fromRows)JSON.stringify(next) 在键内容相同但顺序不同时仍会判定为不相等,从而触发 headerRows 重建。这会丢失用户当前的行顺序与可能正在编辑中的焦点状态。常见触发场景:外部回填时后端返回的 headers 键序与本地 rowsToHeaders 按行顺序产出的键序不一致。

可考虑改为按键集合 + 值的语义比较:

♻️ 建议改写
-watch(
-  () => model.value.headers,
-  (headers) => {
-    const fromRows = rowsToHeaders(headerRows.value)
-    const next = headers ?? {}
-    if (JSON.stringify(fromRows) === JSON.stringify(next)) {
-      return
-    }
-    headerRows.value = headersToRows(next)
-  }
-)
+const headersEqual = (a: Record<string, string>, b: Record<string, string>) => {
+  const ak = Object.keys(a)
+  const bk = Object.keys(b)
+  if (ak.length !== bk.length) return false
+  return ak.every((k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k])
+}
+
+watch(
+  () => model.value.headers,
+  (headers) => {
+    const fromRows = rowsToHeaders(headerRows.value)
+    const next = headers ?? {}
+    if (headersEqual(fromRows, next)) return
+    headerRows.value = headersToRows(next)
+  }
+)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@webui/src/views/settings/components/config/components/push/forms/webhookForm.vue`
around lines 142 - 153, The current watch uses JSON.stringify to compare
fromRows and next which is order-sensitive and can trigger unnecessary
headerRows resets; instead implement an order-insensitive deep equality check
(e.g., a headersEqual(from, next) that compares key sets and value equality for
each key) and use that in the watch before deciding to reassign
headerRows.value; locate the watch block that references model.value.headers,
rowsToHeaders(headerRows.value) and headersToRows(next) and replace the
JSON.stringify comparison with this headersEqual check to avoid rebuilding rows
when only key order differs.

19-19: 在选项中使用枚举值作为标签和值。

Object.values(WebhookMethod)Object.values(WebhookContentType) 都返回字符串数组。ArcoDesign 的 a-select 组件接受字符串数组,并将字符串同时用作标签和值展示。由于 GET/POSTapplication/json/text/plain 都是通用术语,当前的用户体验是可以接受的。

如果后续需要本地化或显示更友好的描述(例如"JSON (application/json)"),建议改为使用显式的 { label, value } 数组格式,但这可以在未来的优化中处理,不是当前 PR 必需的改动。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@webui/src/views/settings/components/config/components/push/forms/webhookForm.vue`
at line 19, 当前在 a-select 中直接使用 Object.values(WebhookMethod) 和
Object.values(WebhookContentType),它们返回字符串数组并被用作标签和值,这在现有场景可接受;若将来需要本地化或更友好的展示,请把这两个枚举映射为显式的
{ label, value } 数组(例如在组件或一个 helper 中将 WebhookMethod/WebhookContentType 转为 {
label, value } 列表),并将结果传给 a-select 的 :options(绑定到 model.method
等字段),以便后续替换为更友好的文本而不改动绑定逻辑。
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 130-138: The current try/catch in WebhookPushProvider wraps an
IllegalStateException thrown for non-success HTTP responses with a generic
"Failed to send..." message, losing the original HTTP failure message; change
the error handling so HTTP-check exceptions are not double-wrapped — e.g., in
the method where you call
httpUtil.newBuilder().build().newCall(request).execute() keep the
response.isSuccessful() check as-is but replace the broad catch (Exception e)
with a narrower catch (IOException e) (or explicitly rethrow
IllegalStateException: if (e instanceof IllegalStateException) throw e;), so
network IO errors are still handled while the original IllegalStateException
containing the HTTP body/message bubbles up unmodified from WebhookPushProvider.
- Around line 189-202: The renderTemplate method currently inserts title/content
directly and can break JSON payloads; update renderTemplate (or its caller) to
accept or read the contentType and, when contentType equals "application/json",
JSON-escape values used in replacements (title, content, channelName returned by
name, and any other inserted fields), then perform the .replace calls with the
escaped strings (keep extractLevel(title) but pass an escaped title if it's also
placed into JSON); implement escaping that handles backslashes, double quotes,
control chars (newline, \r, tabs) and Unicode as needed so the produced JSON
remains valid.
- Around line 78-93: loadFromJson currently assumes
JsonUtil.getGson().fromJson(json, Config.class) returns a non-null Config; add a
null-check right after deserialization in loadFromJson and throw a clear
IllegalArgumentException (or custom config exception) stating the provider name
and that the config JSON is invalid so callers like
PushManagerImpl#createPushProvider receive a precise error instead of an NPE;
reference the Config class and return path to WebhookPushProvider only after the
config is validated and defaults (method, contentType, bodyTemplate, headers)
are applied.
- Around line 159-170: The Content-Type format isn't validated so
MediaType.parse(contentType) can return null and an invalid header gets sent;
update normalizeContentType to validate the incoming contentType by calling
MediaType.parse(contentType) and return a normalized valid string (or
null/empty) when parse fails, or alternatively modify createRequestBody to
handle a null MediaType: call MediaType.parse(contentType) there, and if it
returns null use a safe default MediaType (e.g., application/json or
application/octet-stream) and avoid setting an invalid header in
applyContentType; reference normalizeContentType, createRequestBody,
applyContentType, RequestBody.create and MediaType.parse when making the change.

In `@webui/src/views/settings/components/config/locale/en-US.ts`:
- Around line 171-172: The placeholder value for the localization key
'page.settings.tab.config.push.form.webhook.body_template.placeholder' uses a
CJK delimiter "、"; replace it with ASCII commas and spaces between variables
(e.g. "{l}title{r}, {l}content{r}, {l}level{r}, {l}date{r}, {l}time{r},
{l}datetime{r}, {l}channelName{r}") so the English UI uses proper comma+space
separation.

---

Nitpick comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 189-202: renderTemplate currently constructs three
DateTimeFormatter instances on every call; extract them as reusable class-level
constants (e.g. private static final DateTimeFormatter DATE_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd"), TIME_FMT =
DateTimeFormatter.ofPattern("HH:mm:ss"), DATETIME_FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) and replace the inline
ofPattern(...) calls in renderTemplate with these constants (class
WebhookPushProvider, method renderTemplate) to avoid repeated allocation and
leverage DateTimeFormatter's thread-safety.

In
`@webui/src/views/settings/components/config/components/push/forms/webhookForm.vue`:
- Around line 142-153: The current watch uses JSON.stringify to compare fromRows
and next which is order-sensitive and can trigger unnecessary headerRows resets;
instead implement an order-insensitive deep equality check (e.g., a
headersEqual(from, next) that compares key sets and value equality for each key)
and use that in the watch before deciding to reassign headerRows.value; locate
the watch block that references model.value.headers,
rowsToHeaders(headerRows.value) and headersToRows(next) and replace the
JSON.stringify comparison with this headersEqual check to avoid rebuilding rows
when only key order differs.
- Line 19: 当前在 a-select 中直接使用 Object.values(WebhookMethod) 和
Object.values(WebhookContentType),它们返回字符串数组并被用作标签和值,这在现有场景可接受;若将来需要本地化或更友好的展示,请把这两个枚举映射为显式的
{ label, value } 数组(例如在组件或一个 helper 中将 WebhookMethod/WebhookContentType 转为 {
label, value } 列表),并将结果传给 a-select 的 :options(绑定到 model.method
等字段),以便后续替换为更友好的文本而不改动绑定逻辑。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 41d2e1eb-5fd8-42c7-abef-e092336f8281

📥 Commits

Reviewing files that changed from the base of the PR and between 1cab3ac and 824359c.

📒 Files selected for processing (9)
  • src/main/java/com/ghostchu/peerbanhelper/util/push/PushManagerImpl.java
  • src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java
  • webui/src/api/model/push.ts
  • webui/src/views/settings/components/config/components/push/editPush.vue
  • webui/src/views/settings/components/config/components/push/forms/webhookForm.vue
  • webui/src/views/settings/components/config/components/push/pushCard.vue
  • webui/src/views/settings/components/config/locale/en-US.ts
  • webui/src/views/settings/components/config/locale/zh-CN.ts
  • webui/src/views/settings/components/config/locale/zh-TW.ts

Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Comment thread webui/src/views/settings/components/config/locale/en-US.ts Outdated
Copy link
Copy Markdown
Member

@Ghost-chu Ghost-chu left a comment

Choose a reason for hiding this comment

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

您好👋,感谢提交拉取请求。总的来说后端部分的代码质量相当不错,我已完成后端部分的代码审阅,仍有几个小问题需要更改,需要更改的部分已在 Review 中标出,还麻烦您查看。

要合并到主线中,前端部分仍需前端团队审阅。相关部分的更改将由 @Gaojianli 老师进行审阅。

此外,如果您愿意也可以将自己的 GitHub 用户名添加到 credit.txt 的 Contributors 部分中。再次感谢您的无私贡献 ;)

Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java (1)

185-194: Content-Type 顺序:自定义 header 会覆盖默认 Content-Type,请确认是预期行为。

第 134 行先调用 applyContentType,再调用 applyCustomHeaders;OkHttp 的 Request.Builder.header(name, value) 是覆盖语义,因此用户在 headers 中显式设置 Content-Type 会覆盖根据 RequestBody 推断的值(包括大小写归一化后的值,OkHttp 头匹配大小写不敏感)。一般这是合理的“用户优先”行为,但如果意图是“用户的自定义头不允许覆盖 body 实际的 Content-Type”,则需要在 applyCustomHeaders 中过滤掉 Content-Type。建议在文档/UI 上说明,或在代码中按需调整。

OkHttp Request.Builder.header vs addHeader case-insensitive override behavior
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
around lines 185 - 194, The current flow calls applyContentType(...) before
applyCustomHeaders(...), so custom headers can override the Content-Type
inferred from the RequestBody via Request.Builder.header(...) — if you want to
prevent user headers from replacing the body-derived Content-Type, update
applyCustomHeaders to ignore any header whose name equals "Content-Type"
(case-insensitive, use equalsIgnoreCase) and leave DEFAULT_CONTENT_TYPE and
mediaType logic in applyContentType unchanged; alternatively, if you prefer
“user wins”, document this behavior or move applyCustomHeaders to run after
applyContentType — pick one approach and implement the corresponding change
inside applyCustomHeaders or by swapping the call order that references
applyContentType and applyCustomHeaders.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 208-211: 删除未使用的 OffsetDateTime now 声明并将三次调用的
System.currentTimeMillis() 缓存为一个局部 long 变量(例如 millis),然后用该 millis 依次调用
TimeUtil.formatDateOnly, TimeUtil.formatTimeOnly, TimeUtil.formatDateTime 来初始化
date、time 和 datetime,确保在方法/块中不再引用 OffsetDateTime 并避免跨秒导致的不一致。
- Around line 130-131: The URL template replacements in WebhookPushProvider are
not URL-encoding variables (renderTemplate(config.getUrl(), ... , null)), which
breaks GET/query-string URLs; fix by adding a URL-encoding template renderer
(e.g., renderUrlTemplate or extend renderTemplate with a urlEncode mode) and use
URLEncoder.encode(value, StandardCharsets.UTF_8) for
{title},{content},{level},{date},{time},{datetime},{channelName} when building
renderedUrl (or whenever contentType==null/when method is GET), then replace the
current call in WebhookPushProvider to call that URL-encoding renderer so the
final Request.Builder.url(renderedUrl) receives a valid encoded URL.

---

Nitpick comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 185-194: The current flow calls applyContentType(...) before
applyCustomHeaders(...), so custom headers can override the Content-Type
inferred from the RequestBody via Request.Builder.header(...) — if you want to
prevent user headers from replacing the body-derived Content-Type, update
applyCustomHeaders to ignore any header whose name equals "Content-Type"
(case-insensitive, use equalsIgnoreCase) and leave DEFAULT_CONTENT_TYPE and
mediaType logic in applyContentType unchanged; alternatively, if you prefer
“user wins”, document this behavior or move applyCustomHeaders to run after
applyContentType — pick one approach and implement the corresponding change
inside applyCustomHeaders or by swapping the call order that references
applyContentType and applyCustomHeaders.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d7990e4b-b823-4da0-b7e9-6bdbc2b1aaaf

📥 Commits

Reviewing files that changed from the base of the PR and between 824359c and 85a1bc8.

📒 Files selected for processing (3)
  • src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java
  • src/main/resources/assets/credit.txt
  • webui/src/views/settings/components/config/locale/en-US.ts
✅ Files skipped from review due to trivial changes (2)
  • src/main/resources/assets/credit.txt
  • webui/src/views/settings/components/config/locale/en-US.ts

Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java (1)

165-180: createRequestBody 中存在不可达分支。

normalizeMethod(Lines 146–155)已将 method 限制为 "GET""POST",因此 Line 166 已处理 GET,Line 174 之后 method 必为 "POST",Line 177 的 return null 永远不会执行。可以简化逻辑,避免后续维护误读。

♻️ 建议简化
     private RequestBody createRequestBody(String method, String contentType, String bodyContent) {
         if ("GET".equals(method)) {
             return null;
         }
         MediaType mediaType = MediaType.parse(contentType);
         if (mediaType == null) {
             mediaType = MediaType.parse(DEFAULT_CONTENT_TYPE);
         }
-        if (bodyContent.isEmpty()) {
-            if ("POST".equals(method)) {
-                return RequestBody.create("", mediaType);
-            }
-            return null;
-        }
         return RequestBody.create(bodyContent, mediaType);
     }

RequestBody.create("", mediaType)RequestBody.create(bodyContent, mediaType)bodyContent 为空字符串时等价,无需额外分支。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
around lines 165 - 180, createRequestBody contains an unreachable branch because
normalizeMethod already restricts method to "GET" or "POST"; remove the
redundant empty-body conditional and the unreachable return null (the branch
after checking POST) and simplify to: if method is "GET" return null, resolve
mediaType with MediaType.parse(contentType) fallback to DEFAULT_CONTENT_TYPE,
then always return RequestBody.create(bodyContent, mediaType) for POST
(RequestBody.create("", mediaType) is equivalent when bodyContent is empty).
Update the method createRequestBody accordingly and keep references to
DEFAULT_CONTENT_TYPE and normalizeMethod intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 226-237: The method renderUrlTemplate currently references a
non-existent variable `template` and uses URLEncoder/StandardCharsets without
imports; change the returned expression to operate on the `urlTemplate`
parameter (i.e., use urlTemplate.replace(...)), add imports for
java.net.URLEncoder and java.nio.charset.StandardCharsets, and update the
encoder UnaryOperator in renderUrlTemplate to convert null->"" then
URLEncoder.encode(..., StandardCharsets.UTF_8) and replace "+" with "%20" to
avoid space->'+' issues; keep the existing uses of extractLevel(title),
TimeUtil.formatDateOnly/formatTimeOnly/formatDateTime(now) and name.

---

Nitpick comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 165-180: createRequestBody contains an unreachable branch because
normalizeMethod already restricts method to "GET" or "POST"; remove the
redundant empty-body conditional and the unreachable return null (the branch
after checking POST) and simplify to: if method is "GET" return null, resolve
mediaType with MediaType.parse(contentType) fallback to DEFAULT_CONTENT_TYPE,
then always return RequestBody.create(bodyContent, mediaType) for POST
(RequestBody.create("", mediaType) is equivalent when bodyContent is empty).
Update the method createRequestBody accordingly and keep references to
DEFAULT_CONTENT_TYPE and normalizeMethod intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9652fefc-4405-4ce8-9d33-bc31b79d798b

📥 Commits

Reviewing files that changed from the base of the PR and between 85a1bc8 and f99b105.

📒 Files selected for processing (1)
  • src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java

Comment thread src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java (2)

230-230: 末尾多了一个分号

URLEncoder.encode(...).replace("+", "%20");; 末尾的 ;; 是一个空语句,虽然合法但属于明显笔误,建议清理。

♻️ 建议修改
-        java.util.function.UnaryOperator<String> enc = v -> v == null ? "" : URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");;
+        java.util.function.UnaryOperator<String> enc = v -> v == null ? "" : URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
at line 230, In WebhookPushProvider.java the local UnaryOperator<String> enc
assignment contains a stray empty statement (";;") at the end; locate the enc
variable declaration in class WebhookPushProvider (the line defining
"java.util.function.UnaryOperator<String> enc = v -> v == null ? "" :
URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");;") and remove
the extra trailing semicolon so the assignment ends with a single semicolon,
leaving the encoding logic unchanged.

206-239: renderTemplaterenderUrlTemplate 存在明显重复,建议抽取通用替换逻辑

两个方法仅 esc 策略不同(JSON 转义 vs URL 编码),其余 7 个占位符({title} {content} {level} {date} {time} {datetime} {channelName})的展开逻辑完全一致。可抽取一个接受 UnaryOperator<String> 的私有方法以避免后续新增/重命名变量时出现两边遗漏不同步。

♻️ 重构示例
     private String renderTemplate(String template, String title, String content, String contentType) {
-        long currentTime = System.currentTimeMillis();
-        String date = TimeUtil.formatDateOnly(currentTime);
-        String time = TimeUtil.formatTimeOnly(currentTime);
-        String datetime = TimeUtil.formatDateTime(currentTime);
         boolean json = contentType != null && contentType.toLowerCase(Locale.ROOT).contains("json");
-        java.util.function.UnaryOperator<String> esc = v -> {
+        UnaryOperator<String> esc = v -> {
             if (v == null) return "";
             if (!json) return v;
             String s = JsonUtil.standard().toJson(v);
             return s.substring(1, s.length() - 1);
         };
-        return template
-            .replace("{title}", esc.apply(title))
-            ...
+        return applyVariables(template, title, content, esc);
     }

     private String renderUrlTemplate(String urlTemplate, String title, String content) {
-        long now = System.currentTimeMillis();
-        java.util.function.UnaryOperator<String> enc = v -> v == null ? "" : URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");
-        return urlTemplate
-            .replace("{title}", enc.apply(title))
-            ...
+        UnaryOperator<String> enc = v -> v == null ? "" :
+                URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");
+        return applyVariables(urlTemplate, title, content, enc);
     }

+    private String applyVariables(String template, String title, String content, UnaryOperator<String> esc) {
+        long now = System.currentTimeMillis();
+        return template
+                .replace("{title}", esc.apply(title))
+                .replace("{content}", esc.apply(content))
+                .replace("{level}", esc.apply(extractLevel(title)))
+                .replace("{date}", esc.apply(TimeUtil.formatDateOnly(now)))
+                .replace("{time}", esc.apply(TimeUtil.formatTimeOnly(now)))
+                .replace("{datetime}", esc.apply(TimeUtil.formatDateTime(now)))
+                .replace("{channelName}", esc.apply(name));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
around lines 206 - 239, renderTemplate and renderUrlTemplate duplicate the same
placeholder-replacement logic except for the escaping strategy; extract a
private helper like renderWithEscaper(UnaryOperator<String> esc, String
template, String title, String content) that performs the common
.replace("{title}"...) … .replace("{channelName}"...) sequence and then have
renderTemplate call it with the JSON-escaping operator (the existing esc) and
renderUrlTemplate call it with the URL-encoding operator (the existing enc).
Update renderTemplate to compute date/time/datetime and the esc operator then
delegate, and update renderUrlTemplate to compute now and enc then delegate,
keeping extractLevel(title) and name references intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Line 230: In WebhookPushProvider.java the local UnaryOperator<String> enc
assignment contains a stray empty statement (";;") at the end; locate the enc
variable declaration in class WebhookPushProvider (the line defining
"java.util.function.UnaryOperator<String> enc = v -> v == null ? "" :
URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");;") and remove
the extra trailing semicolon so the assignment ends with a single semicolon,
leaving the encoding logic unchanged.
- Around line 206-239: renderTemplate and renderUrlTemplate duplicate the same
placeholder-replacement logic except for the escaping strategy; extract a
private helper like renderWithEscaper(UnaryOperator<String> esc, String
template, String title, String content) that performs the common
.replace("{title}"...) … .replace("{channelName}"...) sequence and then have
renderTemplate call it with the JSON-escaping operator (the existing esc) and
renderUrlTemplate call it with the URL-encoding operator (the existing enc).
Update renderTemplate to compute date/time/datetime and the esc operator then
delegate, and update renderUrlTemplate to compute now and enc then delegate,
keeping extractLevel(title) and name references intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bad47fc3-ce69-45c6-9d32-52f5328a5ee0

📥 Commits

Reviewing files that changed from the base of the PR and between f99b105 and 3681487.

📒 Files selected for processing (1)
  • src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java (2)

167-182: Line 179 的 return null 是不可达分支,可简化逻辑。

由于上层 normalizeMethod 已将 method 限定为 GET / POST,且 GET 在 Line 168-170 已提前返回 null,此处进入 Line 175 的 bodyContent.isEmpty() 分支时 method 必为 POST,内层 if ("POST".equals(method)) 永远成立,Line 179 的 return null 永远不会执行。OkHttp 4.x 的 Request.Builder.method("POST", null) 会抛 IllegalArgumentException,所以这里也不应返回 null。建议直接简化:

♻️ 建议简化
     private RequestBody createRequestBody(String method, String contentType, String bodyContent) {
         if ("GET".equals(method)) {
             return null;
         }
         MediaType mediaType = MediaType.parse(contentType);
         if (mediaType == null) {
             mediaType = MediaType.parse(DEFAULT_CONTENT_TYPE);
         }
-        if (bodyContent.isEmpty()) {
-            if ("POST".equals(method)) {
-                return RequestBody.create("", mediaType);
-            }
-            return null;
-        }
         return RequestBody.create(bodyContent, mediaType);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
around lines 167 - 182, The createRequestBody method contains an unreachable
return null for the empty-body branch because normalizeMethod restricts method
to GET/POST and GET already returns null; update createRequestBody so that when
bodyContent.isEmpty() it does not try to return null for POST (which would cause
Request.Builder.method("POST", null) errors), instead always create an empty
RequestBody using the resolved mediaType; reference createRequestBody and the
normalizeMethod behavior to locate the logic to simplify/remove the inner if and
the unreachable return null.

206-239: Body 与 URL 时间戳可能不一致,建议在 push() 中统一捕获一次时间。

renderTemplate 在 Line 207 捕获了 currentTime,而 renderUrlTemplate 在 Line 229 又独立捕获了一次 now。当两次调用恰好跨越秒/分/天边界时,Body 中的 {datetime} 与 URL 中的 {datetime} 会出现不同的时间值,日志/审计场景下不易排查。建议把时间在 push() 中捕获一次后作为参数下传。

♻️ 建议示例
     public boolean push(String title, String content) {
         if (config.getUrl() == null || config.getUrl().isBlank()) {
             throw new IllegalArgumentException("Webhook URL cannot be empty");
         }
 
+        long currentTime = System.currentTimeMillis();
         String method = normalizeMethod(config.getMethod());
         String contentType = normalizeContentType(config.getContentType());
         String bodyTemplate = config.getBodyTemplate() == null ? "" : config.getBodyTemplate();
-        String renderedBody = renderTemplate(bodyTemplate, title, content, contentType);
+        String renderedBody = renderTemplate(bodyTemplate, title, content, contentType, currentTime);
         RequestBody requestBody = createRequestBody(method, contentType, renderedBody);
 
-        String renderedUrl = renderUrlTemplate(config.getUrl(), title, content);
+        String renderedUrl = renderUrlTemplate(config.getUrl(), title, content, currentTime);
@@
-    private String renderTemplate(String template, String title, String content, String contentType) {
-        long currentTime = System.currentTimeMillis();
+    private String renderTemplate(String template, String title, String content, String contentType, long currentTime) {
         String date = TimeUtil.formatDateOnly(currentTime);
@@
-    private String renderUrlTemplate(String urlTemplate, String title, String content) {
-        long now = System.currentTimeMillis();
+    private String renderUrlTemplate(String urlTemplate, String title, String content, long now) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
around lines 206 - 239, The body and URL use separate timestamps leading to
inconsistent {datetime}; capture the timestamp once in push() (e.g., long ts =
System.currentTimeMillis()) and pass it into renderTemplate(...) and
renderUrlTemplate(...) as an extra parameter instead of letting each method call
System.currentTimeMillis() internally; update the signatures of renderTemplate
and renderUrlTemplate to accept the timestamp (or formatted date/time strings)
and use that value for TimeUtil.formatDateOnly/formatTimeOnly/formatDateTime so
both body and URL use the identical time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 167-182: The createRequestBody method contains an unreachable
return null for the empty-body branch because normalizeMethod restricts method
to GET/POST and GET already returns null; update createRequestBody so that when
bodyContent.isEmpty() it does not try to return null for POST (which would cause
Request.Builder.method("POST", null) errors), instead always create an empty
RequestBody using the resolved mediaType; reference createRequestBody and the
normalizeMethod behavior to locate the logic to simplify/remove the inner if and
the unreachable return null.
- Around line 206-239: The body and URL use separate timestamps leading to
inconsistent {datetime}; capture the timestamp once in push() (e.g., long ts =
System.currentTimeMillis()) and pass it into renderTemplate(...) and
renderUrlTemplate(...) as an extra parameter instead of letting each method call
System.currentTimeMillis() internally; update the signatures of renderTemplate
and renderUrlTemplate to accept the timestamp (or formatted date/time strings)
and use that value for TimeUtil.formatDateOnly/formatTimeOnly/formatDateTime so
both body and URL use the identical time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8fdbfee4-475a-47bc-a329-b68ca3edd52f

📥 Commits

Reviewing files that changed from the base of the PR and between 3681487 and 48f1d04.

📒 Files selected for processing (1)
  • src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java

@gongfuture
Copy link
Copy Markdown
Contributor Author

🧹 Nitpick comments (2)
src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java (2)

230-230: 末尾多了一个分号

URLEncoder.encode(...).replace("+", "%20");; 末尾的 ;; 是一个空语句,虽然合法但属于明显笔误,建议清理。

♻️ 建议修改
-        java.util.function.UnaryOperator<String> enc = v -> v == null ? "" : URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");;
+        java.util.function.UnaryOperator<String> enc = v -> v == null ? "" : URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
at line 230, In WebhookPushProvider.java the local UnaryOperator<String> enc
assignment contains a stray empty statement (";;") at the end; locate the enc
variable declaration in class WebhookPushProvider (the line defining
"java.util.function.UnaryOperator<String> enc = v -> v == null ? "" :
URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");;") and remove
the extra trailing semicolon so the assignment ends with a single semicolon,
leaving the encoding logic unchanged.

206-239: renderTemplaterenderUrlTemplate 存在明显重复,建议抽取通用替换逻辑

两个方法仅 esc 策略不同(JSON 转义 vs URL 编码),其余 7 个占位符({title} {content} {level} {date} {time} {datetime} {channelName})的展开逻辑完全一致。可抽取一个接受 UnaryOperator<String> 的私有方法以避免后续新增/重命名变量时出现两边遗漏不同步。

♻️ 重构示例
     private String renderTemplate(String template, String title, String content, String contentType) {
-        long currentTime = System.currentTimeMillis();
-        String date = TimeUtil.formatDateOnly(currentTime);
-        String time = TimeUtil.formatTimeOnly(currentTime);
-        String datetime = TimeUtil.formatDateTime(currentTime);
         boolean json = contentType != null && contentType.toLowerCase(Locale.ROOT).contains("json");
-        java.util.function.UnaryOperator<String> esc = v -> {
+        UnaryOperator<String> esc = v -> {
             if (v == null) return "";
             if (!json) return v;
             String s = JsonUtil.standard().toJson(v);
             return s.substring(1, s.length() - 1);
         };
-        return template
-            .replace("{title}", esc.apply(title))
-            ...
+        return applyVariables(template, title, content, esc);
     }

     private String renderUrlTemplate(String urlTemplate, String title, String content) {
-        long now = System.currentTimeMillis();
-        java.util.function.UnaryOperator<String> enc = v -> v == null ? "" : URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");
-        return urlTemplate
-            .replace("{title}", enc.apply(title))
-            ...
+        UnaryOperator<String> enc = v -> v == null ? "" :
+                URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");
+        return applyVariables(urlTemplate, title, content, enc);
     }

+    private String applyVariables(String template, String title, String content, UnaryOperator<String> esc) {
+        long now = System.currentTimeMillis();
+        return template
+                .replace("{title}", esc.apply(title))
+                .replace("{content}", esc.apply(content))
+                .replace("{level}", esc.apply(extractLevel(title)))
+                .replace("{date}", esc.apply(TimeUtil.formatDateOnly(now)))
+                .replace("{time}", esc.apply(TimeUtil.formatTimeOnly(now)))
+                .replace("{datetime}", esc.apply(TimeUtil.formatDateTime(now)))
+                .replace("{channelName}", esc.apply(name));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
around lines 206 - 239, renderTemplate and renderUrlTemplate duplicate the same
placeholder-replacement logic except for the escaping strategy; extract a
private helper like renderWithEscaper(UnaryOperator<String> esc, String
template, String title, String content) that performs the common
.replace("{title}"...) … .replace("{channelName}"...) sequence and then have
renderTemplate call it with the JSON-escaping operator (the existing esc) and
renderUrlTemplate call it with the URL-encoding operator (the existing enc).
Update renderTemplate to compute date/time/datetime and the esc operator then
delegate, and update renderUrlTemplate to compute now and enc then delegate,
keeping extractLevel(title) and name references intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Line 230: In WebhookPushProvider.java the local UnaryOperator<String> enc
assignment contains a stray empty statement (";;") at the end; locate the enc
variable declaration in class WebhookPushProvider (the line defining
"java.util.function.UnaryOperator<String> enc = v -> v == null ? "" :
URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");;") and remove
the extra trailing semicolon so the assignment ends with a single semicolon,
leaving the encoding logic unchanged.
- Around line 206-239: renderTemplate and renderUrlTemplate duplicate the same
placeholder-replacement logic except for the escaping strategy; extract a
private helper like renderWithEscaper(UnaryOperator<String> esc, String
template, String title, String content) that performs the common
.replace("{title}"...) … .replace("{channelName}"...) sequence and then have
renderTemplate call it with the JSON-escaping operator (the existing esc) and
renderUrlTemplate call it with the URL-encoding operator (the existing enc).
Update renderTemplate to compute date/time/datetime and the esc operator then
delegate, and update renderUrlTemplate to compute now and enc then delegate,
keeping extractLevel(title) and name references intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bad47fc3-ce69-45c6-9d32-52f5328a5ee0

📥 Commits

Reviewing files that changed from the base of the PR and between f99b105 and 3681487.

📒 Files selected for processing (1)
  • src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java

明天再改这个

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`:
- Around line 206-228: renderTemplate currently calls extractLevel(title)
without guarding against a null title which causes an NPE when title is null;
modify the code so extractLevel(String) gracefully handles a null/empty input
and returns a safe default like "INFO" (or update renderTemplate to pass a
non-null default when calling extractLevel), and apply the same null-safe change
to the other similar call sites referenced (the second renderTemplate usage
around the 231-237 range). Ensure the unique symbols extractLevel and
renderTemplate are updated so LEVEL_PATTERN.matcher(...) is never invoked with a
null argument.
- Line 135: 当前问题是敏感请求头(如 Authorization、Cookie、Proxy-Authorization、X-API-Key)会被
HttpLoggingInterceptor 的 HEADERS 级别明文输出;定位到 WebhookPushProvider 使用
httpUtil.newBuilder().build().newCall(request).execute(),请在
HTTPUtil.newBuilder()(原 156-203 区段)统一在构造 OkHttpClient/HttpLoggingInterceptor 时调用
interceptor.redactHeader(...) 对上述敏感头进行脱敏(确保在将 interceptor 添加到 client 之前完成
redact),并覆盖所有相同构造分支(注释中提到的 195-203 区间)以避免 webhook token 等凭证被写入日志。
- Around line 118-145: The push(String title, String content) method currently
throws on invalid URL, non-2xx HTTP responses and exceptions; change it to
swallow these errors and return false instead so callers receive a boolean
success indicator rather than an exception. Specifically, in
WebhookPushProvider.push use config.getUrl() validation to log the bad URL and
return false instead of throwing, treat non-successful Response
(response.isSuccessful()) by logging the responseBody and returning false rather
than throwing IllegalStateException, and catch generic Exception from the
httpUtil.newBuilder() call to log the error and return false; keep successful
path returning true. Ensure any logging uses existing logger and do not change
the method signature.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4edd6949-2519-4ac9-8c3b-bec34d66ac71

📥 Commits

Reviewing files that changed from the base of the PR and between 3681487 and bdeaff2.

📒 Files selected for processing (1)
  • src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java

Comment on lines +118 to +145
public boolean push(String title, String content) {
if (config.getUrl() == null || config.getUrl().isBlank()) {
throw new IllegalArgumentException("Webhook URL cannot be empty");
}

String method = normalizeMethod(config.getMethod());
String contentType = normalizeContentType(config.getContentType());
String bodyTemplate = config.getBodyTemplate() == null ? "" : config.getBodyTemplate();
String renderedBody = renderTemplate(bodyTemplate, title, content, contentType, false);
RequestBody requestBody = createRequestBody(method, contentType, renderedBody);

String renderedUrl = renderTemplate(config.getUrl(), title, content, contentType, true);
Request.Builder requestBuilder = new Request.Builder().url(renderedUrl).method(method, requestBody);

applyContentType(requestBuilder, method, requestBody);
applyCustomHeaders(requestBuilder);
Request request = requestBuilder.build();
try (Response response = httpUtil.newBuilder().build().newCall(request).execute()) {
if (!response.isSuccessful()) {
String responseBody = response.body() != null ? response.body().string() : "<empty>";
throw new IllegalStateException("HTTP failed while sending push messages to Webhook: " + responseBody);
}
} catch (IllegalStateException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("Failed to send push message to Webhook", e);
}
return true;
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 | 🟠 Major

push() 的失败路径应收敛为 false,不要直接抛异常。

这里在 URL 校验、HTTP 非 2xx 和执行异常时都会抛出异常,但 PushProvider#push(String, String) 的接口语义是用 boolean 表示成败。当前实现会把单个渠道失败升级成异常路径,调用方没法按“失败返回 false”继续处理其它通知渠道。建议把这些异常留在方法内部,并返回 false

♻️ 可参考的收敛方式
     `@Override`
     public boolean push(String title, String content) {
-        if (config.getUrl() == null || config.getUrl().isBlank()) {
-            throw new IllegalArgumentException("Webhook URL cannot be empty");
-        }
-
-        String method = normalizeMethod(config.getMethod());
-        String contentType = normalizeContentType(config.getContentType());
-        String bodyTemplate = config.getBodyTemplate() == null ? "" : config.getBodyTemplate();
-        String renderedBody = renderTemplate(bodyTemplate, title, content, contentType, false);
-        RequestBody requestBody = createRequestBody(method, contentType, renderedBody);
-
-        String renderedUrl = renderTemplate(config.getUrl(), title, content, contentType, true);
-        Request.Builder requestBuilder = new Request.Builder().url(renderedUrl).method(method, requestBody);  
-
-        applyContentType(requestBuilder, method, requestBody);
-        applyCustomHeaders(requestBuilder);
-        Request request = requestBuilder.build();
-        try (Response response = httpUtil.newBuilder().build().newCall(request).execute()) {
-            if (!response.isSuccessful()) {
-                String responseBody = response.body() != null ? response.body().string() : "<empty>";
-                throw new IllegalStateException("HTTP failed while sending push messages to Webhook: " + responseBody);
-            }
-        } catch (IllegalStateException e) {
-            throw e;
-        } catch (Exception e) {
-            throw new IllegalStateException("Failed to send push message to Webhook", e);
-        }
-        return true;
+        try {
+            if (config.getUrl() == null || config.getUrl().isBlank()) {
+                return false;
+            }
+
+            String method = normalizeMethod(config.getMethod());
+            String contentType = normalizeContentType(config.getContentType());
+            String bodyTemplate = config.getBodyTemplate() == null ? "" : config.getBodyTemplate();
+            String renderedBody = renderTemplate(bodyTemplate, title, content, contentType, false);
+            RequestBody requestBody = createRequestBody(method, contentType, renderedBody);
+
+            String renderedUrl = renderTemplate(config.getUrl(), title, content, contentType, true);
+            Request.Builder requestBuilder = new Request.Builder().url(renderedUrl).method(method, requestBody);
+            applyContentType(requestBuilder, method, requestBody);
+            applyCustomHeaders(requestBuilder);
+
+            try (Response response = httpUtil.newBuilder().build().newCall(requestBuilder.build()).execute()) {
+                return response.isSuccessful();
+            }
+        } catch (Exception e) {
+            return false;
+        }
     }
📝 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
public boolean push(String title, String content) {
if (config.getUrl() == null || config.getUrl().isBlank()) {
throw new IllegalArgumentException("Webhook URL cannot be empty");
}
String method = normalizeMethod(config.getMethod());
String contentType = normalizeContentType(config.getContentType());
String bodyTemplate = config.getBodyTemplate() == null ? "" : config.getBodyTemplate();
String renderedBody = renderTemplate(bodyTemplate, title, content, contentType, false);
RequestBody requestBody = createRequestBody(method, contentType, renderedBody);
String renderedUrl = renderTemplate(config.getUrl(), title, content, contentType, true);
Request.Builder requestBuilder = new Request.Builder().url(renderedUrl).method(method, requestBody);
applyContentType(requestBuilder, method, requestBody);
applyCustomHeaders(requestBuilder);
Request request = requestBuilder.build();
try (Response response = httpUtil.newBuilder().build().newCall(request).execute()) {
if (!response.isSuccessful()) {
String responseBody = response.body() != null ? response.body().string() : "<empty>";
throw new IllegalStateException("HTTP failed while sending push messages to Webhook: " + responseBody);
}
} catch (IllegalStateException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("Failed to send push message to Webhook", e);
}
return true;
public boolean push(String title, String content) {
try {
if (config.getUrl() == null || config.getUrl().isBlank()) {
return false;
}
String method = normalizeMethod(config.getMethod());
String contentType = normalizeContentType(config.getContentType());
String bodyTemplate = config.getBodyTemplate() == null ? "" : config.getBodyTemplate();
String renderedBody = renderTemplate(bodyTemplate, title, content, contentType, false);
RequestBody requestBody = createRequestBody(method, contentType, renderedBody);
String renderedUrl = renderTemplate(config.getUrl(), title, content, contentType, true);
Request.Builder requestBuilder = new Request.Builder().url(renderedUrl).method(method, requestBody);
applyContentType(requestBuilder, method, requestBody);
applyCustomHeaders(requestBuilder);
try (Response response = httpUtil.newBuilder().build().newCall(requestBuilder.build()).execute()) {
return response.isSuccessful();
}
} catch (Exception e) {
return false;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
around lines 118 - 145, The push(String title, String content) method currently
throws on invalid URL, non-2xx HTTP responses and exceptions; change it to
swallow these errors and return false instead so callers receive a boolean
success indicator rather than an exception. Specifically, in
WebhookPushProvider.push use config.getUrl() validation to log the bad URL and
return false instead of throwing, treat non-successful Response
(response.isSuccessful()) by logging the responseBody and returning false rather
than throwing IllegalStateException, and catch generic Exception from the
httpUtil.newBuilder() call to log the error and return false; keep successful
path returning true. Ensure any logging uses existing logger and do not change
the method signature.

applyContentType(requestBuilder, method, requestBody);
applyCustomHeaders(requestBuilder);
Request request = requestBuilder.build();
try (Response response = httpUtil.newBuilder().build().newCall(request).execute()) {
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 | 🟠 Major

自定义敏感请求头会被调试日志明文泄露。

这个 provider 允许用户配置 Authorization 一类的 header,但这里最终走的是 src/main/java/com/ghostchu/peerbanhelper/util/HTTPUtil.java:156-203newBuilder();该处在 debug 模式下启用了 HttpLoggingInterceptor.Level.HEADERS。这样 webhook token 会直接写进日志。建议在 HTTPUtil 统一对 AuthorizationCookieProxy-AuthorizationX-API-Key 等敏感头做 redactHeader(...),否则这个新功能会把用户凭证暴露给日志读取者。

Also applies to: 195-203

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
at line 135, 当前问题是敏感请求头(如 Authorization、Cookie、Proxy-Authorization、X-API-Key)会被
HttpLoggingInterceptor 的 HEADERS 级别明文输出;定位到 WebhookPushProvider 使用
httpUtil.newBuilder().build().newCall(request).execute(),请在
HTTPUtil.newBuilder()(原 156-203 区段)统一在构造 OkHttpClient/HttpLoggingInterceptor 时调用
interceptor.redactHeader(...) 对上述敏感头进行脱敏(确保在将 interceptor 添加到 client 之前完成
redact),并覆盖所有相同构造分支(注释中提到的 195-203 区间)以避免 webhook token 等凭证被写入日志。

Comment on lines +206 to +228
private String renderTemplate(String template, String title, String content, String contentType, boolean urlEncode) {
long currentTime = System.currentTimeMillis();
String date = TimeUtil.formatDateOnly(currentTime);
String time = TimeUtil.formatTimeOnly(currentTime);
String datetime = TimeUtil.formatDateTime(currentTime);
boolean json = contentType != null && contentType.toLowerCase(Locale.ROOT).contains("json");
java.util.function.UnaryOperator<String> transform = v -> {
if (v == null) return "";
if (urlEncode) {
return URLEncoder.encode(v, StandardCharsets.UTF_8).replace("+", "%20");
}
if (!json) return v;
String s = JsonUtil.standard().toJson(v); // 引号包裹的转义字符串
return s.substring(1, s.length() - 1); // 去掉外层引号
};
return template
.replace("{title}", transform.apply(title))
.replace("{content}", transform.apply(content))
.replace("{level}", transform.apply(extractLevel(title)))
.replace("{date}", transform.apply(date))
.replace("{time}", transform.apply(time))
.replace("{datetime}", transform.apply(datetime))
.replace("{channelName}", transform.apply(name));
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 | 🟡 Minor

title 为空时这里会在模板渲染阶段直接 NPE。

renderTemplate() 无论模板里是否用了 {level},都会先执行 extractLevel(title);而 extractLevel() 里的 LEVEL_PATTERN.matcher(title) 不接受 null。如果上游传入空标题,Webhook 会在发请求前直接失败。建议要么在 push() 入参处显式校验 title 非空,要么让 extractLevel(null) 回退为 "INFO"

Also applies to: 231-237

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ghostchu/peerbanhelper/util/push/impl/WebhookPushProvider.java`
around lines 206 - 228, renderTemplate currently calls extractLevel(title)
without guarding against a null title which causes an NPE when title is null;
modify the code so extractLevel(String) gracefully handles a null/empty input
and returns a safe default like "INFO" (or update renderTemplate to pass a
non-null default when calling extractLevel), and apply the same null-safe change
to the other similar call sites referenced (the second renderTemplate usage
around the 231-237 range). Ensure the unique symbols extractLevel and
renderTemplate are updated so LEVEL_PATTERN.matcher(...) is never invoked with a
null argument.

:label="t('page.settings.tab.config.push.form.webhook.body_template')"
required
>
<a-textarea
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

这个接入monaco editor吧

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

这个接入monaco editor吧

加上去感觉怪怪的
而且我认为这里不应该是写模板的主要地方
我webhook抄的是automas,他那边就是文本框来着

msedge_fOn6mumRs3 AUTO-MAS_3M4bOy5upt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

况且还有plaintext的情况,可能还得做个切换?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

monaco也支持plaintext,你这里既然是json,monaco自带格式检查,会好很多。另外你需要适配黑夜模式

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

好嘞

<div v-pre>{title} {content} {level} {date} {time} {datetime} {channelName}</div>
<div v-pre>{level}: TIP, *INFO*, WARN, ERROR, FATAL</div>
</a-alert>
</a-form-item>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

这个用一个tooltips折叠起来吧,放在输入框的右上角

</template>
</a-button>
</a-space>
<a-button long @click="addHeader">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Image

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

两个新增会功能重复吗

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

意思是没必要做这么大个按钮

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

okok理解了

Comment thread webui/src/api/model/push.ts
Comment on lines +118 to +170
const nextRowId = ref(0)
const createRow = (key = '', value = ''): HeaderRow => ({
id: nextRowId.value++,
key,
value
})

const headerRows = ref<HeaderRow[]>([])

const headersToRows = (headers: Record<string, string>): HeaderRow[] =>
Object.entries(headers).map(([key, value]) => createRow(key, value))

const rowsToHeaders = (rows: HeaderRow[]): Record<string, string> => {
const headers: Record<string, string> = {}
rows.forEach((row) => {
const key = row.key.trim()
if (!key) return
headers[key] = row.value
})
return headers
}

headerRows.value = headersToRows(model.value.headers ?? {})

// 仅在外部替换 model.headers 时回填 rows,避免与本地编辑互相覆盖
watch(
() => model.value.headers,
(headers) => {
const fromRows = rowsToHeaders(headerRows.value)
const next = headers ?? {}
if (JSON.stringify(fromRows) === JSON.stringify(next)) {
return
}
headerRows.value = headersToRows(next)
}
)

// rows 作为编辑真源,深度监听后回写 model(过滤空 key 仅影响提交数据,不影响 UI 行)
watch(
headerRows,
(rows) => {
model.value.headers = rowsToHeaders(rows)
},
{ deep: true }
)

const addHeader = () => {
headerRows.value.push(createRow())
}

const removeHeader = (index: number) => {
headerRows.value.splice(index, 1)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

这写的是啥啊。。。。咋搞的这么复杂?AI写的吗?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

是这样的非常抱歉,比起后端 前端我更是一窍不通所以全是ai写的ww

我这就改

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend This is a backend related enhancement 功能增强 java Pull requests that update Java code javascript Pull requests that update Javascript code webui This is a webui related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants