package telegram // TelegramWebhookHandler processes incoming Telegram webhook HTTP requests. // Reference: Chatwoot's webhook controller for Telegram: // - app/controllers/api/v1/accounts/channels/telegram_bots_controller.rb (CRUD) // - Telegram webhook receives Update payloads at /webhooks/telegram/{bot_token} // // The handler: // 1. Receives raw Update JSON from Telegram's webhook POST // 2. Validates the update structure // 3. Routes to appropriate processing (message, edited_message, callback_query) // 4. Returns 200 OK to Telegram (required to prevent retries) // // Telegram webhook requirements: // - Must respond within 10 seconds // - Must return 200 OK even on processing errors (async processing recommended) // - Updates are sent as JSON POST to the webhook URL // - Supported update types: message, edited_message, callback_query import ( "encoding/json" "io" "net/http" channelprovider "github.com/gochat/gochat/internal/channel/provider" "github.com/gochat/gochat/internal/model" applogger "github.com/gochat/gochat/pkg/logger" ) // WebhookHandler processes Telegram webhook requests. type WebhookHandler struct { provider *channelprovider.TelegramProvider // db would be injected here for inbox lookup // inboxRepo InboxRepository } // NewWebhookHandler creates a Telegram webhook handler. func NewWebhookHandler(provider *channelprovider.TelegramProvider) *WebhookHandler { return &WebhookHandler{ provider: provider, } } // HandleWebhookRequest processes an incoming Telegram webhook HTTP request. // This is the Gin HTTP handler function. // // URL pattern: /webhooks/telegram/:bot_token // Method: POST // Content-Type: application/json // // Reference: Chatwoot routes Telegram webhooks at: // post '/webhooks/telegram/:bot_token' => 'telegram_bots#process_message' // // Flow: // 1. Parse bot_token from URL path → lookup Inbox + ChannelTelegram // 2. Read Update JSON from request body // 3. Dispatch to appropriate processing pipeline // 4. Return 200 OK immediately (Telegram requires fast response) func (h *WebhookHandler) HandleWebhookRequest(w http.ResponseWriter, r *http.Request) { // Step 1: Extract bot_token from URL path // In Gin: c.Param("bot_token") botToken := extractBotTokenFromPath(r.URL.Path) if botToken == "" { applogger.L().Warn("Telegram webhook: missing bot_token in path") w.WriteHeader(http.StatusOK) // still return 200 to prevent Telegram retries return } // Step 2: Read request body body, err := io.ReadAll(r.Body) if err != nil { applogger.L().Error("Telegram webhook: failed to read body", "error", err) w.WriteHeader(http.StatusOK) return } defer r.Body.Close() // Step 3: Parse the Telegram Update var update TelegramUpdate if err := json.Unmarshal(body, &update); err != nil { applogger.L().Error("Telegram webhook: failed to parse update", "error", err) w.WriteHeader(http.StatusOK) return } applogger.L().Info("Telegram webhook received", "bot_token_prefix", maskBotToken(botToken), "update_id", update.UpdateID, ) // Step 4: Look up Inbox for this bot_token // In production: h.inboxRepo.FindByBotToken(botToken) inbox, err := h.lookupInbox(botToken) if err != nil { applogger.L().Error("Telegram webhook: inbox lookup failed", "error", err) w.WriteHeader(http.StatusOK) return } // Step 5: Dispatch update for async processing // Reference: Chatwoot processes updates asynchronously via IncomingMessageService // We should process asynchronously to respond within 10 seconds payload := map[string]interface{}{} if err := json.Unmarshal(body, &payload); err != nil { applogger.L().Error("Telegram webhook: failed to re-parse payload", "error", err) w.WriteHeader(http.StatusOK) return } // Process the incoming message via the provider pipeline incomingMsg, err := h.provider.ProcessIncomingMessage(r.Context(), inbox, payload) if err != nil { applogger.L().Error("Telegram webhook: message processing failed", "update_id", update.UpdateID, "error", err, ) } else if incomingMsg != nil { // Enqueue for downstream pipeline processing // Reference: Chatwoot enqueues to the message pipeline after parsing applogger.L().Info("Telegram webhook: message processed", "inbox_id", incomingMsg.InboxID, "source_id", incomingMsg.SourceID, "content_type", incomingMsg.ContentType, ) // TODO: Push to message broker/dispatcher for persistence + notification } // Step 6: Always return 200 OK to Telegram // Telegram retries webhook delivery if response is not 200 // Reference: https://core.telegram.org/bots/api#making-requests w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) } // HandleCallbackQuery processes a Telegram callback query specially. // Reference: Chatwoot handles callback_query for input_select responses // where the user clicks an inline keyboard button. func (h *WebhookHandler) HandleCallbackQuery(w http.ResponseWriter, r *http.Request) { // Same flow as HandleWebhookRequest but specialized for callback queries // The main handler already handles both types h.HandleWebhookRequest(w, r) } // === Helper Functions === // extractBotTokenFromPath extracts the bot_token from the webhook URL path. // URL pattern: /webhooks/telegram/{bot_token} func extractBotTokenFromPath(path string) string { // Expected: /webhooks/telegram/123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 parts := splitPath(path) if len(parts) >= 4 && parts[2] == "telegram" { return parts[3] } return "" } // splitPath splits a URL path into segments. func splitPath(path string) []string { result := []string{} for _, seg := range splitString(path, "/") { if seg != "" { result = append(result, seg) } } return result } // splitString splits a string by a delimiter. func splitString(s, delim string) []string { result := []string{} start := 0 for i := 0; i < len(s); i++ { if s[i:i+1] == delim { result = append(result, s[start:i]) start = i + 1 } } result = append(result, s[start:]) return result } // maskBotToken masks a bot token for safe logging (show first 8 chars only). func maskBotToken(token string) string { if len(token) > 8 { return token[:8] + "..." } return token } // lookupInbox finds the Inbox associated with a Telegram bot token. // In production: queries GORM for Inbox with channel_type=telegram matching the bot_token. // Reference: Chatwoot: Inbox.find_by(channel: telegram, bot_token: token) func (h *WebhookHandler) lookupInbox(botToken string) (*model.Inbox, error) { // TODO: Implement with GORM repository // Placeholder — production would use: // var inbox model.Inbox // var ch channelmodel.ChannelTelegram // db.Where("bot_token = ?", botToken).First(&ch) // db.Where("id = ?", ch.InboxID).First(&inbox) // // For now, return a placeholder inbox inbox := &model.Inbox{ ChannelType: "telegram", } return inbox, nil } // === Gin Integration === // RegisterWebhookRoutes registers Telegram webhook routes with a Gin router. // Reference: Chatwoot: post '/webhooks/telegram/:bot_token' // // Usage: // router.POST("/webhooks/telegram/:bot_token", handler.GinHandler()) func (h *WebhookHandler) GinHandler() func(interface{}) { // Returns a Gin handler function // In production: // return func(c *gin.Context) { // botToken := c.Param("bot_token") // ... process update ... // } return func(c interface{}) { // Placeholder for Gin integration applogger.L().Info("Telegram Gin webhook handler called") } } // === Telegram Update Type (for webhook handler) === type TelegramUpdate struct { UpdateID int64 `json:"update_id"` Message *TelegramMessage `json:"message,omitempty"` EditedMessage *TelegramMessage `json:"edited_message,omitempty"` CallbackQuery *TelegramCallbackQuery `json:"callback_query,omitempty"` } type TelegramMessage struct { MessageID int64 `json:"message_id"` From *TelegramUser `json:"from,omitempty"` Chat *TelegramChat `json:"chat"` Date int64 `json:"date"` Text string `json:"text,omitempty"` } type TelegramUser struct { ID int64 `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name,omitempty"` Username string `json:"username,omitempty"` } type TelegramChat struct { ID int64 `json:"id"` Type string `json:"type"` Title string `json:"title,omitempty"` } type TelegramCallbackQuery struct { ID string `json:"id"` From *TelegramUser `json:"from"` Message *TelegramMessage `json:"message,omitempty"` Data string `json:"data,omitempty"` }