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>
This commit is contained in:
parent
b0fd3976f1
commit
fff8de783a
169 changed files with 55769 additions and 282 deletions
138
deploy/migrations/0154_verification_applications.up.sql
Normal file
138
deploy/migrations/0154_verification_applications.up.sql
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
-- Official platform verification applications.
|
||||
--
|
||||
-- The application is the durable audit subject: it is never deleted, only moved
|
||||
-- through its status machine, and every transition appends an immutable row to
|
||||
-- verification_application_events. Decisions additionally go through the shared
|
||||
-- admin command journal (admin_commands / admin_audit_logs), so the panel keeps
|
||||
-- one audit story for all operator actions.
|
||||
--
|
||||
-- The target is addressed by its stable peer id. target_title / target_username
|
||||
-- are a submission-time snapshot for the review queue and the audit trail,
|
||||
-- because a username can move between peers and a title can change after filing.
|
||||
|
||||
CREATE TABLE public.verification_applications (
|
||||
id bigserial PRIMARY KEY,
|
||||
applicant_user_id bigint NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
target_type text NOT NULL CHECK (target_type IN ('bot', 'channel', 'supergroup', 'user')),
|
||||
target_id bigint NOT NULL CHECK (target_id > 0),
|
||||
target_title text NOT NULL DEFAULT '' CHECK (octet_length(target_title) <= 1024),
|
||||
target_username text NOT NULL DEFAULT '' CHECK (octet_length(target_username) <= 64),
|
||||
target_access_hash bigint NOT NULL DEFAULT 0,
|
||||
category text NOT NULL DEFAULT '' CHECK (octet_length(category) <= 64),
|
||||
description text NOT NULL DEFAULT '' CHECK (octet_length(description) <= 4096),
|
||||
official_website text NOT NULL DEFAULT '' CHECK (octet_length(official_website) <= 512),
|
||||
-- Links are stored as arrays rather than a child table: they are read and
|
||||
-- written as one whole, are bounded, and never need to be queried across
|
||||
-- applications.
|
||||
social_links text[] NOT NULL DEFAULT '{}' CHECK (cardinality(social_links) <= 10),
|
||||
press_links text[] NOT NULL DEFAULT '{}' CHECK (cardinality(press_links) <= 10),
|
||||
additional_note text NOT NULL DEFAULT '' CHECK (octet_length(additional_note) <= 4096),
|
||||
status text NOT NULL CHECK (status IN (
|
||||
'draft', 'submitted', 'in_review', 'approved', 'rejected', 'cancelled'
|
||||
)),
|
||||
reviewer_admin_id text NOT NULL DEFAULT '' CHECK (octet_length(reviewer_admin_id) <= 128),
|
||||
decision_reason text NOT NULL DEFAULT '' CHECK (octet_length(decision_reason) <= 4096),
|
||||
-- internal_note is operator-only and must never be projected to the applicant.
|
||||
internal_note text NOT NULL DEFAULT '' CHECK (octet_length(internal_note) <= 8192),
|
||||
correlation_id text NOT NULL DEFAULT '' CHECK (octet_length(correlation_id) <= 128),
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
submitted_at timestamptz,
|
||||
reviewed_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
CHECK (updated_at >= created_at),
|
||||
-- A decided application always carries its reviewer and timestamp; a rejected
|
||||
-- one additionally carries the reason the applicant is told.
|
||||
CHECK (
|
||||
(status IN ('approved', 'rejected')) =
|
||||
(reviewed_at IS NOT NULL AND reviewer_admin_id <> '')
|
||||
),
|
||||
CHECK (status <> 'rejected' OR decision_reason <> ''),
|
||||
CHECK (status = 'draft' OR submitted_at IS NOT NULL)
|
||||
);
|
||||
|
||||
-- Exactly one live application per target. Draft, submitted and in_review are
|
||||
-- the occupying states; decided and cancelled ones are history and do not block
|
||||
-- a fresh attempt.
|
||||
CREATE UNIQUE INDEX verification_applications_active_target_idx
|
||||
ON public.verification_applications (target_type, target_id)
|
||||
WHERE status IN ('draft', 'submitted', 'in_review');
|
||||
|
||||
-- One draft per applicant: the bot dialog is a single conversation, so a second
|
||||
-- draft would have no way to be addressed.
|
||||
CREATE UNIQUE INDEX verification_applications_applicant_draft_idx
|
||||
ON public.verification_applications (applicant_user_id)
|
||||
WHERE status = 'draft';
|
||||
|
||||
CREATE INDEX verification_applications_queue_idx
|
||||
ON public.verification_applications (status, created_at DESC, id DESC);
|
||||
|
||||
CREATE INDEX verification_applications_applicant_idx
|
||||
ON public.verification_applications (applicant_user_id, id DESC);
|
||||
|
||||
CREATE INDEX verification_applications_target_idx
|
||||
ON public.verification_applications (target_type, target_id, id DESC);
|
||||
|
||||
CREATE INDEX verification_applications_reviewer_idx
|
||||
ON public.verification_applications (reviewer_admin_id, reviewed_at DESC, id DESC)
|
||||
WHERE reviewer_admin_id <> '';
|
||||
|
||||
-- Username search in the review queue is a prefix match on the snapshot.
|
||||
CREATE INDEX verification_applications_username_idx
|
||||
ON public.verification_applications (lower(target_username))
|
||||
WHERE target_username <> '';
|
||||
|
||||
-- Cooldown lookups after a rejection: newest decision per applicant+target.
|
||||
CREATE INDEX verification_applications_cooldown_idx
|
||||
ON public.verification_applications (applicant_user_id, target_type, target_id, reviewed_at DESC)
|
||||
WHERE status = 'rejected';
|
||||
|
||||
-- Immutable per-application history. Rows are append-only: there is no UPDATE or
|
||||
-- DELETE path in the store, and the panel renders this as the application
|
||||
-- timeline.
|
||||
CREATE TABLE public.verification_application_events (
|
||||
id bigserial PRIMARY KEY,
|
||||
application_id bigint NOT NULL REFERENCES public.verification_applications(id)
|
||||
ON DELETE RESTRICT,
|
||||
kind text NOT NULL CHECK (kind IN (
|
||||
'created', 'updated', 'submitted', 'claimed', 'approved', 'rejected',
|
||||
'cancelled', 'revoked', 'notified'
|
||||
)),
|
||||
from_status text NOT NULL DEFAULT '' CHECK (octet_length(from_status) <= 32),
|
||||
to_status text NOT NULL DEFAULT '' CHECK (octet_length(to_status) <= 32),
|
||||
actor text NOT NULL DEFAULT '' CHECK (octet_length(actor) <= 128),
|
||||
reason text NOT NULL DEFAULT '' CHECK (octet_length(reason) <= 4096),
|
||||
note text NOT NULL DEFAULT '' CHECK (octet_length(note) <= 8192),
|
||||
correlation_id text NOT NULL DEFAULT '' CHECK (octet_length(correlation_id) <= 128),
|
||||
created_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX verification_application_events_app_idx
|
||||
ON public.verification_application_events (application_id, id DESC);
|
||||
|
||||
CREATE INDEX verification_application_events_actor_idx
|
||||
ON public.verification_application_events (actor, created_at DESC, id DESC)
|
||||
WHERE actor <> '';
|
||||
|
||||
-- Applicant notifications are delivered by @verifybot after the decision commits.
|
||||
-- The outbox keeps that delivery exactly-once across restarts and makes a
|
||||
-- repeated approve idempotent: the unique key is the decision, not the attempt.
|
||||
CREATE TABLE public.verification_notification_outbox (
|
||||
id bigserial PRIMARY KEY,
|
||||
application_id bigint NOT NULL REFERENCES public.verification_applications(id)
|
||||
ON DELETE CASCADE,
|
||||
recipient_user_id bigint NOT NULL CHECK (recipient_user_id > 0),
|
||||
kind text NOT NULL CHECK (kind IN ('approved', 'rejected', 'revoked')),
|
||||
payload jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (
|
||||
jsonb_typeof(payload) = 'object' AND octet_length(payload::text) <= 8192
|
||||
),
|
||||
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
||||
delivered_at timestamptz,
|
||||
last_error text NOT NULL DEFAULT '' CHECK (octet_length(last_error) <= 1024),
|
||||
created_at timestamptz NOT NULL,
|
||||
CONSTRAINT verification_notification_once UNIQUE (application_id, kind)
|
||||
);
|
||||
|
||||
CREATE INDEX verification_notification_pending_idx
|
||||
ON public.verification_notification_outbox (created_at, id)
|
||||
WHERE delivered_at IS NULL;
|
||||
Loading…
Add table
Add a link
Reference in a new issue