212 lines
7.2 KiB
Go
212 lines
7.2 KiB
Go
package automation
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// BotRuleService provides CRUD + condition matching + action execution for bot rules.
|
|
// Reference: Chatwoot AutomationRules pattern adapted for AgentBot scope.
|
|
// BotRuleService reuses ConditionsFilterService.MatchConditions and ActionService.Execute
|
|
// to avoid duplicating condition matching and action execution logic.
|
|
type BotRuleService struct {
|
|
db DBProvider
|
|
searchIndexer ActionSearchIndexer
|
|
}
|
|
|
|
// NewBotRuleService creates a new BotRuleService.
|
|
func NewBotRuleService(db DBProvider) *BotRuleService {
|
|
return &BotRuleService{db: db}
|
|
}
|
|
|
|
func (s *BotRuleService) SetSearchIndexer(indexer ActionSearchIndexer) {
|
|
s.searchIndexer = indexer
|
|
}
|
|
|
|
// GetByID retrieves a bot rule by ID.
|
|
func (s *BotRuleService) GetByID(ctx context.Context, id uint) (*BotRule, error) {
|
|
var rule BotRule
|
|
if err := s.db.DB().WithContext(ctx).First(&rule, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &rule, nil
|
|
}
|
|
|
|
// ListByAccount retrieves all bot rules for an account.
|
|
func (s *BotRuleService) ListByAccount(ctx context.Context, accountID uint) ([]BotRule, error) {
|
|
var rules []BotRule
|
|
if err := s.db.DB().WithContext(ctx).
|
|
Where("account_id = ?", accountID).
|
|
Order("id ASC").
|
|
Find(&rules).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// ListByAgentBot retrieves all bot rules for a specific AgentBot.
|
|
func (s *BotRuleService) ListByAgentBot(ctx context.Context, accountID uint, agentBotID uint) ([]BotRule, error) {
|
|
var rules []BotRule
|
|
if err := s.db.DB().WithContext(ctx).
|
|
Where("account_id = ? AND agent_bot_id = ?", accountID, agentBotID).
|
|
Order("id ASC").
|
|
Find(&rules).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// ListActiveByAccountAndEvent retrieves active bot rules for a given account and event.
|
|
// Reference: mirrors AutomationRuleService.ListActiveByAccountAndEvent
|
|
func (s *BotRuleService) ListActiveByAccountAndEvent(ctx context.Context, accountID uint, eventName string) ([]BotRule, error) {
|
|
var rules []BotRule
|
|
if err := s.db.DB().WithContext(ctx).
|
|
Where("account_id = ? AND event_name = ? AND status = ?", accountID, eventName, BotRuleStatusActive).
|
|
Order("id ASC").
|
|
Find(&rules).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return rules, nil
|
|
}
|
|
|
|
// Create creates a new bot rule.
|
|
// New rules default to active status unless explicitly set to inactive.
|
|
func (s *BotRuleService) Create(ctx context.Context, rule *BotRule) error {
|
|
// Validate conditions before saving
|
|
if err := ValidateConditions(rule.Conditions); err != nil {
|
|
return fmt.Errorf("invalid conditions: %w", err)
|
|
}
|
|
// Use Select to force all fields including zero-value fields
|
|
return s.db.DB().WithContext(ctx).Select(
|
|
"AccountID", "AgentBotID", "EventName", "Name", "Description",
|
|
"Conditions", "Actions", "Status",
|
|
).Create(rule).Error
|
|
}
|
|
|
|
// Update updates an existing bot rule.
|
|
func (s *BotRuleService) Update(ctx context.Context, rule *BotRule) error {
|
|
// Validate conditions before saving
|
|
if err := ValidateConditions(rule.Conditions); err != nil {
|
|
return fmt.Errorf("invalid conditions: %w", err)
|
|
}
|
|
return s.db.DB().WithContext(ctx).Save(rule).Error
|
|
}
|
|
|
|
// Delete soft-deletes a bot rule.
|
|
func (s *BotRuleService) Delete(ctx context.Context, id uint) error {
|
|
return s.db.DB().WithContext(ctx).Delete(&BotRule{}, id).Error
|
|
}
|
|
|
|
// Clone duplicates a bot rule, creating a new copy with "(copy)" appended to the name.
|
|
func (s *BotRuleService) Clone(ctx context.Context, id uint) (*BotRule, error) {
|
|
original, err := s.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cloned := &BotRule{
|
|
AccountID: original.AccountID,
|
|
AgentBotID: original.AgentBotID,
|
|
EventName: original.EventName,
|
|
Name: original.Name + " (copy)",
|
|
Description: original.Description,
|
|
Conditions: original.Conditions,
|
|
Actions: original.Actions,
|
|
Status: original.Status,
|
|
}
|
|
|
|
if err := s.db.DB().WithContext(ctx).Create(cloned).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return cloned, nil
|
|
}
|
|
|
|
// ToggleStatus toggles the active/inactive status of a bot rule.
|
|
func (s *BotRuleService) ToggleStatus(ctx context.Context, id uint, status BotRuleStatus) error {
|
|
return s.db.DB().WithContext(ctx).
|
|
Model(&BotRule{}).
|
|
Where("id = ?", id).
|
|
Update("status", status).Error
|
|
}
|
|
|
|
// MatchAndExecute checks all active bot rules for the given event and account,
|
|
// runs condition filters for each rule, and executes actions for matching rules.
|
|
// Reference: mirrors AutomationRuleService.MatchAndExecute but for BotRule scope.
|
|
// Reuses ConditionsFilterService.MatchConditions and ActionService.Execute.
|
|
func (s *BotRuleService) MatchAndExecute(ctx context.Context, accountID uint, eventName string, conversationID uint, eventData map[string]interface{}) error {
|
|
rules, err := s.ListActiveByAccountAndEvent(ctx, accountID, eventName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load bot rules: %w", err)
|
|
}
|
|
|
|
if len(rules) == 0 {
|
|
applogger.L().Debugf("no active bot rules for account %d event %s", accountID, eventName)
|
|
return nil
|
|
}
|
|
|
|
// Load the conversation for condition matching
|
|
conversation, err := loadConversationForFilter(ctx, s.db.DB(), conversationID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load conversation %d: %w", conversationID, err)
|
|
}
|
|
|
|
actionSvc := NewActionService(s.db)
|
|
actionSvc.SetSearchIndexer(s.searchIndexer)
|
|
|
|
for _, rule := range rules {
|
|
matched, err := MatchConditions(ctx, s.db.DB(), rule.Conditions, conversation, eventData)
|
|
if err != nil {
|
|
applogger.L().Errorf("condition matching failed for bot rule %d: %v", rule.ID, err)
|
|
continue
|
|
}
|
|
if !matched {
|
|
applogger.L().Debugf("bot rule %d did not match conditions for conversation %d", rule.ID, conversationID)
|
|
continue
|
|
}
|
|
|
|
applogger.L().Infof("bot rule %d matched for conversation %d, executing %d actions", rule.ID, conversationID, len(rule.Actions))
|
|
|
|
// Execute actions — stamp source as "bot_rule" with rule.ID for audit trail
|
|
actionsExecuted := 0
|
|
actionsFailed := 0
|
|
var firstErrorMsg string
|
|
for _, action := range rule.Actions {
|
|
if err := actionSvc.Execute(ctx, accountID, conversationID, action, ActionSourceBotRule, rule.ID); err != nil {
|
|
applogger.L().Errorf("action %s failed for bot rule %d on conversation %d: %v", action.ActionName, rule.ID, conversationID, err)
|
|
actionsFailed++
|
|
if firstErrorMsg == "" {
|
|
firstErrorMsg = err.Error()
|
|
}
|
|
} else {
|
|
actionsExecuted++
|
|
}
|
|
}
|
|
|
|
if actionsFailed > 0 {
|
|
applogger.L().Warnf("bot rule %d: %d/%d actions executed, %d failed (first error: %s)",
|
|
rule.ID, actionsExecuted, len(rule.Actions), actionsFailed, firstErrorMsg)
|
|
} else {
|
|
applogger.L().Infof("bot rule %d: all %d actions executed successfully", rule.ID, actionsExecuted)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadConversationForFilter loads a conversation for condition matching.
|
|
// Standalone helper shared by BotRuleService and BotTriggerConfigService.
|
|
// Reference: mirrors AutomationRuleService.loadConversation
|
|
func loadConversationForFilter(ctx context.Context, db *gorm.DB, conversationID uint) (*ConversationForFilter, error) {
|
|
var conv ConversationForFilter
|
|
if err := db.WithContext(ctx).
|
|
Table("conversations").
|
|
Where("id = ?", conversationID).
|
|
First(&conv).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &conv, nil
|
|
}
|