Files
descrybe/apps/api/internal/campaigns/service.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

80 lines
2.1 KiB
Go

package campaigns
import (
"net/http"
"sync"
"time"
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprompts"
"github.com/descrybe/descrybe-v2/apps/api/internal/aiprovider"
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
"github.com/descrybe/descrybe-v2/apps/api/internal/email"
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
"github.com/jackc/pgx/v5/pgxpool"
)
// Service is the email campaigns API surface (CRUD + generate + schedule/send).
// Tenant sending goes through email.Service (verified provider, rate limits, unsub).
type Service struct {
Pool *pgxpool.Pool
Billing *billing.Service
Email *email.Service
Completer processing.Completer
AI *aiprovider.Service
Prompts *aiprompts.Service
WebOrigin string
PublicAPIURL string
// TokenSigningSecret signs public brand-logo URLs for email embeds.
TokenSigningSecret string
HTTP *http.Client
genMu sync.Mutex
genHit map[string][]time.Time
sendMu sync.Mutex
sendHit map[string][]time.Time
}
func NewService(pool *pgxpool.Pool, billingSvc *billing.Service, emailSvc *email.Service) *Service {
return &Service{
Pool: pool,
Billing: billingSvc,
Email: emailSvc,
HTTP: &http.Client{Timeout: 30 * time.Second},
genHit: make(map[string][]time.Time),
sendHit: make(map[string][]time.Time),
}
}
const (
generateRPM = 10
sendRPM = 20
)
func (s *Service) allowGenerate(companyID string) bool {
return allowWindow(&s.genMu, s.genHit, companyID, generateRPM, time.Minute)
}
func (s *Service) allowSend(companyID string) bool {
return allowWindow(&s.sendMu, s.sendHit, companyID, sendRPM, time.Minute)
}
func allowWindow(mu *sync.Mutex, hits map[string][]time.Time, key string, limit int, window time.Duration) bool {
now := time.Now()
cutoff := now.Add(-window)
mu.Lock()
defer mu.Unlock()
ts := hits[key]
kept := ts[:0]
for _, t := range ts {
if t.After(cutoff) {
kept = append(kept, t)
}
}
if len(kept) >= limit {
hits[key] = kept
return false
}
hits[key] = append(kept, now)
return true
}