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.
This commit is contained in:
Aaron Vu committed 2026-06-29 23:23:02 +07:00
1 parent 09610e4bfe
commit a62980bf7c
2 files changed
+95 -20

No files matched your search

+22 -20
View File
@@ -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).
@@ -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")
}
}