Files
gochat/internal/service/webhook_subscription_service.go
T
2026-06-04 15:44:48 +08:00

104 lines
3.4 KiB
Go

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
}