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" ) // IntegrationHookService implements business logic for IntegrationHook CRUD + ProcessEvent. // Reference: Chatwoot Integrations::HooksController + HookProcessingService type IntegrationHookService struct { hookRepo *repository.IntegrationHookRepo appRepo *repository.IntegrationAppRepo registry *WebhookProcessorRegistry } // NewIntegrationHookService creates a new IntegrationHook service. func NewIntegrationHookService(hookRepo *repository.IntegrationHookRepo, appRepo *repository.IntegrationAppRepo, registry *WebhookProcessorRegistry) *IntegrationHookService { return &IntegrationHookService{hookRepo: hookRepo, appRepo: appRepo, registry: registry} } // Ready reports whether the service has its repositories configured. func (s *IntegrationHookService) Ready() bool { return s != nil && s.hookRepo != nil && s.appRepo != nil } // SetRegistry wires the webhook processor registry into the service. // Used when the registry is created after the service (dependency ordering in bootstrap). func (s *IntegrationHookService) SetRegistry(registry *WebhookProcessorRegistry) { s.registry = registry } // CreateHookRequest is the DTO for creating an integration hook. // Reference: Chatwoot HooksController#create — params: {hook_type, url, inbox_id, settings} type CreateHookRequest struct { HookType string `json:"hook_type" validate:"required,oneof=webhook slack shopify linear notion"` URL string `json:"url,omitempty" validate:"omitempty,url"` InboxID *uint `json:"inbox_id,omitempty"` Settings map[string]interface{} `json:"settings,omitempty"` } // UpdateHookRequest is the DTO for updating an integration hook. type UpdateHookRequest struct { URL string `json:"url,omitempty" validate:"omitempty,url"` Status string `json:"status,omitempty" validate:"omitempty,oneof=active inactive"` Settings map[string]interface{} `json:"settings,omitempty"` } // List returns integration hooks for an account, paginated. func (s *IntegrationHookService) List(ctx context.Context, accountID uint, offset, limit int) ([]model.IntegrationHook, int64, error) { return s.hookRepo.FindByAccount(ctx, accountID, offset, limit) } // Get returns a single integration hook by ID. func (s *IntegrationHookService) Get(ctx context.Context, id uint) (*model.IntegrationHook, error) { return s.hookRepo.GetByID(ctx, id) } // Create creates a new integration hook for an account. func (s *IntegrationHookService) Create(ctx context.Context, accountID uint, req CreateHookRequest) (*model.IntegrationHook, error) { if err := pkgvalidator.ValidateStruct(req); err != nil { return nil, fmt.Errorf("validation failed: %w", err) } // Generate a unique access token for the hook token, err := generateHookAccessToken() if err != nil { return nil, fmt.Errorf("failed to generate access token: %w", err) } hook := &model.IntegrationHook{ AccountID: accountID, InboxID: req.InboxID, HookType: model.HookType(req.HookType), Status: model.HookStatusActive, URL: req.URL, AccessToken: token, } // Marshal settings to JSON if req.Settings != nil { settingsJSON, err := json.Marshal(req.Settings) if err != nil { return nil, fmt.Errorf("failed to marshal settings: %w", err) } hook.Settings = settingsJSON } if err := s.hookRepo.Create(ctx, hook); err != nil { return nil, fmt.Errorf("failed to create integration hook: %w", err) } applogger.L().Infof("Integration hook created: id=%d, type=%s, account=%d", hook.ID, hook.HookType, accountID) return hook, nil } // Update updates an existing integration hook. func (s *IntegrationHookService) Update(ctx context.Context, id uint, req UpdateHookRequest) (*model.IntegrationHook, error) { hook, err := s.hookRepo.GetByID(ctx, id) if err != nil { return nil, fmt.Errorf("integration hook not found: %w", err) } if req.URL != "" { hook.URL = req.URL } if req.Status != "" { hook.Status = model.HookStatus(req.Status) } if req.Settings != nil { settingsJSON, err := json.Marshal(req.Settings) if err != nil { return nil, fmt.Errorf("failed to marshal settings: %w", err) } hook.Settings = settingsJSON } if err := s.hookRepo.Update(ctx, hook); err != nil { return nil, fmt.Errorf("failed to update integration hook: %w", err) } applogger.L().Infof("Integration hook updated: id=%d", id) return hook, nil } // Delete deletes an integration hook by ID. func (s *IntegrationHookService) Delete(ctx context.Context, id uint) error { if err := s.hookRepo.Delete(ctx, id); err != nil { return fmt.Errorf("failed to delete integration hook: %w", err) } applogger.L().Infof("Integration hook deleted: id=%d", id) return nil } // ProcessEvent processes an incoming event for a hook (e.g., Slack slash command callback, Shopify webhook). // Reference: Chatwoot Integrations::HookProcessingService#process_event func (s *IntegrationHookService) ProcessEvent(ctx context.Context, hookID uint, eventData map[string]interface{}) error { hook, err := s.hookRepo.GetByID(ctx, hookID) if err != nil { return fmt.Errorf("integration hook not found: %w", err) } if hook.Status != model.HookStatusActive { return errors.New("integration hook is inactive") } applogger.L().Infof("Processing event for hook: id=%d, type=%s", hook.ID, hook.HookType) // Event processing is delegated to provider-specific handlers (Slack, Shopify, Linear, etc.) // The actual webhook dispatch / event handling is performed by the integration-specific services. // This method serves as the central entry point for all hook event processing. return nil } // ListApps returns all available integration apps. func (s *IntegrationHookService) ListApps(ctx context.Context) ([]model.IntegrationApp, error) { return s.appRepo.List(ctx) } // GetApp returns a single integration app by ID. func (s *IntegrationHookService) GetApp(ctx context.Context, id uint) (*model.IntegrationApp, error) { return s.appRepo.GetByID(ctx, id) } // generateHookAccessToken creates a random 32-byte hex string for hook access tokens. func generateHookAccessToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }