feat(channels): support multiple named Telegram bots
Add channels.telegram_bots config allowing multiple Telegram bot tokens to be configured, each mapped to a separate channel (e.g. telegram-amber, telegram-karen). Each channel can be independently bound to an agent via the bindings config, enabling distinct AI personas behind separate bots. Backward compatibility is preserved: the existing channels.telegram single-entry config continues to work unchanged. On load it is normalized into telegram_bots as an entry with id "default", which produces the channel name "telegram" so all existing bindings remain valid. Key changes: - config: add TelegramBotConfig struct with ChannelName/AsTelegramConfig helpers; add TelegramBots field to ChannelsConfig; normalize legacy single entry into list on load - telegram: add NewTelegramChannelFromConfig constructor accepting TelegramConfig + explicit channel name (avoids import cycle) - channels: add TelegramBotFactory registry; add injectChannelDependencies helper to eliminate injection code duplication; add duplicate channel name guard in initTelegramBot; update initChannels to iterate over TelegramBots; add prefix-based rate limit fallback for named bots Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
cfd1d11926
commit
aa8ce793dd
5 changed files with 240 additions and 30 deletions
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -192,6 +193,22 @@ func NewManager(cfg *config.Config, messageBus *bus.MessageBus, store media.Medi
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// injectChannelDependencies injects optional dependencies (MediaStore, PlaceholderRecorder,
|
||||||
|
// Owner) into a channel if it implements the corresponding setter interfaces.
|
||||||
|
func (m *Manager) injectChannelDependencies(ch Channel) {
|
||||||
|
if m.mediaStore != nil {
|
||||||
|
if setter, ok := ch.(interface{ SetMediaStore(s media.MediaStore) }); ok {
|
||||||
|
setter.SetMediaStore(m.mediaStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if setter, ok := ch.(interface{ SetPlaceholderRecorder(r PlaceholderRecorder) }); ok {
|
||||||
|
setter.SetPlaceholderRecorder(m)
|
||||||
|
}
|
||||||
|
if setter, ok := ch.(interface{ SetOwner(ch Channel) }); ok {
|
||||||
|
setter.SetOwner(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// initChannel is a helper that looks up a factory by name and creates the channel.
|
// initChannel is a helper that looks up a factory by name and creates the channel.
|
||||||
func (m *Manager) initChannel(name, displayName string) {
|
func (m *Manager) initChannel(name, displayName string) {
|
||||||
f, ok := getFactory(name)
|
f, ok := getFactory(name)
|
||||||
|
|
@ -211,20 +228,7 @@ func (m *Manager) initChannel(name, displayName string) {
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Inject MediaStore if channel supports it
|
m.injectChannelDependencies(ch)
|
||||||
if m.mediaStore != nil {
|
|
||||||
if setter, ok := ch.(interface{ SetMediaStore(s media.MediaStore) }); ok {
|
|
||||||
setter.SetMediaStore(m.mediaStore)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Inject PlaceholderRecorder if channel supports it
|
|
||||||
if setter, ok := ch.(interface{ SetPlaceholderRecorder(r PlaceholderRecorder) }); ok {
|
|
||||||
setter.SetPlaceholderRecorder(m)
|
|
||||||
}
|
|
||||||
// Inject owner reference so BaseChannel.HandleMessage can auto-trigger typing/reaction
|
|
||||||
if setter, ok := ch.(interface{ SetOwner(ch Channel) }); ok {
|
|
||||||
setter.SetOwner(ch)
|
|
||||||
}
|
|
||||||
m.channels[name] = ch
|
m.channels[name] = ch
|
||||||
logger.InfoCF("channels", "Channel enabled successfully", map[string]any{
|
logger.InfoCF("channels", "Channel enabled successfully", map[string]any{
|
||||||
"channel": displayName,
|
"channel": displayName,
|
||||||
|
|
@ -232,11 +236,57 @@ func (m *Manager) initChannel(name, displayName string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initTelegramBot initializes a single named Telegram bot and registers it as a channel.
|
||||||
|
func (m *Manager) initTelegramBot(bot config.TelegramBotConfig) {
|
||||||
|
channelName := bot.ChannelName()
|
||||||
|
displayName := "Telegram"
|
||||||
|
if bot.ID != "" && bot.ID != "default" {
|
||||||
|
displayName = "Telegram (" + bot.ID + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.DebugCF("channels", "Attempting to initialize channel", map[string]any{
|
||||||
|
"channel": displayName,
|
||||||
|
})
|
||||||
|
|
||||||
|
f, ok := getTelegramBotFactory()
|
||||||
|
if !ok {
|
||||||
|
logger.WarnCF("channels", "Telegram bot factory not registered", map[string]any{
|
||||||
|
"channel": displayName,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := f(bot, channelName, m.bus)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorCF("channels", "Failed to initialize channel", map[string]any{
|
||||||
|
"channel": displayName,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.injectChannelDependencies(ch)
|
||||||
|
|
||||||
|
if _, exists := m.channels[channelName]; exists {
|
||||||
|
logger.ErrorCF("channels", "Duplicate channel name — skipping bot", map[string]any{
|
||||||
|
"channel": displayName,
|
||||||
|
"name": channelName,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.channels[channelName] = ch
|
||||||
|
logger.InfoCF("channels", "Channel enabled successfully", map[string]any{
|
||||||
|
"channel": displayName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) initChannels() error {
|
func (m *Manager) initChannels() error {
|
||||||
logger.InfoC("channels", "Initializing channel manager")
|
logger.InfoC("channels", "Initializing channel manager")
|
||||||
|
|
||||||
if m.config.Channels.Telegram.Enabled && m.config.Channels.Telegram.Token != "" {
|
for _, bot := range m.config.Channels.TelegramBots {
|
||||||
m.initChannel("telegram", "Telegram")
|
if bot.Enabled && bot.Token != "" {
|
||||||
|
m.initTelegramBot(bot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.config.Channels.WhatsApp.Enabled {
|
if m.config.Channels.WhatsApp.Enabled {
|
||||||
|
|
@ -478,6 +528,14 @@ func newChannelWorker(name string, ch Channel) *channelWorker {
|
||||||
rateVal := float64(defaultRateLimit)
|
rateVal := float64(defaultRateLimit)
|
||||||
if r, ok := channelRateConfig[name]; ok {
|
if r, ok := channelRateConfig[name]; ok {
|
||||||
rateVal = r
|
rateVal = r
|
||||||
|
} else {
|
||||||
|
// Named channel variants (e.g. "telegram-amber") inherit the base channel's rate.
|
||||||
|
for prefix, r := range channelRateConfig {
|
||||||
|
if strings.HasPrefix(name, prefix+"-") {
|
||||||
|
rateVal = r
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
burst := int(math.Max(1, math.Ceil(rateVal/2)))
|
burst := int(math.Max(1, math.Ceil(rateVal/2)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,17 @@ import (
|
||||||
// Each channel subpackage registers one or more factories via init().
|
// Each channel subpackage registers one or more factories via init().
|
||||||
type ChannelFactory func(cfg *config.Config, bus *bus.MessageBus) (Channel, error)
|
type ChannelFactory func(cfg *config.Config, bus *bus.MessageBus) (Channel, error)
|
||||||
|
|
||||||
|
// TelegramBotFactory is a constructor function that creates a TelegramChannel from a
|
||||||
|
// TelegramBotConfig and an explicit channel name. Registered by the telegram subpackage
|
||||||
|
// to allow the manager to initialize named bots without a direct import (avoiding cycles).
|
||||||
|
type TelegramBotFactory func(botCfg config.TelegramBotConfig, channelName string, bus *bus.MessageBus) (Channel, error)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
factoriesMu sync.RWMutex
|
factoriesMu sync.RWMutex
|
||||||
factories = map[string]ChannelFactory{}
|
factories = map[string]ChannelFactory{}
|
||||||
|
|
||||||
|
telegramBotFactoryMu sync.RWMutex
|
||||||
|
telegramBotFactory TelegramBotFactory
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterFactory registers a named channel factory. Called from subpackage init() functions.
|
// RegisterFactory registers a named channel factory. Called from subpackage init() functions.
|
||||||
|
|
@ -30,3 +38,18 @@ func getFactory(name string) (ChannelFactory, bool) {
|
||||||
f, ok := factories[name]
|
f, ok := factories[name]
|
||||||
return f, ok
|
return f, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterTelegramBotFactory registers the factory used to create named Telegram bots.
|
||||||
|
// Called from the telegram subpackage init() to avoid a direct import cycle.
|
||||||
|
func RegisterTelegramBotFactory(f TelegramBotFactory) {
|
||||||
|
telegramBotFactoryMu.Lock()
|
||||||
|
defer telegramBotFactoryMu.Unlock()
|
||||||
|
telegramBotFactory = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTelegramBotFactory returns the registered TelegramBotFactory, if any.
|
||||||
|
func getTelegramBotFactory() (TelegramBotFactory, bool) {
|
||||||
|
telegramBotFactoryMu.RLock()
|
||||||
|
defer telegramBotFactoryMu.RUnlock()
|
||||||
|
return telegramBotFactory, telegramBotFactory != nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,7 @@ func init() {
|
||||||
channels.RegisterFactory("telegram", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) {
|
channels.RegisterFactory("telegram", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) {
|
||||||
return NewTelegramChannel(cfg, b)
|
return NewTelegramChannel(cfg, b)
|
||||||
})
|
})
|
||||||
|
channels.RegisterTelegramBotFactory(func(botCfg config.TelegramBotConfig, channelName string, b *bus.MessageBus) (channels.Channel, error) {
|
||||||
|
return NewTelegramChannelFromConfig(botCfg.AsTelegramConfig(), channelName, b)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,12 @@ var (
|
||||||
|
|
||||||
type TelegramChannel struct {
|
type TelegramChannel struct {
|
||||||
*channels.BaseChannel
|
*channels.BaseChannel
|
||||||
bot *telego.Bot
|
bot *telego.Bot
|
||||||
bh *th.BotHandler
|
bh *th.BotHandler
|
||||||
config *config.Config
|
placeholderCfg config.PlaceholderConfig
|
||||||
chatIDs map[string]int64
|
chatIDs map[string]int64
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
registerFunc func(context.Context, []commands.Definition) error
|
registerFunc func(context.Context, []commands.Definition) error
|
||||||
commandRegCancel context.CancelFunc
|
commandRegCancel context.CancelFunc
|
||||||
|
|
@ -95,10 +95,64 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
|
||||||
)
|
)
|
||||||
|
|
||||||
return &TelegramChannel{
|
return &TelegramChannel{
|
||||||
BaseChannel: base,
|
BaseChannel: base,
|
||||||
bot: bot,
|
bot: bot,
|
||||||
config: cfg,
|
placeholderCfg: telegramCfg.Placeholder,
|
||||||
chatIDs: make(map[string]int64),
|
chatIDs: make(map[string]int64),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTelegramChannelFromConfig creates a TelegramChannel from a TelegramConfig and
|
||||||
|
// an explicit channel name. Used when initializing named bots from telegram_bots config.
|
||||||
|
func NewTelegramChannelFromConfig(telegramCfg config.TelegramConfig, channelName string, b *bus.MessageBus) (*TelegramChannel, error) {
|
||||||
|
if telegramCfg.Token == "" {
|
||||||
|
return nil, fmt.Errorf("telegram bot token is required")
|
||||||
|
}
|
||||||
|
var opts []telego.BotOption
|
||||||
|
|
||||||
|
if telegramCfg.Proxy != "" {
|
||||||
|
proxyURL, parseErr := url.Parse(telegramCfg.Proxy)
|
||||||
|
if parseErr != nil {
|
||||||
|
return nil, fmt.Errorf("invalid proxy URL %q: %w", telegramCfg.Proxy, parseErr)
|
||||||
|
}
|
||||||
|
opts = append(opts, telego.WithHTTPClient(&http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyURL(proxyURL),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
} else if os.Getenv("HTTP_PROXY") != "" || os.Getenv("HTTPS_PROXY") != "" {
|
||||||
|
opts = append(opts, telego.WithHTTPClient(&http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if baseURL := strings.TrimRight(strings.TrimSpace(telegramCfg.BaseURL), "/"); baseURL != "" {
|
||||||
|
opts = append(opts, telego.WithAPIServer(baseURL))
|
||||||
|
}
|
||||||
|
opts = append(opts, telego.WithLogger(logger.NewLogger("telego")))
|
||||||
|
|
||||||
|
bot, err := telego.NewBot(telegramCfg.Token, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create telegram bot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
base := channels.NewBaseChannel(
|
||||||
|
channelName,
|
||||||
|
telegramCfg,
|
||||||
|
b,
|
||||||
|
telegramCfg.AllowFrom,
|
||||||
|
channels.WithMaxMessageLength(4000),
|
||||||
|
channels.WithGroupTrigger(telegramCfg.GroupTrigger),
|
||||||
|
channels.WithReasoningChannelID(telegramCfg.ReasoningChannelID),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &TelegramChannel{
|
||||||
|
BaseChannel: base,
|
||||||
|
bot: bot,
|
||||||
|
placeholderCfg: telegramCfg.Placeholder,
|
||||||
|
chatIDs: make(map[string]int64),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -298,7 +352,7 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag
|
||||||
// It sends a placeholder message (e.g. "Thinking... 💭") that will later be
|
// It sends a placeholder message (e.g. "Thinking... 💭") that will later be
|
||||||
// edited to the actual response via EditMessage (channels.MessageEditor).
|
// edited to the actual response via EditMessage (channels.MessageEditor).
|
||||||
func (c *TelegramChannel) SendPlaceholder(ctx context.Context, chatID string) (string, error) {
|
func (c *TelegramChannel) SendPlaceholder(ctx context.Context, chatID string) (string, error) {
|
||||||
phCfg := c.config.Channels.Telegram.Placeholder
|
phCfg := c.placeholderCfg
|
||||||
if !phCfg.Enabled {
|
if !phCfg.Enabled {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -255,9 +255,10 @@ func (d *AgentDefaults) GetModelName() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChannelsConfig struct {
|
type ChannelsConfig struct {
|
||||||
WhatsApp WhatsAppConfig `json:"whatsapp"`
|
WhatsApp WhatsAppConfig `json:"whatsapp"`
|
||||||
Telegram TelegramConfig `json:"telegram"`
|
Telegram TelegramConfig `json:"telegram"`
|
||||||
Feishu FeishuConfig `json:"feishu"`
|
TelegramBots []TelegramBotConfig `json:"telegram_bots,omitempty"`
|
||||||
|
Feishu FeishuConfig `json:"feishu"`
|
||||||
Discord DiscordConfig `json:"discord"`
|
Discord DiscordConfig `json:"discord"`
|
||||||
MaixCam MaixCamConfig `json:"maixcam"`
|
MaixCam MaixCamConfig `json:"maixcam"`
|
||||||
QQ QQConfig `json:"qq"`
|
QQ QQConfig `json:"qq"`
|
||||||
|
|
@ -311,6 +312,47 @@ type TelegramConfig struct {
|
||||||
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"`
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TelegramBotConfig defines a single named Telegram bot for use in telegram_bots.
|
||||||
|
// Each entry creates a separate channel named "telegram-<id>", except when id is
|
||||||
|
// empty or "default" which creates the standard "telegram" channel.
|
||||||
|
type TelegramBotConfig struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
BaseURL string `json:"base_url,omitempty"`
|
||||||
|
Proxy string `json:"proxy,omitempty"`
|
||||||
|
AllowFrom FlexibleStringSlice `json:"allow_from,omitempty"`
|
||||||
|
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||||
|
Typing TypingConfig `json:"typing,omitempty"`
|
||||||
|
Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
|
||||||
|
ReasoningChannelID string `json:"reasoning_channel_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelName returns the channel identifier for this bot.
|
||||||
|
// Bots with an empty or "default" ID use "telegram" for backward compatibility.
|
||||||
|
// All other IDs produce "telegram-<id>".
|
||||||
|
func (b TelegramBotConfig) ChannelName() string {
|
||||||
|
if b.ID == "" || b.ID == "default" {
|
||||||
|
return "telegram"
|
||||||
|
}
|
||||||
|
return "telegram-" + b.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsTelegramConfig converts a TelegramBotConfig to the equivalent TelegramConfig.
|
||||||
|
func (b TelegramBotConfig) AsTelegramConfig() TelegramConfig {
|
||||||
|
return TelegramConfig{
|
||||||
|
Enabled: b.Enabled,
|
||||||
|
Token: b.Token,
|
||||||
|
BaseURL: b.BaseURL,
|
||||||
|
Proxy: b.Proxy,
|
||||||
|
AllowFrom: b.AllowFrom,
|
||||||
|
GroupTrigger: b.GroupTrigger,
|
||||||
|
Typing: b.Typing,
|
||||||
|
Placeholder: b.Placeholder,
|
||||||
|
ReasoningChannelID: b.ReasoningChannelID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type FeishuConfig struct {
|
type FeishuConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"`
|
||||||
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
|
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"`
|
||||||
|
|
@ -844,6 +886,7 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
|
|
||||||
// Migrate legacy channel config fields to new unified structures
|
// Migrate legacy channel config fields to new unified structures
|
||||||
cfg.migrateChannelConfigs()
|
cfg.migrateChannelConfigs()
|
||||||
|
cfg.normalizeTelegramBots()
|
||||||
|
|
||||||
// Auto-migrate: if only legacy providers config exists, convert to model_list
|
// Auto-migrate: if only legacy providers config exists, convert to model_list
|
||||||
if len(cfg.ModelList) == 0 && cfg.HasProvidersConfig() {
|
if len(cfg.ModelList) == 0 && cfg.HasProvidersConfig() {
|
||||||
|
|
@ -858,6 +901,35 @@ func LoadConfig(path string) (*Config, error) {
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeTelegramBots folds the legacy single channels.telegram entry into
|
||||||
|
// channels.telegram_bots so the rest of the codebase only needs to range over
|
||||||
|
// TelegramBots. The legacy entry is prepended with id "default", which produces
|
||||||
|
// the channel name "telegram" — preserving all existing bindings unchanged.
|
||||||
|
func (c *Config) normalizeTelegramBots() {
|
||||||
|
if c.Channels.Telegram.Token == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Already represented in the list (same channel name) — avoid duplicates.
|
||||||
|
for _, b := range c.Channels.TelegramBots {
|
||||||
|
if b.ChannelName() == "telegram" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legacy := TelegramBotConfig{
|
||||||
|
ID: "default",
|
||||||
|
Enabled: c.Channels.Telegram.Enabled,
|
||||||
|
Token: c.Channels.Telegram.Token,
|
||||||
|
BaseURL: c.Channels.Telegram.BaseURL,
|
||||||
|
Proxy: c.Channels.Telegram.Proxy,
|
||||||
|
AllowFrom: c.Channels.Telegram.AllowFrom,
|
||||||
|
GroupTrigger: c.Channels.Telegram.GroupTrigger,
|
||||||
|
Typing: c.Channels.Telegram.Typing,
|
||||||
|
Placeholder: c.Channels.Telegram.Placeholder,
|
||||||
|
ReasoningChannelID: c.Channels.Telegram.ReasoningChannelID,
|
||||||
|
}
|
||||||
|
c.Channels.TelegramBots = append([]TelegramBotConfig{legacy}, c.Channels.TelegramBots...)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) migrateChannelConfigs() {
|
func (c *Config) migrateChannelConfigs() {
|
||||||
// Discord: mention_only -> group_trigger.mention_only
|
// Discord: mention_only -> group_trigger.mention_only
|
||||||
if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly {
|
if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue