package service import ( "context" "crypto/rand" "encoding/json" "fmt" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" ) // WebhookSubscriptionService provides business logic for managing webhook subscriptions. // Reference: Chatwoot webhook integration + P2B M8 spec type WebhookSubscriptionService struct { webhookSubRepo *repository.WebhookSubscriptionRepo } // NewWebhookSubscriptionService creates a new WebhookSubscription service with required dependencies. func NewWebhookSubscriptionService(webhookSubRepo *repository.WebhookSubscriptionRepo) *WebhookSubscriptionService { return &WebhookSubscriptionService{ webhookSubRepo: webhookSubRepo, } } // ListSubscriptions retrieves all webhook subscriptions for an account. func (s *WebhookSubscriptionService) ListSubscriptions(ctx context.Context, accountID uint) ([]model.WebhookSubscription, error) { return s.webhookSubRepo.ListByAccount(ctx, accountID) } // CreateSubscription creates a new webhook subscription with a generated signing secret. func (s *WebhookSubscriptionService) CreateSubscription(ctx context.Context, accountID uint, url string, events []string) (*model.WebhookSubscription, error) { eventsJSON, err := json.Marshal(events) if err != nil { return nil, fmt.Errorf("marshal events: %w", err) } // Generate a random signing secret for HMAC-SHA256 webhook payload signing secret, err := generateWebhookSecret() if err != nil { return nil, fmt.Errorf("generate secret: %w", err) } sub := &model.WebhookSubscription{ AccountID: accountID, URL: url, Events: eventsJSON, Secret: secret, Active: true, } if err := s.webhookSubRepo.Create(ctx, sub); err != nil { return nil, err } return sub, nil } // UpdateSubscription updates a webhook subscription's URL, events, or active status. func (s *WebhookSubscriptionService) UpdateSubscription(ctx context.Context, id uint, url string, events []string, active bool) (*model.WebhookSubscription, error) { sub, err := s.webhookSubRepo.FindByID(ctx, id) if err != nil { return nil, fmt.Errorf("find subscription: %w", err) } if url != "" { sub.URL = url } if len(events) > 0 { eventsJSON, err := json.Marshal(events) if err != nil { return nil, fmt.Errorf("marshal events: %w", err) } sub.Events = eventsJSON } sub.Active = active if err := s.webhookSubRepo.Update(ctx, sub); err != nil { return nil, err } return sub, nil } // DeleteSubscription soft-deletes a webhook subscription. func (s *WebhookSubscriptionService) DeleteSubscription(ctx context.Context, id uint) error { return s.webhookSubRepo.Delete(ctx, id) } // GetSubscription retrieves a single webhook subscription by ID. func (s *WebhookSubscriptionService) GetSubscription(ctx context.Context, id uint) (*model.WebhookSubscription, error) { return s.webhookSubRepo.FindByID(ctx, id) } // ListDeliveries retrieves recent delivery records for a subscription. func (s *WebhookSubscriptionService) ListDeliveries(ctx context.Context, subscriptionID uint, limit int) ([]model.WebhookDelivery, error) { return s.webhookSubRepo.ListDeliveriesBySubscription(ctx, subscriptionID, limit) } // generateWebhookSecret creates a random 32-byte hex-encoded secret for HMAC signing. func generateWebhookSecret() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return fmt.Sprintf("%x", b), nil }