80 lines
2.1 KiB
Go
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
|
||
|
|
}
|