130 lines
5.8 KiB
Go
130 lines
5.8 KiB
Go
package domain
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// BroadcastTargetMode selects who a broadcast's recipients are.
|
|
type BroadcastTargetMode string
|
|
|
|
const (
|
|
// BroadcastTargetAll snapshots every non-bot, non-system account as of
|
|
// creation time (mirrors the exclusion cmd/telesrv-admin's CountAccounts
|
|
// already applies: real users only, not @BotFather/@Stickers/@ChatBot/777000
|
|
// itself) by recording the current max user id and enumerating up to it
|
|
// incrementally, rather than resolving the whole list inline.
|
|
BroadcastTargetAll BroadcastTargetMode = "all"
|
|
// BroadcastTargetSelected sends only to the operator-picked user list
|
|
// carried on the create request.
|
|
BroadcastTargetSelected BroadcastTargetMode = "selected"
|
|
)
|
|
|
|
// BroadcastRecipientStatus is one recipient row's delivery state.
|
|
type BroadcastRecipientStatus string
|
|
|
|
const (
|
|
BroadcastRecipientPending BroadcastRecipientStatus = "pending"
|
|
// BroadcastRecipientProcessing means a delivery worker currently holds a
|
|
// time-bounded lease on this row (see LeaseToken/LeaseUntil). If the
|
|
// worker dies before finishing, the lease simply expires and another
|
|
// worker cycle reclaims the row -- no separate crash-recovery pass needed.
|
|
BroadcastRecipientProcessing BroadcastRecipientStatus = "processing"
|
|
BroadcastRecipientSent BroadcastRecipientStatus = "sent"
|
|
// BroadcastRecipientFailed is terminal: MaxBroadcastRecipientAttempts was
|
|
// reached, so the worker stops retrying this row. A blocked or deleted
|
|
// recipient must not spin forever alongside everyone else's real deliveries.
|
|
BroadcastRecipientFailed BroadcastRecipientStatus = "failed"
|
|
)
|
|
|
|
// MaxBroadcastRecipientAttempts bounds retries per recipient before the
|
|
// worker gives up and marks the row permanently failed.
|
|
const MaxBroadcastRecipientAttempts = 5
|
|
|
|
// MaxBroadcastMessageBytes caps a broadcast's message body, matching the
|
|
// broadcasts.message CHECK added in
|
|
// deploy/migrations/20260901000024_broadcast_lease_delivery_and_entities.up.sql.
|
|
const MaxBroadcastMessageBytes = 4096
|
|
|
|
// MaxBroadcastSelectedRecipients caps how many user ids one "selected"-mode
|
|
// broadcast may carry in its create request, so a hand-built recipient list
|
|
// can't smuggle in an "all users" sized payload through the wrong target mode.
|
|
const MaxBroadcastSelectedRecipients = 200
|
|
|
|
// Broadcast is one admin-triggered system message campaign, sent from
|
|
// OfficialSystemUserID (777000) to every recipient targeted by TargetMode.
|
|
//
|
|
// For BroadcastTargetAll, recipient rows are not all inserted at creation:
|
|
// SnapshotMaxUserID/EnumerationCursorUserID/EnumerationDone track an
|
|
// incremental keyset walk over the users table (see
|
|
// store.BroadcastStore.MaterializeBroadcastRecipients), so creating a
|
|
// campaign for a large user base is a single cheap insert, not one giant
|
|
// blocking transaction. MaterializedCount is how many recipient rows exist
|
|
// so far; TargetCount is the (possibly still-growing, for "all") total this
|
|
// campaign is aimed at. SentCount/FailedCount are maintained incrementally
|
|
// by the delivery worker as it closes out each recipient row.
|
|
type Broadcast struct {
|
|
ID int64
|
|
Message string
|
|
Entities []MessageEntity
|
|
TargetMode BroadcastTargetMode
|
|
TargetCount int64
|
|
MaterializedCount int64
|
|
SentCount int64
|
|
FailedCount int64
|
|
EnumerationDone bool
|
|
CreatedBy string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// BroadcastRecipient is one durable outbox row: one user's delivery state
|
|
// for one broadcast.
|
|
//
|
|
// A worker claims a batch of eligible rows by writing LeaseToken/LeaseUntil
|
|
// (see store.BroadcastStore.ClaimBroadcastRecipients), delivers the message,
|
|
// then either closes the row as 'sent' (recording PrivateMessageID/
|
|
// MessageBoxID/Pts, the same identifiers domain.Message carries, so a
|
|
// campaign's delivery history is independently auditable without joining
|
|
// back through the shared message store) or releases it back to 'pending'
|
|
// (or terminally 'failed', once MaxBroadcastRecipientAttempts is reached) on
|
|
// error. A lease that is never renewed simply expires, so a worker that
|
|
// crashes mid-delivery cannot leave a row stuck in 'processing' forever.
|
|
type BroadcastRecipient struct {
|
|
ID int64
|
|
BroadcastID int64
|
|
UserID int64
|
|
Status BroadcastRecipientStatus
|
|
Attempts int
|
|
// NextAttemptAt gates retries with exponential backoff after a failed
|
|
// delivery; a 'pending' row isn't eligible for claiming again until then.
|
|
NextAttemptAt time.Time
|
|
LeaseToken string
|
|
LeaseUntil *time.Time
|
|
LastError string
|
|
// PrivateMessageID/MessageBoxID/Pts identify the delivered message once
|
|
// Status is 'sent'. A pre-migration row that was marked 'sent' before
|
|
// this tracking existed carries all three as zero -- see the CHECK
|
|
// constraint added in
|
|
// deploy/migrations/20260901000024_broadcast_lease_delivery_and_entities.up.sql,
|
|
// which treats that as a legitimate legacy/untracked case.
|
|
PrivateMessageID int64
|
|
MessageBoxID int
|
|
Pts int
|
|
SentAt *time.Time
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
var (
|
|
ErrBroadcastInvalid = errors.New("broadcast invalid")
|
|
ErrBroadcastMessageEmpty = errors.New("broadcast message is empty")
|
|
ErrBroadcastMessageTooLong = errors.New("broadcast message exceeds the maximum length")
|
|
ErrBroadcastNoRecipients = errors.New("broadcast has no recipients")
|
|
ErrBroadcastRecipientInvalid = errors.New("broadcast recipient invalid")
|
|
ErrBroadcastNotFound = errors.New("broadcast not found")
|
|
// ErrBroadcastLeaseLost means the delivery worker's lease on a recipient
|
|
// row was reclaimed (expired and re-claimed by another cycle, or the row
|
|
// otherwise changed underneath it) before delivery finished. The caller
|
|
// should simply drop the result: the row is someone else's to finish now.
|
|
ErrBroadcastLeaseLost = errors.New("broadcast recipient lease lost")
|
|
)
|