owpengram-server/internal/app/verification/worker.go
Egor Egorov fff8de783a
feat: add NFT usernames and bot verification (#22)
Implements collectible usernames, official verification workflows, and third-party bot verification after maintainer protocol and migration review.

The composite activity/moderation rating remains an admin-only read model; Telegram Stars Rating wire fields stay unset pending a dedicated official-semantics implementation.

Reviewed-Head: 2796345775ea0f908fb7734601e5e1dee4b653b9
Original-Head: fa082b892fd5180c9c9bc53c81c21cf5d250a75b

Co-authored-by: Egor Egorov <business.egor.sg@gmail.com>
2026-07-28 01:18:00 +08:00

88 lines
2.3 KiB
Go

package verification
import (
"context"
"time"
"go.uber.org/zap"
)
// defaultNotifyInterval matches the shipped
// TELESRV_VERIFICATION_NOTIFY_INTERVAL default.
const defaultNotifyInterval = 15 * time.Second
// NotificationWorker drains the applicant-notification outbox.
//
// A decision is committed together with its outbox row, never with a message
// send: @verifybot may be blocked, the applicant may be deleted, and the panel
// must not wait on either. Delivery is therefore a separate, retrying cycle over
// durable rows, and this worker is only its cadence.
type NotificationWorker struct {
service *Service
logger *zap.Logger
interval time.Duration
batch int
}
// NewNotificationWorker creates the periodic delivery worker. Non-positive
// interval/batch fall back to the shipped defaults, matching the rating
// recompute worker's contract.
func NewNotificationWorker(service *Service, logger *zap.Logger, interval time.Duration, batch int) *NotificationWorker {
if logger == nil {
logger = zap.NewNop()
}
if interval <= 0 {
interval = defaultNotifyInterval
}
if batch <= 0 {
batch = defaultNotifyBatch
}
return &NotificationWorker{service: service, logger: logger, interval: interval, batch: batch}
}
// Run delivers one batch immediately and then on every tick until ctx is done. A
// disabled or store-less service exits immediately with one explicit log line
// instead of ticking forever over a no-op.
func (w *NotificationWorker) Run(ctx context.Context) {
if w == nil {
return
}
if !w.service.Ready() {
w.logger.Info("verification notification worker disabled",
zap.Bool("enabled", w.service.Enabled()))
return
}
w.runOnce(ctx)
ticker := time.NewTicker(w.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
w.runOnce(ctx)
}
}
}
func (w *NotificationWorker) runOnce(ctx context.Context) {
if w == nil || w.service == nil {
return
}
delivered, err := w.service.RunNotificationCycle(ctx, w.batch)
if err != nil {
if ctx.Err() != nil {
return
}
w.logger.Warn("verification notification cycle failed",
zap.Int("delivered", delivered),
zap.Int("batch", w.batch),
zap.Error(err))
return
}
if delivered > 0 {
w.logger.Info("verification notification cycle completed",
zap.Int("delivered", delivered),
zap.Int("batch", w.batch))
}
}