300 lines
10 KiB
Go
300 lines
10 KiB
Go
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
|
|
} |