393 lines
13 KiB
Go
393 lines
13 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
|
)
|
|
|
|
// AgentBotService implements business logic for AgentBot CRUD + ResetToken + ResetSecret.
|
|
// Reference: Chatwoot app/controllers/api/v1/accounts/agent_bots_controller.rb
|
|
type AgentBotService struct {
|
|
repo *repository.AgentBotRepo
|
|
}
|
|
|
|
// NewAgentBotService creates a new AgentBot service.
|
|
func NewAgentBotService(repo *repository.AgentBotRepo) *AgentBotService {
|
|
return &AgentBotService{repo: repo}
|
|
}
|
|
|
|
// CreateAgentBotRequest is the DTO for creating an agent bot.
|
|
type CreateAgentBotRequest struct {
|
|
Name string `json:"name" form:"name" validate:"required,min=2"`
|
|
Description string `json:"description,omitempty" form:"description"`
|
|
AvatarURL string `json:"avatar_url,omitempty" form:"avatar_url"`
|
|
OutgoingURL string `json:"outgoing_url,omitempty" form:"outgoing_url"`
|
|
BotType string `json:"bot_type,omitempty" form:"bot_type" validate:"omitempty,oneof=webhook default custom"`
|
|
AccountID *uint `json:"account_id,omitempty"` // nil = platform-level (global) bot
|
|
Config json.RawMessage `json:"config,omitempty"`
|
|
BotConfig json.RawMessage `json:"bot_config,omitempty" form:"bot_config"`
|
|
}
|
|
|
|
// UpdateAgentBotRequest is the DTO for updating an agent bot.
|
|
type UpdateAgentBotRequest struct {
|
|
Name *string `json:"name,omitempty" form:"name" validate:"omitempty,min=2"`
|
|
Description *string `json:"description,omitempty" form:"description"`
|
|
AvatarURL *string `json:"avatar_url,omitempty" form:"avatar_url"`
|
|
OutgoingURL *string `json:"outgoing_url,omitempty" form:"outgoing_url"`
|
|
BotType *string `json:"bot_type,omitempty" form:"bot_type" validate:"omitempty,oneof=webhook default custom"`
|
|
Config json.RawMessage `json:"config,omitempty"`
|
|
BotConfig json.RawMessage `json:"bot_config,omitempty" form:"bot_config"`
|
|
}
|
|
|
|
// generateBotAccessToken creates a cryptographically secure random token for bot access.
|
|
func generateBotAccessToken() (string, error) {
|
|
b := make([]byte, 32) // 256-bit token
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|
|
|
|
// generateBotSecret creates a cryptographically secure random secret for webhook signing.
|
|
func generateBotSecret() (string, error) {
|
|
b := make([]byte, 24) // 192-bit secret
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|
|
|
|
// Create creates a new AgentBot with auto-generated AccessToken and Secret.
|
|
// Reference: Chatwoot AccessTokenable concern — auto-create AccessToken on bot creation
|
|
func (s *AgentBotService) Create(ctx context.Context, req CreateAgentBotRequest) (*model.AgentBot, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
if req.Config == nil && req.BotConfig != nil {
|
|
req.Config = req.BotConfig
|
|
}
|
|
|
|
token, err := generateBotAccessToken()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate access token: %w", err)
|
|
}
|
|
|
|
secret, err := generateBotSecret()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate secret: %w", err)
|
|
}
|
|
|
|
bot := &model.AgentBot{
|
|
Name: req.Name,
|
|
Description: req.Description,
|
|
AvatarURL: req.AvatarURL,
|
|
OutgoingURL: req.OutgoingURL,
|
|
BotType: req.BotType,
|
|
AccountID: req.AccountID,
|
|
Config: req.Config,
|
|
AccessToken: token,
|
|
Secret: secret,
|
|
}
|
|
|
|
if bot.BotType == "" {
|
|
bot.BotType = "webhook"
|
|
}
|
|
|
|
if err := s.repo.Create(ctx, bot); err != nil {
|
|
applogger.L().Errorf("Create agent bot: %v", err)
|
|
return nil, err
|
|
}
|
|
applogger.L().Infof("Created agent bot %d (%s) for account %v", bot.ID, bot.Name, bot.AccountID)
|
|
return bot, nil
|
|
}
|
|
|
|
// Get retrieves an agent bot by ID.
|
|
func (s *AgentBotService) Get(ctx context.Context, id uint) (*model.AgentBot, error) {
|
|
bot, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
applogger.L().Errorf("Get agent bot %d: %v", id, err)
|
|
return nil, err
|
|
}
|
|
return bot, nil
|
|
}
|
|
|
|
// GetAccessible retrieves a system or account-owned bot visible to the account.
|
|
func (s *AgentBotService) GetAccessible(ctx context.Context, accountID uint, id uint) (*model.AgentBot, error) {
|
|
bot, err := s.repo.FindAccessibleByID(ctx, accountID, id)
|
|
if err != nil {
|
|
applogger.L().Errorf("Get accessible agent bot %d account %d: %v", id, accountID, err)
|
|
return nil, fmt.Errorf("agent bot not found: %w", err)
|
|
}
|
|
return bot, nil
|
|
}
|
|
|
|
// ListAccessible retrieves agent bots accessible to an account (global + account-scoped).
|
|
// Reference: Chatwoot AgentBot.scope :accessible_to(account)
|
|
func (s *AgentBotService) ListAccessible(ctx context.Context, accountID uint, offset, limit int) ([]model.AgentBot, int64, error) {
|
|
bots, total, err := s.repo.FindAccessiblePaginated(ctx, accountID, offset, limit)
|
|
if err != nil {
|
|
applogger.L().Errorf("ListAccessible agent bots account %d: %v", accountID, err)
|
|
return nil, 0, err
|
|
}
|
|
return bots, total, nil
|
|
}
|
|
|
|
// ListAccessibleAll retrieves all system and account-owned bots visible to the
|
|
// account. Chatwoot AgentBotsController#index does not paginate this list.
|
|
func (s *AgentBotService) ListAccessibleAll(ctx context.Context, accountID uint) ([]model.AgentBot, error) {
|
|
bots, err := s.repo.FindAccessible(ctx, accountID)
|
|
if err != nil {
|
|
applogger.L().Errorf("ListAccessibleAll agent bots account %d: %v", accountID, err)
|
|
return nil, err
|
|
}
|
|
return bots, nil
|
|
}
|
|
|
|
// List retrieves all agent bots for an account (account-scoped only).
|
|
func (s *AgentBotService) List(ctx context.Context, accountID uint) ([]model.AgentBot, error) {
|
|
bots, err := s.repo.FindByAccountID(ctx, accountID)
|
|
if err != nil {
|
|
applogger.L().Errorf("List agent bots account %d: %v", accountID, err)
|
|
return nil, err
|
|
}
|
|
return bots, nil
|
|
}
|
|
|
|
// Update modifies an existing AgentBot.
|
|
func (s *AgentBotService) Update(ctx context.Context, id uint, req UpdateAgentBotRequest) (*model.AgentBot, error) {
|
|
return s.update(ctx, nil, id, req)
|
|
}
|
|
|
|
// UpdateByAccount modifies an account-owned agent bot.
|
|
func (s *AgentBotService) UpdateByAccount(ctx context.Context, accountID uint, id uint, req UpdateAgentBotRequest) (*model.AgentBot, error) {
|
|
return s.update(ctx, &accountID, id, req)
|
|
}
|
|
|
|
func (s *AgentBotService) update(ctx context.Context, accountID *uint, id uint, req UpdateAgentBotRequest) (*model.AgentBot, error) {
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var bot *model.AgentBot
|
|
var err error
|
|
if accountID != nil {
|
|
bot, err = s.repo.FindByAccountAndID(ctx, *accountID, id)
|
|
} else {
|
|
bot, err = s.repo.GetByID(ctx, id)
|
|
}
|
|
if err != nil {
|
|
applogger.L().Errorf("Update agent bot %d: get failed: %v", id, err)
|
|
return nil, fmt.Errorf("agent bot not found: %w", err)
|
|
}
|
|
if req.Config == nil && req.BotConfig != nil {
|
|
req.Config = req.BotConfig
|
|
}
|
|
|
|
if req.Name != nil {
|
|
bot.Name = *req.Name
|
|
}
|
|
if req.Description != nil {
|
|
bot.Description = *req.Description
|
|
}
|
|
if req.AvatarURL != nil {
|
|
bot.AvatarURL = *req.AvatarURL
|
|
}
|
|
if req.OutgoingURL != nil {
|
|
bot.OutgoingURL = *req.OutgoingURL
|
|
}
|
|
if req.BotType != nil {
|
|
bot.BotType = *req.BotType
|
|
}
|
|
if req.Config != nil {
|
|
bot.Config = req.Config
|
|
}
|
|
|
|
if err := s.repo.Update(ctx, bot); err != nil {
|
|
applogger.L().Errorf("Update agent bot %d: %v", id, err)
|
|
return nil, err
|
|
}
|
|
return bot, nil
|
|
}
|
|
|
|
// DeleteByAccount removes an account-owned AgentBot by ID.
|
|
func (s *AgentBotService) DeleteByAccount(ctx context.Context, accountID uint, id uint) error {
|
|
if _, err := s.repo.FindByAccountAndID(ctx, accountID, id); err != nil {
|
|
return fmt.Errorf("agent bot not found: %w", err)
|
|
}
|
|
if err := s.repo.DeleteByAccount(ctx, accountID, id); err != nil {
|
|
applogger.L().Errorf("Delete agent bot %d account %d: %v", id, accountID, err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Delete removes an AgentBot by ID.
|
|
func (s *AgentBotService) Delete(ctx context.Context, id uint) error {
|
|
if err := s.repo.Delete(ctx, id); err != nil {
|
|
applogger.L().Errorf("Delete agent bot %d: %v", id, err)
|
|
return err
|
|
}
|
|
applogger.L().Infof("Deleted agent bot %d", id)
|
|
return nil
|
|
}
|
|
|
|
// ResetToken generates a new random access token for the bot, replacing the
|
|
// previous one. Returns the new token string.
|
|
// Reference: Chatwoot reset_access_token action on agent_bots_controller
|
|
func (s *AgentBotService) ResetToken(ctx context.Context, id uint) (string, error) {
|
|
bot, err := s.ResetTokenByAccount(ctx, 0, id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return bot.AccessToken, nil
|
|
}
|
|
|
|
// ResetTokenByAccount regenerates the access token on an account-owned bot.
|
|
func (s *AgentBotService) ResetTokenByAccount(ctx context.Context, accountID uint, id uint) (*model.AgentBot, error) {
|
|
var bot *model.AgentBot
|
|
var err error
|
|
if accountID == 0 {
|
|
bot, err = s.repo.GetByID(ctx, id)
|
|
} else {
|
|
bot, err = s.repo.FindByAccountAndID(ctx, accountID, id)
|
|
}
|
|
if err != nil {
|
|
applogger.L().Errorf("ResetToken agent bot %d: get failed: %v", id, err)
|
|
return nil, fmt.Errorf("agent bot not found: %w", err)
|
|
}
|
|
|
|
newToken, err := generateBotAccessToken()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate new access token: %w", err)
|
|
}
|
|
|
|
bot.AccessToken = newToken
|
|
if err := s.repo.Update(ctx, bot); err != nil {
|
|
applogger.L().Errorf("ResetToken agent bot %d: update failed: %v", id, err)
|
|
return nil, err
|
|
}
|
|
applogger.L().Infof("Reset access token for agent bot %d", id)
|
|
return bot, nil
|
|
}
|
|
|
|
// ResetSecret generates a new random webhook signing secret for the bot.
|
|
// Reference: Chatwoot AgentBotsController#reset_secret — regenerates the secret used for HMAC signing
|
|
func (s *AgentBotService) ResetSecret(ctx context.Context, id uint) (string, error) {
|
|
bot, err := s.ResetSecretByAccount(ctx, 0, id)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return bot.Secret, nil
|
|
}
|
|
|
|
// ResetSecretByAccount regenerates the webhook secret on an account-owned bot.
|
|
func (s *AgentBotService) ResetSecretByAccount(ctx context.Context, accountID uint, id uint) (*model.AgentBot, error) {
|
|
var bot *model.AgentBot
|
|
var err error
|
|
if accountID == 0 {
|
|
bot, err = s.repo.GetByID(ctx, id)
|
|
} else {
|
|
bot, err = s.repo.FindByAccountAndID(ctx, accountID, id)
|
|
}
|
|
if err != nil {
|
|
applogger.L().Errorf("ResetSecret agent bot %d: get failed: %v", id, err)
|
|
return nil, fmt.Errorf("agent bot not found: %w", err)
|
|
}
|
|
|
|
newSecret, err := generateBotSecret()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate new secret: %w", err)
|
|
}
|
|
|
|
bot.Secret = newSecret
|
|
if err := s.repo.Update(ctx, bot); err != nil {
|
|
applogger.L().Errorf("ResetSecret agent bot %d: update failed: %v", id, err)
|
|
return nil, err
|
|
}
|
|
applogger.L().Infof("Reset webhook secret for agent bot %d", id)
|
|
return bot, nil
|
|
}
|
|
|
|
// DeleteAvatar removes the bot's avatar URL (sets it to empty string).
|
|
// Reference: Chatwoot AgentBotsController#destroy_avatar
|
|
func (s *AgentBotService) DeleteAvatar(ctx context.Context, id uint) (*model.AgentBot, error) {
|
|
return s.deleteAvatar(ctx, nil, id)
|
|
}
|
|
|
|
// DeleteAvatarByAccount removes the avatar from an account-owned bot.
|
|
func (s *AgentBotService) DeleteAvatarByAccount(ctx context.Context, accountID uint, id uint) (*model.AgentBot, error) {
|
|
return s.deleteAvatar(ctx, &accountID, id)
|
|
}
|
|
|
|
func (s *AgentBotService) deleteAvatar(ctx context.Context, accountID *uint, id uint) (*model.AgentBot, error) {
|
|
var bot *model.AgentBot
|
|
var err error
|
|
if accountID != nil {
|
|
bot, err = s.repo.FindByAccountAndID(ctx, *accountID, id)
|
|
} else {
|
|
bot, err = s.repo.GetByID(ctx, id)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("agent bot not found: %w", err)
|
|
}
|
|
|
|
bot.AvatarURL = ""
|
|
if err := s.repo.Update(ctx, bot); err != nil {
|
|
applogger.L().Errorf("DeleteAvatar agent bot %d: %v", id, err)
|
|
return nil, err
|
|
}
|
|
applogger.L().Infof("Deleted avatar for agent bot %d", id)
|
|
return bot, nil
|
|
}
|
|
|
|
// AgentBotUpdateAvatarRequest is the DTO for updating an agent bot's avatar.
|
|
type AgentBotUpdateAvatarRequest struct {
|
|
AvatarURL string `json:"avatar_url" validate:"required,url"`
|
|
}
|
|
|
|
// UpdateAvatar sets a new avatar URL for the bot.
|
|
// Reference: Chatwoot AgentBotsController#update (avatar-only update via platform API)
|
|
func (s *AgentBotService) UpdateAvatar(ctx context.Context, id uint, req AgentBotUpdateAvatarRequest) (*model.AgentBot, error) {
|
|
bot, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("agent bot not found: %w", err)
|
|
}
|
|
|
|
bot.AvatarURL = req.AvatarURL
|
|
if err := s.repo.Update(ctx, bot); err != nil {
|
|
applogger.L().Errorf("UpdateAvatar agent bot %d: %v", id, err)
|
|
return nil, err
|
|
}
|
|
applogger.L().Infof("Updated avatar for agent bot %d", id)
|
|
return bot, nil
|
|
}
|
|
|
|
// ResetConfig clears the bot's configuration (sets Config to null).
|
|
// Reference: Chatwoot AgentBotsController#reset (platform-level config reset)
|
|
func (s *AgentBotService) ResetConfig(ctx context.Context, id uint) (*model.AgentBot, error) {
|
|
bot, err := s.repo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("agent bot not found: %w", err)
|
|
}
|
|
|
|
bot.Config = json.RawMessage("null")
|
|
if err := s.repo.Update(ctx, bot); err != nil {
|
|
applogger.L().Errorf("ResetConfig agent bot %d: %v", id, err)
|
|
return nil, err
|
|
}
|
|
applogger.L().Infof("Reset config for agent bot %d", id)
|
|
return bot, nil
|
|
}
|
|
|
|
// ErrBotNotFound is returned when the bot cannot be found.
|
|
var ErrBotNotFound = errors.New("agent bot not found")
|