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.
This commit is contained in:
Vishnuvardhan Reddy 2026-02-26 14:00:01 +00:00
parent f871eb4880
commit c2b92d0ae6

View file

@ -42,6 +42,7 @@ type TelegramChannel struct {
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
@ -93,6 +94,7 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
bot: bot,
config: cfg,
chatIDs: make(map[string]int64),
usernameCache: make(map[string]int64),
transcriber: nil,
placeholders: sync.Map{},
stopThinking: sync.Map{},
@ -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 {