From c2b92d0ae61e0a56bdf489ffc042b5d9587dd3cc Mon Sep 17 00:00:00 2001 From: Vishnuvardhan Reddy Date: Thu, 26 Feb 2026 14:00:01 +0000 Subject: [PATCH] feat(channels): implement automatic telegram username resolution Add support for resolving Telegram usernames to chat IDs, allowing users to use friendly @username format instead of numeric IDs in: - allow_from configuration - direct message targets - API calls Changes: - Add usernameCache to TelegramChannel for caching resolved chat IDs - Implement resolveUsername() method using Telegram Bot API's getChat - Convert parseChatID to method and add automatic username resolution - Support both numeric IDs and @username format How it works: 1. When parseChatID receives a non-numeric value, it tries to resolve it 2. resolveUsername uses the getChat API with @username prefix 3. Successfully resolved usernames are cached for future use 4. Numeric IDs continue to work as before Benefits: - Users can use "@myusername" instead of "123456789" - More user-friendly configuration - Cached resolution for better performance - Backward compatible with existing numeric IDs Resolves issue where users had to find their numeric ID from https://t.me/getdivid or bot admin tools. --- pkg/channels/telegram.go | 82 +++++++++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 18 deletions(-) diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 6592d9bc0..db9047716 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -38,13 +38,14 @@ var ( type TelegramChannel struct { *BaseChannel - bot *telego.Bot - commands TelegramCommander - config *config.Config - chatIDs map[string]int64 - transcriber *voice.GroqTranscriber - placeholders sync.Map // chatID -> messageID - stopThinking sync.Map // chatID -> thinkingCancel + bot *telego.Bot + commands TelegramCommander + config *config.Config + chatIDs map[string]int64 + usernameCache map[string]int64 // Cache username -> chat ID resolution + transcriber *voice.GroqTranscriber + placeholders sync.Map // chatID -> messageID + stopThinking sync.Map // chatID -> thinkingCancel } type thinkingCancel struct { @@ -88,14 +89,15 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann base := NewBaseChannel("telegram", telegramCfg, bus, telegramCfg.AllowFrom) return &TelegramChannel{ - BaseChannel: base, - commands: NewTelegramCommands(bot, cfg), - bot: bot, - config: cfg, - chatIDs: make(map[string]int64), - transcriber: nil, - placeholders: sync.Map{}, - stopThinking: sync.Map{}, + BaseChannel: base, + commands: NewTelegramCommands(bot, cfg), + bot: bot, + config: cfg, + chatIDs: make(map[string]int64), + usernameCache: make(map[string]int64), + transcriber: nil, + placeholders: sync.Map{}, + stopThinking: sync.Map{}, }, nil } @@ -103,6 +105,34 @@ func (c *TelegramChannel) SetTranscriber(transcriber *voice.GroqTranscriber) { c.transcriber = transcriber } +func (c *TelegramChannel) resolveUsername(ctx context.Context, username string) (int64, error) { + // Check cache first + chatID, exists := c.usernameCache[username] + if exists { + return chatID, nil + } + + // Use getChat API to resolve username to chat ID + // Username must be prefixed with @ for the API + usernameWithAt := "@" + username + chat, err := c.bot.GetChat(ctx, &telego.GetChatParams{ + ChatID: tu.Username(usernameWithAt), + }) + if err != nil { + return 0, fmt.Errorf("failed to resolve username @%s: %w", username, err) + } + + // Cache the result + c.usernameCache[username] = chat.ID + + logger.DebugCF("telegram", "Resolved username to chat ID", map[string]any{ + "username": username, + "chat_id": chat.ID, + }) + + return chat.ID, nil +} + func (c *TelegramChannel) Start(ctx context.Context) error { logger.InfoC("telegram", "Starting Telegram bot (polling mode)...") @@ -164,7 +194,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err return fmt.Errorf("telegram bot not running") } - chatID, err := parseChatID(msg.ChatID) + chatID, err := c.parseChatID(ctx, msg.ChatID) if err != nil { return fmt.Errorf("invalid chat ID: %w", err) } @@ -427,10 +457,26 @@ func (c *TelegramChannel) downloadFile(ctx context.Context, fileID, ext string) return c.downloadFileWithInfo(file, ext) } -func parseChatID(chatIDStr string) (int64, error) { +func (c *TelegramChannel) parseChatID(ctx context.Context, chatIDStr string) (int64, error) { + // Strip @ prefix if present (username format) + chatIDStr = strings.TrimPrefix(chatIDStr, "@") + + // Try parsing as numeric ID var id int64 _, err := fmt.Sscanf(chatIDStr, "%d", &id) - return id, err + if err == nil { + return id, nil + } + + // If not an integer, try to resolve as username + chatID, err := c.resolveUsername(ctx, chatIDStr) + if err != nil { + return 0, fmt.Errorf("chat ID must be numeric (e.g., 123456789) or a valid username (@%s). "+ + "Use your numeric user ID from https://t.me/getdivid or ensure the username is correct", + chatIDStr) + } + + return chatID, nil } func markdownToTelegramHTML(text string) string {