From 3c946559f5d53dd8d4c858e290accd4089a38e3a Mon Sep 17 00:00:00 2001 From: ShukeBta Date: Sat, 30 May 2026 02:16:18 +0800 Subject: [PATCH] fix: simplify telegram channel access rules --- README.md | 24 ++++----- README_EN.md | 22 ++++----- docker-compose.yml | 2 +- internal/service/notify_channels.go | 70 +++++++++++++++++++++------ internal/service/notify_telegram.go | 26 ++++++---- internal/service/telegram_api_test.go | 37 ++++++++++++++ internal/service/telegram_bot.go | 4 +- web/src/pages/NotifyChannelsPage.tsx | 54 +++++++++------------ 8 files changed, 157 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 8eb2029..65c0ef0 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ mkdir -p data cache media downloads ```bash cat > .env <<'EOF' # 固定版本;需要升级时改成新的 MediaStationGo-vX.Y.Z 后执行 docker compose pull && docker compose up -d -MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23 +MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.24 MEDIASTATION_HTTP_PORT=18080 # 程序数据和缓存建议放在 MediaStationGo 部署目录下,便于备份和迁移。 @@ -307,7 +307,7 @@ vim docker-compose.yml # # 镜像版本: # 默认拉取 latest;如需固定版本,创建 .env 并写入: -# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23 +# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.24 # # 路径映射总览: # /data 程序数据目录。保存 SQLite 数据库、JWT secret、系统配置等,必须持久化。 @@ -516,7 +516,7 @@ docker compose up -d ```bash cat > .env <<'EOF' -MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23 +MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.24 MEDIASTATION_HTTP_PORT=18080 MEDIASTATION_DATA_DIR=./data MEDIASTATION_CACHE_DIR=./cache @@ -779,26 +779,26 @@ cd MediaStationGo | 平台 | 包名示例 | | --- | --- | -| Linux x86_64 | `MediaStationGo-v0.0.23-linux-amd64.tar.gz` | -| Linux ARM64 | `MediaStationGo-v0.0.23-linux-arm64.tar.gz` | -| Windows x86_64 | `MediaStationGo-v0.0.23-windows-amd64.zip` | -| macOS Intel | `MediaStationGo-v0.0.23-darwin-amd64.tar.gz` | -| macOS Apple Silicon | `MediaStationGo-v0.0.23-darwin-arm64.tar.gz` | +| Linux x86_64 | `MediaStationGo-v0.0.24-linux-amd64.tar.gz` | +| Linux ARM64 | `MediaStationGo-v0.0.24-linux-arm64.tar.gz` | +| Windows x86_64 | `MediaStationGo-v0.0.24-windows-amd64.zip` | +| macOS Intel | `MediaStationGo-v0.0.24-darwin-amd64.tar.gz` | +| macOS Apple Silicon | `MediaStationGo-v0.0.24-darwin-arm64.tar.gz` | 部署步骤: ```bash # Linux 示例 -tar -xzf MediaStationGo-v0.0.23-linux-amd64.tar.gz -cd MediaStationGo-v0.0.23-linux-amd64 +tar -xzf MediaStationGo-v0.0.24-linux-amd64.tar.gz +cd MediaStationGo-v0.0.24-linux-amd64 MEDIASTATION_APP_PORT=18080 ./mediastation-go ``` Windows: ```powershell -Expand-Archive .\MediaStationGo-v0.0.23-windows-amd64.zip -cd .\MediaStationGo-v0.0.23-windows-amd64 +Expand-Archive .\MediaStationGo-v0.0.24-windows-amd64.zip +cd .\MediaStationGo-v0.0.24-windows-amd64 $env:MEDIASTATION_APP_PORT = "18080" .\mediastation-go.exe ``` diff --git a/README_EN.md b/README_EN.md index 582d72d..ee438b4 100644 --- a/README_EN.md +++ b/README_EN.md @@ -235,7 +235,7 @@ mkdir -p data cache media downloads ```bash cat > .env <<'EOF' -MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23 +MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.24 MEDIASTATION_HTTP_PORT=18080 MEDIASTATION_DATA_DIR=./data MEDIASTATION_CACHE_DIR=./cache @@ -344,7 +344,7 @@ For production, pin a specific release tag instead of using `latest`. Recommende ```bash cat > .env <<'EOF' -MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23 +MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.24 MEDIASTATION_HTTP_PORT=18080 MEDIASTATION_DATA_DIR=./data MEDIASTATION_CACHE_DIR=./cache @@ -593,25 +593,25 @@ Each release provides multi-platform archives: | Platform | Package example | | --- | --- | -| Linux x86_64 | `MediaStationGo-v0.0.23-linux-amd64.tar.gz` | -| Linux ARM64 | `MediaStationGo-v0.0.23-linux-arm64.tar.gz` | -| Windows x86_64 | `MediaStationGo-v0.0.23-windows-amd64.zip` | -| macOS Intel | `MediaStationGo-v0.0.23-darwin-amd64.tar.gz` | -| macOS Apple Silicon | `MediaStationGo-v0.0.23-darwin-arm64.tar.gz` | +| Linux x86_64 | `MediaStationGo-v0.0.24-linux-amd64.tar.gz` | +| Linux ARM64 | `MediaStationGo-v0.0.24-linux-arm64.tar.gz` | +| Windows x86_64 | `MediaStationGo-v0.0.24-windows-amd64.zip` | +| macOS Intel | `MediaStationGo-v0.0.24-darwin-amd64.tar.gz` | +| macOS Apple Silicon | `MediaStationGo-v0.0.24-darwin-arm64.tar.gz` | Linux example: ```bash -tar -xzf MediaStationGo-v0.0.23-linux-amd64.tar.gz -cd MediaStationGo-v0.0.23-linux-amd64 +tar -xzf MediaStationGo-v0.0.24-linux-amd64.tar.gz +cd MediaStationGo-v0.0.24-linux-amd64 MEDIASTATION_APP_PORT=18080 ./mediastation-go ``` Windows example: ```powershell -Expand-Archive .\MediaStationGo-v0.0.23-windows-amd64.zip -cd .\MediaStationGo-v0.0.23-windows-amd64 +Expand-Archive .\MediaStationGo-v0.0.24-windows-amd64.zip +cd .\MediaStationGo-v0.0.24-windows-amd64 $env:MEDIASTATION_APP_PORT = "18080" .\mediastation-go.exe ``` diff --git a/docker-compose.yml b/docker-compose.yml index ec66800..33ddbc8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ # # 镜像版本: # 默认拉取 latest;如需固定版本,创建 .env 并写入: -# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.23 +# MEDIASTATION_IMAGE_TAG=MediaStationGo-v0.0.24 # # 路径映射总览: # /data 程序数据目录。保存 SQLite 数据库、JWT secret、系统配置等,必须持久化。 diff --git a/internal/service/notify_channels.go b/internal/service/notify_channels.go index 03a2810..63e79d5 100644 --- a/internal/service/notify_channels.go +++ b/internal/service/notify_channels.go @@ -222,16 +222,22 @@ func (s *NotifyChannelService) dispatchOne(ctx context.Context, n model.NotifyCh case "telegram": telegramCfg := telegramStringConfigFromAny(cfg) token := telegramCfg["bot_token"] - chat := telegramCfg["chat_id"] - if token == "" || chat == "" { - return errors.New("telegram missing bot_token / chat_id") + chats := telegramTargetChatIDs(telegramCfg) + if token == "" || len(chats) == 0 { + return errors.New("telegram missing bot_token / group_chat_id / channel_chat_id") } text := fmt.Sprintf("%s\n\n%s", escapeHTML(title), escapeHTML(body)) - form := url.Values{} - form.Set("chat_id", chat) - form.Set("text", text) - form.Set("parse_mode", "HTML") - return telegramPostForm(ctx, telegramCfg, "sendMessage", form, 15*time.Second) + var firstErr error + for _, chat := range chats { + form := url.Values{} + form.Set("chat_id", chat) + form.Set("text", text) + form.Set("parse_mode", "HTML") + if err := telegramPostForm(ctx, telegramCfg, "sendMessage", form, 15*time.Second); err != nil && firstErr == nil { + firstErr = err + } + } + return firstErr case "bark": key := str(cfg["device_key"]) @@ -325,15 +331,9 @@ func validateChannel(in ChannelInput) error { if str(cfg["bot_token"]) == "" { return errors.New("telegram bot_token required") } - if str(cfg["chat_id"]) == "" { - return errors.New("telegram notification chat_id required") - } if str(cfg["admin_user_ids"]) == "" { return errors.New("telegram admin_user_ids required") } - if str(cfg["group_chat_id"]) == "" && str(cfg["channel_chat_id"]) == "" && str(cfg["command_chat_id"]) == "" { - return errors.New("telegram group_chat_id or channel_chat_id required: 请填写绑定群组 ID 或绑定频道 ID;如果通知 Chat ID 是群组/频道负数 ID,也可以直接填在 Chat ID") - } } return nil } @@ -354,6 +354,48 @@ func normalizeChannelInput(in *ChannelInput) { } } +func telegramTargetChatIDs(cfg map[string]string) []string { + seen := map[string]bool{} + targets := []string{} + for _, key := range []string{"group_chat_id", "channel_chat_id"} { + chatID := strings.TrimSpace(cfg[key]) + if chatID == "" || seen[chatID] { + continue + } + seen[chatID] = true + targets = append(targets, chatID) + } + if len(targets) == 0 { + chatID := strings.TrimSpace(cfg["chat_id"]) + if strings.HasPrefix(chatID, "-") { + targets = append(targets, chatID) + } + } + if len(targets) == 0 { + for _, userID := range telegramConfiguredUserIDs(cfg["admin_user_ids"]) { + if seen[userID] { + continue + } + seen[userID] = true + targets = append(targets, userID) + } + } + return targets +} + +func telegramConfiguredUserIDs(raw string) []string { + out := []string{} + for _, value := range strings.FieldsFunc(raw, func(r rune) bool { + return r == ',' || r == ';' || r == ',' || r == ' ' || r == '\n' || r == '\t' + }) { + value = strings.TrimSpace(value) + if value != "" { + out = append(out, value) + } + } + return out +} + // str safely extracts a string from an interface{} loaded from JSON. func str(v any) string { if v == nil { diff --git a/internal/service/notify_telegram.go b/internal/service/notify_telegram.go index 17b8dfd..5847c8e 100644 --- a/internal/service/notify_telegram.go +++ b/internal/service/notify_telegram.go @@ -14,24 +14,30 @@ type TelegramProvider struct{} // Send 发送 Telegram 消息。 func (p *TelegramProvider) Send(ctx context.Context, cfg map[string]string, event NotifyEvent) error { botToken := cfg["bot_token"] - chatID := cfg["chat_id"] + chatIDs := telegramTargetChatIDs(cfg) parseMode := cfg["parse_mode"] if parseMode == "" { parseMode = "HTML" } - if botToken == "" || chatID == "" { - return fmt.Errorf("telegram: bot_token and chat_id are required") + if botToken == "" || len(chatIDs) == 0 { + return fmt.Errorf("telegram: bot_token and group_chat_id/channel_chat_id are required") } text := formatTelegramMessage(event, parseMode) - payload := map[string]string{ - "chat_id": chatID, - "text": text, - "parse_mode": parseMode, + var firstErr error + for _, chatID := range chatIDs { + payload := map[string]string{ + "chat_id": chatID, + "text": text, + "parse_mode": parseMode, + } + if err := telegramPostJSON(ctx, cfg, "sendMessage", payload, 15*time.Second); err != nil && firstErr == nil { + firstErr = err + } } - return telegramPostJSON(ctx, cfg, "sendMessage", payload, 15*time.Second) + return firstErr } // ValidateConfig 验证 Telegram 配置。 @@ -39,8 +45,8 @@ func (p *TelegramProvider) ValidateConfig(cfg map[string]string) error { if cfg["bot_token"] == "" { return fmt.Errorf("telegram: bot_token is required") } - if cfg["chat_id"] == "" { - return fmt.Errorf("telegram: chat_id is required") + if len(telegramTargetChatIDs(cfg)) == 0 { + return fmt.Errorf("telegram: group_chat_id or channel_chat_id is required") } return nil } diff --git a/internal/service/telegram_api_test.go b/internal/service/telegram_api_test.go index 3d5a696..c6911cd 100644 --- a/internal/service/telegram_api_test.go +++ b/internal/service/telegram_api_test.go @@ -32,3 +32,40 @@ func TestSanitizeTelegramErrorRedactsBotToken(t *testing.T) { t.Fatalf("expected timeout hint, got: %s", msg) } } + +func TestValidateTelegramChannelDoesNotRequireLegacyChatID(t *testing.T) { + err := validateChannel(ChannelInput{ + Name: "Telegram", + Type: "telegram", + Config: map[string]any{ + "bot_token": "123456:ABC-def", + "admin_user_ids": "10001", + }, + }) + if err != nil { + t.Fatalf("validateChannel returned error: %v", err) + } +} + +func TestTelegramTargetChatIDsFallsBackToAdmins(t *testing.T) { + got := telegramTargetChatIDs(map[string]string{ + "admin_user_ids": "10001, 10002", + }) + if len(got) != 2 || got[0] != "10001" || got[1] != "10002" { + t.Fatalf("got %#v, want admin user ids", got) + } +} + +func TestNormalizeTelegramChannelMigratesLegacyChatID(t *testing.T) { + input := ChannelInput{ + Name: "Telegram", + Type: "telegram", + Config: map[string]any{ + "chat_id": "-10001", + }, + } + normalizeChannelInput(&input) + if got := str(input.Config["group_chat_id"]); got != "-10001" { + t.Fatalf("group_chat_id = %q, want -10001", got) + } +} diff --git a/internal/service/telegram_bot.go b/internal/service/telegram_bot.go index ee6691c..8874899 100644 --- a/internal/service/telegram_bot.go +++ b/internal/service/telegram_bot.go @@ -152,7 +152,7 @@ func (s *TelegramBotService) executeCommand(ctx context.Context, channel *model. cmd := strings.ToLower(parts[0]) args := parts[1:] if msg.Chat.Type != "" && msg.Chat.Type != "private" && !s.telegramChatAllowed(channel, msg.Chat.ID) { - return telegramCommandReply{Text: "此群组/频道未绑定到 Bot 管理入口,请在通知渠道里填写「命令群组/频道 Chat ID」。"}, nil + return telegramCommandReply{Text: "此群组/频道未绑定到 Bot 管理入口,请在通知渠道里填写「绑定群组 ID」或「绑定频道 ID」。"}, nil } switch cmd { @@ -723,7 +723,7 @@ func (s *TelegramBotService) telegramUserCanBind(ctx context.Context, channel *m groupID := strings.TrimSpace(cfg["group_chat_id"]) channelID := strings.TrimSpace(cfg["channel_chat_id"]) if groupID == "" && channelID == "" { - return true + return false } for _, chatID := range []string{groupID, channelID} { if chatID == "" { diff --git a/web/src/pages/NotifyChannelsPage.tsx b/web/src/pages/NotifyChannelsPage.tsx index 564cf06..1242f0b 100644 --- a/web/src/pages/NotifyChannelsPage.tsx +++ b/web/src/pages/NotifyChannelsPage.tsx @@ -183,7 +183,7 @@ function channelSummary(ch: NotifyChannel): string { const cfg = ch.config ?? {} switch (ch.type) { case 'telegram': - return `Bot ${cfg.bot_token ? '已配置' : '未配置'} → 通知 ${cfg.chat_id ?? '-'} · 管理员 ${cfg.admin_user_ids ?? '-'} · 群组 ${cfg.group_chat_id ?? '-'} · 频道 ${cfg.channel_chat_id ?? '-'}` + return `Bot ${cfg.bot_token ? '已配置' : '未配置'} → 管理员 ${cfg.admin_user_ids ?? '-'} · 群组 ${cfg.group_chat_id ?? '-'} · 频道 ${cfg.channel_chat_id ?? '-'}` case 'wechat': return `SendKey ${String(cfg.sendkey ?? '').slice(0, 10)}…` case 'bark': @@ -202,7 +202,6 @@ function channelSummary(ch: NotifyChannel): string { const EMPTY_CONFIG: Record> = { telegram: { bot_token: '', - chat_id: '', admin_user_ids: '', group_chat_id: '', channel_chat_id: '', @@ -215,6 +214,18 @@ const EMPTY_CONFIG: Record> = { email: { smtp_host: '', smtp_port: '465', username: '', password: '', from: '', to: '', tls: 'true' }, } +function normalizeInitialConfig(type: NotifyChannel['type'], raw: Record): Record { + const base = { ...EMPTY_CONFIG[type] } + for (const [key, value] of Object.entries(raw ?? {})) { + base[key] = String(value ?? '') + } + if (type === 'telegram' && !base.group_chat_id && !base.channel_chat_id && base.chat_id?.startsWith('-')) { + base.group_chat_id = base.chat_id + } + delete base.chat_id + return base +} + function ChannelFormModal({ editing, onClose, @@ -229,7 +240,7 @@ function ChannelFormModal({ editing?.type ?? 'telegram', ) const [config, setConfig] = useState>( - { ...EMPTY_CONFIG[editing?.type ?? 'telegram'], ...(editing?.config ?? {}) }, + normalizeInitialConfig(editing?.type ?? 'telegram', editing?.config ?? {}), ) const [enabled, setEnabled] = useState(editing?.enabled ?? true) const [saving, setSaving] = useState(false) @@ -246,33 +257,21 @@ function ChannelFormModal({ toast.error('请填写 Telegram Bot Token') return } - if (!String(config.chat_id ?? '').trim()) { - toast.error('请填写通知 Chat ID') - return - } if (!String(config.admin_user_ids ?? '').trim()) { toast.error('请填写管理员 Telegram ID') return } - const chatID = String(config.chat_id ?? '').trim() - const groupChatID = String(config.group_chat_id ?? '').trim() - const channelChatID = String(config.channel_chat_id ?? '').trim() - if (!groupChatID && !channelChatID && !chatID.startsWith('-')) { - toast.error('请至少填写绑定群组 ID 或绑定频道 ID;或把群组/频道负数 ID 填到 Chat ID') - return - } - if (!groupChatID && !channelChatID && chatID.startsWith('-')) { - config.group_chat_id = chatID - } } setSaving(true) try { + const cleanedConfig = Object.fromEntries( + Object.entries(config).map(([key, value]) => [key, String(value ?? '').trim()]), + ) + delete cleanedConfig.chat_id const input: NotifyChannelInput = { name: name.trim(), type: type, - config: Object.fromEntries( - Object.entries(config).map(([key, value]) => [key, String(value ?? '').trim()]), - ), + config: cleanedConfig, enabled, } if (editing) { @@ -336,15 +335,6 @@ function ChannelFormModal({ onChange={(e) => updateConfig('bot_token', e.target.value)} /> - - updateConfig('chat_id', e.target.value)} - /> - updateConfig('group_chat_id', e.target.value)} /> @@ -365,7 +355,7 @@ function ChannelFormModal({ updateConfig('channel_chat_id', e.target.value)} /> @@ -387,7 +377,7 @@ function ChannelFormModal({ />
- 必须至少填写群组 ID 或频道 ID。只有配置群组/频道中的成员可以唤醒 Bot、使用 /start 用户名 密码 绑定账号和隐藏成人目录;/status、/search、/downloads、/stats 仅管理员 Telegram ID 或已绑定的本地管理员可用。若测试通知超时,可填写反代 API 地址或代理地址。 + 群组 ID、频道 ID 均为选填,可填一个、两个都填,也可以不填。不填时只有管理员 Telegram ID 可以私聊 Bot 使用;填写后,对应群组/频道成员可唤醒 Bot、使用 /start 用户名 密码 绑定账号和隐藏成人目录。/status、/search、/downloads、/stats 仍仅管理员 Telegram ID 或已绑定的本地管理员可用。若测试通知超时,可填写反代 API 地址或代理地址。
)}