package telegram // Pipeline integration for Telegram channel with gochat's message processing system. // Reference: Chatwoot's message pipeline architecture: // - Incoming: Telegram webhook → IncomingMessageService → Conversation + Message creation // - Outgoing: Message creation → SendOnTelegramService → Telegram Bot API // - Updates: Message.updated → editMessageText, Message.deleted → deleteMessage // // gochat's pipeline architecture (from P5/P6): // - IncomingMessageProcessor: receives raw payloads → transforms to IncomingMessage → creates Conversation + Message // - OutgoingMessageProcessor: receives outgoing Message → sends via channel → returns SendResult // - Broker: routes messages between providers and pipelines // - Event system: message.created, message.updated, message.deleted events // // This file provides the bridge between the Telegram provider and gochat's pipeline. import ( "context" "fmt" "strconv" channelpkg "github.com/gochat/gochat/internal/channel" "github.com/gochat/gochat/internal/channel/provider" "github.com/gochat/gochat/internal/model" applogger "github.com/gochat/gochat/pkg/logger" ) // === Incoming Pipeline Integration === // IncomingProcessor bridges Telegram updates to gochat's IncomingMessageProcessor. // Reference: Chatwoot IncomingMessageService.perform — creates contact, conversation, message // // Flow: // 1. Receive Telegram Update (via webhook) // 2. Parse into IncomingMessage (via TelegramProvider.ProcessIncomingMessage) // 3. Create/update Contact (from TelegramUser) // 4. Create/update Conversation (from chat_id) // 5. Create Message (with content + attachments) // 6. Handle special cases (edited messages, callback queries, group messages) type IncomingProcessor struct { provider *provider.TelegramProvider // contactRepo ContactRepository // conversationRepo ConversationRepository // messageRepo MessageRepository // channelRepo ChannelRepository } // NewIncomingProcessor creates an incoming message processor for Telegram. func NewIncomingProcessor(provider *provider.TelegramProvider) *IncomingProcessor { return &IncomingProcessor{ provider: provider, } } // ProcessUpdate processes a Telegram Update through the incoming pipeline. // This is called by the webhook handler after receiving an update. // // Reference: Chatwoot IncomingMessageService steps: // 1. Find or create contact from Telegram user info // 2. Find or create conversation (keyed on chat_id) // 3. Create message with content/attachments // 4. Handle edited_message updates // 5. Handle callback_query responses func (p *IncomingProcessor) ProcessUpdate(ctx context.Context, inbox *model.Inbox, payload map[string]interface{}) error { // Step 1: Transform raw payload into IncomingMessage incomingMsg, err := p.provider.ProcessIncomingMessage(ctx, inbox, payload) if err != nil { return fmt.Errorf("telegram incoming pipeline: transform failed: %w", err) } if incomingMsg == nil { return nil // no processable content } // Step 2: Resolve or create Contact // Reference: Chatwoot: find_or_create_contact using source_id (telegram user_id) contact, err := p.resolveOrCreateContact(ctx, inbox, incomingMsg) if err != nil { return fmt.Errorf("telegram incoming pipeline: contact resolution failed: %w", err) } // Step 3: Resolve or create Conversation // Reference: Chatwoot: find_or_create_conversation using inbox_id + contact_id + chat_id conversation, err := p.resolveOrCreateConversation(ctx, inbox, contact, incomingMsg) if err != nil { return fmt.Errorf("telegram incoming pipeline: conversation resolution failed: %w", err) } // Step 4: Create or update Message // Reference: Chatwoot: create_message with content + content_type + attachments if err := p.createOrUpdateMessage(ctx, conversation, contact, incomingMsg); err != nil { return fmt.Errorf("telegram incoming pipeline: message creation failed: %w", err) } // Step 5: Handle welcome message on first contact // Reference: Chatwoot: send welcome_message if configured and this is a new contact if contact.CreatedAt.Equal(contact.UpdatedAt) && incomingMsg.Extra["is_first_contact"] != nil { botToken := p.getBotTokenFromInbox(inbox) welcomeMessage := p.getWelcomeMessageFromInbox(inbox) if botToken != "" && welcomeMessage != "" { if err := p.provider.SendWelcomeMessage(ctx, botToken, contact.Identifier, welcomeMessage); err != nil { applogger.L().Warn("Failed to send Telegram welcome message", "error", err) } } } applogger.L().Info("Telegram incoming pipeline completed", "inbox_id", inbox.ID, "contact_id", contact.ID, "conversation_id", conversation.ID, ) return nil } // resolveOrCreateContact finds or creates a Contact for the Telegram user. // Reference: Chatwoot: find_or_create_contact using source_id // // In Chatwoot: // contact = account.contacts.where(source_id: telegram_user_id, inbox: inbox).first_or_create // contact.update(name: telegram_name, avatar: telegram_avatar) func (p *IncomingProcessor) resolveOrCreateContact(ctx context.Context, inbox *model.Inbox, msg *channelpkg.IncomingMessage) (*model.Contact, error) { // TODO: Implement with GORM repository // In production: // contact = contactRepo.FindBySourceID(inbox.AccountID, msg.SenderID, "telegram") // if contact == nil: // contact = contactRepo.Create(accountID, name, identifier=chatID, sourceID=senderID) // contact.UpdateName(msg.SenderName) // contact.UpdateAvatar(msg.SenderExtra["avatar_url"]) // contact.MarkAsNew() // for welcome message check contact := &model.Contact{ AccountID: inbox.AccountID, Name: msg.SenderName, Identifier: msg.SenderID, } return contact, nil } // resolveOrCreateConversation finds or creates a Conversation for the chat. // Reference: Chatwoot: find_or_create_conversation // // Keyed on: inbox_id + contact_id // For group messages: additionally keyed on chat_id // Chatwoot uses additional_attributes to store chat_id for groups func (p *IncomingProcessor) resolveOrCreateConversation(ctx context.Context, inbox *model.Inbox, contact *model.Contact, msg *channelpkg.IncomingMessage) (*model.Conversation, error) { // TODO: Implement with GORM repository // In production: // conversation = conversationRepo.FindByInboxAndContact(inbox.ID, contact.ID) // if conversation == nil: // conversation = conversationRepo.Create(inbox.ID, contact.ID, status="open") // conversation.UpdateAdditionalAttributes(msg.ConversationExtra) conversation := &model.Conversation{ InboxID: inbox.ID, AccountID: inbox.AccountID, ContactID: contact.ID, Status: "open", } return conversation, nil } // createOrUpdateMessage creates a new message or updates an existing one. // Reference: Chatwoot: creates Message with content_type, content, attachments // // For edited messages (edited_message update type): // - Find existing message by source_id (telegram message_id) // - Update content with new text // - Mark as edited // // For callback queries: // - Create a new message with the callback data as content func (p *IncomingProcessor) createOrUpdateMessage(ctx context.Context, conversation *model.Conversation, contact *model.Contact, msg *channelpkg.IncomingMessage) error { // TODO: Implement with GORM repository // // Regular message: // message = messageRepo.Create(conversation.ID, contact.ID, msg.ContentType, msg.Content, msg.SourceID) // for attachment in msg.Attachments: // attachmentRepo.Create(message.ID, attachment.URL, attachment.ContentType, attachment.Filename) // // Edited message: // existing = messageRepo.FindBySourceID(msg.Extra["original_message_id"]) // existing.UpdateContent(msg.Content) // existing.MarkAsEdited() // // Callback query: // message = messageRepo.Create(conversation.ID, contact.ID, "text", msg.Content, msg.SourceID) // message.MarkAsCallbackQuery(msg.Extra["callback_data"]) return nil } // === Outgoing Pipeline Integration === // OutgoingProcessor bridges gochat's outgoing messages to Telegram Bot API. // Reference: Chatwoot SendOnTelegramService + SendAttachmentsService // // Flow: // 1. Receive outgoing Message (from agent/user) // 2. Determine content type (text, attachment, input_select) // 3. Send via Telegram Bot API (sendMessage, sendPhoto, sendDocument, etc.) // 4. Store external message ID for future edit/delete operations // 5. Return SendResult with external_id type OutgoingProcessor struct { provider *provider.TelegramProvider // messageRepo MessageRepository // contactRepo ContactRepository // channelRepo ChannelRepository } // NewOutgoingProcessor creates an outgoing message processor for Telegram. func NewOutgoingProcessor(provider *provider.TelegramProvider) *OutgoingProcessor { return &OutgoingProcessor{ provider: provider, } } // SendMessage sends an outgoing message through the Telegram channel. // Reference: Chatwoot SendOnTelegramService.perform // // Steps: // 1. Resolve bot_token from inbox's ChannelTelegram // 2. Resolve chat_id from contact's identifier // 3. Determine message type → select API method // 4. Send via TelegramProvider // 5. Update message with external_id (telegram message_id) func (p *OutgoingProcessor) SendMessage(ctx context.Context, inbox *model.Inbox, message *model.Message, contact *model.Contact) (*channelpkg.SendResult, error) { sendResult, err := p.provider.ProcessOutgoingMessage(ctx, inbox, message, contact) if err != nil { return nil, fmt.Errorf("telegram outgoing pipeline: send failed: %w", err) } // Update message with external ID for future edit/delete // Reference: Chatwoot stores telegram message_id as message.source_id if sendResult.ExternalID != "" { // TODO: messageRepo.UpdateSourceID(message.ID, sendResult.ExternalID) chatID := "" if sendResult.Extra != nil { chatID, _ = sendResult.Extra["chat_id"].(string) } applogger.L().Info("Telegram message sent", "message_id", message.ID, "external_id", sendResult.ExternalID, "chat_id", chatID, ) } return sendResult, nil } // EditMessage edits a previously sent message on Telegram. // Reference: Chatwoot: message.updated event → editMessageText // // Triggered when an agent edits a message in the UI. // Flow: // 1. Resolve bot_token + chat_id + message telegram_id // 2. Call editMessageText API // 3. Update message content in DB func (p *OutgoingProcessor) EditMessage(ctx context.Context, inbox *model.Inbox, message *model.Message) error { botToken := p.getBotTokenFromInbox(inbox) if botToken == "" { return fmt.Errorf("telegram bot_token not configured for inbox %d", inbox.ID) } // Extract chat_id and telegram message_id from message metadata chatID := p.getChatIDFromMessage(message) messageID := p.getTelegramMessageID(message) if chatID == "" || messageID == 0 { return fmt.Errorf("missing chat_id or telegram message_id for edit operation") } return p.provider.EditMessageText(ctx, botToken, chatID, messageID, message.Content) } // DeleteMessage deletes a previously sent message on Telegram. // Reference: Chatwoot: message.deleted event → deleteMessage // // Triggered when an agent deletes a message in the UI. func (p *OutgoingProcessor) DeleteMessage(ctx context.Context, inbox *model.Inbox, message *model.Message) error { botToken := p.getBotTokenFromInbox(inbox) if botToken == "" { return fmt.Errorf("telegram bot_token not configured for inbox %d", inbox.ID) } chatID := p.getChatIDFromMessage(message) messageID := p.getTelegramMessageID(message) if chatID == "" || messageID == 0 { return fmt.Errorf("missing chat_id or telegram message_id for delete operation") } return p.provider.DeleteMessage(ctx, botToken, chatID, messageID) } // === Helper Functions === func (p *IncomingProcessor) getBotTokenFromInbox(inbox *model.Inbox) string { // TODO: Implement with channel repository return "" } func (p *IncomingProcessor) getWelcomeMessageFromInbox(inbox *model.Inbox) string { // TODO: channelRepo.FindByInboxID(inbox.ID).WelcomeMessage return "" } func (p *OutgoingProcessor) getBotTokenFromInbox(inbox *model.Inbox) string { // TODO: channelRepo.FindByInboxID(inbox.ID).BotToken return "" } func (p *OutgoingProcessor) getChatIDFromMessage(message *model.Message) string { // TODO: Extract from message.SourceID or conversation additional_attributes // In Chatwoot: conversation.additional_attributes["chat_id"] return "" } func (p *OutgoingProcessor) getTelegramMessageID(message *model.Message) int64 { // TODO: Extract from message.SourceID (stored as telegram message_id) // Parse the source_id as int64 if message.SourceID != "" { id, err := strconv.ParseInt(message.SourceID, 10, 64) if err == nil { return id } } return 0 } // === Event Bridge === // EventBridge connects gochat's event system to Telegram operations. // Reference: Chatwoot uses event callbacks for message lifecycle: // - message.created → SendOnTelegramService // - message.updated → editMessageText // - message.deleted → deleteMessage // // gochat uses a similar event-driven approach via the Broker/Dispatcher. type EventBridge struct { outgoing *OutgoingProcessor } // NewEventBridge creates an event bridge for Telegram message lifecycle events. func NewEventBridge(outgoing *OutgoingProcessor) *EventBridge { return &EventBridge{outgoing: outgoing} } // OnMessageCreated handles a message.created event — sends the message via Telegram. func (b *EventBridge) OnMessageCreated(ctx context.Context, inbox *model.Inbox, message *model.Message, contact *model.Contact) (*channelpkg.SendResult, error) { return b.outgoing.SendMessage(ctx, inbox, message, contact) } // OnMessageUpdated handles a message.updated event — edits the message on Telegram. func (b *EventBridge) OnMessageUpdated(ctx context.Context, inbox *model.Inbox, message *model.Message) error { return b.outgoing.EditMessage(ctx, inbox, message) } // OnMessageDeleted handles a message.deleted event — deletes the message on Telegram. func (b *EventBridge) OnMessageDeleted(ctx context.Context, inbox *model.Inbox, message *model.Message) error { return b.outgoing.DeleteMessage(ctx, inbox, message) }