From 92d80c0ee4e941db684c0687b29327062dae3c12 Mon Sep 17 00:00:00 2001 From: rabbitblood Date: Fri, 17 Apr 2026 17:52:10 -0700 Subject: [PATCH] =?UTF-8?q?feat(telegram):=20poll=20for=20callback=5Fquery?= =?UTF-8?q?=20=E2=80=94=20CEO=20decision=20buttons=20work=20locally?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds callback_query to AllowedUpdates in Telegram polling. When CEO clicks Yes/No inline keyboard buttons: 1. Acknowledges press (removes loading spinner) 2. Updates message with 'CEO approved/rejected' 3. Routes 'CEO_DECISION: approve:xyz' as inbound to the agent Only one workspace polls per bot token (Triage Operator) — other workspaces with Telegram use outbound-only via direct API. Fixed: duplicate pollers causing 'terminated by other getUpdates' errors — removed PM/DevLead/ResearchLead Telegram channel rows (they send outbound via direct Telegram API calls, not channel manager). Co-Authored-By: Claude Opus 4.6 (1M context) --- platform/internal/channels/telegram.go | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/platform/internal/channels/telegram.go b/platform/internal/channels/telegram.go index 95dabd68..a37b6bde 100644 --- a/platform/internal/channels/telegram.go +++ b/platform/internal/channels/telegram.go @@ -438,6 +438,8 @@ func (t *TelegramAdapter) StartPolling(ctx context.Context, config map[string]in u.Timeout = 30 u.AllowedUpdates = []string{"message", "channel_post", "my_chat_member"} + u.AllowedUpdates = append(u.AllowedUpdates, "callback_query") + log.Printf("Channels: Telegram polling started for chats %v (bot: @%s)", chatIDs, bot.Self.UserName) for { @@ -480,6 +482,45 @@ func (t *TelegramAdapter) StartPolling(ctx context.Context, config map[string]in for _, update := range updates { u.Offset = update.UpdateID + 1 + // Handle callback_query (inline keyboard button clicks) + if update.CallbackQuery != nil { + cb := update.CallbackQuery + chatID := strconv.FormatInt(cb.Message.Chat.ID, 10) + + // Acknowledge the button press (removes loading spinner) + ackCfg := tgbotapi.NewCallback(cb.ID, "Received") + bot.Send(ackCfg) + + // Update the message to show what was clicked + decision := "approved" + if strings.HasPrefix(cb.Data, "reject") { + decision = "rejected" + } + editMsg := tgbotapi.NewEditMessageText( + cb.Message.Chat.ID, + cb.Message.MessageID, + cb.Message.Text+"\n\nāœ… CEO "+decision, + ) + bot.Send(editMsg) + + // Route the decision as an inbound message to the agent + inbound := &InboundMessage{ + ChatID: chatID, + UserID: strconv.FormatInt(cb.From.ID, 10), + Username: cb.From.UserName, + Text: "CEO_DECISION: " + cb.Data, + MessageID: strconv.Itoa(cb.Message.MessageID), + Metadata: map[string]string{ + "callback_data": cb.Data, + "decision": decision, + }, + } + if err := onMessage(ctx, channelID, inbound); err != nil { + log.Printf("Channels: Telegram callback handler error: %v", err) + } + continue + } + // Handle my_chat_member: auto-greet when bot is added to a new chat if update.MyChatMember != nil { handleMyChatMember(bot, update.MyChatMember)