feat: sync community aggregates

Sync telesrv 36eda30 (feat(communities): implement Layer 228 community aggregates).

Skipped telesrv docs changes per public sync rules; normalized the public appearance seed label.
This commit is contained in:
A 2026-07-20 16:46:02 +08:00
parent 7de1766941
commit da6a57e1a3
52 changed files with 5687 additions and 289 deletions

View file

@ -0,0 +1,20 @@
ALTER TABLE public.user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_peer_type_check;
ALTER TABLE public.user_update_events
ADD CONSTRAINT user_update_events_peer_type_check
CHECK (peer_type IS NULL OR peer_type IN ('user','channel'));
DELETE FROM public.notify_settings WHERE scope_kind='peer' AND peer_type='community';
DROP TRIGGER IF EXISTS users_linked_community_read_model_changed ON public.users;
DROP FUNCTION IF EXISTS public.telesrv_notify_user_linked_community_read_model();
DROP INDEX IF EXISTS public.channels_linked_community_idx;
DROP INDEX IF EXISTS public.users_linked_community_idx;
ALTER TABLE public.channels DROP COLUMN IF EXISTS linked_community_id;
ALTER TABLE public.users DROP COLUMN IF EXISTS linked_community_id;
DROP TABLE IF EXISTS public.community_user_states;
DROP TABLE IF EXISTS public.community_peer_link_requests;
DROP TABLE IF EXISTS public.community_peer_links;
DROP TABLE IF EXISTS public.community_members;
DROP TABLE IF EXISTS public.communities;

View file

