203 lines
7.2 KiB
Go
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
|
|
}
|