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.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user