@ -0,0 +1,130 @@
-- Layer 228 Communities are aggregation containers. Linked peer dialogs keep
-- their own messages/read state/pts; these tables only persist the container,
-- links, moderation, pending requests and per-user dialog presentation.
ALTER TABLE public.user_update_events
DROP CONSTRAINT IF EXISTS user_update_events_peer_type_check;
ALTER TABLE public.user_update_events
ADD CONSTRAINT user_update_events_peer_type_check
CHECK (peer_type IS NULL OR peer_type IN ('user','channel','community'));
CREATE TABLE public.communities (
id bigint PRIMARY KEY,
access_hash bigint NOT NULL,
creator_user_id bigint NOT NULL REFERENCES public.users(id),
title text NOT NULL,
about text DEFAULT ''::text NOT NULL,
default_banned_rights jsonb DEFAULT '{}'::jsonb NOT NULL,
photo_id bigint DEFAULT 0 NOT NULL,
photo_dc_id integer DEFAULT 0 NOT NULL,
photo_stripped bytea DEFAULT '\x'::bytea NOT NULL,
date integer NOT NULL,
deleted boolean DEFAULT false NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT communities_positive_check CHECK (id > 0 AND creator_user_id > 0),
CONSTRAINT communities_title_check CHECK (length(btrim(title)) > 0)
);
CREATE UNIQUE INDEX communities_access_hash_idx ON public.communities(access_hash);
CREATE INDEX communities_creator_idx ON public.communities(creator_user_id, id) WHERE NOT deleted;
CREATE TABLE public.community_members (
community_id bigint NOT NULL REFERENCES public.communities(id) ON DELETE CASCADE,
user_id bigint NOT NULL REFERENCES public.users(id),
role text DEFAULT 'member'::text NOT NULL,
status text DEFAULT 'active'::text NOT NULL,
admin_rights jsonb DEFAULT '{}'::jsonb NOT NULL,
rank text DEFAULT ''::text NOT NULL,
date integer NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
PRIMARY KEY (community_id, user_id),
CONSTRAINT community_members_role_check CHECK (role IN ('creator','admin','member')),
CONSTRAINT community_members_status_check CHECK (status IN ('active','kicked'))
);
CREATE UNIQUE INDEX community_one_creator_idx
ON public.community_members(community_id) WHERE role = 'creator' AND status = 'active';
CREATE INDEX community_members_user_idx ON public.community_members(user_id, community_id) WHERE status = 'active';
CREATE INDEX community_members_kicked_idx ON public.community_members(community_id, user_id) WHERE status = 'kicked';
CREATE TABLE public.community_peer_links (
community_id bigint NOT NULL REFERENCES public.communities(id) ON DELETE CASCADE,
peer_type text NOT NULL,
peer_id bigint NOT NULL,
visibility text NOT NULL,
created_by bigint NOT NULL REFERENCES public.users(id),
date integer NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
PRIMARY KEY (community_id, peer_type, peer_id),
CONSTRAINT community_peer_links_type_check CHECK (peer_type IN ('channel','user')),
CONSTRAINT community_peer_links_visibility_check CHECK (visibility IN ('visible','hidden')),
CONSTRAINT community_peer_links_positive_check CHECK (peer_id > 0 AND created_by > 0)
);
-- A group/channel/bot may belong to only one Community.
CREATE UNIQUE INDEX community_peer_links_unique_peer_idx ON public.community_peer_links(peer_type, peer_id);
CREATE INDEX community_peer_links_community_idx ON public.community_peer_links(community_id, date, peer_type, peer_id);
CREATE TABLE public.community_peer_link_requests (
community_id bigint NOT NULL REFERENCES public.communities(id) ON DELETE CASCADE,
peer_type text NOT NULL,
peer_id bigint NOT NULL,
requested_by bigint NOT NULL REFERENCES public.users(id),
visibility text NOT NULL,
date integer NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
PRIMARY KEY (community_id, peer_type, peer_id),
CONSTRAINT community_peer_requests_type_check CHECK (peer_type IN ('channel','user')),
CONSTRAINT community_peer_requests_visibility_check CHECK (visibility IN ('visible','hidden')),
CONSTRAINT community_peer_requests_positive_check CHECK (peer_id > 0 AND requested_by > 0)
);
CREATE INDEX community_peer_requests_page_idx
ON public.community_peer_link_requests(community_id, date DESC, peer_type, peer_id);
CREATE TABLE public.community_user_states (
community_id bigint NOT NULL REFERENCES public.communities(id) ON DELETE CASCADE,
user_id bigint NOT NULL REFERENCES public.users(id),
collapsed boolean DEFAULT false NOT NULL,
pinned boolean DEFAULT false NOT NULL,
pinned_order integer DEFAULT 0 NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL,
PRIMARY KEY (community_id, user_id),
CONSTRAINT community_user_states_order_check CHECK (pinned_order >= 0)
);
CREATE INDEX community_user_states_pinned_idx
ON public.community_user_states(user_id, pinned_order, community_id) WHERE pinned;
ALTER TABLE public.users
ADD COLUMN linked_community_id bigint DEFAULT 0 NOT NULL;
ALTER TABLE public.channels
ADD COLUMN linked_community_id bigint DEFAULT 0 NOT NULL;
CREATE INDEX users_linked_community_idx ON public.users(linked_community_id) WHERE linked_community_id <> 0;
CREATE INDEX channels_linked_community_idx ON public.channels(linked_community_id) WHERE linked_community_id <> 0;
-- linked_community_id is part of the Layer 228 user projection. The legacy
-- user-base trigger intentionally lists projected columns and therefore does
-- not notice this newly-added column; emit the same read-model bumps here so
-- Redis/base-user and contact projections cannot retain a stale bot link.
CREATE FUNCTION public.telesrv_notify_user_linked_community_read_model() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
PERFORM telesrv_bump_read_model_version('user_base', NEW.id, 'user', NEW.id);
PERFORM telesrv_bump_read_model_version('contact_account', c.user_id, 'user', c.user_id)
FROM contacts c
WHERE c.contact_user_id = NEW.id;
PERFORM telesrv_bump_private_dialog_light_for_user(NEW.id);
RETURN NULL;
END;
$$;
CREATE TRIGGER users_linked_community_read_model_changed
AFTER UPDATE OF linked_community_id ON public.users
FOR EACH ROW
WHEN (OLD.linked_community_id IS DISTINCT FROM NEW.linked_community_id)
EXECUTE FUNCTION public.telesrv_notify_user_linked_community_read_model();
-- Community notification settings reuse the existing peer-scoped table with a
-- distinct peer_type. No new scope_kind is required.