From a62980bf7ca80d554b1db26a8cf15b77f6a5f2e7 Mon Sep 17 00:00:00 2001 From: Aaron Vu Date: Mon, 29 Jun 2026 23:23:02 +0700 Subject: [PATCH] fix(telegram): honor per-group/topic overrides for DB instances The dashboard persists per-group/per-topic overrides (enabled, require_mention, skills, tools, system_prompt, ...) into the channel instance config, but telegramInstanceConfig had no groups field, so the payload was dropped on unmarshal. config.TelegramConfig.Groups stayed nil, resolveTopicConfig fell back to global defaults, and the bot replied in every forum topic regardless of the configured per-topic gating. Map the groups field through the DB-instance factory so per-topic config takes effect for dashboard-created channels, matching the static file-config path. --- internal/channels/telegram/factory.go | 42 +++++++------ internal/channels/telegram/factory_test.go | 73 ++++++++++++++++++++++ 2 files changed, 95 insertions(+), 20 deletions(-) create mode 100644 internal/channels/telegram/factory_test.go diff --git a/internal/channels/telegram/factory.go b/internal/channels/telegram/factory.go index 7bf16f46..a9a6155c 100644 --- a/internal/channels/telegram/factory.go +++ b/internal/channels/telegram/factory.go @@ -20,26 +20,27 @@ type telegramCreds struct { // telegramInstanceConfig maps the non-secret config JSONB from the channel_instances table. type telegramInstanceConfig struct { - APIServer string `json:"api_server,omitempty"` - Proxy string `json:"proxy,omitempty"` - DMPolicy string `json:"dm_policy,omitempty"` - GroupPolicy string `json:"group_policy,omitempty"` - RequireMention *bool `json:"require_mention,omitempty"` - MentionMode string `json:"mention_mode,omitempty"` - HistoryLimit int `json:"history_limit,omitempty"` - DMStream *bool `json:"dm_stream,omitempty"` - GroupStream *bool `json:"group_stream,omitempty"` - DraftTransport *bool `json:"draft_transport,omitempty"` // sendMessageDraft for DM streaming (default true) - ReasoningDelivery string `json:"reasoning_delivery,omitempty"` - ReasoningStream *bool `json:"reasoning_stream,omitempty"` // show reasoning as separate message (default true) - ReactionLevel string `json:"reaction_level,omitempty"` - MediaMaxMB int64 `json:"media_max_mb,omitempty"` - MediaMaxBytes int64 `json:"media_max_bytes,omitempty"` // deprecated: use media_max_mb - LinkPreview *bool `json:"link_preview,omitempty"` - BlockReply *bool `json:"block_reply,omitempty"` - ChatBehavior *config.ChatBehaviorConfig `json:"chat_behavior,omitempty"` - ForceIPv4 bool `json:"force_ipv4,omitempty"` - AllowFrom []string `json:"allow_from,omitempty"` + APIServer string `json:"api_server,omitempty"` + Proxy string `json:"proxy,omitempty"` + DMPolicy string `json:"dm_policy,omitempty"` + GroupPolicy string `json:"group_policy,omitempty"` + RequireMention *bool `json:"require_mention,omitempty"` + MentionMode string `json:"mention_mode,omitempty"` + HistoryLimit int `json:"history_limit,omitempty"` + DMStream *bool `json:"dm_stream,omitempty"` + GroupStream *bool `json:"group_stream,omitempty"` + DraftTransport *bool `json:"draft_transport,omitempty"` // sendMessageDraft for DM streaming (default true) + ReasoningDelivery string `json:"reasoning_delivery,omitempty"` + ReasoningStream *bool `json:"reasoning_stream,omitempty"` // show reasoning as separate message (default true) + ReactionLevel string `json:"reaction_level,omitempty"` + MediaMaxMB int64 `json:"media_max_mb,omitempty"` + MediaMaxBytes int64 `json:"media_max_bytes,omitempty"` // deprecated: use media_max_mb + LinkPreview *bool `json:"link_preview,omitempty"` + BlockReply *bool `json:"block_reply,omitempty"` + ChatBehavior *config.ChatBehaviorConfig `json:"chat_behavior,omitempty"` + ForceIPv4 bool `json:"force_ipv4,omitempty"` + AllowFrom []string `json:"allow_from,omitempty"` + Groups map[string]*config.TelegramGroupConfig `json:"groups,omitempty"` } // Factory creates a Telegram channel from DB instance data (no extra stores). @@ -119,6 +120,7 @@ func buildChannel(name string, creds json.RawMessage, cfg json.RawMessage, BlockReply: ic.BlockReply, ChatBehavior: ic.ChatBehavior, ForceIPv4: ic.ForceIPv4, + Groups: ic.Groups, } // DB instances default to "pairing" for groups (secure by default). diff --git a/internal/channels/telegram/factory_test.go b/internal/channels/telegram/factory_test.go new file mode 100644 index 00000000..6ea72cf6 --- /dev/null +++ b/internal/channels/telegram/factory_test.go @@ -0,0 +1,73 @@ +package telegram + +import ( + "encoding/json" + "testing" +) + +const validBotToken = "123456:abcdefghijklmnopqrstuvwxyzABCDE1234" + +func TestFactory_PropagatesGroupsAndTopics(t *testing.T) { + creds := json.RawMessage(`{"token":"` + validBotToken + `"}`) + cfg := json.RawMessage(`{ + "groups": { + "-1003957927661": { + "enabled": false, + "topics": { + "13": { "enabled": true } + } + } + } + }`) + + ch, err := Factory("telegram-test", creds, cfg, nil, nil) + if err != nil { + t.Fatalf("Factory: %v", err) + } + tg, ok := ch.(*Channel) + if !ok { + t.Fatalf("Factory returned %T, want *Channel", ch) + } + + if tg.config.Groups == nil { + t.Fatal("config.Groups is nil — dashboard groups/topics payload was dropped on unmarshal") + } + group, ok := tg.config.Groups["-1003957927661"] + if !ok || group == nil { + t.Fatal("group -1003957927661 missing from resolved config") + } + if group.Enabled == nil || *group.Enabled != false { + t.Errorf("group.Enabled = %v, want false", group.Enabled) + } + + allowed := resolveTopicConfig(tg.config, "-1003957927661", 13) + if !allowed.isEnabled() { + t.Error("topic 13: isEnabled() = false, want true") + } + other := resolveTopicConfig(tg.config, "-1003957927661", 99) + if other.isEnabled() { + t.Error("topic 99: isEnabled() = true, want false") + } + general := resolveTopicConfig(tg.config, "-1003957927661", telegramGeneralTopicID) + if general.isEnabled() { + t.Error("General topic: isEnabled() = true, want false") + } +} + +func TestFactory_NilGroupsWhenAbsent(t *testing.T) { + creds := json.RawMessage(`{"token":"` + validBotToken + `"}`) + cfg := json.RawMessage(`{"group_policy":"open"}`) + + ch, err := Factory("telegram-test", creds, cfg, nil, nil) + if err != nil { + t.Fatalf("Factory: %v", err) + } + tg := ch.(*Channel) + if tg.config.Groups != nil { + t.Errorf("config.Groups = %v, want nil when groups absent", tg.config.Groups) + } + def := resolveTopicConfig(tg.config, "-100123", 0) + if !def.isEnabled() { + t.Error("isEnabled() = false, want true by default when no overrides set") + } +}