fix: simplify telegram channel access rules

This commit is contained in:
ShukeBta
2026-05-30 02:16:18 +08:00
parent 7e01c42857
commit 3c946559f5
8 changed files with 157 additions and 82 deletions
+56 -14
View File
@@ -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("<b>%s</b>\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 {
+16 -10
View File
@@ -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
}
+37
View File
@@ -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)
}
}
+2 -2
View File
@@ -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 == "" {