Files
gochat/internal/repository/inbox_repo.go
T

203 lines
7.2 KiB
Go

package repository
import (
"context"
"encoding/json"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
channelmodel "github.com/gochat/gochat/internal/model/channel"
)
// InboxRepo implements GORM repository for Inbox.
// Reference: Chatwoot app/models/inbox.rb
type InboxRepo struct {
db *gorm.DB
}
// NewInboxRepo creates a new Inbox repository.
func NewInboxRepo(db *gorm.DB) *InboxRepo {
return &InboxRepo{db: db}
}
// DB returns the underlying gorm.DB for direct queries.
func (r *InboxRepo) DB() *gorm.DB { return r.db }
// FindByID retrieves an inbox by primary key.
func (r *InboxRepo) FindByID(ctx context.Context, id uint) (*model.Inbox, error) {
var inbox model.Inbox
err := r.withWorkingHours(r.db.WithContext(ctx)).First(&inbox, id).Error
if err != nil {
return nil, err
}
return &inbox, nil
}
// FindByAccount retrieves all inboxes for an account.
func (r *InboxRepo) FindByAccount(ctx context.Context, accountID uint, offset, limit int) ([]model.Inbox, int64, error) {
var inboxes []model.Inbox
var total int64
countDB := r.db.WithContext(ctx).Model(&model.Inbox{}).Where("account_id = ?", accountID)
if err := countDB.Count(&total).Error; err != nil {
return nil, 0, err
}
err := r.withWorkingHours(r.db.WithContext(ctx)).Where("account_id = ?", accountID).
Offset(offset).Limit(limit).Order("id DESC").
Find(&inboxes).Error
return inboxes, total, err
}
// Create inserts a new inbox.
func (r *InboxRepo) Create(ctx context.Context, inbox *model.Inbox) error {
return r.db.WithContext(ctx).Create(inbox).Error
}
// Update modifies an existing inbox.
func (r *InboxRepo) Update(ctx context.Context, inbox *model.Inbox) error {
return r.db.WithContext(ctx).Omit("WorkingHours").Save(inbox).Error
}
// Delete soft-deletes an inbox.
func (r *InboxRepo) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.Inbox{}, id).Error
}
// FindByAccountAndID retrieves an inbox scoped to an account.
func (r *InboxRepo) FindByAccountAndID(ctx context.Context, accountID, id uint) (*model.Inbox, error) {
var inbox model.Inbox
err := r.withWorkingHours(r.db.WithContext(ctx)).Where("account_id = ? AND id = ?", accountID, id).First(&inbox).Error
if err != nil {
return nil, err
}
return &inbox, nil
}
func (r *InboxRepo) withWorkingHours(db *gorm.DB) *gorm.DB {
if r == nil || r.db == nil || !r.db.Migrator().HasTable(&model.WorkingHour{}) {
return db
}
return db.Preload("WorkingHours", func(tx *gorm.DB) *gorm.DB {
return tx.Order("day_of_week ASC")
})
}
// CountByAccount returns total number of inboxes in an account.
func (r *InboxRepo) CountByAccount(ctx context.Context, accountID uint) (int64, error) {
var total int64
err := r.db.WithContext(ctx).Model(&model.Inbox{}).Where("account_id = ?", accountID).Count(&total).Error
return total, err
}
// FindByChannelType retrieves all inboxes of a given channel type (e.g. "telegram").
// Used by FindTelegramInboxByBotToken to scan telegram inboxes without account scoping.
func (r *InboxRepo) FindByChannelType(ctx context.Context, channelType string, limit int) ([]model.Inbox, error) {
var inboxes []model.Inbox
err := r.db.WithContext(ctx).Where("channel_type = ?", channelType).
Limit(limit).Order("id DESC").Find(&inboxes).Error
return inboxes, err
}
// FindByChannelTypeAndIdentifier finds an inbox by channel type and identifier.
// For Telegram, the identifier is the bot_token stored in the ChannelConfig JSON field.
// This scans all inboxes of the given channel_type, parses each inbox's ChannelConfig JSON,
// and returns the first one whose config contains a matching identifier key.
func (r *InboxRepo) FindByChannelTypeAndIdentifier(channelType string, identifier string) (*model.Inbox, error) {
var inboxes []model.Inbox
if err := r.db.Where("channel_type = ?", channelType).Find(&inboxes).Error; err != nil {
return nil, err
}
for i := range inboxes {
config := parseChannelConfig(&inboxes[i])
if botToken, ok := config["bot_token"].(string); ok && botToken == identifier {
return &inboxes[i], nil
}
}
return nil, gorm.ErrRecordNotFound
}
// FindByChannelTypeAndIdentifierCtx is the context-aware variant of FindByChannelTypeAndIdentifier.
func (r *InboxRepo) FindByChannelTypeAndIdentifierCtx(ctx context.Context, channelType string, identifier string) (*model.Inbox, error) {
var inboxes []model.Inbox
if err := r.db.WithContext(ctx).Where("channel_type = ?", channelType).Find(&inboxes).Error; err != nil {
return nil, err
}
for i := range inboxes {
config := parseChannelConfig(&inboxes[i])
if botToken, ok := config["bot_token"].(string); ok && botToken == identifier {
return &inboxes[i], nil
}
}
return nil, gorm.ErrRecordNotFound
}
// FindByWebsiteToken finds a web_widget inbox whose channel_config contains the given website_token.
// website_token is stored inside the JSON channel_config field, not as a separate column.
func (r *InboxRepo) FindByWebsiteToken(ctx context.Context, websiteToken string) (*model.Inbox, error) {
var inboxes []model.Inbox
if err := r.db.WithContext(ctx).Where("channel_type = ?", "web_widget").Find(&inboxes).Error; err != nil {
return nil, err
}
for i := range inboxes {
config := parseChannelConfig(&inboxes[i])
if token, ok := config["website_token"].(string); ok && token == websiteToken {
return &inboxes[i], nil
}
}
return nil, gorm.ErrRecordNotFound
}
// FindAPIInboxByIdentifier resolves a public API inbox via Channel::Api identifier.
func (r *InboxRepo) FindAPIInboxByIdentifier(ctx context.Context, identifier string) (*model.Inbox, *channelmodel.ChannelAPI, error) {
var channelAPI channelmodel.ChannelAPI
if err := r.db.WithContext(ctx).Where("identifier = ?", identifier).First(&channelAPI).Error; err != nil {
return nil, nil, err
}
inbox, err := r.FindByID(ctx, channelAPI.InboxID)
if err != nil {
return nil, nil, err
}
return inbox, &channelAPI, nil
}
// parseChannelConfig parses the JSON-encoded ChannelConfig string into a map.
func parseChannelConfig(inbox *model.Inbox) map[string]interface{} {
if inbox.ChannelConfig == "" {
return map[string]interface{}{}
}
var config map[string]interface{}
if err := json.Unmarshal([]byte(inbox.ChannelConfig), &config); err != nil {
return map[string]interface{}{}
}
return config
}
// UpdateAvatarURL updates the avatar_url field for an inbox.
// Reference: Chatwoot InboxesController#avatar (DELETE action destroys avatar attachment)
func (r *InboxRepo) UpdateAvatarURL(ctx context.Context, id uint, avatarURL string) error {
return r.db.WithContext(ctx).Model(&model.Inbox{}).Where("id = ?", id).
Update("avatar_url", avatarURL).Error
}
// UpdateSecret regenerates the secret field for an inbox (used by API inboxes).
// Reference: Chatwoot InboxesController#reset_secret
func (r *InboxRepo) UpdateSecret(ctx context.Context, id uint, secret string) error {
return r.db.WithContext(ctx).Model(&model.Inbox{}).Where("id = ?", id).
Update("secret", secret).Error
}
// UpdateFields selectively updates specific fields on an inbox by map.
// Used for partial updates like CSAT config, working hours, etc.
func (r *InboxRepo) UpdateFields(ctx context.Context, id uint, updates map[string]interface{}) error {
return r.db.WithContext(ctx).Model(&model.Inbox{}).Where("id = ?", id).
Updates(updates).Error
}