package webhook import ( "encoding/json" "fmt" "io" "net/http" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/channel" channelprovider "github.com/gochat/gochat/internal/channel/provider" telegramchannel "github.com/gochat/gochat/internal/channel/telegram" "github.com/gochat/gochat/internal/model" channelmodel "github.com/gochat/gochat/internal/model/channel" "github.com/gochat/gochat/internal/worker" applogger "github.com/gochat/gochat/pkg/logger" "gorm.io/gorm" ) // TelegramWebhookHandler processes incoming Telegram webhook HTTP requests via Gin. // Reference: Chatwoot routes Telegram webhooks at: // post '/webhooks/telegram/:bot_token' => 'telegram_bots#process_message' // // 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 // TelegramWebhookHandler processes Telegram webhook requests via Gin. type TelegramWebhookHandler struct { provider *channelprovider.TelegramProvider telWebhook *telegramchannel.WebhookHandler db *gorm.DB persister *IncomingPersister } // NewTelegramWebhookHandler creates a Telegram webhook handler for Gin integration. func NewTelegramWebhookHandler( provider *channelprovider.TelegramProvider, telWebhook *telegramchannel.WebhookHandler, db *gorm.DB, dispatcher ...*channel.Dispatcher, ) *TelegramWebhookHandler { return &TelegramWebhookHandler{ provider: provider, telWebhook: telWebhook, db: db, persister: NewIncomingPersister(db, dispatcher...), } } func (h *TelegramWebhookHandler) WithWorkerPool(wp *worker.WorkerPool) *TelegramWebhookHandler { if h != nil && h.persister != nil { h.persister.SetWorkerPool(wp) } return h } func (h *TelegramWebhookHandler) WithSearchIndexer(indexer IncomingSearchIndexer) *TelegramWebhookHandler { if h != nil && h.persister != nil { h.persister.SetSearchIndexer(indexer) } return h } // HandleTelegramWebhook processes an incoming Telegram webhook Gin request. // URL pattern: /webhooks/telegram/:bot_token // Method: POST // Content-Type: application/json // // Reference: Chatwoot: Telegram webhook receives Update payloads // at /webhooks/telegram/{bot_token} // // 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 *TelegramWebhookHandler) HandleTelegramWebhook(c *gin.Context) { botToken := c.Param("bot_token") if botToken == "" { applogger.L().Warn("Telegram webhook: missing bot_token in path") // Still return 200 to prevent Telegram retries c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } // Read request body body, err := io.ReadAll(c.Request.Body) if err != nil { applogger.L().Errorf("Telegram webhook: failed to read body: %v", err) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } defer c.Request.Body.Close() applogger.L().Info("Telegram webhook received") // Parse the update to extract update_id for logging var update telegramchannel.TelegramUpdate if err := json.Unmarshal(body, &update); err != nil { applogger.L().Errorf("Telegram webhook: failed to parse update: %v", err) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } // Look up Inbox for this bot_token // In production: query GORM for Inbox with channel_type=telegram matching the bot_token inbox, err := h.lookupInbox(botToken) if err != nil { applogger.L().Errorf("Telegram webhook: inbox lookup failed: %v", err) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } // Parse raw payload for provider processing var payload map[string]interface{} if err := json.Unmarshal(body, &payload); err != nil { applogger.L().Errorf("Telegram webhook: failed to parse payload: %v", err) c.JSON(http.StatusOK, gin.H{"status": "ignored"}) return } // Process the incoming message via the provider pipeline // Reference: Chatwoot processes updates via IncomingMessageService incomingMsg, err := h.provider.ProcessIncomingMessage(c.Request.Context(), inbox, payload) if err != nil { applogger.L().Errorf("Telegram webhook: message processing failed (update_id=%d): %v", update.UpdateID, err) } else if incomingMsg != nil { if _, persistErr := h.persister.PersistIncoming(c.Request.Context(), inbox, incomingMsg); persistErr != nil { applogger.L().Errorf("Telegram webhook: persist message failed (update_id=%d source_id=%s): %v", update.UpdateID, incomingMsg.SourceID, persistErr) } else { applogger.L().Infof("Telegram webhook: message persisted (inbox_id=%d, source_id=%s)", incomingMsg.InboxID, incomingMsg.SourceID) } } // Always return 200 OK to Telegram — it retries if not 200 c.JSON(http.StatusOK, gin.H{"status": "ok"}) } // 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 *TelegramWebhookHandler) lookupInbox(botToken string) (*model.Inbox, error) { if h.db == nil { return nil, fmt.Errorf("telegram webhook database is not configured") } var channel channelmodel.ChannelTelegram if err := h.db.Where("bot_token = ?", botToken).First(&channel).Error; err != nil { return nil, fmt.Errorf("telegram channel not found for bot_token: %w", err) } var inbox model.Inbox if err := h.db.Where("id = ? AND channel_type = ?", channel.InboxID, "telegram").First(&inbox).Error; err != nil { return nil, fmt.Errorf("telegram inbox not found for channel inbox_id=%d: %w", channel.InboxID, err) } return &inbox, nil } // 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 }