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
45
deploy/migrations/0151_collectible_usernames.down.sql
Normal file
45
deploy/migrations/0151_collectible_usernames.down.sql
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
-- Restore the single-username-per-peer invariant. Collectible registry rows are
|
||||
-- dropped first so the old unique index can be recreated; the assets and their
|
||||
-- provenance log are then removed with the tables.
|
||||
|
||||
DELETE FROM public.peer_usernames WHERE collectible_id IS NOT NULL OR NOT editable;
|
||||
|
||||
DROP INDEX IF EXISTS public.peer_usernames_collectible_idx;
|
||||
DROP INDEX IF EXISTS public.peer_usernames_peer_order_idx;
|
||||
DROP INDEX IF EXISTS public.peer_usernames_peer_editable_idx;
|
||||
|
||||
ALTER TABLE public.peer_usernames
|
||||
DROP CONSTRAINT IF EXISTS peer_usernames_sort_order_check,
|
||||
DROP CONSTRAINT IF EXISTS peer_usernames_collectible_not_editable_check,
|
||||
DROP CONSTRAINT IF EXISTS peer_usernames_username_case_check;
|
||||
|
||||
ALTER TABLE public.peer_usernames
|
||||
DROP COLUMN IF EXISTS collectible_id,
|
||||
DROP COLUMN IF EXISTS sort_order,
|
||||
DROP COLUMN IF EXISTS editable,
|
||||
DROP COLUMN IF EXISTS active,
|
||||
DROP COLUMN IF EXISTS username;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS peer_usernames_peer_unique_idx
|
||||
ON public.peer_usernames (peer_type, peer_id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.delete_user_peer_username() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
DELETE FROM public.peer_usernames WHERE peer_type = 'user' AND peer_id = OLD.id;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.delete_channel_peer_username() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
DELETE FROM public.peer_usernames WHERE peer_type = 'channel' AND peer_id = OLD.id;
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TABLE IF EXISTS public.collectible_username_transfers;
|
||||
DROP TABLE IF EXISTS public.collectible_usernames;
|
||||
194
deploy/migrations/0151_collectible_usernames.up.sql
Normal file
194
deploy/migrations/0151_collectible_usernames.up.sql
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
-- Collectible (Fragment-style) usernames.
|
||||
--
|
||||
-- A peer keeps exactly one editable username -- the slot that
|
||||
-- account.updateUsername / channels.updateUsername owns -- plus any number of
|
||||
-- collectible usernames it holds. peer_usernames stays the single authoritative
|
||||
-- registry for global uniqueness across users and channels, so ResolveUsername,
|
||||
-- public landing pages and occupancy checks keep working for collectible names
|
||||
-- without a second lookup path.
|
||||
--
|
||||
-- The pre-existing one-row-per-peer unique index is replaced by a partial index
|
||||
-- covering only editable rows: the old invariant is preserved exactly for the
|
||||
-- editable slot while collectible rows are free to accumulate.
|
||||
--
|
||||
-- Ownership lives in collectible_usernames (the asset) and is projected into
|
||||
-- peer_usernames (the registry). A collectible row in the registry always
|
||||
-- carries collectible_id and is never editable, so client-driven username edits
|
||||
-- cannot mutate or release an owned asset.
|
||||
|
||||
CREATE TABLE public.collectible_usernames (
|
||||
id bigserial PRIMARY KEY,
|
||||
username text NOT NULL,
|
||||
username_lower text NOT NULL CHECK (
|
||||
username_lower <> '' AND lower(username) = username_lower
|
||||
),
|
||||
status text NOT NULL CHECK (status IN ('vault', 'owned', 'burned')),
|
||||
-- Empty owner is the vault/burned state; 'owned' always has a real peer.
|
||||
owner_peer_type text NOT NULL CHECK (owner_peer_type IN ('', 'user', 'channel')),
|
||||
owner_peer_id bigint NOT NULL CHECK (owner_peer_id >= 0),
|
||||
CHECK (
|
||||
(status = 'owned' AND owner_peer_type <> '' AND owner_peer_id > 0)
|
||||
OR (status <> 'owned' AND owner_peer_type = '' AND owner_peer_id = 0)
|
||||
),
|
||||
-- fragment.collectibleInfo projection. Amounts are minor units for fiat and
|
||||
-- nanotons for TON, matching the star gift lifecycle ledger convention.
|
||||
purchase_date timestamptz NOT NULL,
|
||||
currency text NOT NULL CHECK (currency IN ('XTR', 'TON', 'USD')),
|
||||
amount bigint NOT NULL CHECK (amount >= 0),
|
||||
crypto_currency text NOT NULL DEFAULT '' CHECK (crypto_currency IN ('', 'TON')),
|
||||
crypto_amount bigint NOT NULL DEFAULT 0 CHECK (crypto_amount >= 0),
|
||||
CHECK (
|
||||
(crypto_currency = '' AND crypto_amount = 0)
|
||||
OR (crypto_currency <> '' AND crypto_amount > 0)
|
||||
),
|
||||
url text NOT NULL DEFAULT '' CHECK (octet_length(url) <= 512),
|
||||
-- Provenance: the first holder, kept even after transfers and burns.
|
||||
original_owner_peer_type text NOT NULL DEFAULT '' CHECK (
|
||||
original_owner_peer_type IN ('', 'user', 'channel')
|
||||
),
|
||||
original_owner_peer_id bigint NOT NULL DEFAULT 0 CHECK (original_owner_peer_id >= 0),
|
||||
transfer_count integer NOT NULL DEFAULT 0 CHECK (transfer_count >= 0),
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
CHECK (updated_at >= created_at)
|
||||
);
|
||||
|
||||
-- A burned asset releases the name for a fresh issue while retaining immutable
|
||||
-- provenance. At most one live asset may own a name; any number of burned
|
||||
-- historical rows may remain.
|
||||
CREATE UNIQUE INDEX collectible_usernames_live_name_idx
|
||||
ON public.collectible_usernames (username_lower)
|
||||
WHERE status <> 'burned';
|
||||
|
||||
CREATE INDEX collectible_usernames_name_history_idx
|
||||
ON public.collectible_usernames (username_lower, id DESC);
|
||||
|
||||
CREATE INDEX collectible_usernames_owner_idx
|
||||
ON public.collectible_usernames (owner_peer_type, owner_peer_id, id DESC)
|
||||
WHERE status = 'owned';
|
||||
|
||||
CREATE INDEX collectible_usernames_status_idx
|
||||
ON public.collectible_usernames (status, id DESC);
|
||||
|
||||
-- Append-only provenance log. command_key makes admin mint/transfer/revoke
|
||||
-- replay-safe the same way star_gift_admin_grant_commands does for gifts.
|
||||
CREATE TABLE public.collectible_username_transfers (
|
||||
id bigserial PRIMARY KEY,
|
||||
collectible_id bigint NOT NULL REFERENCES public.collectible_usernames(id)
|
||||
ON DELETE CASCADE,
|
||||
kind text NOT NULL CHECK (kind IN ('mint', 'transfer', 'revoke', 'burn')),
|
||||
from_peer_type text NOT NULL CHECK (from_peer_type IN ('', 'user', 'channel')),
|
||||
from_peer_id bigint NOT NULL CHECK (from_peer_id >= 0),
|
||||
to_peer_type text NOT NULL CHECK (to_peer_type IN ('', 'user', 'channel')),
|
||||
to_peer_id bigint NOT NULL CHECK (to_peer_id >= 0),
|
||||
currency text NOT NULL DEFAULT '' CHECK (currency IN ('', 'XTR', 'TON', 'USD')),
|
||||
amount bigint NOT NULL DEFAULT 0 CHECK (amount >= 0),
|
||||
actor text NOT NULL DEFAULT '' CHECK (octet_length(actor) <= 128),
|
||||
reason text NOT NULL DEFAULT '' CHECK (octet_length(reason) <= 512),
|
||||
command_key text CHECK (command_key IS NULL OR octet_length(command_key) BETWEEN 1 AND 128),
|
||||
created_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX collectible_username_transfers_command_idx
|
||||
ON public.collectible_username_transfers (command_key)
|
||||
WHERE command_key IS NOT NULL;
|
||||
|
||||
CREATE INDEX collectible_username_transfers_asset_idx
|
||||
ON public.collectible_username_transfers (collectible_id, id DESC);
|
||||
|
||||
ALTER TABLE public.peer_usernames
|
||||
ADD COLUMN username text NOT NULL DEFAULT '',
|
||||
ADD COLUMN active boolean NOT NULL DEFAULT true,
|
||||
ADD COLUMN editable boolean NOT NULL DEFAULT true,
|
||||
ADD COLUMN sort_order integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN collectible_id bigint REFERENCES public.collectible_usernames(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Recover the original-case display form for rows written before the column
|
||||
-- existed; fall back to the lowercase key when the peer row is already gone.
|
||||
UPDATE public.peer_usernames pu
|
||||
SET username = u.username
|
||||
FROM public.users u
|
||||
WHERE pu.peer_type = 'user'
|
||||
AND pu.peer_id = u.id
|
||||
AND pu.username = ''
|
||||
AND lower(u.username) = pu.username_lower;
|
||||
|
||||
UPDATE public.peer_usernames pu
|
||||
SET username = c.username
|
||||
FROM public.channels c
|
||||
WHERE pu.peer_type = 'channel'
|
||||
AND pu.peer_id = c.id
|
||||
AND pu.username = ''
|
||||
AND lower(COALESCE(c.username, '')) = pu.username_lower;
|
||||
|
||||
UPDATE public.peer_usernames
|
||||
SET username = username_lower
|
||||
WHERE username = '';
|
||||
|
||||
ALTER TABLE public.peer_usernames
|
||||
ALTER COLUMN username DROP DEFAULT,
|
||||
ADD CONSTRAINT peer_usernames_username_case_check
|
||||
CHECK (lower(username) = username_lower),
|
||||
ADD CONSTRAINT peer_usernames_collectible_not_editable_check
|
||||
CHECK (collectible_id IS NULL OR NOT editable),
|
||||
ADD CONSTRAINT peer_usernames_sort_order_check
|
||||
CHECK (sort_order >= 0 AND sort_order <= 1024);
|
||||
|
||||
DROP INDEX IF EXISTS public.peer_usernames_peer_unique_idx;
|
||||
|
||||
CREATE UNIQUE INDEX peer_usernames_peer_editable_idx
|
||||
ON public.peer_usernames (peer_type, peer_id)
|
||||
WHERE editable;
|
||||
|
||||
CREATE INDEX peer_usernames_peer_order_idx
|
||||
ON public.peer_usernames (peer_type, peer_id, sort_order, username_lower);
|
||||
|
||||
CREATE UNIQUE INDEX peer_usernames_collectible_idx
|
||||
ON public.peer_usernames (collectible_id)
|
||||
WHERE collectible_id IS NOT NULL;
|
||||
|
||||
-- Peer deletion must not destroy a collectible asset: the registry row goes
|
||||
-- away with the peer, the asset returns to the vault and keeps its provenance.
|
||||
CREATE OR REPLACE FUNCTION public.delete_user_peer_username() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE public.collectible_usernames
|
||||
SET status = 'vault',
|
||||
owner_peer_type = '',
|
||||
owner_peer_id = 0,
|
||||
version = version + 1,
|
||||
updated_at = now()
|
||||
WHERE status = 'owned'
|
||||
AND owner_peer_type = 'user'
|
||||
AND owner_peer_id = OLD.id;
|
||||
|
||||
DELETE FROM public.peer_usernames
|
||||
WHERE peer_type = 'user' AND peer_id = OLD.id;
|
||||
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.delete_channel_peer_username() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
UPDATE public.collectible_usernames
|
||||
SET status = 'vault',
|
||||
owner_peer_type = '',
|
||||
owner_peer_id = 0,
|
||||
version = version + 1,
|
||||
updated_at = now()
|
||||
WHERE status = 'owned'
|
||||
AND owner_peer_type = 'channel'
|
||||
AND owner_peer_id = OLD.id;
|
||||
|
||||
DELETE FROM public.peer_usernames
|
||||
WHERE peer_type = 'channel' AND peer_id = OLD.id;
|
||||
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$;
|
||||
3
deploy/migrations/0152_account_rating.down.sql
Normal file
3
deploy/migrations/0152_account_rating.down.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
DROP INDEX IF EXISTS public.moderation_cases_target_history_idx;
|
||||
DROP TABLE IF EXISTS public.account_rating_events;
|
||||
DROP TABLE IF EXISTS public.account_rating;
|
||||
73
deploy/migrations/0152_account_rating.up.sql
Normal file
73
deploy/migrations/0152_account_rating.up.sql
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
-- Server-local composite account rating for moderation/operations in the admin
|
||||
-- panel. This is intentionally not projected into Telegram's userFull
|
||||
-- stars_rating/stars_my_pending_rating fields: those fields describe official
|
||||
-- Stars transaction-volume semantics, not this activity/moderation score.
|
||||
--
|
||||
-- account_rating is a derived read model: it can always be rebuilt from the
|
||||
-- contributing sources (stars_transactions, message counts, moderation state)
|
||||
-- plus the manual adjustments recorded in account_rating_events. Every stored
|
||||
-- component is kept separately so the admin panel can show why a level was
|
||||
-- reached, and so recomputing one signal never silently discards another.
|
||||
--
|
||||
-- 'stars' is the composite score used by the local admin model, not a wallet
|
||||
-- balance and not an official Telegram Stars Rating value.
|
||||
|
||||
CREATE TABLE public.account_rating (
|
||||
user_id bigint PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
level integer NOT NULL DEFAULT 0 CHECK (level >= 0),
|
||||
stars bigint NOT NULL DEFAULT 0,
|
||||
current_level_stars bigint NOT NULL DEFAULT 0 CHECK (current_level_stars >= 0),
|
||||
-- NULL means the top local admin level has been reached.
|
||||
next_level_stars bigint CHECK (next_level_stars IS NULL OR next_level_stars > 0),
|
||||
CHECK (next_level_stars IS NULL OR next_level_stars > current_level_stars),
|
||||
-- Signed contributions. penalty_component is stored as a non-negative
|
||||
-- magnitude and subtracted, so an audit never has to guess the sign.
|
||||
stars_component bigint NOT NULL DEFAULT 0 CHECK (stars_component >= 0),
|
||||
activity_component bigint NOT NULL DEFAULT 0 CHECK (activity_component >= 0),
|
||||
penalty_component bigint NOT NULL DEFAULT 0 CHECK (penalty_component >= 0),
|
||||
manual_component bigint NOT NULL DEFAULT 0,
|
||||
-- Rating earned but not yet applied to the visible level.
|
||||
pending_stars bigint NOT NULL DEFAULT 0,
|
||||
pending_date timestamptz,
|
||||
CHECK ((pending_stars = 0 AND pending_date IS NULL) OR (pending_stars <> 0 AND pending_date IS NOT NULL)),
|
||||
computed_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX account_rating_leaderboard_idx
|
||||
ON public.account_rating (level DESC, stars DESC, user_id);
|
||||
|
||||
CREATE INDEX account_rating_stale_idx
|
||||
ON public.account_rating (computed_at, user_id);
|
||||
|
||||
-- Append-only contribution log. 'manual' rows are admin adjustments and are the
|
||||
-- only rows that survive a full recompute; command_key gives them the same
|
||||
-- replay safety as other admin commands.
|
||||
CREATE TABLE public.account_rating_events (
|
||||
id bigserial PRIMARY KEY,
|
||||
user_id bigint NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
kind text NOT NULL CHECK (kind IN ('stars', 'activity', 'moderation', 'manual', 'recompute')),
|
||||
amount bigint NOT NULL,
|
||||
reason text NOT NULL DEFAULT '' CHECK (octet_length(reason) <= 512),
|
||||
actor text NOT NULL DEFAULT '' CHECK (octet_length(actor) <= 128),
|
||||
command_key text CHECK (command_key IS NULL OR octet_length(command_key) BETWEEN 1 AND 128),
|
||||
created_at timestamptz NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX account_rating_events_command_idx
|
||||
ON public.account_rating_events (command_key)
|
||||
WHERE command_key IS NOT NULL;
|
||||
|
||||
CREATE INDEX account_rating_events_user_idx
|
||||
ON public.account_rating_events (user_id, id DESC);
|
||||
|
||||
CREATE INDEX account_rating_events_kind_idx
|
||||
ON public.account_rating_events (kind, created_at DESC, id DESC);
|
||||
|
||||
-- Rating recompute counts upheld cases per target. The existing target index is
|
||||
-- partial on the undecided states, so without this one the count degrades to a
|
||||
-- sequential scan on every recompute.
|
||||
CREATE INDEX moderation_cases_target_history_idx
|
||||
ON public.moderation_cases (target_peer_type, target_peer_id)
|
||||
WHERE status IN ('action_pending', 'action_failed', 'resolved');
|
||||
9
deploy/migrations/0153_verify_service_bot.down.sql
Normal file
9
deploy/migrations/0153_verify_service_bot.down.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
-- Remove the built-in @verifybot seed. Its private history is left alone: chat
|
||||
-- rows reference the account, and dropping them would rewrite users' dialogs.
|
||||
DELETE FROM public.read_model_versions
|
||||
WHERE owner_user_id = 1250000011 AND peer_type = 'user' AND peer_id = 1250000011;
|
||||
|
||||
DELETE FROM public.peer_usernames
|
||||
WHERE peer_type = 'user' AND peer_id = 1250000011;
|
||||
|
||||
DELETE FROM public.bots WHERE bot_user_id = 1250000011;
|
||||
89
deploy/migrations/0153_verify_service_bot.up.sql
Normal file
89
deploy/migrations/0153_verify_service_bot.up.sql
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
-- Built-in @verifybot: the front door for official platform verification.
|
||||
--
|
||||
-- Seeded here rather than lazily on first message so the username is occupied
|
||||
-- from the moment the schema is current: peer_usernames.username_lower is the
|
||||
-- only thing standing between a reserved bot handle and an ordinary user
|
||||
-- claiming it, and a lazily created account would leave that window open.
|
||||
--
|
||||
-- access_hash is double-written with domain.VerifyBotAccessHash; the two must
|
||||
-- never drift, exactly as for the other service bots (0044, 0045, 0047).
|
||||
--
|
||||
-- The peer_usernames insert carries the multi-username registry columns added in
|
||||
-- 0149, so the handle occupies the editable slot.
|
||||
|
||||
INSERT INTO public.users (
|
||||
id, access_hash, phone, first_name, last_name, username, country_code,
|
||||
created_at, updated_at, verified, support, about, last_seen_at,
|
||||
default_history_ttl_period, is_bot, bot_info_version, premium_expires_at,
|
||||
emoji_status_document_id, emoji_status_until, color_set, color,
|
||||
color_background_emoji_id, profile_color_set, profile_color,
|
||||
profile_color_background_emoji_id
|
||||
) VALUES (
|
||||
1250000011, 7802113947355620887, '', 'Verify Bot', '', 'verifybot', '',
|
||||
now(), now(), true, false,
|
||||
'Apply for official verification of a public channel, supergroup or bot.',
|
||||
0, 0, true, 1, NULL, 0, 0, false, 0, 0, false, 0, 0
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
access_hash = EXCLUDED.access_hash,
|
||||
phone = EXCLUDED.phone,
|
||||
first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
username = EXCLUDED.username,
|
||||
verified = EXCLUDED.verified,
|
||||
support = EXCLUDED.support,
|
||||
about = EXCLUDED.about,
|
||||
is_bot = EXCLUDED.is_bot,
|
||||
bot_info_version = GREATEST(public.users.bot_info_version, EXCLUDED.bot_info_version),
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO public.bots (
|
||||
bot_user_id, owner_user_id, token_secret, description, commands,
|
||||
bot_chat_history, bot_nochats, inline_placeholder, created_at, updated_at,
|
||||
menu_button_type, menu_button_text, menu_button_url, bot_inline_geo
|
||||
) VALUES (
|
||||
1250000011, 1250000011, '',
|
||||
'Apply for official verification of a public channel, supergroup or bot. The bot collects the application and reports the decision back to you.',
|
||||
'[
|
||||
{"command": "start", "description": "how verification works"},
|
||||
{"command": "new", "description": "file a verification application"},
|
||||
{"command": "status", "description": "check your applications"},
|
||||
{"command": "cancel", "description": "cancel the current application"},
|
||||
{"command": "help", "description": "show help"}
|
||||
]'::jsonb,
|
||||
false, true, '', now(), now(), 0, '', '', false
|
||||
)
|
||||
ON CONFLICT (bot_user_id) DO UPDATE SET
|
||||
owner_user_id = EXCLUDED.owner_user_id,
|
||||
token_secret = EXCLUDED.token_secret,
|
||||
description = EXCLUDED.description,
|
||||
commands = EXCLUDED.commands,
|
||||
bot_chat_history = EXCLUDED.bot_chat_history,
|
||||
bot_nochats = EXCLUDED.bot_nochats,
|
||||
inline_placeholder = EXCLUDED.inline_placeholder,
|
||||
menu_button_type = EXCLUDED.menu_button_type,
|
||||
menu_button_text = EXCLUDED.menu_button_text,
|
||||
menu_button_url = EXCLUDED.menu_button_url,
|
||||
bot_inline_geo = EXCLUDED.bot_inline_geo,
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO public.peer_usernames (
|
||||
username_lower, username, peer_type, peer_id, active, editable, sort_order, updated_at
|
||||
)
|
||||
VALUES ('verifybot', 'verifybot', 'user', 1250000011, true, true, 0, now())
|
||||
ON CONFLICT (username_lower) DO UPDATE SET
|
||||
username = EXCLUDED.username,
|
||||
peer_type = EXCLUDED.peer_type,
|
||||
peer_id = EXCLUDED.peer_id,
|
||||
active = EXCLUDED.active,
|
||||
editable = EXCLUDED.editable,
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO public.read_model_versions (model, owner_user_id, peer_type, peer_id, version, updated_at, hash)
|
||||
VALUES
|
||||
('contact_account', 1250000011, 'user', 1250000011, 1, now(), 2500001100001),
|
||||
('channel_active_memberships', 1250000011, 'user', 1250000011, 1, now(), 2500001100002)
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO UPDATE SET
|
||||
version = GREATEST(public.read_model_versions.version, EXCLUDED.version),
|
||||
updated_at = now(),
|
||||
hash = EXCLUDED.hash;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
DROP TABLE IF EXISTS public.verification_notification_outbox;
|
||||
DROP TABLE IF EXISTS public.verification_application_events;
|
||||
DROP TABLE IF EXISTS public.verification_applications;
|
||||
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;
|
||||
4
deploy/migrations/0155_bot_verification.down.sql
Normal file
4
deploy/migrations/0155_bot_verification.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
DROP TABLE IF EXISTS public.custom_verification_requests;
|
||||
DROP TABLE IF EXISTS public.custom_verifications;
|
||||
DROP TABLE IF EXISTS public.bot_verifier_settings;
|
||||
DROP TABLE IF EXISTS public.verification_icons;
|
||||
140
deploy/migrations/0155_bot_verification.up.sql
Normal file
140
deploy/migrations/0155_bot_verification.up.sql
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
-- Third-party bot verification (core.telegram.org/api/bots/verification).
|
||||
--
|
||||
-- This is a SEPARATE mechanism from the official platform badge implemented in
|
||||
-- 0153/0154. Official verification is a boolean on the peer that only the
|
||||
-- operator can set and that clients render as the standard checkmark. Third-party
|
||||
-- verification is an attributed mark granted by a verifier bot: it carries that
|
||||
-- verifier's own custom-emoji icon and a human-readable description, renders
|
||||
-- BEFORE the name, and never becomes the official checkmark. Both can coexist on
|
||||
-- one peer, and neither reads the other's tables.
|
||||
--
|
||||
-- Layer 228 surfaces:
|
||||
-- user#b1b8cc83 bot_verification_icon:flags2.14?long
|
||||
-- channel#d49f34c6 bot_verification_icon:flags2.13?long
|
||||
-- userFull#6cbe645 bot_verification:flags2.12?BotVerification
|
||||
-- channelFull#a04e8d3a bot_verification:flags2.17?BotVerification
|
||||
-- chatInvite#5c9d3702 bot_verification:flags.13?BotVerification
|
||||
-- botInfo#4d8a0299 verifier_settings:flags.9?BotVerifierSettings
|
||||
|
||||
-- The icon catalogue. An icon is a custom emoji document the client resolves with
|
||||
-- messages.getCustomEmojiDocuments, so document_id must name a real document:
|
||||
-- clients render nothing for an id they cannot fetch.
|
||||
CREATE TABLE public.verification_icons (
|
||||
id bigserial PRIMARY KEY,
|
||||
document_id bigint NOT NULL UNIQUE CHECK (document_id > 0),
|
||||
-- owner_bot_id is 0 for a shared catalogue entry any verifier may use, and a
|
||||
-- bot id when the operator reserved the icon for one verifier.
|
||||
owner_bot_id bigint NOT NULL DEFAULT 0 CHECK (owner_bot_id >= 0),
|
||||
name text NOT NULL CHECK (octet_length(name) BETWEEN 1 AND 512),
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
CHECK (updated_at >= created_at)
|
||||
);
|
||||
|
||||
CREATE INDEX verification_icons_active_idx
|
||||
ON public.verification_icons (active, id DESC);
|
||||
|
||||
CREATE INDEX verification_icons_owner_idx
|
||||
ON public.verification_icons (owner_bot_id, id DESC)
|
||||
WHERE owner_bot_id <> 0;
|
||||
|
||||
-- Verifier status. A row here is what makes a bot a verifier: it is projected as
|
||||
-- botInfo.verifier_settings and is the only authority bots.setCustomVerification
|
||||
-- consults, which is why granting it is an operator action and never a bot one.
|
||||
CREATE TABLE public.bot_verifier_settings (
|
||||
bot_id bigint PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
icon_document_id bigint NOT NULL CHECK (icon_document_id > 0),
|
||||
company_name text NOT NULL CHECK (octet_length(company_name) BETWEEN 1 AND 512),
|
||||
default_description text NOT NULL DEFAULT '' CHECK (octet_length(default_description) <= 280),
|
||||
-- can_modify_custom_description mirrors the TL flag: when false the verifier
|
||||
-- may only apply default_description, so a per-peer description cannot be
|
||||
-- smuggled past the operator.
|
||||
can_modify_custom_description boolean NOT NULL DEFAULT false,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
granted_by text NOT NULL DEFAULT '' CHECK (octet_length(granted_by) <= 128),
|
||||
grant_reason text NOT NULL DEFAULT '' CHECK (octet_length(grant_reason) <= 4096),
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
CHECK (updated_at >= created_at)
|
||||
);
|
||||
|
||||
CREATE INDEX bot_verifier_settings_enabled_idx
|
||||
ON public.bot_verifier_settings (enabled, bot_id);
|
||||
|
||||
-- Granted marks. The wire model carries one BotVerification per peer, so a new
|
||||
-- verifier replaces the previous mark instead of leaving hidden rows that could
|
||||
-- reappear after a later revocation.
|
||||
CREATE TABLE public.custom_verifications (
|
||||
id bigserial PRIMARY KEY,
|
||||
verifier_bot_id bigint NOT NULL REFERENCES public.bot_verifier_settings(bot_id)
|
||||
ON DELETE CASCADE,
|
||||
peer_type text NOT NULL CHECK (peer_type IN ('user', 'channel')),
|
||||
peer_id bigint NOT NULL CHECK (peer_id > 0),
|
||||
-- icon_document_id is denormalised from the verifier at grant time: the mark
|
||||
-- must keep rendering the icon it was granted with even if the verifier later
|
||||
-- changes its own.
|
||||
icon_document_id bigint NOT NULL CHECK (icon_document_id > 0),
|
||||
description text NOT NULL DEFAULT '' CHECK (octet_length(description) <= 4096),
|
||||
granted_by_user_id bigint NOT NULL DEFAULT 0 CHECK (granted_by_user_id >= 0),
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
CONSTRAINT custom_verifications_peer_once UNIQUE (peer_type, peer_id),
|
||||
CHECK (updated_at >= created_at)
|
||||
);
|
||||
|
||||
-- Projection and ownership lookup by peer.
|
||||
CREATE INDEX custom_verifications_peer_idx
|
||||
ON public.custom_verifications (peer_type, peer_id, id DESC);
|
||||
|
||||
CREATE INDEX custom_verifications_verifier_idx
|
||||
ON public.custom_verifications (verifier_bot_id, id DESC);
|
||||
|
||||
-- Applications a peer files with a verifier bot. The mark itself lives in
|
||||
-- custom_verifications; this is the review queue in front of it, so a rejected or
|
||||
-- revoked application stays as history without implying a mark.
|
||||
CREATE TABLE public.custom_verification_requests (
|
||||
id bigserial PRIMARY KEY,
|
||||
verifier_bot_id bigint NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
applicant_user_id bigint NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
peer_type text NOT NULL CHECK (peer_type IN ('user', 'channel')),
|
||||
peer_id bigint NOT NULL CHECK (peer_id > 0),
|
||||
peer_title text NOT NULL DEFAULT '' CHECK (octet_length(peer_title) <= 1024),
|
||||
peer_username text NOT NULL DEFAULT '' CHECK (octet_length(peer_username) <= 64),
|
||||
reason text NOT NULL DEFAULT '' CHECK (octet_length(reason) <= 16384),
|
||||
requested_description text NOT NULL DEFAULT '' CHECK (octet_length(requested_description) <= 280),
|
||||
status text NOT NULL CHECK (status IN ('pending', 'approved', 'rejected', 'revoked')),
|
||||
decided_by text NOT NULL DEFAULT '' CHECK (octet_length(decided_by) <= 128),
|
||||
decision_reason text NOT NULL DEFAULT '' CHECK (octet_length(decision_reason) <= 4096),
|
||||
internal_note text NOT NULL DEFAULT '' CHECK (octet_length(internal_note) <= 32768),
|
||||
correlation_id text NOT NULL DEFAULT '' CHECK (octet_length(correlation_id) <= 128),
|
||||
created_at timestamptz NOT NULL,
|
||||
updated_at timestamptz NOT NULL,
|
||||
approved_at timestamptz,
|
||||
rejected_at timestamptz,
|
||||
version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
|
||||
CHECK (updated_at >= created_at),
|
||||
CHECK ((status = 'approved') = (approved_at IS NOT NULL)),
|
||||
CHECK ((status = 'rejected') = (rejected_at IS NOT NULL)),
|
||||
CHECK (status <> 'rejected' OR decision_reason <> '')
|
||||
);
|
||||
|
||||
-- One live application per (verifier, peer): a second pending row would let two
|
||||
-- decisions race for one mark.
|
||||
CREATE UNIQUE INDEX custom_verification_requests_pending_idx
|
||||
ON public.custom_verification_requests (verifier_bot_id, peer_type, peer_id)
|
||||
WHERE status = 'pending';
|
||||
|
||||
CREATE INDEX custom_verification_requests_queue_idx
|
||||
ON public.custom_verification_requests (status, created_at DESC, id DESC);
|
||||
|
||||
CREATE INDEX custom_verification_requests_verifier_idx
|
||||
ON public.custom_verification_requests (verifier_bot_id, id DESC);
|
||||
|
||||
CREATE INDEX custom_verification_requests_applicant_idx
|
||||
ON public.custom_verification_requests (applicant_user_id, id DESC);
|
||||
|
||||
CREATE INDEX custom_verification_requests_peer_idx
|
||||
ON public.custom_verification_requests (peer_type, peer_id, id DESC);
|
||||
12
deploy/migrations/0156_verifier_service_bot.down.sql
Normal file
12
deploy/migrations/0156_verifier_service_bot.down.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
-- Remove the built-in @verifierbot seed. Its private history is left alone: chat
|
||||
-- rows reference the account, and dropping them would rewrite users' dialogs.
|
||||
--
|
||||
-- Any verifier status an operator granted this bot lives in bot_verifier_settings
|
||||
-- (0155) and cascades when this account seed is removed.
|
||||
DELETE FROM public.read_model_versions
|
||||
WHERE owner_user_id = 1250000013 AND peer_type = 'user' AND peer_id = 1250000013;
|
||||
|
||||
DELETE FROM public.peer_usernames
|
||||
WHERE peer_type = 'user' AND peer_id = 1250000013;
|
||||
|
||||
DELETE FROM public.bots WHERE bot_user_id = 1250000013;
|
||||
103
deploy/migrations/0156_verifier_service_bot.up.sql
Normal file
103
deploy/migrations/0156_verifier_service_bot.up.sql
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
-- Built-in @verifierbot: the applicant front door for THIRD-PARTY verification
|
||||
-- (core.telegram.org/api/bots/verification).
|
||||
--
|
||||
-- This is the first verifier bot of a deployment, shipped so the feature has a
|
||||
-- working reference: it collects applications for its own icon+description mark
|
||||
-- and reports the operator's decision back to the applicant. It is deliberately
|
||||
-- NOT a second way to get the platform checkmark -- @verifybot (0153) owns that
|
||||
-- mechanism, and the two never read each other's state.
|
||||
--
|
||||
-- verified = false on purpose. The official flag is granted by the operator to
|
||||
-- peers that passed platform review; a verifier bot carrying it would blur exactly
|
||||
-- the distinction this bot has to explain to every applicant. What makes this
|
||||
-- account a verifier is a row in bot_verifier_settings (0155), which an operator
|
||||
-- grants by hand in the admin panel -- never this migration: seeding verifier
|
||||
-- status here would hand out a badge printer with the schema.
|
||||
--
|
||||
-- Seeded rather than created lazily on first message so the username is occupied
|
||||
-- from the moment the schema is current: peer_usernames.username_lower is the only
|
||||
-- thing standing between a reserved bot handle and an ordinary user claiming it,
|
||||
-- and a lazily created account would leave that window open.
|
||||
--
|
||||
-- access_hash is double-written with domain.VerifierBotAccessHash; the two must
|
||||
-- never drift, exactly as for the other service bots (0044, 0045, 0047, 0153).
|
||||
--
|
||||
-- The peer_usernames insert carries the multi-username registry columns added in
|
||||
-- 0149, so the handle occupies the editable slot.
|
||||
|
||||
INSERT INTO public.users (
|
||||
id, access_hash, phone, first_name, last_name, username, country_code,
|
||||
created_at, updated_at, verified, support, about, last_seen_at,
|
||||
default_history_ttl_period, is_bot, bot_info_version, premium_expires_at,
|
||||
emoji_status_document_id, emoji_status_until, color_set, color,
|
||||
color_background_emoji_id, profile_color_set, profile_color,
|
||||
profile_color_background_emoji_id
|
||||
) VALUES (
|
||||
1250000013, 6913402578811563729, '', 'Verifier Bot', '', 'verifierbot', '',
|
||||
now(), now(), false, false,
|
||||
'Third-party verification: my icon before your name and a line in your profile. Not the official checkmark.',
|
||||
0, 0, true, 1, NULL, 0, 0, false, 0, 0, false, 0, 0
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
access_hash = EXCLUDED.access_hash,
|
||||
phone = EXCLUDED.phone,
|
||||
first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
username = EXCLUDED.username,
|
||||
verified = EXCLUDED.verified,
|
||||
support = EXCLUDED.support,
|
||||
about = EXCLUDED.about,
|
||||
is_bot = EXCLUDED.is_bot,
|
||||
bot_info_version = GREATEST(public.users.bot_info_version, EXCLUDED.bot_info_version),
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO public.bots (
|
||||
bot_user_id, owner_user_id, token_secret, description, commands,
|
||||
bot_chat_history, bot_nochats, inline_placeholder, created_at, updated_at,
|
||||
menu_button_type, menu_button_text, menu_button_url, bot_inline_geo
|
||||
) VALUES (
|
||||
1250000013, 1250000013, '',
|
||||
'I grant third-party verification marks: my own icon before the name of your bot, channel or account, plus a description in its profile. This is not the official platform checkmark. I collect the application, an operator decides, and I message you here with the outcome.',
|
||||
'[
|
||||
{"command": "start", "description": "what a third-party mark is"},
|
||||
{"command": "verify", "description": "apply for the mark"},
|
||||
{"command": "status", "description": "your applications and marks"},
|
||||
{"command": "revoke", "description": "remove a mark from your peer"},
|
||||
{"command": "help", "description": "show help"}
|
||||
]'::jsonb,
|
||||
false, true, '', now(), now(), 0, '', '', false
|
||||
)
|
||||
ON CONFLICT (bot_user_id) DO UPDATE SET
|
||||
owner_user_id = EXCLUDED.owner_user_id,
|
||||
token_secret = EXCLUDED.token_secret,
|
||||
description = EXCLUDED.description,
|
||||
commands = EXCLUDED.commands,
|
||||
bot_chat_history = EXCLUDED.bot_chat_history,
|
||||
bot_nochats = EXCLUDED.bot_nochats,
|
||||
inline_placeholder = EXCLUDED.inline_placeholder,
|
||||
menu_button_type = EXCLUDED.menu_button_type,
|
||||
menu_button_text = EXCLUDED.menu_button_text,
|
||||
menu_button_url = EXCLUDED.menu_button_url,
|
||||
bot_inline_geo = EXCLUDED.bot_inline_geo,
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO public.peer_usernames (
|
||||
username_lower, username, peer_type, peer_id, active, editable, sort_order, updated_at
|
||||
)
|
||||
VALUES ('verifierbot', 'verifierbot', 'user', 1250000013, true, true, 0, now())
|
||||
ON CONFLICT (username_lower) DO UPDATE SET
|
||||
username = EXCLUDED.username,
|
||||
peer_type = EXCLUDED.peer_type,
|
||||
peer_id = EXCLUDED.peer_id,
|
||||
active = EXCLUDED.active,
|
||||
editable = EXCLUDED.editable,
|
||||
updated_at = now();
|
||||
|
||||
INSERT INTO public.read_model_versions (model, owner_user_id, peer_type, peer_id, version, updated_at, hash)
|
||||
VALUES
|
||||
('contact_account', 1250000013, 'user', 1250000013, 1, now(), 2500001300001),
|
||||
('channel_active_memberships', 1250000013, 'user', 1250000013, 1, now(), 2500001300002)
|
||||
ON CONFLICT (model, owner_user_id, peer_type, peer_id) DO UPDATE SET
|
||||
version = GREATEST(public.read_model_versions.version, EXCLUDED.version),
|
||||
updated_at = now(),
|
||||
hash = EXCLUDED.hash;
|
||||
23
deploy/migrations/0157_custom_emoji_reactions.down.sql
Normal file
23
deploy/migrations/0157_custom_emoji_reactions.down.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- Narrowing back can only succeed once no custom-emoji reaction rows remain, so
|
||||
-- drop them first: a CHECK that a stored row violates cannot be added.
|
||||
DELETE FROM public.channel_message_reactions WHERE (reaction_type)::text <> 'emoji';
|
||||
DELETE FROM public.user_top_reactions WHERE (reaction_type)::text <> 'emoji';
|
||||
DELETE FROM public.user_recent_reactions WHERE (reaction_type)::text <> 'emoji';
|
||||
|
||||
ALTER TABLE public.channel_message_reactions
|
||||
DROP CONSTRAINT IF EXISTS channel_message_reactions_type_check;
|
||||
ALTER TABLE public.channel_message_reactions
|
||||
ADD CONSTRAINT channel_message_reactions_type_check
|
||||
CHECK ((reaction_type)::text = 'emoji'::text);
|
||||
|
||||
ALTER TABLE public.user_top_reactions
|
||||
DROP CONSTRAINT IF EXISTS user_top_reactions_reaction_type_check;
|
||||
ALTER TABLE public.user_top_reactions
|
||||
ADD CONSTRAINT user_top_reactions_reaction_type_check
|
||||
CHECK ((reaction_type)::text = 'emoji'::text);
|
||||
|
||||
ALTER TABLE public.user_recent_reactions
|
||||
DROP CONSTRAINT IF EXISTS user_recent_reactions_reaction_type_check;
|
||||
ALTER TABLE public.user_recent_reactions
|
||||
ADD CONSTRAINT user_recent_reactions_reaction_type_check
|
||||
CHECK ((reaction_type)::text = 'emoji'::text);
|
||||
28
deploy/migrations/0157_custom_emoji_reactions.up.sql
Normal file
28
deploy/migrations/0157_custom_emoji_reactions.up.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
-- Custom-emoji reactions on channel messages.
|
||||
--
|
||||
-- The squashed 0001_init schema constrains channel_message_reactions.reaction_type,
|
||||
-- user_top_reactions.reaction_type and user_recent_reactions.reaction_type to
|
||||
-- 'emoji' only, while the saved-tag tables already allow 'custom_emoji' and the
|
||||
-- store writes reactionCustomEmoji through all of them. On a database built from
|
||||
-- these migrations a custom-emoji reaction in a channel therefore fails with
|
||||
-- "violates check constraint ..._type_check"; upstream's own
|
||||
-- TestChannelStoreCustomEmojiReactionPolicyRoundTrips fails for exactly this
|
||||
-- reason. Widen the three CHECKs to the pair the saved-tag tables already use.
|
||||
|
||||
ALTER TABLE public.channel_message_reactions
|
||||
DROP CONSTRAINT IF EXISTS channel_message_reactions_type_check;
|
||||
ALTER TABLE public.channel_message_reactions
|
||||
ADD CONSTRAINT channel_message_reactions_type_check
|
||||
CHECK ((reaction_type)::text = ANY (ARRAY['emoji'::text, 'custom_emoji'::text]));
|
||||
|
||||
ALTER TABLE public.user_top_reactions
|
||||
DROP CONSTRAINT IF EXISTS user_top_reactions_reaction_type_check;
|
||||
ALTER TABLE public.user_top_reactions
|
||||
ADD CONSTRAINT user_top_reactions_reaction_type_check
|
||||
CHECK ((reaction_type)::text = ANY (ARRAY['emoji'::text, 'custom_emoji'::text]));
|
||||
|
||||
ALTER TABLE public.user_recent_reactions
|
||||
DROP CONSTRAINT IF EXISTS user_recent_reactions_reaction_type_check;
|
||||
ALTER TABLE public.user_recent_reactions
|
||||
ADD CONSTRAINT user_recent_reactions_reaction_type_check
|
||||
CHECK ((reaction_type)::text = ANY (ARRAY['emoji'::text, 'custom_emoji'::text]));
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-- Nothing to restore: the rating read model is derived from live signals, so a
|
||||
-- deleted projection is recomputed rather than recovered, and the seeding pass
|
||||
-- deliberately never offers these accounts again. Down is a no-op rather than a
|
||||
-- lie about being able to bring the rows back.
|
||||
SELECT 1;
|
||||
20
deploy/migrations/0158_drop_service_account_ratings.up.sql
Normal file
20
deploy/migrations/0158_drop_service_account_ratings.up.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-- Service accounts carry no composite rating.
|
||||
--
|
||||
-- The rating measures what an account did with Stars, so a bot -- which does not
|
||||
-- transact on its own behalf -- and the built-in service accounts -- which are
|
||||
-- infrastructure rather than participants -- have no meaningful score. The seeding
|
||||
-- pass now excludes both, but the platform account (777000) is not flagged is_bot,
|
||||
-- so a deployment that already ran a recompute cycle has a projection row for it
|
||||
-- and would show a level badge on the platform account's profile.
|
||||
--
|
||||
-- Drop those rows and their ledger. The read model is derived, so deleting a row
|
||||
-- loses nothing that cannot be recomputed; the ledger rows go with it because a
|
||||
-- manual adjustment for an account that can no longer be rated is unreachable
|
||||
-- bookkeeping.
|
||||
DELETE FROM public.account_rating_events
|
||||
WHERE user_id IN (777000, 93372553, 1063110917, 1250000007, 1250000011, 1250000013)
|
||||
OR user_id IN (SELECT id FROM public.users WHERE is_bot);
|
||||
|
||||
DELETE FROM public.account_rating
|
||||
WHERE user_id IN (777000, 93372553, 1063110917, 1250000007, 1250000011, 1250000013)
|
||||
OR user_id IN (SELECT id FROM public.users WHERE is_bot);
|
||||
Loading…
Add table
Add a link
Reference in a new issue