mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-24 18:56:36 +08:00
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:
@@ -1587,8 +1587,31 @@
|
||||
"subHappGroupNetwork": "Network & TUN Engine",
|
||||
"subHappGroupThemes": "Appearance & Theme",
|
||||
"subHappGroupFailover": "Migration & App Management",
|
||||
"subHappGroupAndroid": "Android Per-App Proxy"
|
||||
|
||||
"subHappGroupAndroid": "Android Per-App Proxy",
|
||||
"discordSettings": "Bot Discord",
|
||||
"discordBotEnable": "Bật thông báo Discord",
|
||||
"discordBotEnableDesc": "Gửi cảnh báo hệ thống và sự kiện đến kênh Discord qua bot",
|
||||
"discordBotToken": "Token bot Discord",
|
||||
"discordBotTokenDesc": "Token bot từ Discord Developer Portal",
|
||||
"discordTokenConfigured": "Token đã được định cấu hình. Nhập token mới để thay thế.",
|
||||
"discordTokenPlaceholder": "Nhập token bot",
|
||||
"discordChannelId": "ID kênh",
|
||||
"discordChannelIdDesc": "ID kênh Discord nơi thông báo sẽ được gửi",
|
||||
"discordAdminIds": "ID người dùng quản trị",
|
||||
"discordAdminIdsDesc": "ID người dùng Discord được phép chạy lệnh bot, phân tách bằng dấu phẩy. Tin nhắn của người khác bị bỏ qua, và danh sách trống sẽ tắt lệnh.",
|
||||
"discordEventBusNotify": "Thông báo Discord",
|
||||
"testDiscord": "Gửi thông báo thử nghiệm",
|
||||
"testDiscordDesc": "Gửi thông báo thử nghiệm để xác minh token bot và ID kênh",
|
||||
"discordNotInitialized": "Dịch vụ Discord chưa được khởi tạo",
|
||||
"discordBotNotEnabled": "Bot Discord chưa được bật",
|
||||
"discordTestFailed": "Kiểm tra Discord thất bại",
|
||||
"discordTestSuccess": "Đã gửi thông báo thử nghiệm thành công",
|
||||
"discordBotLanguage": "Ngôn ngữ của Bot Discord",
|
||||
"discordNotifyTime": "Thời gian thông báo của bot Discord",
|
||||
"discordNotifyTimeDesc": "Tần suất bot Discord gửi báo cáo định kỳ. Chọn một khoảng thời gian có sẵn, hoặc chọn Tùy chỉnh để nhập biểu thức crontab.",
|
||||
"discordNotifyBackup": "Sao lưu Cơ sở dữ liệu",
|
||||
"discordNotifyBackupDesc": "Bao gồm tệp sao lưu cơ sở dữ liệu với thông báo báo cáo.",
|
||||
"discordEventBusNotifyDesc": "Chọn những sự kiện nào sẽ kích hoạt thông báo qua Discord"
|
||||
},
|
||||
"xray": {
|
||||
"importRules": "Nhập quy tắc",
|
||||
@@ -2390,6 +2413,81 @@
|
||||
"chooseInbound": "Chọn một Inbound"
|
||||
}
|
||||
},
|
||||
"discord": {
|
||||
"test": {
|
||||
"title": "Kiểm tra thông báo Discord của 3x-ui",
|
||||
"body": "Đây là thông báo thử nghiệm xác nhận rằng thông báo Discord đã được cấu hình đúng."
|
||||
},
|
||||
"footer": "Bảng điều khiển 3x-ui",
|
||||
"fields": {
|
||||
"panelVersion": "Phiên bản bảng điều khiển",
|
||||
"xrayCore": "Lõi Xray",
|
||||
"systemLoad": "Tải hệ thống",
|
||||
"networkTraffic": "Lưu lượng mạng",
|
||||
"totalUsed": "Tổng đã dùng",
|
||||
"quota": "Hạn mức",
|
||||
"error": "Lỗi",
|
||||
"delay": "Độ trễ",
|
||||
"outbound": "Outbound",
|
||||
"node": "Nút",
|
||||
"threshold": "Ngưỡng",
|
||||
"reason": "Lý do",
|
||||
"time": "Thời gian",
|
||||
"source": "Nguồn"
|
||||
},
|
||||
"values": {
|
||||
"uptime": "{{ .Days }} ngày {{ .Hours }} giờ",
|
||||
"traffic": "↑{{ .Up }} ↓{{ .Down }} (Tổng: {{ .Total }})",
|
||||
"counts": "Tổng: {{ .Total }} | Sắp hết: {{ .Depleting }} | Đã tắt: {{ .Disabled }}",
|
||||
"inbound": "Giao thức: `{{ .Protocol }}` | Cổng: `{{ .Port }}` | Máy khách: `{{ .Clients }}` | Lưu lượng: `↑{{ .Up }} ↓{{ .Down }}` | Trạng thái: `{{ .State }}`"
|
||||
},
|
||||
"alerts": {
|
||||
"outboundDown": "Outbound ngừng hoạt động",
|
||||
"outboundUp": "Outbound hoạt động trở lại",
|
||||
"nodeDown": "Nút ngừng hoạt động",
|
||||
"nodeUp": "Nút hoạt động trở lại",
|
||||
"xrayCrash": "Lõi Xray bị sập",
|
||||
"cpuHigh": "Vượt ngưỡng CPU",
|
||||
"memoryHigh": "Vượt ngưỡng bộ nhớ",
|
||||
"loginSuccess": "Đăng nhập thành công",
|
||||
"loginFailed": "Đăng nhập thất bại"
|
||||
},
|
||||
"report": {
|
||||
"title": "📊 Báo cáo trạng thái 3x-ui",
|
||||
"summary": "Báo cáo định kỳ trạng thái máy chủ và proxy của **{{ .Host }}**",
|
||||
"footer": "Báo cáo định kỳ 3x-ui • Lịch: {{ .RunTime }}"
|
||||
},
|
||||
"commands": {
|
||||
"helpTitle": "🤖 Lệnh bot Discord của 3x-ui",
|
||||
"helpDescription": "Các lệnh có sẵn để giám sát và quản lý máy chủ 3x-ui:",
|
||||
"helpStatus": "Hiển thị tải hệ thống, RAM, CPU, kết nối và người dùng trực tuyến",
|
||||
"helpReport": "Tạo báo cáo trạng thái đầy đủ (kèm bản sao lưu CSDL nếu được cấu hình)",
|
||||
"helpBackup": "Gửi ngay tệp sao lưu cơ sở dữ liệu",
|
||||
"helpUsage": "Tra cứu lưu lượng đã dùng, hạn mức và thời hạn của một máy khách",
|
||||
"helpInbounds": "Liệt kê tất cả inbound đã cấu hình kèm cổng và thống kê máy khách",
|
||||
"helpRestart": "Khởi động lại lõi Xray",
|
||||
"helpHelp": "Hiển thị danh sách lệnh này",
|
||||
"statusTitle": "⚡ Trạng thái máy chủ 3x-ui",
|
||||
"statusDescription": "Chỉ số vận hành hiện tại của **{{ .Host }}**",
|
||||
"backupTitle": "🗄️ Sao lưu cơ sở dữ liệu",
|
||||
"backupDescription": "Bản sao lưu 3x-ui được tạo lúc `{{ .Time }}`",
|
||||
"backupUnavailable": "❌ Dịch vụ sao lưu không khả dụng",
|
||||
"backupFailed": "❌ Không thể đọc bản sao lưu cơ sở dữ liệu: {{ .Error }}",
|
||||
"usageHint": "⚠️ Cách dùng: `!usage <email>` hoặc `/usage <email>`",
|
||||
"usageTitle": "👤 Mức dùng của máy khách: {{ .Email }}",
|
||||
"usageDescription": "Inbound: **{{ .Remark }}** (Cổng {{ .Port }})",
|
||||
"clientNotFound": "⚠️ Không tìm thấy máy khách `{{ .Email }}` trong bất kỳ inbound nào đã cấu hình.",
|
||||
"inboundsUnavailable": "❌ Dịch vụ inbound không khả dụng",
|
||||
"inboundsFailed": "❌ Không thể tải inbound: {{ .Error }}",
|
||||
"inboundsTitle": "🔌 Các inbound đã cấu hình",
|
||||
"inboundsDescription": "Tổng số inbound: **{{ .Count }}**",
|
||||
"noInbounds": "ℹ️ Chưa cấu hình inbound nào.",
|
||||
"xrayUnavailable": "❌ Dịch vụ Xray không khả dụng",
|
||||
"restarting": "🔄 Đang khởi động lại lõi Xray...",
|
||||
"restartFailed": "❌ Không thể khởi động lại Xray: {{ .Error }}",
|
||||
"restartSuccess": "✅ Đã khởi động lại lõi Xray thành công."
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"labelStatus": "Trạng thái",
|
||||
"labelOutbound": "Outbound",
|
||||
|
||||
Reference in New Issue
Block a user