Files
3m-ui/backend/internal/auth/api.go
T
風雪 dc13e69951 feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
feat(telegram): full bot enhancement with inline menus, cron reports, events
Reference: https://docs.sanaei.dev/zh/docs/operations/telegram-bot/
Field naming follows existing 3m-ui snake_case convention (not sanaei's
tg* prefix) per project maintainer's request.
== Data layer ==
- ProxyUser: add TelegramID (indexed) + TelegramName fields for linking
proxy users to their Telegram accounts (self-service queries).
- SafeUser + ToSafeUser: expose both new fields.
- user.Service: BindTelegram / UnbindTelegram / GetByTelegramID methods.
- user API: GET /by-telegram/:tgid, PUT /:id/telegram, DELETE /:id/telegram
(registered before /:id to avoid path capture).
- UpdateInput/CreateInput: accept TelegramID + TelegramName.
== Settings expansion (8 new fields, snake_case) ==
- schedule: cron expr or @daily/@every 6h for periodic reports (default @daily)
- attach_backup: include DB backup zip in periodic report
- language: bot message language zh/en (default zh)
- enabled_events: comma-sep event list (default 'login,cpu,crash')
Supports: login,cpu,memory,crash,node_down,node_up,block,unblock,expiry,traffic
- expiry_warn_days: remind N days before expiry (0=off)
- traffic_warn_gb: remind when remaining traffic < N GB (0=off)
- proxy_url: socks5:// or http:// proxy for Telegram API
- api_server: custom Bot API server (empty=official)
- EventEnabled(name) helper for per-event gating.
== Client enhancements ==
- Proxy support: http(s):// via http.Transport.Proxy; socks5:// via
golang.org/x/net/proxy SOCKS5 dialer. Parse failure → direct fallback.
- Custom API server (apiBase() replaces hardcoded URL).
- SendDocument: multipart sendDocument for backup file attachment.
- GetMe + SetMyCommands: for bot-info + set-my-commands endpoints.
- sendWithKeyboard: sendMessage with reply_markup (inline keyboards).
- answerCallbackQuery: clear callback loading spinner.
== Bot commands + inline menus ==
- /id: reply with sender's Telegram numeric ID.
- /usage: admin → search users; bound user → self-query traffic/expiry/sub URL.
- /start + /help: send inline keyboard menu (admin menu for admin chats,
user menu for bound users, plain text for unbound).
- Admin menu: 8 buttons (status/users/traffic/online/listeners/deldepleted/
backup/restart) in 4 rows.
- User menu: my_usage + my_sub buttons.
- callback_query handling: answerCallbackQuery + dispatch on callback_data.
- Access control: admin chats always pass; non-admin chats pass only when
FromID matches a ProxyUser.TelegramID.
- Bot struct: added userSvc field for GetByTelegramID lookups.
- NewBot signature: NewBot(db, mihomoSvc, userSvc).
== Cron scheduler (new telegram/scheduler.go) ==
- Periodic report goroutine driven by Settings.Schedule.
- Supports @hourly/@daily/@weekly/@monthly/@every <dur>/5-field cron.
- Report: host, version, uptime, CPU/mem/disk, user stats, expiring+depleted
counts.
- Optional backup: AttachBackup → system.WriteZip → SendDocument.
- Settings reload on every tick (no restart needed for schedule changes).
- NotifyEvent(event, details): generic event emitter for one-shot alerts.
== Events layer ==
- login.attempt: NotifyLogin (success) + NotifyLoginFailed (failure).
Failure path added to auth/api.go Login handler. Both gated by
EventEnabled('login') — admins can mute success spam yet keep failure alerts.
- memory.high: emitMemoryWarning via gopsutil/v4/mem. Reuses CPUWarnPct as
shared 'resource warn %' threshold. 1-hour cooldown dedup.
- crash: mihomo ProcessManager.SetCrashHandler → telegram.NotifyCrash.
Fires on non-zero exit while desired==true (unexpected crash, not admin stop).
Wired in app/container.go.
- Dual thresholds: expiry (hours + days), traffic (percent + GB) with
separate dedup keys so neither suppresses the other.
== API endpoints ==
- POST /telegram/set-my-commands: registers 12-command menu with Telegram.
- GET /telegram/bot-info: returns bot username/first_name/id.
== Frontend ==
- telegram.ts: 8 new fields in TelegramSettings interface + setTelegramCommands
+ fetchTelegramBotInfo API functions.
- Settings.tsx: 8 new form controls (schedule Input, language Select,
enabled_events multi-Select with 10 options, expiry_warn_days + traffic_warn_gb
InputNumber, attach_backup Switch, proxy_url Input, api_server Input).
Added 'Set bot commands' button calling setTelegramCommands.
- i18n: 11 new keys (zh + en) for the new settings labels.
== Verification ==
- go build ./... : PASS
- go vet ./... : PASS
- go test ./... : all 15 packages PASS
- npm run build (tsc + vite) : PASS, 3241 modules
- gofmt : clean
2026-08-29 12:19:46 +00:00

345 lines
10 KiB
Go

package auth
import (
"crypto/subtle"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/kazeyukiro/3m-ui/backend/internal/config"
"github.com/kazeyukiro/3m-ui/backend/internal/database/models"
"github.com/kazeyukiro/3m-ui/backend/internal/telegram"
"gorm.io/gorm"
)
type loginAttempt struct {
count int
blocked time.Time
last time.Time
}
var loginLimiter = struct {
sync.Mutex
items map[string]loginAttempt
}{items: make(map[string]loginAttempt)}
var passwordChangeLimiter = struct {
sync.Mutex
items map[string]loginAttempt
}{items: make(map[string]loginAttempt)}
const (
loginWindow = 15 * time.Minute
loginMaxAttempt = 8
passwordChangeWindow = 15 * time.Minute
passwordChangeMaxAttempt = 3
)
// clientIdentifier returns a stable identifier for rate-limiting.
//
// By default the direct RemoteAddr IP is used so that a client cannot rotate
// arbitrary IPs via a spoofed X-Forwarded-For header. As a pragmatic
// improvement, when the direct connection originates from a loopback /
// private / link-local address (the typical case for a same-host reverse
// proxy such as nginx or caddy) we DO trust the right-most valid IP in
// X-Forwarded-For. This restores per-client rate-limiting granularity when
// deployed behind a local trusted proxy while still rejecting client-supplied
// XFF values on direct (public) exposure.
func clientIdentifier(c *gin.Context) string {
remote, _, err := net.SplitHostPort(c.Request.RemoteAddr)
if err == nil {
if ip := net.ParseIP(remote); ip != nil {
if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() {
if xff := strings.TrimSpace(c.GetHeader("X-Forwarded-For")); xff != "" {
parts := strings.Split(xff, ",")
for i := len(parts) - 1; i >= 0; i-- {
candidate := strings.TrimSpace(parts[i])
if p := net.ParseIP(candidate); p != nil {
return p.String()
}
}
}
}
return ip.String()
}
}
if ip := net.ParseIP(strings.TrimSpace(c.Request.RemoteAddr)); ip != nil {
return ip.String()
}
return "unknown"
}
func allowLogin(ip string) bool {
now := time.Now()
loginLimiter.Lock()
defer loginLimiter.Unlock()
for key, attempt := range loginLimiter.items {
if now.Sub(attempt.last) > loginWindow {
delete(loginLimiter.items, key)
}
}
attempt := loginLimiter.items[ip]
if !attempt.blocked.IsZero() && now.Before(attempt.blocked) {
return false
}
if attempt.last.IsZero() || now.Sub(attempt.last) > loginWindow {
attempt.count = 0
}
attempt.last = now
if attempt.count >= loginMaxAttempt {
attempt.blocked = now.Add(loginWindow)
loginLimiter.items[ip] = attempt
return false
}
attempt.count++
loginLimiter.items[ip] = attempt
return true
}
func resetLoginLimit(ip string) {
loginLimiter.Lock()
delete(loginLimiter.items, ip)
loginLimiter.Unlock()
}
// allowPasswordChange applies a per-client throttle to the password-change
// endpoint. The bound is intentionally tight (3 attempts / 15 min) because the
// caller is already authenticated, so the only legitimate traffic is a single
// user choosing a new password. Anything beyond that pattern is brute-forcing
// the current_password field or probing for hash collisions.
func allowPasswordChange(ip string) bool {
now := time.Now()
passwordChangeLimiter.Lock()
defer passwordChangeLimiter.Unlock()
for key, attempt := range passwordChangeLimiter.items {
if now.Sub(attempt.last) > passwordChangeWindow {
delete(passwordChangeLimiter.items, key)
}
}
attempt := passwordChangeLimiter.items[ip]
if !attempt.blocked.IsZero() && now.Before(attempt.blocked) {
return false
}
if attempt.last.IsZero() || now.Sub(attempt.last) > passwordChangeWindow {
attempt.count = 0
}
attempt.last = now
if attempt.count >= passwordChangeMaxAttempt {
attempt.blocked = now.Add(passwordChangeWindow)
passwordChangeLimiter.items[ip] = attempt
return false
}
attempt.count++
passwordChangeLimiter.items[ip] = attempt
return true
}
func resetPasswordChangeLimit(ip string) {
passwordChangeLimiter.Lock()
delete(passwordChangeLimiter.items, ip)
passwordChangeLimiter.Unlock()
}
type Handler struct {
db *gorm.DB
secret string
cfg *config.Config
}
func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
secret := ""
if cfg != nil {
secret = cfg.JWT.Secret
}
return &Handler{db: db, secret: secret, cfg: cfg}
}
func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) {
rg.POST("/login", h.Login)
rg.POST("/password", RequireAuth(h.db, h.secret), h.ChangePassword)
rg.GET("/me", RequireAuth(h.db, h.secret), h.Me)
}
func (h *Handler) Login(c *gin.Context) {
clientID := clientIdentifier(c)
if !allowLogin(clientID) {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many login attempts; try again later"})
return
}
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}
if h.db == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "database is not configured"})
return
}
result, err := Login(h.db, h.secret, input)
if err != nil {
status := http.StatusUnauthorized
if err.Error() != "invalid username or password" {
status = http.StatusInternalServerError
}
// Fire-and-forget: a failed login is worth a Telegram alert even when
// the success path is muted. NotifyLoginFailed self-gates via
// settings.EventEnabled("login") and silently no-ops if Telegram is
// disabled, misconfigured, or the DB is unreachable.
go telegram.NotifyLoginFailed(h.db, input.Username, clientID)
c.JSON(status, gin.H{"error": "invalid username or password"})
return
}
resetLoginLimit(clientID)
// optional Telegram notice on successful panel login.
go telegram.NotifyLogin(h.db, input.Username, clientID)
c.JSON(http.StatusOK, result)
}
func (h *Handler) ChangePassword(c *gin.Context) {
clientID := clientIdentifier(c)
if !allowPasswordChange(clientID) {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many password change attempts; try again later"})
return
}
claims, ok := ClaimsFromContext(c)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
var req struct {
CurrentPassword string `json:"current_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "current_password and new_password are required"})
return
}
if len([]rune(req.NewPassword)) < 8 {
c.JSON(http.StatusBadRequest, gin.H{"error": "new password must be at least 8 characters"})
return
}
if subtle.ConstantTimeCompare([]byte(req.NewPassword), []byte(req.CurrentPassword)) == 1 {
c.JSON(http.StatusBadRequest, gin.H{"error": "new password must differ from current password"})
return
}
var user models.User
if err := h.db.First(&user, claims.UserID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
if !CheckPasswordHash(req.CurrentPassword, user.PasswordHash) {
c.JSON(http.StatusBadRequest, gin.H{"error": "current password is incorrect"})
return
}
hash, err := HashPassword(req.NewPassword)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
return
}
nextSessionVersion := user.SessionVersion + 1
if nextSessionVersion == 0 {
nextSessionVersion = 1
}
if err := h.db.Model(&user).Updates(map[string]any{
"password_hash": hash,
"must_change_password": false,
"session_version": nextSessionVersion,
}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save password"})
return
}
resetPasswordChangeLimit(clientID)
c.JSON(http.StatusOK, gin.H{"status": "ok", "message": "password changed successfully"})
}
func (h *Handler) Me(c *gin.Context) {
claims, ok := ClaimsFromContext(c)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
var user models.User
if err := h.db.First(&user, claims.UserID).Error; err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"user_id": claims.UserID,
"username": claims.Username,
"role": claims.Role,
"expires_at": claims.ExpiresAt,
"must_change_password": user.MustChangePassword,
})
}
// RequireAuth validates the Bearer JWT against the provided secret and loads
// the administrator from the provided database.
func RequireAuth(db *gorm.DB, secret string) gin.HandlerFunc {
return func(c *gin.Context) {
token := TokenFromRequest(c.GetHeader("Authorization"))
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
claims, err := ParseToken(secret, token)
if err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
return
}
if db == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
var user models.User
if err := db.First(&user, claims.UserID).Error; err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
if !strings.EqualFold(user.Role, "admin") {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "administrator access required"})
return
}
if user.SessionVersion == 0 || claims.SessionVersion == 0 || user.SessionVersion != claims.SessionVersion {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "session has been invalidated; please log in again"})
return
}
c.Set("auth.claims", claims)
c.Set("auth.user", &user)
path := c.Request.URL.Path
if path != "/api/v1/auth/password" && path != "/api/v1/auth/me" && user.MustChangePassword {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
"error": "password change required",
"code": "PASSWORD_CHANGE_REQUIRED",
})
return
}
c.Next()
}
}
func ClaimsFromContext(c *gin.Context) (*JWTClaims, bool) {
value, ok := c.Get("auth.claims")
if !ok {
return nil, false
}
claims, ok := value.(*JWTClaims)
return claims, ok
}