owpengram-server/internal/store/verification.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

89 lines
5.8 KiB
Go

package store
import (
"context"
"telesrv/internal/domain"
)
// VerificationStore owns official verification applications, their immutable
// history and the applicant-notification outbox.
//
// Every mutation is expected to be atomic with the history row it produces, and
// every status change is guarded by the caller-supplied version: two reviewers
// deciding the same application concurrently must produce exactly one decision
// and one notification.
type VerificationStore interface {
// CreateVerificationDraft opens a draft for the applicant/target pair. An
// active application on the target reports domain.ErrVerificationApplicationExists;
// an existing draft of the same applicant is returned with created=false so
// the bot dialog can resume it.
CreateVerificationDraft(ctx context.Context, req domain.SubmitVerificationApplicationRequest) (app domain.VerificationApplication, created bool, err error)
// SaveVerificationDraft rewrites the applicant-supplied payload of a draft.
// The application must be in draft and at the given version.
SaveVerificationDraft(ctx context.Context, applicationID int64, version int64, draft domain.VerificationDraftInput) (domain.VerificationApplication, error)
// SubmitVerificationApplication moves a draft to submitted and stamps
// submitted_at.
SubmitVerificationApplication(ctx context.Context, applicationID int64, version int64) (domain.VerificationApplication, error)
// CancelVerificationApplication withdraws an active application on the
// applicant's behalf.
CancelVerificationApplication(ctx context.Context, applicationID int64, version int64, reason string) (domain.VerificationApplication, error)
// ClaimVerificationApplication assigns a reviewer and moves the application to
// in_review.
ClaimVerificationApplication(ctx context.Context, decision domain.VerificationDecision) (domain.VerificationApplication, error)
// DecideVerificationApplication records an approval or a rejection.
//
// approve=true is the only path that flips the platform flag, and the store
// does it in the same transaction as the status change through the supplied
// applyVerified callback, so the invariant "approved implies target verified"
// cannot be broken by a crash between two writes. The callback receives the
// application as it will be stored and must set the flag on the target peer.
//
// The notification outbox row is written in the same transaction and is unique
// per (application, kind), which makes a repeated approve a no-op that returns
// changed=false instead of notifying twice.
DecideVerificationApplication(ctx context.Context, decision domain.VerificationDecision, approve bool, applyVerified func(ctx context.Context, app domain.VerificationApplication) error) (app domain.VerificationApplication, changed bool, err error)
// RevokeVerification clears the platform flag of a previously approved target
// through the same callback discipline, appends a revoked event to the newest
// approved application for that target, and enqueues the applicant
// notification. The application itself stays approved: it is history.
RevokeVerification(ctx context.Context, req domain.VerificationRevocation, clearVerified func(ctx context.Context, target domain.Peer) error) (app domain.VerificationApplication, changed bool, err error)
// VerificationApplication reads one application by id.
VerificationApplication(ctx context.Context, applicationID int64) (domain.VerificationApplication, error)
// ActiveVerificationApplicationForTarget returns the live application
// occupying a target, if any.
ActiveVerificationApplicationForTarget(ctx context.Context, target domain.VerificationTargetType, targetID int64) (domain.VerificationApplication, error)
// VerificationDraftForApplicant returns the applicant's open draft, if any.
VerificationDraftForApplicant(ctx context.Context, applicantUserID int64) (domain.VerificationApplication, error)
// ListVerificationApplications is the review-queue query with keyset paging.
ListVerificationApplications(ctx context.Context, filter domain.VerificationApplicationFilter) ([]domain.VerificationApplication, error)
// VerificationApplicationsForApplicant returns the applicant's own history,
// newest first, for the bot's /status command.
VerificationApplicationsForApplicant(ctx context.Context, applicantUserID int64, limit int) ([]domain.VerificationApplication, error)
// VerificationStatusCounts is the queue summary.
VerificationStatusCounts(ctx context.Context) (domain.VerificationStatusCounts, error)
// VerificationApplicationEvents returns the immutable history, newest first.
VerificationApplicationEvents(ctx context.Context, applicationID int64, limit int) ([]domain.VerificationApplicationEvent, error)
// LastVerificationRejection returns the newest rejected application for the
// applicant/target pair, which is what the re-application cooldown is measured
// from.
LastVerificationRejection(ctx context.Context, applicantUserID int64, target domain.VerificationTargetType, targetID int64) (domain.VerificationApplication, error)
// PendingVerificationNotifications returns undelivered outbox rows, oldest
// first, for the delivery worker.
PendingVerificationNotifications(ctx context.Context, limit int) ([]VerificationNotification, error)
// MarkVerificationNotificationDelivered closes an outbox row.
MarkVerificationNotificationDelivered(ctx context.Context, id int64) error
// MarkVerificationNotificationFailed records a delivery attempt so a poisoned
// row cannot spin forever without a trace.
MarkVerificationNotificationFailed(ctx context.Context, id int64, reason string) error
}
// VerificationNotification is one queued applicant notification.
type VerificationNotification struct {
ID int64
ApplicationID int64
RecipientUserID int64
Kind string
Attempts int
Application domain.VerificationApplication
}