feat(discord): add Discord notification bot service (#6486)

* feat(discord): add Discord notification bot service, settings UI, and event subscriber
- internal/web/service/discord: implement lightweight Discord REST API v10 client and EventBus subscriber
- internal/web/service/setting: add discordBotEnable, discordBotToken, discordChannelId, discordEnabledEvents, discordCpu, discordMemory settings and secret protection
- internal/web/controller: register POST /panel/api/setting/testDiscord endpoint
- frontend: add Discord settings tab, notifications configuration, sidebar navigation, and command palette integration
- translation: add localization keys across all 13 locales
- tests: add comprehensive unit tests with httptest server and verify route/i18n contracts

* fix(discord): address PR review findings on concurrency, linting, i18n, and stories

- subscriber: eliminate unbounded goroutines, sending inline per EventBus contract
- discord: accept context.Context in SendMessage, SendEmbed, SendTest with http.NewRequestWithContext
- format: apply gofumpt to controller and entity struct alignments
- i18n: localize testDiscord controller responses across all 13 locales
- storybook: add DiscordNotifications.stories.tsx component story

* docs: add Discord bot setup and operations guide

- add docs/content/docs/en/operations/discord-bot.mdx with setup steps, event indicators, settings, and troubleshooting
- add docs/content/docs/ru/operations/discord-bot.mdx with localized instructions
- update operations/meta.json across en, ru, zh, fa
- link Discord bot from panel configuration overview

* feat(discord): add discordLang, discordRunTime, discordBotBackup settings and update settings UI

- internal/web/entity: add DiscordRunTime, DiscordBotBackup, DiscordLang fields to AllSetting
- internal/web/service/setting: add defaultValueMap entries, getters, and setters
- frontend: update AllSetting schema, model defaults, and generate OpenAPI / Zod contracts
- frontend: extract shared NotifyTimeField component and update DiscordTab with General and Notifications tabs
- translation: add localization keys across all 13 locales

* feat(discord): implement scheduled status reports and database backup attachments

- internal/web/service/discord: add SendMessageWithFiles supporting multipart uploads
- internal/web/service/discord: implement BuildReport and SendReport generating rich status embeds
- internal/web/service/discord: attach database backup (and config.json) when discordBotBackup is enabled
- internal/web/job: implement DiscordNotifyJob scheduled via robfig/cron
- internal/web/locale: add LocalizerFor and I18nForLang helpers
- internal/web/controller: trigger reloadDiscordFunc to dynamically reschedule cron upon setting changes
- internal/web/web: register and reschedule DiscordNotifyJob
- tests: comprehensive unit tests for multipart uploads, status reporting, and job execution

* feat(discord): add interactive bot commands via Gateway WebSocket and update documentation

- internal/web/service/discord/gateway: connect to Discord Gateway v10 via WebSocket (gorilla/websocket)
- internal/web/service/discord/gateway: handle heartbeat loop, reconnection, and command dispatch
- commands: implement !status, !report, !backup, !usage <email>, !inbounds, !restart, !help (with ! and / prefixes)
- internal/web/web: start/stop Gateway client with server and reload dynamically on setting updates
- docs: update operations guide (en, ru) with scheduled reports, backups, commands, and privileged intents
- tests: add end-to-end WebSocket Gateway test verifying command handling

* style(discord): fix goimports formatting and add 3x-ui to gitignore

* fix(discord): stop gateway panics, reconnect storms and proxy bypass

The Gateway client wrote to its websocket from both the heartbeat ticker
and the read loop answering server-requested op 1 heartbeats. gorilla
panics on concurrent writes and neither goroutine recovers, so a colliding
heartbeat took the whole panel process down; writes now share writeMu.

It also reconnected every 5s forever after close codes Discord marks
non-reconnectable (4004 bad token, 4010-4014, including 4014 when Message
Content Intent is off), re-identifying and logging a warning each time.
The loop now stops on those codes; the docs say to restart the panel.

The gateway dialed with websocket.DefaultDialer, bypassing the panel
egress proxy the REST client already uses, so where Discord is filtered
notifications arrived but commands never connected.

* fix(discord): deliver the scheduled report when the backup upload fails

SendReport posted the report embed and the x-ui.db/config.json attachments
in one multipart request. Once the database outgrows Discord's upload cap
(20 MiB by default) the request is rejected and the report embed is lost
with it on every run, leaving only a log warning. Send the embed first and
the attachments as a second message.

* chore(discord): delete tests that pass whether or not the code works

TestDiscordNotifyJob_NilServiceNoPanic and TestHandleEvent_NilDiscordService
feed a nil DiscordService that web.go never passes, and
TestDiscordNotifyJob_DisabledNoPanic passes with or without the enable
guard because Xray is not running under test.

* fix(discord): require admin user IDs for bot commands and honor discordLang

Any member who could post in the configured channel could run !backup
(the whole x-ui.db and config.json, even with discordBotBackup off),
!restart and !usage. Commands now run only for the Discord user IDs in
the new discordAdminIds setting; an empty list turns commands off.

discordLang was saved and offered in the UI, but nothing read it, so
every embed stayed English. The test message, alerts, the scheduled
report and command replies now render through I18nForLang in the chosen
language, with a discord section in all 13 locales. InitLocalizer takes
an fs.FS so tests load the real translation files.

---------

Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
This commit is contained in:
Egor
2026-09-13 17:04:53 +05:00
committed by GitHub
parent cba8f0672f
commit bf7ce2daaa
59 changed files with 6271 additions and 206 deletions
+100 -2
View File
@@ -1587,8 +1587,31 @@
"subHappGroupNetwork": "网络与 TUN 引擎",
"subHappGroupThemes": "界面与主题外观",
"subHappGroupFailover": "迁移与客户端管理",
"subHappGroupAndroid": "Android 分应用代理"
"subHappGroupAndroid": "Android 分应用代理",
"discordSettings": "Discord 機器人",
"discordBotEnable": "啟用 Discord 通知",
"discordBotEnableDesc": "透過機器人向 Discord 頻道發送系統與事件警報",
"discordBotToken": "Discord 機器人權杖",
"discordBotTokenDesc": "來自 Discord 開發者門戶的機器人權杖",
"discordTokenConfigured": "權杖已設定。輸入新權杖以取代。",
"discordTokenPlaceholder": "輸入機器人權杖",
"discordChannelId": "頻道 ID",
"discordChannelIdDesc": "接收通知的 Discord 頻道 ID",
"discordAdminIds": "管理員使用者 ID",
"discordAdminIdsDesc": "允許執行機器人指令的 Discord 使用者 ID,以英文逗號分隔。其他人的訊息會被忽略,清單為空時指令將被停用。",
"discordEventBusNotify": "Discord 通知",
"testDiscord": "傳送測試通知",
"testDiscordDesc": "傳送測試通知以驗證您的機器人權杖與頻道 ID",
"discordNotInitialized": "Discord 服務未初始化",
"discordBotNotEnabled": "Discord 機器人未啟用",
"discordTestFailed": "Discord 測試失敗",
"discordTestSuccess": "測試通知發送成功",
"discordBotLanguage": "Discord 機器人語言",
"discordNotifyTime": "通知時間",
"discordNotifyTimeDesc": "Discord 機器人傳送週期性報告的頻率。選擇預設間隔,或選擇「自訂」以輸入 crontab 運算式。",
"discordNotifyBackup": "資料庫備份",
"discordNotifyBackupDesc": "傳送帶有報告的資料庫備份檔案",
"discordEventBusNotifyDesc": "選擇觸發 Discord 通知的事件"
},
"xray": {
"save": "儲存",
@@ -2390,6 +2413,81 @@
"chooseInbound": "選擇一個入站"
}
},
"discord": {
"test": {
"title": "3x-ui Discord 通知測試",
"body": "這是一則測試通知,確認 Discord 通知已正確設定。"
},
"footer": "3x-ui 面板",
"fields": {
"panelVersion": "面板版本",
"xrayCore": "Xray 核心",
"systemLoad": "系統負載",
"networkTraffic": "網路流量",
"totalUsed": "已用總量",
"quota": "配額",
"error": "錯誤",
"delay": "延遲",
"outbound": "出站",
"node": "節點",
"threshold": "閾值",
"reason": "原因",
"time": "時間",
"source": "來源"
},
"values": {
"uptime": "{{ .Days }}天 {{ .Hours }}小時",
"traffic": "↑{{ .Up }} ↓{{ .Down }}(總計:{{ .Total }}",
"counts": "總計:{{ .Total }} | 即將耗盡:{{ .Depleting }} | 已停用:{{ .Disabled }}",
"inbound": "協定:`{{ .Protocol }}` | 連接埠:`{{ .Port }}` | 用戶端:`{{ .Clients }}` | 流量:`↑{{ .Up }} ↓{{ .Down }}` | 狀態:`{{ .State }}`"
},
"alerts": {
"outboundDown": "出站已中斷",
"outboundUp": "出站已恢復",
"nodeDown": "節點已離線",
"nodeUp": "節點已恢復",
"xrayCrash": "Xray 核心當機",
"cpuHigh": "CPU 超出閾值",
"memoryHigh": "記憶體超出閾值",
"loginSuccess": "登入成功",
"loginFailed": "登入失敗"
},
"report": {
"title": "📊 3x-ui 狀態報告",
"summary": "**{{ .Host }}** 的伺服器與代理定期狀態報告",
"footer": "3x-ui 排程報告 • 排程:{{ .RunTime }}"
},
"commands": {
"helpTitle": "🤖 3x-ui Discord 機器人指令",
"helpDescription": "用於監控和管理 3x-ui 伺服器的可用指令:",
"helpStatus": "顯示系統負載、記憶體、CPU、連線數和線上使用者",
"helpReport": "產生完整狀態報告(如已設定則附帶資料庫備份)",
"helpBackup": "立即傳送資料庫備份檔案",
"helpUsage": "查詢用戶端的流量使用、配額和到期時間",
"helpInbounds": "列出所有已設定的入站及其連接埠和用戶端統計",
"helpRestart": "重新啟動 Xray 核心",
"helpHelp": "顯示此可用指令清單",
"statusTitle": "⚡ 3x-ui 伺服器狀態",
"statusDescription": "**{{ .Host }}** 的目前運作指標",
"backupTitle": "🗄️ 資料庫備份",
"backupDescription": "3x-ui 備份產生於 `{{ .Time }}`",
"backupUnavailable": "❌ 備份服務無法使用",
"backupFailed": "❌ 讀取資料庫備份失敗:{{ .Error }}",
"usageHint": "⚠️ 用法:`!usage <email>` 或 `/usage <email>`",
"usageTitle": "👤 用戶端用量:{{ .Email }}",
"usageDescription": "入站:**{{ .Remark }}**(連接埠 {{ .Port }}",
"clientNotFound": "⚠️ 在任何已設定的入站中均未找到用戶端 `{{ .Email }}`。",
"inboundsUnavailable": "❌ 入站服務無法使用",
"inboundsFailed": "❌ 載入入站失敗:{{ .Error }}",
"inboundsTitle": "🔌 已設定的入站",
"inboundsDescription": "入站總數:**{{ .Count }}**",
"noInbounds": "ℹ️ 尚未設定任何入站。",
"xrayUnavailable": "❌ Xray 服務無法使用",
"restarting": "🔄 正在重新啟動 Xray 核心...",
"restartFailed": "❌ 重新啟動 Xray 失敗:{{ .Error }}",
"restartSuccess": "✅ Xray 核心已成功重新啟動。"
}
},
"email": {
"labelStatus": "狀態",
"labelOutbound": "出站",