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") + } +}