package telegram // TelegramRepository provides GORM-based persistence for Telegram channel data. // Reference: Chatwoot uses ActiveRecord for ChannelTelegram CRUD with callbacks: // - before_validation :ensure_valid_bot_token // - before_save :setup_telegram_webhook // - after_destroy :delete_telegram_webhook // // Design: Repository pattern abstracts GORM operations, matching gochat's // existing repository pattern used by other channel providers. import ( "context" "fmt" "time" channelmodel "github.com/gochat/gochat/internal/model/channel" "github.com/gochat/gochat/internal/model" "gorm.io/gorm" applogger "github.com/gochat/gochat/pkg/logger" ) // Repository handles Telegram channel database operations. type Repository struct { db *gorm.DB } // NewRepository creates a Telegram repository with GORM DB. func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } // === CRUD Operations === // Create persists a new ChannelTelegram record. // Reference: Chatwoot ChannelTelegram.create with before_validation + before_save callbacks // // The Chatwoot model has these lifecycle hooks: // before_validation :ensure_valid_bot_token → validates via getMe API // before_save :setup_telegram_webhook → calls setWebhook API // after_destroy :delete_telegram_webhook → calls deleteWebhook API // // In gochat, we implement these as explicit service-level operations rather than // model callbacks, since Go doesn't have ActiveRecord-style lifecycle hooks. func (r *Repository) Create(ctx context.Context, channel *channelmodel.ChannelTelegram) error { if err := r.db.WithContext(ctx).Create(channel).Error; err != nil { return fmt.Errorf("failed to create ChannelTelegram: %w", err) } applogger.L().Info("ChannelTelegram created", "id", channel.ID, "account_id", channel.AccountID, "bot_name", channel.BotName, ) return nil } // Update modifies a ChannelTelegram record. // Reference: Chatwoot ChannelTelegram.update — only bot_token is editable func (r *Repository) Update(ctx context.Context, channel *channelmodel.ChannelTelegram) error { result := r.db.WithContext(ctx).Save(channel) if result.Error != nil { return fmt.Errorf("failed to update ChannelTelegram: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("ChannelTelegram not found (id=%d)", channel.ID) } applogger.L().Info("ChannelTelegram updated", "id", channel.ID) return nil } // UpdateBotToken updates only the bot_token field (and re-validates + re-sets webhook). // Reference: Chatwoot only allows bot_token updates on existing channels func (r *Repository) UpdateBotToken(ctx context.Context, channelID uint, newBotToken string) error { result := r.db.WithContext(ctx). Model(&channelmodel.ChannelTelegram{}). Where("id = ?", channelID). Update("bot_token", newBotToken) if result.Error != nil { return fmt.Errorf("failed to update bot_token: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("ChannelTelegram not found (id=%d)", channelID) } applogger.L().Info("ChannelTelegram bot_token updated", "id", channelID) return nil } // UpdateWelcomeMessage updates only the welcome_message field. func (r *Repository) UpdateWelcomeMessage(ctx context.Context, channelID uint, welcomeMessage string) error { result := r.db.WithContext(ctx). Model(&channelmodel.ChannelTelegram{}). Where("id = ?", channelID). Update("welcome_message", welcomeMessage) if result.Error != nil { return fmt.Errorf("failed to update welcome_message: %w", result.Error) } return nil } // Delete removes a ChannelTelegram record (soft delete via GORM). // Reference: Chatwoot after_destroy :delete_telegram_webhook // Note: The webhook cleanup should be done BEFORE calling this Delete method // (in the service layer), since Go doesn't have after_destroy callbacks. func (r *Repository) Delete(ctx context.Context, channelID uint) error { result := r.db.WithContext(ctx). Where("id = ?", channelID). Delete(&channelmodel.ChannelTelegram{}) if result.Error != nil { return fmt.Errorf("failed to delete ChannelTelegram: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("ChannelTelegram not found (id=%d)", channelID) } applogger.L().Info("ChannelTelegram deleted", "id", channelID) return nil } // === Query Operations === // FindByID retrieves a ChannelTelegram by its primary key. func (r *Repository) FindByID(ctx context.Context, channelID uint) (*channelmodel.ChannelTelegram, error) { var channel channelmodel.ChannelTelegram if err := r.db.WithContext(ctx).First(&channel, channelID).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("ChannelTelegram not found (id=%d)", channelID) } return nil, fmt.Errorf("failed to find ChannelTelegram: %w", err) } return &channel, nil } // FindByBotToken retrieves a ChannelTelegram by its bot_token. // Reference: Chatwoot finds inbox by bot_token for webhook routing func (r *Repository) FindByBotToken(ctx context.Context, botToken string) (*channelmodel.ChannelTelegram, error) { var channel channelmodel.ChannelTelegram if err := r.db.WithContext(ctx).Where("bot_token = ?", botToken).First(&channel).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("ChannelTelegram not found for bot_token") } return nil, fmt.Errorf("failed to find ChannelTelegram by bot_token: %w", err) } return &channel, nil } // FindByAccountID retrieves all ChannelTelegram records for an account. // Reference: Chatwoot scopes channels to account (account_id foreign key) func (r *Repository) FindByAccountID(ctx context.Context, accountID uint) ([]channelmodel.ChannelTelegram, error) { var channels []channelmodel.ChannelTelegram if err := r.db.WithContext(ctx). Where("account_id = ?", accountID). Find(&channels).Error; err != nil { return nil, fmt.Errorf("failed to find ChannelTelegram by account_id: %w", err) } return channels, nil } // FindByInboxID retrieves the ChannelTelegram associated with an Inbox. // Reference: Chatwoot: inbox.channel (polymorphic association) func (r *Repository) FindByInboxID(ctx context.Context, inboxID uint) (*channelmodel.ChannelTelegram, error) { var channel channelmodel.ChannelTelegram if err := r.db.WithContext(ctx).Where("inbox_id = ?", inboxID).First(&channel).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("ChannelTelegram not found for inbox_id=%d", inboxID) } return nil, fmt.Errorf("failed to find ChannelTelegram by inbox_id: %w", err) } return &channel, nil } // === Inbox Repository === // InboxRepository handles Inbox model operations for Telegram channels. // Reference: Chatwoot creates Inbox alongside ChannelTelegram type InboxRepository struct { db *gorm.DB } // NewInboxRepository creates an Inbox repository. func NewInboxRepository(db *gorm.DB) *InboxRepository { return &InboxRepository{db: db} } // FindByBotToken finds the Inbox associated with a Telegram bot token. // This is the primary lookup for webhook routing. // Reference: Chatwoot: Inbox.joins(:channel_telegram).find_by(channel_telegrams: {bot_token: token}) func (r *InboxRepository) FindByBotToken(ctx context.Context, botToken string) (*model.Inbox, error) { var channel channelmodel.ChannelTelegram if err := r.db.WithContext(ctx).Where("bot_token = ?", botToken).First(&channel).Error; err != nil { return nil, fmt.Errorf("no ChannelTelegram found for bot_token: %w", err) } var inbox model.Inbox if err := r.db.WithContext(ctx). Where("channel_type = ? AND id = ?", "telegram", channel.InboxID). First(&inbox).Error; err != nil { return nil, fmt.Errorf("no Inbox found for ChannelTelegram: %w", err) } return &inbox, nil } // CreateWithChannel creates an Inbox and its associated ChannelTelegram in one transaction. // Reference: Chatwoot creates both Inbox and ChannelTelegram together func (r *InboxRepository) CreateWithChannel(ctx context.Context, accountID uint, name string, channelTelegram *channelmodel.ChannelTelegram) (*model.Inbox, error) { inbox := &model.Inbox{ AccountID: accountID, Name: name, ChannelType: "telegram", } err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // Create Inbox first if err := tx.Create(inbox).Error; err != nil { return fmt.Errorf("failed to create Inbox: %w", err) } // Set InboxID on ChannelTelegram and create channelTelegram.InboxID = inbox.ID if err := tx.Create(channelTelegram).Error; err != nil { return fmt.Errorf("failed to create ChannelTelegram: %w", err) } return nil }) if err != nil { return nil, err } applogger.L().Info("Telegram Inbox created with Channel", "inbox_id", inbox.ID, "account_id", accountID, ) return inbox, nil } // === Auto-Migration === // AutoMigrate runs GORM auto-migration for ChannelTelegram model. func (r *Repository) AutoMigrate(ctx context.Context) error { if err := r.db.WithContext(ctx).AutoMigrate(&channelmodel.ChannelTelegram{}); err != nil { return fmt.Errorf("failed to auto-migrate ChannelTelegram: %w", err) } applogger.L().Info("ChannelTelegram auto-migration completed") return nil } // === Reauthorization Tracking === // MarkReauthorizationRequired flags a channel as needing bot token reauthorization. // Reference: Chatwoot tracks reauthorization_required on ChannelTelegram // when webhook delivery fails or bot token becomes invalid. func (r *Repository) MarkReauthorizationRequired(ctx context.Context, channelID uint, reason string) error { result := r.db.WithContext(ctx). Model(&channelmodel.ChannelTelegram{}). Where("id = ?", channelID). Updates(map[string]interface{}{ "reauthorization_required": true, "updated_at": time.Now(), }) if result.Error != nil { return fmt.Errorf("failed to mark reauthorization_required: %w", result.Error) } applogger.L().Warn("ChannelTelegram marked for reauthorization", "id", channelID, "reason", reason, ) return nil } // ClearReauthorizationRequired clears the reauthorization flag after successful reauth. func (r *Repository) ClearReauthorizationRequired(ctx context.Context, channelID uint) error { result := r.db.WithContext(ctx). Model(&channelmodel.ChannelTelegram{}). Where("id = ?", channelID). Updates(map[string]interface{}{ "reauthorization_required": false, "updated_at": time.Now(), }) if result.Error != nil { return fmt.Errorf("failed to clear reauthorization_required: %w", result.Error) } return nil }