Merge remote-tracking branch 'upstream/main' into merge-gramsrv-2965f5d
This commit is contained in:
commit
ebb0be38d9
355 changed files with 44640 additions and 2320 deletions
|
|
@ -5554,7 +5554,7 @@ SET row_security = off;
|
|||
-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
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 (777000, 6599886787491911851, '42777', 'Telegram', '', 'telegram', '', '2026-06-19 13:35:51.253491+00', '2026-06-19 13:35:51.253491+00', true, true, '', 0, 0, false, 0, NULL, 0, 0, false, 0, 0, false, 0, 0);
|
||||
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 (777000, 6599886787491911851, '42777', 'Telesrv', '', 'telesrv', '', '2026-06-19 13:35:51.253491+00', '2026-06-19 13:35:51.253491+00', true, true, '', 0, 0, false, 0, NULL, 0, 0, false, 0, 0, false, 0, 0);
|
||||
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 (93372553, 7421896403922962293, '', 'BotFather', '', 'BotFather', '', '2026-06-19 13:35:52.688367+00', '2026-06-19 13:35:52.688367+00', true, false, '', 0, 0, true, 1, NULL, 0, 0, false, 0, 0, false, 0, 0);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM public.star_gift_collectible_models
|
||||
WHERE rarity_kind <> 'permille' OR crafted
|
||||
) THEN
|
||||
RAISE EXCEPTION 'cannot downgrade while categorical/crafted collectible models exist';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP INDEX IF EXISTS public.star_gift_catalog_revisions_official_source_idx;
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_backdrops
|
||||
DROP CONSTRAINT star_gift_collectible_backdrop_rarity_check,
|
||||
ALTER COLUMN rarity_permille SET NOT NULL,
|
||||
DROP COLUMN rarity_kind,
|
||||
ADD CONSTRAINT star_gift_collectible_backdrop_rarity_check CHECK (rarity_permille BETWEEN 1 AND 1000);
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
DROP CONSTRAINT star_gift_collectible_pattern_official_document_check,
|
||||
DROP CONSTRAINT star_gift_collectible_pattern_rarity_check,
|
||||
ALTER COLUMN rarity_permille SET NOT NULL,
|
||||
DROP COLUMN official_document_id,
|
||||
DROP COLUMN rarity_kind,
|
||||
ADD CONSTRAINT star_gift_collectible_pattern_rarity_check CHECK (rarity_permille BETWEEN 1 AND 1000);
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_models
|
||||
DROP CONSTRAINT star_gift_collectible_model_official_document_check,
|
||||
DROP CONSTRAINT star_gift_collectible_model_rarity_check,
|
||||
ALTER COLUMN rarity_permille SET NOT NULL,
|
||||
DROP COLUMN official_document_id,
|
||||
DROP COLUMN crafted,
|
||||
DROP COLUMN rarity_kind,
|
||||
ADD CONSTRAINT star_gift_collectible_model_rarity_check CHECK (rarity_permille BETWEEN 1 AND 1000);
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_revisions
|
||||
DROP CONSTRAINT star_gift_collectible_official_source_check,
|
||||
DROP COLUMN source_manifest_sha256,
|
||||
DROP COLUMN official_gift_id;
|
||||
|
||||
ALTER TABLE public.star_gift_catalog_revisions
|
||||
DROP CONSTRAINT star_gift_catalog_official_source_check,
|
||||
DROP COLUMN official_source,
|
||||
DROP COLUMN source_manifest_sha256,
|
||||
DROP COLUMN official_gift_id;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_guard_collectible_revision() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
IF OLD.status = 'published' THEN
|
||||
RAISE EXCEPTION 'published collectible revision is immutable';
|
||||
END IF;
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
IF OLD.status = 'published' THEN
|
||||
IF NEW.gift_id <> OLD.gift_id OR NEW.revision <> OLD.revision OR
|
||||
NEW.upgrade_stars <> OLD.upgrade_stars OR NEW.supply_total <> OLD.supply_total OR
|
||||
NEW.slug_prefix <> OLD.slug_prefix OR NEW.status <> OLD.status OR
|
||||
NEW.created_by <> OLD.created_by OR NEW.command_id <> OLD.command_id OR
|
||||
NEW.created_at <> OLD.created_at OR NEW.published_at <> OLD.published_at THEN
|
||||
RAISE EXCEPTION 'published collectible revision is immutable';
|
||||
END IF;
|
||||
IF NEW.issued <> OLD.issued + 1 THEN
|
||||
RAISE EXCEPTION 'published collectible issuance must advance exactly once';
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
88
deploy/migrations/0093_official_star_gift_attributes.up.sql
Normal file
88
deploy/migrations/0093_official_star_gift_attributes.up.sql
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
-- Preserve the complete Layer 228 official collectible attribute shape. Display rarity is
|
||||
-- distinct from regular-upgrade selection eligibility, and official provenance is recorded
|
||||
-- on both immutable revisions created by one import command.
|
||||
|
||||
ALTER TABLE public.star_gift_catalog_revisions
|
||||
ADD COLUMN official_gift_id bigint,
|
||||
ADD COLUMN source_manifest_sha256 bytea,
|
||||
ADD COLUMN official_source jsonb,
|
||||
ADD CONSTRAINT star_gift_catalog_official_source_check CHECK (
|
||||
(official_gift_id IS NULL AND source_manifest_sha256 IS NULL AND official_source IS NULL) OR
|
||||
(official_gift_id > 0 AND source_manifest_sha256 IS NOT NULL AND official_source IS NOT NULL AND
|
||||
octet_length(source_manifest_sha256) = 32 AND jsonb_typeof(official_source) = 'object')
|
||||
);
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_revisions
|
||||
ADD COLUMN official_gift_id bigint,
|
||||
ADD COLUMN source_manifest_sha256 bytea,
|
||||
ADD CONSTRAINT star_gift_collectible_official_source_check CHECK (
|
||||
(official_gift_id IS NULL AND source_manifest_sha256 IS NULL) OR
|
||||
(official_gift_id > 0 AND source_manifest_sha256 IS NOT NULL AND octet_length(source_manifest_sha256) = 32)
|
||||
);
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_models
|
||||
DROP CONSTRAINT star_gift_collectible_model_rarity_check,
|
||||
ALTER COLUMN rarity_permille DROP NOT NULL,
|
||||
ADD COLUMN rarity_kind text DEFAULT 'permille' NOT NULL,
|
||||
ADD COLUMN crafted boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN official_document_id bigint,
|
||||
ADD CONSTRAINT star_gift_collectible_model_rarity_check CHECK (
|
||||
rarity_kind IN ('permille', 'uncommon', 'rare', 'epic', 'legendary') AND
|
||||
((rarity_kind = 'permille' AND rarity_permille BETWEEN 1 AND 1000 AND NOT crafted) OR
|
||||
(rarity_kind <> 'permille' AND rarity_permille IS NULL AND crafted))
|
||||
),
|
||||
ADD CONSTRAINT star_gift_collectible_model_official_document_check CHECK (
|
||||
official_document_id IS NULL OR official_document_id > 0
|
||||
);
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
DROP CONSTRAINT star_gift_collectible_pattern_rarity_check,
|
||||
ALTER COLUMN rarity_permille DROP NOT NULL,
|
||||
ADD COLUMN rarity_kind text DEFAULT 'permille' NOT NULL,
|
||||
ADD COLUMN official_document_id bigint,
|
||||
ADD CONSTRAINT star_gift_collectible_pattern_rarity_check CHECK (
|
||||
rarity_kind = 'permille' AND rarity_permille BETWEEN 1 AND 1000
|
||||
),
|
||||
ADD CONSTRAINT star_gift_collectible_pattern_official_document_check CHECK (
|
||||
official_document_id IS NULL OR official_document_id > 0
|
||||
);
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_backdrops
|
||||
DROP CONSTRAINT star_gift_collectible_backdrop_rarity_check,
|
||||
ALTER COLUMN rarity_permille DROP NOT NULL,
|
||||
ADD COLUMN rarity_kind text DEFAULT 'permille' NOT NULL,
|
||||
ADD CONSTRAINT star_gift_collectible_backdrop_rarity_check CHECK (
|
||||
rarity_kind = 'permille' AND rarity_permille BETWEEN 1 AND 1000
|
||||
);
|
||||
|
||||
CREATE INDEX star_gift_catalog_revisions_official_source_idx
|
||||
ON public.star_gift_catalog_revisions(official_gift_id, id DESC)
|
||||
WHERE official_gift_id IS NOT NULL;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_guard_collectible_revision() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
IF OLD.status = 'published' THEN
|
||||
RAISE EXCEPTION 'published collectible revision is immutable';
|
||||
END IF;
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
|
||||
IF OLD.status = 'published' THEN
|
||||
IF NEW.gift_id <> OLD.gift_id OR NEW.revision <> OLD.revision OR
|
||||
NEW.upgrade_stars <> OLD.upgrade_stars OR NEW.supply_total <> OLD.supply_total OR
|
||||
NEW.slug_prefix <> OLD.slug_prefix OR NEW.status <> OLD.status OR
|
||||
NEW.created_by <> OLD.created_by OR NEW.command_id <> OLD.command_id OR
|
||||
NEW.created_at <> OLD.created_at OR NEW.published_at <> OLD.published_at OR
|
||||
NEW.official_gift_id IS DISTINCT FROM OLD.official_gift_id OR
|
||||
NEW.source_manifest_sha256 IS DISTINCT FROM OLD.source_manifest_sha256 THEN
|
||||
RAISE EXCEPTION 'published collectible revision is immutable';
|
||||
END IF;
|
||||
IF NEW.issued <> OLD.issued + 1 THEN
|
||||
RAISE EXCEPTION 'published collectible issuance must advance exactly once';
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
34
deploy/migrations/0094_star_gift_catalog_shape.down.sql
Normal file
34
deploy/migrations/0094_star_gift_catalog_shape.down.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
DROP TABLE IF EXISTS public.star_gift_user_purchases;
|
||||
|
||||
ALTER TABLE public.star_gift_catalog_revisions
|
||||
DROP CONSTRAINT IF EXISTS star_gift_catalog_revision_background_check,
|
||||
DROP CONSTRAINT IF EXISTS star_gift_catalog_revision_auction_check,
|
||||
DROP CONSTRAINT IF EXISTS star_gift_catalog_revision_released_by_check,
|
||||
DROP CONSTRAINT IF EXISTS star_gift_catalog_revision_supply_check,
|
||||
DROP COLUMN IF EXISTS background_text_color,
|
||||
DROP COLUMN IF EXISTS background_edge_color,
|
||||
DROP COLUMN IF EXISTS background_center_color,
|
||||
DROP COLUMN IF EXISTS upgrade_variants,
|
||||
DROP COLUMN IF EXISTS auction_start_date,
|
||||
DROP COLUMN IF EXISTS gifts_per_round,
|
||||
DROP COLUMN IF EXISTS auction_slug,
|
||||
DROP COLUMN IF EXISTS locked_until_date,
|
||||
DROP COLUMN IF EXISTS per_user_total,
|
||||
DROP COLUMN IF EXISTS released_by_peer_id,
|
||||
DROP COLUMN IF EXISTS released_by_peer_type,
|
||||
DROP COLUMN IF EXISTS availability_total,
|
||||
DROP COLUMN IF EXISTS auction,
|
||||
DROP COLUMN IF EXISTS peer_color_available,
|
||||
DROP COLUMN IF EXISTS limited_per_user,
|
||||
DROP COLUMN IF EXISTS require_premium,
|
||||
DROP COLUMN IF EXISTS birthday,
|
||||
DROP COLUMN IF EXISTS sold_out,
|
||||
DROP COLUMN IF EXISTS limited;
|
||||
|
||||
ALTER TABLE public.star_gift_catalog
|
||||
DROP CONSTRAINT IF EXISTS star_gift_catalog_inventory_check,
|
||||
DROP COLUMN IF EXISTS last_sale_date,
|
||||
DROP COLUMN IF EXISTS first_sale_date,
|
||||
DROP COLUMN IF EXISTS availability_resale,
|
||||
DROP COLUMN IF EXISTS resell_min_stars,
|
||||
DROP COLUMN IF EXISTS availability_remains;
|
||||
68
deploy/migrations/0094_star_gift_catalog_shape.up.sql
Normal file
68
deploy/migrations/0094_star_gift_catalog_shape.up.sql
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
-- Preserve the complete Layer 228 regular StarGift shape. Release facts are immutable
|
||||
-- catalog-revision data; inventory and sale timestamps belong to the mutable catalog
|
||||
-- aggregate. Per-user ownership limits are enforced by the transaction boundary rather
|
||||
-- than reconstructed from peer_star_gifts after the fact.
|
||||
|
||||
ALTER TABLE public.star_gift_catalog
|
||||
ADD COLUMN availability_remains integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN availability_resale bigint DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN resell_min_stars bigint DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN first_sale_date integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN last_sale_date integer DEFAULT 0 NOT NULL,
|
||||
ADD CONSTRAINT star_gift_catalog_inventory_check CHECK (
|
||||
availability_remains >= 0 AND availability_resale >= 0 AND resell_min_stars >= 0 AND
|
||||
first_sale_date >= 0 AND last_sale_date >= 0 AND
|
||||
(last_sale_date = 0 OR first_sale_date > 0) AND
|
||||
(first_sale_date = 0 OR last_sale_date = 0 OR last_sale_date >= first_sale_date)
|
||||
);
|
||||
|
||||
ALTER TABLE public.star_gift_catalog_revisions
|
||||
ADD COLUMN limited boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN sold_out boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN birthday boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN require_premium boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN limited_per_user boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN peer_color_available boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN auction boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN availability_total integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN released_by_peer_type text,
|
||||
ADD COLUMN released_by_peer_id bigint,
|
||||
ADD COLUMN per_user_total integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN locked_until_date integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN auction_slug text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN gifts_per_round integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN auction_start_date integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN upgrade_variants integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN background_center_color integer,
|
||||
ADD COLUMN background_edge_color integer,
|
||||
ADD COLUMN background_text_color integer,
|
||||
ADD CONSTRAINT star_gift_catalog_revision_supply_check CHECK (
|
||||
availability_total >= 0 AND
|
||||
per_user_total >= 0 AND locked_until_date >= 0 AND upgrade_variants >= 0 AND
|
||||
((limited AND availability_total > 0) OR (NOT limited AND availability_total = 0)) AND
|
||||
(NOT sold_out OR limited) AND
|
||||
((limited_per_user AND per_user_total > 0) OR (NOT limited_per_user AND per_user_total = 0))
|
||||
),
|
||||
ADD CONSTRAINT star_gift_catalog_revision_released_by_check CHECK (
|
||||
(released_by_peer_type IS NULL AND released_by_peer_id IS NULL) OR
|
||||
(released_by_peer_type IN ('user', 'chat', 'channel') AND released_by_peer_id > 0)
|
||||
),
|
||||
ADD CONSTRAINT star_gift_catalog_revision_auction_check CHECK (
|
||||
(auction AND limited AND auction_slug <> '' AND gifts_per_round > 0 AND auction_start_date > 0) OR
|
||||
(NOT auction AND auction_slug = '' AND gifts_per_round = 0 AND auction_start_date = 0)
|
||||
),
|
||||
ADD CONSTRAINT star_gift_catalog_revision_background_check CHECK (
|
||||
(background_center_color IS NULL AND background_edge_color IS NULL AND background_text_color IS NULL) OR
|
||||
(background_center_color BETWEEN 0 AND 16777215 AND
|
||||
background_edge_color BETWEEN 0 AND 16777215 AND
|
||||
background_text_color BETWEEN 0 AND 16777215)
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_user_purchases (
|
||||
user_id bigint NOT NULL,
|
||||
gift_id bigint NOT NULL REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT,
|
||||
purchased_count integer DEFAULT 0 NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT star_gift_user_purchases_pkey PRIMARY KEY (user_id, gift_id),
|
||||
CONSTRAINT star_gift_user_purchases_count_check CHECK (user_id > 0 AND purchased_count >= 0)
|
||||
);
|
||||
76
deploy/migrations/0095_star_gift_lifecycle.down.sql
Normal file
76
deploy/migrations/0095_star_gift_lifecycle.down.sql
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
DROP TRIGGER IF EXISTS peer_unique_star_gift_owner_guard ON public.peer_star_gifts;
|
||||
DROP TRIGGER IF EXISTS unique_star_gift_owner_guard ON public.unique_star_gifts;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_check_unique_star_gift_owner();
|
||||
DROP TRIGGER IF EXISTS star_gift_listing_guard ON public.star_gift_listings;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_guard_star_gift_listing();
|
||||
|
||||
DROP TABLE IF EXISTS public.ton_transactions;
|
||||
DROP TABLE IF EXISTS public.ton_balances;
|
||||
DROP TABLE IF EXISTS public.star_gift_auction_acquired;
|
||||
DROP TABLE IF EXISTS public.star_gift_auction_bid_payments;
|
||||
DROP TABLE IF EXISTS public.star_gift_auction_bids;
|
||||
DROP TABLE IF EXISTS public.star_gift_auctions;
|
||||
DROP TABLE IF EXISTS public.star_gift_withdrawal_requests;
|
||||
DROP TABLE IF EXISTS public.star_gift_notification_settings;
|
||||
DROP TABLE IF EXISTS public.star_gift_craft_commands;
|
||||
DROP TABLE IF EXISTS public.star_gift_transfer_commands;
|
||||
DROP TABLE IF EXISTS public.star_gift_purchase_commands;
|
||||
DROP TABLE IF EXISTS public.star_gift_drop_details_commands;
|
||||
DROP TABLE IF EXISTS public.star_gift_prepaid_upgrade_commands;
|
||||
DROP TABLE IF EXISTS public.star_gift_offers;
|
||||
DROP TABLE IF EXISTS public.star_gift_sales;
|
||||
DROP TABLE IF EXISTS public.star_gift_listings;
|
||||
|
||||
ALTER TABLE public.unique_star_gifts
|
||||
DROP CONSTRAINT IF EXISTS unique_star_gift_value_check,
|
||||
DROP CONSTRAINT IF EXISTS unique_star_gift_original_owner_check,
|
||||
DROP CONSTRAINT IF EXISTS unique_star_gift_host_peer_check,
|
||||
DROP CONSTRAINT IF EXISTS unique_star_gift_theme_peer_check,
|
||||
DROP CONSTRAINT IF EXISTS unique_star_gift_released_by_check,
|
||||
DROP CONSTRAINT IF EXISTS unique_star_gift_owner_check,
|
||||
DROP COLUMN IF EXISTS last_sale_amount,
|
||||
DROP COLUMN IF EXISTS last_sale_currency,
|
||||
DROP COLUMN IF EXISTS last_sale_date,
|
||||
DROP COLUMN IF EXISTS craft_chance_permille,
|
||||
DROP COLUMN IF EXISTS offer_min_stars,
|
||||
DROP COLUMN IF EXISTS host_peer_id,
|
||||
DROP COLUMN IF EXISTS host_peer_type,
|
||||
DROP COLUMN IF EXISTS theme_peer_id,
|
||||
DROP COLUMN IF EXISTS theme_peer_type,
|
||||
DROP COLUMN IF EXISTS value_usd_amount,
|
||||
DROP COLUMN IF EXISTS value_currency,
|
||||
DROP COLUMN IF EXISTS value_amount,
|
||||
DROP COLUMN IF EXISTS released_by_peer_id,
|
||||
DROP COLUMN IF EXISTS released_by_peer_type,
|
||||
DROP COLUMN IF EXISTS gift_address,
|
||||
DROP COLUMN IF EXISTS owner_address,
|
||||
DROP COLUMN IF EXISTS owner_name,
|
||||
DROP COLUMN IF EXISTS crafted,
|
||||
DROP COLUMN IF EXISTS original_owner_peer_id,
|
||||
DROP COLUMN IF EXISTS original_owner_peer_type,
|
||||
DROP COLUMN IF EXISTS burned,
|
||||
DROP COLUMN IF EXISTS theme_available,
|
||||
DROP COLUMN IF EXISTS resale_ton_only,
|
||||
DROP COLUMN IF EXISTS require_premium;
|
||||
|
||||
UPDATE public.unique_star_gifts u
|
||||
SET owner_peer_type=p.owner_peer_type, owner_peer_id=p.owner_peer_id
|
||||
FROM public.peer_star_gifts p
|
||||
WHERE p.unique_gift_id=u.id AND (u.owner_peer_type IS NULL OR u.owner_peer_id IS NULL);
|
||||
ALTER TABLE public.unique_star_gifts
|
||||
ALTER COLUMN owner_peer_type SET NOT NULL,
|
||||
ALTER COLUMN owner_peer_id SET NOT NULL,
|
||||
ADD CONSTRAINT unique_star_gift_owner_check CHECK (owner_peer_type IN ('user','channel') AND owner_peer_id>0);
|
||||
|
||||
ALTER TABLE public.peer_star_gifts
|
||||
DROP COLUMN IF EXISTS prepaid_upgrade_hash,
|
||||
DROP COLUMN IF EXISTS gift_num,
|
||||
DROP CONSTRAINT IF EXISTS peer_star_gifts_lifecycle_check,
|
||||
DROP COLUMN IF EXISTS can_craft_at,
|
||||
DROP COLUMN IF EXISTS drop_original_details_stars,
|
||||
DROP COLUMN IF EXISTS can_resell_at,
|
||||
DROP COLUMN IF EXISTS can_transfer_at,
|
||||
DROP COLUMN IF EXISTS can_export_at,
|
||||
DROP COLUMN IF EXISTS transfer_stars,
|
||||
DROP COLUMN IF EXISTS lifecycle_status,
|
||||
ADD CONSTRAINT peer_star_gifts_terminal_state_check CHECK (NOT converted OR unique_gift_id IS NULL);
|
||||
425
deploy/migrations/0095_star_gift_lifecycle.up.sql
Normal file
425
deploy/migrations/0095_star_gift_lifecycle.up.sql
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
-- Complete collectible Star Gift lifecycle: ownership state, transfer/resale, purchase
|
||||
-- offers, crafting, auctions, notification preferences and the explicit TON boundary.
|
||||
|
||||
ALTER TABLE public.peer_star_gifts
|
||||
DROP CONSTRAINT IF EXISTS peer_star_gifts_terminal_state_check,
|
||||
ADD COLUMN lifecycle_status text DEFAULT 'active' NOT NULL,
|
||||
ADD COLUMN transfer_stars bigint DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN prepaid_upgrade_hash text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN gift_num integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN can_export_at integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN can_transfer_at integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN can_resell_at integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN drop_original_details_stars bigint DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN can_craft_at integer DEFAULT 0 NOT NULL;
|
||||
|
||||
UPDATE public.peer_star_gifts SET lifecycle_status='converted' WHERE converted;
|
||||
|
||||
ALTER TABLE public.peer_star_gifts
|
||||
ADD CONSTRAINT peer_star_gifts_lifecycle_check CHECK (
|
||||
lifecycle_status IN ('active', 'converted', 'burned', 'exported') AND
|
||||
transfer_stars >= 0 AND gift_num >= 0 AND can_export_at >= 0 AND can_transfer_at >= 0 AND
|
||||
can_resell_at >= 0 AND drop_original_details_stars >= 0 AND can_craft_at >= 0 AND
|
||||
((lifecycle_status='converted' AND converted AND unique_gift_id IS NULL) OR
|
||||
(lifecycle_status='active' AND NOT converted) OR
|
||||
(lifecycle_status IN ('burned','exported') AND NOT converted AND unique_gift_id IS NOT NULL))
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX peer_star_gifts_prepaid_upgrade_hash_uniq
|
||||
ON public.peer_star_gifts(prepaid_upgrade_hash) WHERE prepaid_upgrade_hash<>'';
|
||||
|
||||
ALTER TABLE public.unique_star_gifts
|
||||
ALTER COLUMN owner_peer_type DROP NOT NULL,
|
||||
ALTER COLUMN owner_peer_id DROP NOT NULL,
|
||||
DROP CONSTRAINT IF EXISTS unique_star_gift_owner_check,
|
||||
ADD COLUMN require_premium boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN resale_ton_only boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN theme_available boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN burned boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN crafted boolean DEFAULT false NOT NULL,
|
||||
ADD COLUMN original_owner_peer_type text,
|
||||
ADD COLUMN original_owner_peer_id bigint,
|
||||
ADD COLUMN owner_name text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN owner_address text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN gift_address text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN released_by_peer_type text,
|
||||
ADD COLUMN released_by_peer_id bigint,
|
||||
ADD COLUMN value_amount bigint DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN value_currency text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN value_usd_amount bigint DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN theme_peer_type text,
|
||||
ADD COLUMN theme_peer_id bigint,
|
||||
ADD COLUMN host_peer_type text,
|
||||
ADD COLUMN host_peer_id bigint,
|
||||
ADD COLUMN offer_min_stars integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN craft_chance_permille integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN last_sale_date integer DEFAULT 0 NOT NULL,
|
||||
ADD COLUMN last_sale_currency text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN last_sale_amount bigint DEFAULT 0 NOT NULL,
|
||||
ADD CONSTRAINT unique_star_gift_owner_check CHECK (
|
||||
(owner_peer_type IN ('user','channel') AND owner_peer_id > 0 AND owner_address='') OR
|
||||
(owner_peer_type IS NULL AND owner_peer_id IS NULL AND owner_address<>'')
|
||||
),
|
||||
ADD CONSTRAINT unique_star_gift_released_by_check CHECK (
|
||||
(released_by_peer_type IS NULL AND released_by_peer_id IS NULL) OR
|
||||
(released_by_peer_type IN ('user','channel') AND released_by_peer_id > 0)
|
||||
),
|
||||
ADD CONSTRAINT unique_star_gift_theme_peer_check CHECK (
|
||||
(theme_peer_type IS NULL AND theme_peer_id IS NULL) OR
|
||||
(theme_peer_type IN ('user','channel') AND theme_peer_id > 0)
|
||||
),
|
||||
ADD CONSTRAINT unique_star_gift_host_peer_check CHECK (
|
||||
(host_peer_type IS NULL AND host_peer_id IS NULL) OR
|
||||
(host_peer_type IN ('user','channel') AND host_peer_id > 0)
|
||||
),
|
||||
ADD CONSTRAINT unique_star_gift_value_check CHECK (
|
||||
value_amount >= 0 AND value_usd_amount >= 0 AND offer_min_stars >= 0 AND
|
||||
craft_chance_permille BETWEEN 0 AND 1000 AND last_sale_date >= 0 AND last_sale_amount >= 0 AND
|
||||
((value_currency='' AND value_amount=0) OR value_currency<>'') AND
|
||||
((last_sale_currency='' AND last_sale_amount=0 AND last_sale_date=0) OR
|
||||
(last_sale_currency IN ('XTR','TON') AND last_sale_amount>0 AND last_sale_date>0)) AND
|
||||
(NOT burned OR owner_address='') AND
|
||||
((owner_address='' AND gift_address='') OR (owner_address<>'' AND gift_address<>''))
|
||||
);
|
||||
|
||||
UPDATE public.unique_star_gifts u
|
||||
SET original_owner_peer_type=p.owner_peer_type, original_owner_peer_id=p.owner_peer_id
|
||||
FROM public.peer_star_gifts p WHERE p.id=u.source_saved_gift_id;
|
||||
|
||||
ALTER TABLE public.unique_star_gifts
|
||||
ALTER COLUMN original_owner_peer_type SET NOT NULL,
|
||||
ALTER COLUMN original_owner_peer_id SET NOT NULL,
|
||||
ADD CONSTRAINT unique_star_gift_original_owner_check CHECK (
|
||||
original_owner_peer_type IN ('user','channel') AND original_owner_peer_id>0
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_listings (
|
||||
unique_gift_id bigint PRIMARY KEY REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT,
|
||||
seller_peer_type text NOT NULL,
|
||||
seller_peer_id bigint NOT NULL,
|
||||
currency text NOT NULL,
|
||||
amount bigint NOT NULL,
|
||||
listed_at integer NOT NULL,
|
||||
updated_at integer NOT NULL,
|
||||
version bigint DEFAULT 1 NOT NULL,
|
||||
CONSTRAINT star_gift_listing_seller_check CHECK (seller_peer_type IN ('user','channel') AND seller_peer_id>0),
|
||||
CONSTRAINT star_gift_listing_amount_check CHECK (currency IN ('XTR','TON') AND amount>0 AND listed_at>0 AND updated_at>=listed_at)
|
||||
);
|
||||
CREATE INDEX star_gift_listings_gift_price_idx ON public.star_gift_listings(currency, amount, unique_gift_id);
|
||||
CREATE INDEX star_gift_listings_updated_idx ON public.star_gift_listings(updated_at DESC, unique_gift_id DESC);
|
||||
|
||||
CREATE TABLE public.star_gift_sales (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
unique_gift_id bigint NOT NULL REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT,
|
||||
seller_peer_type text NOT NULL,
|
||||
seller_peer_id bigint NOT NULL,
|
||||
buyer_peer_type text NOT NULL,
|
||||
buyer_peer_id bigint NOT NULL,
|
||||
currency text NOT NULL,
|
||||
amount bigint NOT NULL,
|
||||
commission_amount bigint DEFAULT 0 NOT NULL,
|
||||
sold_at integer NOT NULL,
|
||||
command_key text NOT NULL,
|
||||
CONSTRAINT star_gift_sales_command_uniq UNIQUE(command_key),
|
||||
CONSTRAINT star_gift_sales_peer_check CHECK (
|
||||
seller_peer_type IN ('user','channel') AND seller_peer_id>0 AND
|
||||
buyer_peer_type IN ('user','channel') AND buyer_peer_id>0),
|
||||
CONSTRAINT star_gift_sales_amount_check CHECK (
|
||||
currency IN ('XTR','TON') AND amount>0 AND commission_amount>=0 AND commission_amount<=amount AND sold_at>0)
|
||||
);
|
||||
CREATE INDEX star_gift_sales_unique_date_idx ON public.star_gift_sales(unique_gift_id, sold_at DESC);
|
||||
|
||||
CREATE TABLE public.star_gift_offers (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
buyer_user_id bigint NOT NULL,
|
||||
owner_peer_type text NOT NULL,
|
||||
owner_peer_id bigint NOT NULL,
|
||||
unique_gift_id bigint NOT NULL REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT,
|
||||
currency text NOT NULL,
|
||||
amount bigint NOT NULL,
|
||||
random_id bigint NOT NULL,
|
||||
offer_msg_id integer DEFAULT 0 NOT NULL,
|
||||
buyer_msg_id integer DEFAULT 0 NOT NULL,
|
||||
status text DEFAULT 'pending' NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
expires_at integer NOT NULL,
|
||||
resolved_at integer DEFAULT 0 NOT NULL,
|
||||
balance_after bigint DEFAULT 0 NOT NULL,
|
||||
expiry_notified boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT star_gift_offer_random_uniq UNIQUE(buyer_user_id, random_id),
|
||||
CONSTRAINT star_gift_offer_owner_msg_uniq UNIQUE(owner_peer_type, owner_peer_id, offer_msg_id),
|
||||
CONSTRAINT star_gift_offer_peer_check CHECK (buyer_user_id>0 AND owner_peer_type IN ('user','channel') AND owner_peer_id>0),
|
||||
CONSTRAINT star_gift_offer_amount_check CHECK (currency IN ('XTR','TON') AND amount>0),
|
||||
CONSTRAINT star_gift_offer_status_check CHECK (status IN ('pending','accepted','declined','expired','cancelled')),
|
||||
CONSTRAINT star_gift_offer_time_check CHECK (created_at>0 AND expires_at>created_at AND resolved_at>=0 AND
|
||||
((status='pending' AND resolved_at=0) OR (status<>'pending' AND resolved_at>=created_at)))
|
||||
);
|
||||
CREATE INDEX star_gift_offers_pending_expiry_idx ON public.star_gift_offers(expires_at, id) WHERE status='pending';
|
||||
CREATE INDEX star_gift_offers_unique_pending_idx ON public.star_gift_offers(unique_gift_id, id) WHERE status='pending';
|
||||
|
||||
CREATE TABLE public.star_gift_transfer_commands (
|
||||
actor_user_id bigint NOT NULL,
|
||||
command_key text NOT NULL,
|
||||
unique_gift_id bigint NOT NULL REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT,
|
||||
from_peer_type text NOT NULL,
|
||||
from_peer_id bigint NOT NULL,
|
||||
to_peer_type text NOT NULL,
|
||||
to_peer_id bigint NOT NULL,
|
||||
charge_stars bigint DEFAULT 0 NOT NULL,
|
||||
balance_after bigint DEFAULT 0 NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
CONSTRAINT star_gift_transfer_commands_pkey PRIMARY KEY(actor_user_id, command_key),
|
||||
CONSTRAINT star_gift_transfer_command_peer_check CHECK (
|
||||
actor_user_id>0 AND from_peer_type IN ('user','channel') AND from_peer_id>0 AND
|
||||
to_peer_type IN ('user','channel') AND to_peer_id>0 AND charge_stars>=0 AND created_at>0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_purchase_commands (
|
||||
buyer_user_id bigint NOT NULL,
|
||||
command_key text NOT NULL,
|
||||
gift_id bigint NOT NULL REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT,
|
||||
recipient_peer_type text NOT NULL,
|
||||
recipient_peer_id bigint NOT NULL,
|
||||
saved_gift_id bigint NOT NULL REFERENCES public.peer_star_gifts(id) ON DELETE RESTRICT,
|
||||
form_id bigint NOT NULL,
|
||||
charge_stars bigint NOT NULL,
|
||||
balance_after bigint NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
CONSTRAINT star_gift_purchase_commands_pkey PRIMARY KEY(buyer_user_id,command_key),
|
||||
CONSTRAINT star_gift_purchase_commands_form_uniq UNIQUE(buyer_user_id,form_id),
|
||||
CONSTRAINT star_gift_purchase_command_shape_check CHECK (
|
||||
buyer_user_id>0 AND recipient_peer_type IN ('user','channel') AND recipient_peer_id>0 AND
|
||||
form_id>0 AND charge_stars>0 AND balance_after>=0 AND created_at>0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_prepaid_upgrade_commands (
|
||||
payer_user_id bigint NOT NULL,
|
||||
command_key text NOT NULL,
|
||||
saved_gift_id bigint NOT NULL REFERENCES public.peer_star_gifts(id) ON DELETE RESTRICT,
|
||||
form_id bigint NOT NULL,
|
||||
charge_stars bigint NOT NULL,
|
||||
balance_after bigint NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
CONSTRAINT star_gift_prepaid_upgrade_commands_pkey PRIMARY KEY(payer_user_id, command_key),
|
||||
CONSTRAINT star_gift_prepaid_upgrade_commands_form_uniq UNIQUE(payer_user_id, form_id),
|
||||
CONSTRAINT star_gift_prepaid_upgrade_command_shape_check CHECK (
|
||||
payer_user_id>0 AND form_id>0 AND charge_stars>0 AND balance_after>=0 AND created_at>0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_drop_details_commands (
|
||||
user_id bigint NOT NULL,
|
||||
command_key text NOT NULL,
|
||||
saved_gift_id bigint NOT NULL REFERENCES public.peer_star_gifts(id) ON DELETE RESTRICT,
|
||||
unique_gift_id bigint NOT NULL REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT,
|
||||
form_id bigint NOT NULL,
|
||||
charge_stars bigint NOT NULL,
|
||||
balance_after bigint NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
CONSTRAINT star_gift_drop_details_commands_pkey PRIMARY KEY(user_id, command_key),
|
||||
CONSTRAINT star_gift_drop_details_commands_form_uniq UNIQUE(user_id, form_id),
|
||||
CONSTRAINT star_gift_drop_details_command_shape_check CHECK (
|
||||
user_id>0 AND form_id>0 AND charge_stars>0 AND balance_after>=0 AND created_at>0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_craft_commands (
|
||||
user_id bigint NOT NULL,
|
||||
command_key text NOT NULL,
|
||||
input_unique_gift_ids bigint[] NOT NULL,
|
||||
gift_id bigint NOT NULL REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT,
|
||||
success boolean NOT NULL,
|
||||
result_unique_gift_id bigint REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT,
|
||||
chance_permille integer NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
CONSTRAINT star_gift_craft_commands_pkey PRIMARY KEY(user_id, command_key),
|
||||
CONSTRAINT star_gift_craft_shape_check CHECK (
|
||||
user_id>0 AND cardinality(input_unique_gift_ids) BETWEEN 1 AND 4 AND
|
||||
chance_permille BETWEEN 0 AND 1000 AND created_at>0 AND
|
||||
((success AND result_unique_gift_id IS NOT NULL) OR (NOT success AND result_unique_gift_id IS NULL)))
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_notification_settings (
|
||||
user_id bigint NOT NULL,
|
||||
channel_id bigint NOT NULL,
|
||||
enabled boolean DEFAULT true NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT star_gift_notification_settings_pkey PRIMARY KEY(user_id, channel_id),
|
||||
CONSTRAINT star_gift_notification_settings_peer_check CHECK (user_id>0 AND channel_id>0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_withdrawal_requests (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
unique_gift_id bigint NOT NULL REFERENCES public.unique_star_gifts(id) ON DELETE RESTRICT,
|
||||
owner_user_id bigint NOT NULL,
|
||||
provider text NOT NULL,
|
||||
provider_request_id text NOT NULL,
|
||||
url text NOT NULL,
|
||||
status text DEFAULT 'pending' NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
expires_at integer NOT NULL,
|
||||
completed_at integer DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT star_gift_withdrawal_request_unique UNIQUE(unique_gift_id),
|
||||
CONSTRAINT star_gift_withdrawal_provider_request_uniq UNIQUE(provider, provider_request_id),
|
||||
CONSTRAINT star_gift_withdrawal_shape_check CHECK (
|
||||
owner_user_id>0 AND provider<>'' AND provider_request_id<>'' AND url<>'' AND
|
||||
status IN ('pending','completed','failed') AND created_at>0 AND expires_at>created_at AND completed_at>=0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_auctions (
|
||||
gift_id bigint PRIMARY KEY REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT,
|
||||
slug text NOT NULL UNIQUE,
|
||||
version integer DEFAULT 1 NOT NULL,
|
||||
start_date integer NOT NULL,
|
||||
end_date integer NOT NULL,
|
||||
round_duration integer NOT NULL,
|
||||
gifts_per_round integer NOT NULL,
|
||||
total_rounds integer NOT NULL,
|
||||
current_round integer DEFAULT 0 NOT NULL,
|
||||
next_round_at integer NOT NULL,
|
||||
last_gift_num integer DEFAULT 0 NOT NULL,
|
||||
gifts_left integer NOT NULL,
|
||||
min_bid_amount bigint NOT NULL,
|
||||
status text DEFAULT 'pending' NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT star_gift_auction_shape_check CHECK (
|
||||
version>0 AND start_date>0 AND end_date>start_date AND round_duration>0 AND
|
||||
gifts_per_round>0 AND total_rounds>0 AND current_round BETWEEN 0 AND total_rounds AND
|
||||
next_round_at>=start_date AND last_gift_num>=0 AND gifts_left>=0 AND min_bid_amount>0 AND
|
||||
status IN ('pending','active','completed','cancelled'))
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_auction_bids (
|
||||
gift_id bigint NOT NULL REFERENCES public.star_gift_auctions(gift_id) ON DELETE RESTRICT,
|
||||
bidder_user_id bigint NOT NULL,
|
||||
recipient_peer_type text NOT NULL,
|
||||
recipient_peer_id bigint NOT NULL,
|
||||
amount bigint NOT NULL,
|
||||
bid_date integer NOT NULL,
|
||||
hide_name boolean DEFAULT false NOT NULL,
|
||||
message text DEFAULT '' NOT NULL,
|
||||
returned boolean DEFAULT false NOT NULL,
|
||||
acquired_count integer DEFAULT 0 NOT NULL,
|
||||
active boolean DEFAULT true NOT NULL,
|
||||
version bigint DEFAULT 1 NOT NULL,
|
||||
CONSTRAINT star_gift_auction_bids_pkey PRIMARY KEY(gift_id, bidder_user_id),
|
||||
CONSTRAINT star_gift_auction_bid_peer_check CHECK (
|
||||
bidder_user_id>0 AND recipient_peer_type IN ('user','channel') AND recipient_peer_id>0),
|
||||
CONSTRAINT star_gift_auction_bid_amount_check CHECK (amount>0 AND bid_date>0 AND acquired_count>=0 AND version>0)
|
||||
);
|
||||
CREATE INDEX star_gift_auction_bids_rank_idx ON public.star_gift_auction_bids(gift_id, amount DESC, bid_date, bidder_user_id) WHERE active;
|
||||
|
||||
CREATE TABLE public.star_gift_auction_bid_payments (
|
||||
user_id bigint NOT NULL,
|
||||
form_id bigint NOT NULL,
|
||||
gift_id bigint NOT NULL REFERENCES public.star_gift_auctions(gift_id) ON DELETE RESTRICT,
|
||||
bid_amount bigint NOT NULL,
|
||||
balance_after bigint NOT NULL,
|
||||
created_at integer NOT NULL,
|
||||
CONSTRAINT star_gift_auction_bid_payments_pkey PRIMARY KEY(user_id, form_id),
|
||||
CONSTRAINT star_gift_auction_bid_payment_shape_check CHECK (
|
||||
user_id>0 AND form_id>0 AND bid_amount>0 AND balance_after>=0 AND created_at>0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.star_gift_auction_acquired (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
gift_id bigint NOT NULL REFERENCES public.star_gift_auctions(gift_id) ON DELETE RESTRICT,
|
||||
bidder_user_id bigint NOT NULL,
|
||||
recipient_peer_type text NOT NULL,
|
||||
recipient_peer_id bigint NOT NULL,
|
||||
saved_gift_id bigint REFERENCES public.peer_star_gifts(id) ON DELETE RESTRICT,
|
||||
bid_amount bigint NOT NULL,
|
||||
round integer NOT NULL,
|
||||
pos integer NOT NULL,
|
||||
gift_num integer,
|
||||
acquired_at integer NOT NULL,
|
||||
hide_name boolean DEFAULT false NOT NULL,
|
||||
message text DEFAULT '' NOT NULL,
|
||||
CONSTRAINT star_gift_auction_acquired_round_pos_uniq UNIQUE(gift_id, round, pos),
|
||||
CONSTRAINT star_gift_auction_acquired_shape_check CHECK (
|
||||
bidder_user_id>0 AND recipient_peer_type IN ('user','channel') AND recipient_peer_id>0 AND
|
||||
bid_amount>0 AND round>0 AND pos>0 AND acquired_at>0 AND (gift_num IS NULL OR gift_num>0))
|
||||
);
|
||||
CREATE INDEX star_gift_auction_acquired_user_idx ON public.star_gift_auction_acquired(bidder_user_id, gift_id, id);
|
||||
|
||||
CREATE TABLE public.ton_balances (
|
||||
user_id bigint PRIMARY KEY,
|
||||
balance_nanoton bigint DEFAULT 0 NOT NULL,
|
||||
granted boolean DEFAULT false NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT ton_balances_check CHECK (user_id>0 AND balance_nanoton>=0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.ton_transactions (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
user_id bigint NOT NULL,
|
||||
amount_nanoton bigint NOT NULL,
|
||||
reason text NOT NULL,
|
||||
peer_type text,
|
||||
peer_id bigint,
|
||||
gift_id bigint,
|
||||
date integer NOT NULL,
|
||||
CONSTRAINT ton_transaction_amount_check CHECK (user_id>0 AND amount_nanoton<>0 AND date>0),
|
||||
CONSTRAINT ton_transaction_peer_check CHECK (
|
||||
(peer_type IS NULL AND peer_id IS NULL) OR (peer_type IN ('user','channel') AND peer_id>0))
|
||||
);
|
||||
CREATE INDEX ton_transactions_user_idx ON public.ton_transactions(user_id, id DESC);
|
||||
|
||||
CREATE FUNCTION public.telesrv_guard_star_gift_listing() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
gift_owner_type text;
|
||||
gift_owner_id bigint;
|
||||
gift_burned boolean;
|
||||
BEGIN
|
||||
SELECT owner_peer_type, owner_peer_id, burned
|
||||
INTO gift_owner_type, gift_owner_id, gift_burned
|
||||
FROM public.unique_star_gifts WHERE id=NEW.unique_gift_id FOR SHARE;
|
||||
IF gift_burned OR gift_owner_type IS DISTINCT FROM NEW.seller_peer_type OR gift_owner_id IS DISTINCT FROM NEW.seller_peer_id THEN
|
||||
RAISE EXCEPTION 'star gift listing owner/state mismatch';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE TRIGGER star_gift_listing_guard BEFORE INSERT OR UPDATE ON public.star_gift_listings
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_guard_star_gift_listing();
|
||||
|
||||
CREATE FUNCTION public.telesrv_check_unique_star_gift_owner() RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
unique_id bigint;
|
||||
gift_owner_type text;
|
||||
gift_owner_id bigint;
|
||||
gift_owner_address text;
|
||||
gift_burned boolean;
|
||||
saved_status text;
|
||||
saved_owner_type text;
|
||||
saved_owner_id bigint;
|
||||
BEGIN
|
||||
IF TG_TABLE_NAME = 'unique_star_gifts' THEN
|
||||
unique_id := COALESCE(NEW.id, OLD.id);
|
||||
ELSE
|
||||
unique_id := COALESCE(NEW.unique_gift_id, OLD.unique_gift_id);
|
||||
END IF;
|
||||
IF unique_id IS NULL THEN RETURN NULL; END IF;
|
||||
SELECT owner_peer_type, owner_peer_id, owner_address, burned
|
||||
INTO gift_owner_type, gift_owner_id, gift_owner_address, gift_burned
|
||||
FROM public.unique_star_gifts WHERE id=unique_id;
|
||||
IF NOT FOUND THEN RETURN NULL; END IF;
|
||||
SELECT lifecycle_status, owner_peer_type, owner_peer_id
|
||||
INTO saved_status, saved_owner_type, saved_owner_id
|
||||
FROM public.peer_star_gifts WHERE unique_gift_id=unique_id;
|
||||
IF NOT FOUND THEN RAISE EXCEPTION 'unique star gift missing saved aggregate'; END IF;
|
||||
IF gift_burned THEN
|
||||
IF saved_status <> 'burned' THEN RAISE EXCEPTION 'burned unique star gift has live saved aggregate'; END IF;
|
||||
ELSIF gift_owner_address <> '' THEN
|
||||
IF saved_status <> 'exported' THEN RAISE EXCEPTION 'exported unique star gift has non-exported saved aggregate'; END IF;
|
||||
ELSIF saved_status <> 'active' OR gift_owner_type IS DISTINCT FROM saved_owner_type OR gift_owner_id IS DISTINCT FROM saved_owner_id THEN
|
||||
RAISE EXCEPTION 'unique star gift owner mismatch';
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$;
|
||||
CREATE CONSTRAINT TRIGGER unique_star_gift_owner_guard
|
||||
AFTER INSERT OR UPDATE ON public.unique_star_gifts DEFERRABLE INITIALLY DEFERRED
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_check_unique_star_gift_owner();
|
||||
CREATE CONSTRAINT TRIGGER peer_unique_star_gift_owner_guard
|
||||
AFTER INSERT OR UPDATE ON public.peer_star_gifts DEFERRABLE INITIALLY DEFERRED
|
||||
FOR EACH ROW WHEN (NEW.unique_gift_id IS NOT NULL)
|
||||
EXECUTE FUNCTION public.telesrv_check_unique_star_gift_owner();
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
DROP INDEX IF EXISTS public.star_gift_auction_acquired_delivery_idx;
|
||||
DROP INDEX IF EXISTS public.star_gift_auctions_due_idx;
|
||||
DROP INDEX IF EXISTS public.star_gift_offers_resolution_outbox_idx;
|
||||
|
||||
ALTER TABLE public.star_gift_offers
|
||||
RENAME COLUMN resolution_notified TO expiry_notified;
|
||||
16
deploy/migrations/0096_star_gift_lifecycle_sweeper.up.sql
Normal file
16
deploy/migrations/0096_star_gift_lifecycle_sweeper.up.sql
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
-- Durable lifecycle sweep support. 0095 originally named this column after the
|
||||
-- first use case (expiry); cancelled offers use the same outbox boundary.
|
||||
ALTER TABLE public.star_gift_offers
|
||||
RENAME COLUMN expiry_notified TO resolution_notified;
|
||||
|
||||
CREATE INDEX star_gift_offers_resolution_outbox_idx
|
||||
ON public.star_gift_offers(id)
|
||||
WHERE status IN ('expired','cancelled') AND NOT resolution_notified;
|
||||
|
||||
CREATE INDEX star_gift_auctions_due_idx
|
||||
ON public.star_gift_auctions(status, next_round_at, gift_id)
|
||||
WHERE status IN ('pending','active');
|
||||
|
||||
CREATE INDEX star_gift_auction_acquired_delivery_idx
|
||||
ON public.star_gift_auction_acquired(gift_id, id)
|
||||
WHERE saved_gift_id IS NULL;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
DROP TABLE IF EXISTS public.star_gift_conversions;
|
||||
DROP TABLE IF EXISTS public.channel_stars_transactions;
|
||||
DROP TABLE IF EXISTS public.channel_stars_balances;
|
||||
40
deploy/migrations/0097_star_gift_peer_stars_ledger.up.sql
Normal file
40
deploy/migrations/0097_star_gift_peer_stars_ledger.up.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
-- Owner-scoped Stars revenue for channel Star Gifts. These are internal
|
||||
-- telesrv ledgers only; no blockchain, wallet, TON node or Fragment endpoint is
|
||||
-- contacted. Conversion is recorded as one aggregate terminal transition.
|
||||
|
||||
CREATE TABLE public.channel_stars_balances (
|
||||
channel_id bigint PRIMARY KEY,
|
||||
balance bigint DEFAULT 0 NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT channel_stars_balances_shape_check CHECK (channel_id>0 AND balance>=0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.channel_stars_transactions (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
channel_id bigint NOT NULL,
|
||||
actor_user_id bigint NOT NULL,
|
||||
amount bigint NOT NULL,
|
||||
reason text NOT NULL,
|
||||
peer_type text DEFAULT '' NOT NULL,
|
||||
peer_id bigint DEFAULT 0 NOT NULL,
|
||||
gift_id bigint,
|
||||
date integer NOT NULL,
|
||||
CONSTRAINT channel_stars_transactions_shape_check CHECK (
|
||||
channel_id>0 AND actor_user_id>0 AND amount<>0 AND date>0 AND
|
||||
((peer_type='' AND peer_id=0) OR (peer_type IN ('user','channel') AND peer_id>0)))
|
||||
);
|
||||
CREATE INDEX channel_stars_transactions_channel_idx
|
||||
ON public.channel_stars_transactions(channel_id,id DESC);
|
||||
|
||||
CREATE TABLE public.star_gift_conversions (
|
||||
saved_gift_id bigint PRIMARY KEY REFERENCES public.peer_star_gifts(id) ON DELETE RESTRICT,
|
||||
actor_user_id bigint NOT NULL,
|
||||
owner_peer_type text NOT NULL,
|
||||
owner_peer_id bigint NOT NULL,
|
||||
amount bigint NOT NULL,
|
||||
balance_after bigint NOT NULL,
|
||||
converted_at integer NOT NULL,
|
||||
CONSTRAINT star_gift_conversions_shape_check CHECK (
|
||||
actor_user_id>0 AND owner_peer_type IN ('user','channel') AND owner_peer_id>0 AND
|
||||
amount>=0 AND balance_after>=0 AND converted_at>0)
|
||||
);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS public.channel_ton_transactions;
|
||||
DROP TABLE IF EXISTS public.channel_ton_balances;
|
||||
25
deploy/migrations/0098_star_gift_channel_ton_ledger.up.sql
Normal file
25
deploy/migrations/0098_star_gift_channel_ton_ledger.up.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
-- TON-denominated channel marketplace proceeds remain a telesrv-local ledger.
|
||||
-- No wallet, chain node, smart contract, Fragment or external RPC is involved.
|
||||
CREATE TABLE public.channel_ton_balances (
|
||||
channel_id bigint PRIMARY KEY,
|
||||
balance_nanoton bigint DEFAULT 0 NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT channel_ton_balances_shape_check CHECK (channel_id>0 AND balance_nanoton>=0)
|
||||
);
|
||||
|
||||
CREATE TABLE public.channel_ton_transactions (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
channel_id bigint NOT NULL,
|
||||
actor_user_id bigint NOT NULL,
|
||||
amount_nanoton bigint NOT NULL,
|
||||
reason text NOT NULL,
|
||||
peer_type text DEFAULT '' NOT NULL,
|
||||
peer_id bigint DEFAULT 0 NOT NULL,
|
||||
gift_id bigint,
|
||||
date integer NOT NULL,
|
||||
CONSTRAINT channel_ton_transactions_shape_check CHECK (
|
||||
channel_id>0 AND actor_user_id>0 AND amount_nanoton<>0 AND date>0 AND
|
||||
((peer_type='' AND peer_id=0) OR (peer_type IN ('user','channel') AND peer_id>0)))
|
||||
);
|
||||
CREATE INDEX channel_ton_transactions_channel_idx
|
||||
ON public.channel_ton_transactions(channel_id,id DESC);
|
||||
20
deploy/migrations/0099_star_gift_signed_form_ids.down.sql
Normal file
20
deploy/migrations/0099_star_gift_signed_form_ids.down.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
ALTER TABLE public.star_gift_purchase_commands
|
||||
DROP CONSTRAINT star_gift_purchase_command_shape_check,
|
||||
ADD CONSTRAINT star_gift_purchase_command_shape_check CHECK (
|
||||
buyer_user_id>0 AND recipient_peer_type IN ('user','channel') AND recipient_peer_id>0 AND
|
||||
form_id>0 AND charge_stars>0 AND balance_after>=0 AND created_at>0) NOT VALID;
|
||||
|
||||
ALTER TABLE public.star_gift_prepaid_upgrade_commands
|
||||
DROP CONSTRAINT star_gift_prepaid_upgrade_command_shape_check,
|
||||
ADD CONSTRAINT star_gift_prepaid_upgrade_command_shape_check CHECK (
|
||||
payer_user_id>0 AND form_id>0 AND charge_stars>0 AND balance_after>=0 AND created_at>0) NOT VALID;
|
||||
|
||||
ALTER TABLE public.star_gift_drop_details_commands
|
||||
DROP CONSTRAINT star_gift_drop_details_command_shape_check,
|
||||
ADD CONSTRAINT star_gift_drop_details_command_shape_check CHECK (
|
||||
user_id>0 AND form_id>0 AND charge_stars>0 AND balance_after>=0 AND created_at>0) NOT VALID;
|
||||
|
||||
ALTER TABLE public.star_gift_auction_bid_payments
|
||||
DROP CONSTRAINT star_gift_auction_bid_payment_shape_check,
|
||||
ADD CONSTRAINT star_gift_auction_bid_payment_shape_check CHECK (
|
||||
user_id>0 AND form_id>0 AND bid_amount>0 AND balance_after>=0 AND created_at>0) NOT VALID;
|
||||
20
deploy/migrations/0099_star_gift_signed_form_ids.up.sql
Normal file
20
deploy/migrations/0099_star_gift_signed_form_ids.up.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
ALTER TABLE public.star_gift_purchase_commands
|
||||
DROP CONSTRAINT star_gift_purchase_command_shape_check,
|
||||
ADD CONSTRAINT star_gift_purchase_command_shape_check CHECK (
|
||||
buyer_user_id>0 AND recipient_peer_type IN ('user','channel') AND recipient_peer_id>0 AND
|
||||
form_id<>0 AND charge_stars>0 AND balance_after>=0 AND created_at>0);
|
||||
|
||||
ALTER TABLE public.star_gift_prepaid_upgrade_commands
|
||||
DROP CONSTRAINT star_gift_prepaid_upgrade_command_shape_check,
|
||||
ADD CONSTRAINT star_gift_prepaid_upgrade_command_shape_check CHECK (
|
||||
payer_user_id>0 AND form_id<>0 AND charge_stars>0 AND balance_after>=0 AND created_at>0);
|
||||
|
||||
ALTER TABLE public.star_gift_drop_details_commands
|
||||
DROP CONSTRAINT star_gift_drop_details_command_shape_check,
|
||||
ADD CONSTRAINT star_gift_drop_details_command_shape_check CHECK (
|
||||
user_id>0 AND form_id<>0 AND charge_stars>0 AND balance_after>=0 AND created_at>0);
|
||||
|
||||
ALTER TABLE public.star_gift_auction_bid_payments
|
||||
DROP CONSTRAINT star_gift_auction_bid_payment_shape_check,
|
||||
ADD CONSTRAINT star_gift_auction_bid_payment_shape_check CHECK (
|
||||
user_id>0 AND form_id<>0 AND bid_amount>0 AND balance_after>=0 AND created_at>0);
|
||||
35
deploy/migrations/0100_star_gift_upgrade_semantics.down.sql
Normal file
35
deploy/migrations/0100_star_gift_upgrade_semantics.down.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
UPDATE public.message_boxes
|
||||
SET media = jsonb_set(media, '{service_action,star_gift,upgrade_stars}', media #> '{service_action,star_gift,upgrade_price_stars}', true)
|
||||
#- '{service_action,star_gift,upgrade_price_stars}'
|
||||
WHERE media #>> '{service_action,kind}' = 'star_gift'
|
||||
AND media #> '{service_action,star_gift,upgrade_price_stars}' IS NOT NULL;
|
||||
|
||||
UPDATE public.channel_messages
|
||||
SET action = jsonb_set(action, '{StarGift,upgrade_stars}', action #> '{StarGift,upgrade_price_stars}', true)
|
||||
#- '{StarGift,upgrade_price_stars}'
|
||||
WHERE action #>> '{Type}' = 'star_gift'
|
||||
AND action #> '{StarGift,upgrade_price_stars}' IS NOT NULL;
|
||||
|
||||
UPDATE public.channel_admin_log_events
|
||||
SET message = jsonb_set(message, '{Action,StarGift,upgrade_stars}', message #> '{Action,StarGift,upgrade_price_stars}', true)
|
||||
#- '{Action,StarGift,upgrade_price_stars}'
|
||||
WHERE message #>> '{Action,Type}' = 'star_gift'
|
||||
AND message #> '{Action,StarGift,upgrade_price_stars}' IS NOT NULL;
|
||||
|
||||
UPDATE public.channel_admin_log_events
|
||||
SET prev_message = jsonb_set(prev_message, '{Action,StarGift,upgrade_stars}', prev_message #> '{Action,StarGift,upgrade_price_stars}', true)
|
||||
#- '{Action,StarGift,upgrade_price_stars}'
|
||||
WHERE prev_message #>> '{Action,Type}' = 'star_gift'
|
||||
AND prev_message #> '{Action,StarGift,upgrade_price_stars}' IS NOT NULL;
|
||||
|
||||
UPDATE public.channel_admin_log_events
|
||||
SET new_message = jsonb_set(new_message, '{Action,StarGift,upgrade_stars}', new_message #> '{Action,StarGift,upgrade_price_stars}', true)
|
||||
#- '{Action,StarGift,upgrade_price_stars}'
|
||||
WHERE new_message #>> '{Action,Type}' = 'star_gift'
|
||||
AND new_message #> '{Action,StarGift,upgrade_price_stars}' IS NOT NULL;
|
||||
|
||||
ALTER TABLE public.star_gift_upgrade_commands
|
||||
DROP CONSTRAINT IF EXISTS star_gift_upgrade_command_replay_shape_check,
|
||||
DROP COLUMN IF EXISTS keep_original_details,
|
||||
DROP COLUMN IF EXISTS require_prepaid,
|
||||
DROP COLUMN IF EXISTS charge_stars;
|
||||
79
deploy/migrations/0100_star_gift_upgrade_semantics.up.sql
Normal file
79
deploy/migrations/0100_star_gift_upgrade_semantics.up.sql
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
-- Split the two TL fields that TDesktop consumes differently:
|
||||
-- StarGift.upgrade_stars = current paid-upgrade price
|
||||
-- messageActionStarGift.upgrade_stars = amount already prepaid by sender
|
||||
-- Also persist the immutable command envelope required to replay an upgrade
|
||||
-- after the saved gift has entered its unique terminal state.
|
||||
|
||||
ALTER TABLE public.star_gift_upgrade_commands
|
||||
ADD COLUMN charge_stars bigint NOT NULL DEFAULT 0,
|
||||
ADD COLUMN require_prepaid boolean NOT NULL DEFAULT false,
|
||||
ADD COLUMN keep_original_details boolean NOT NULL DEFAULT false;
|
||||
|
||||
UPDATE public.star_gift_upgrade_commands c
|
||||
SET charge_stars = CASE WHEN c.form_id = 0 THEN 0 ELSE r.upgrade_stars END,
|
||||
require_prepaid = (c.form_id = 0),
|
||||
keep_original_details = u.keep_original_details
|
||||
FROM public.unique_star_gifts u
|
||||
JOIN public.star_gift_collectible_revisions r ON r.id = u.collectible_revision_id
|
||||
WHERE u.id = c.unique_gift_id;
|
||||
|
||||
ALTER TABLE public.star_gift_upgrade_commands
|
||||
ADD CONSTRAINT star_gift_upgrade_command_replay_shape_check CHECK (
|
||||
(require_prepaid AND form_id = 0 AND charge_stars = 0)
|
||||
OR
|
||||
(NOT require_prepaid AND form_id <> 0 AND charge_stars > 0)
|
||||
);
|
||||
|
||||
-- Private message boxes are the canonical durable snapshots used by history,
|
||||
-- live outbox delivery and updates.getDifference.
|
||||
UPDATE public.message_boxes
|
||||
SET media = CASE
|
||||
WHEN COALESCE((media #>> '{service_action,star_gift,prepaid_upgrade}')::boolean, false)
|
||||
THEN jsonb_set(media, '{service_action,star_gift,upgrade_price_stars}', media #> '{service_action,star_gift,upgrade_stars}', true)
|
||||
ELSE jsonb_set(media, '{service_action,star_gift,upgrade_price_stars}', media #> '{service_action,star_gift,upgrade_stars}', true)
|
||||
#- '{service_action,star_gift,upgrade_stars}'
|
||||
END
|
||||
WHERE media #>> '{service_action,kind}' = 'star_gift'
|
||||
AND media #> '{service_action,star_gift,upgrade_stars}' IS NOT NULL;
|
||||
|
||||
-- Channel service-message and Recent Actions snapshots use exported Go field
|
||||
-- names for their outer objects and the same snake_case StarGift payload.
|
||||
UPDATE public.channel_messages
|
||||
SET action = CASE
|
||||
WHEN COALESCE((action #>> '{StarGift,prepaid_upgrade}')::boolean, false)
|
||||
THEN jsonb_set(action, '{StarGift,upgrade_price_stars}', action #> '{StarGift,upgrade_stars}', true)
|
||||
ELSE jsonb_set(action, '{StarGift,upgrade_price_stars}', action #> '{StarGift,upgrade_stars}', true)
|
||||
#- '{StarGift,upgrade_stars}'
|
||||
END
|
||||
WHERE action #>> '{Type}' = 'star_gift'
|
||||
AND action #> '{StarGift,upgrade_stars}' IS NOT NULL;
|
||||
|
||||
UPDATE public.channel_admin_log_events
|
||||
SET message = CASE
|
||||
WHEN COALESCE((message #>> '{Action,StarGift,prepaid_upgrade}')::boolean, false)
|
||||
THEN jsonb_set(message, '{Action,StarGift,upgrade_price_stars}', message #> '{Action,StarGift,upgrade_stars}', true)
|
||||
ELSE jsonb_set(message, '{Action,StarGift,upgrade_price_stars}', message #> '{Action,StarGift,upgrade_stars}', true)
|
||||
#- '{Action,StarGift,upgrade_stars}'
|
||||
END
|
||||
WHERE message #>> '{Action,Type}' = 'star_gift'
|
||||
AND message #> '{Action,StarGift,upgrade_stars}' IS NOT NULL;
|
||||
|
||||
UPDATE public.channel_admin_log_events
|
||||
SET prev_message = CASE
|
||||
WHEN COALESCE((prev_message #>> '{Action,StarGift,prepaid_upgrade}')::boolean, false)
|
||||
THEN jsonb_set(prev_message, '{Action,StarGift,upgrade_price_stars}', prev_message #> '{Action,StarGift,upgrade_stars}', true)
|
||||
ELSE jsonb_set(prev_message, '{Action,StarGift,upgrade_price_stars}', prev_message #> '{Action,StarGift,upgrade_stars}', true)
|
||||
#- '{Action,StarGift,upgrade_stars}'
|
||||
END
|
||||
WHERE prev_message #>> '{Action,Type}' = 'star_gift'
|
||||
AND prev_message #> '{Action,StarGift,upgrade_stars}' IS NOT NULL;
|
||||
|
||||
UPDATE public.channel_admin_log_events
|
||||
SET new_message = CASE
|
||||
WHEN COALESCE((new_message #>> '{Action,StarGift,prepaid_upgrade}')::boolean, false)
|
||||
THEN jsonb_set(new_message, '{Action,StarGift,upgrade_price_stars}', new_message #> '{Action,StarGift,upgrade_stars}', true)
|
||||
ELSE jsonb_set(new_message, '{Action,StarGift,upgrade_price_stars}', new_message #> '{Action,StarGift,upgrade_stars}', true)
|
||||
#- '{Action,StarGift,upgrade_stars}'
|
||||
END
|
||||
WHERE new_message #>> '{Action,Type}' = 'star_gift'
|
||||
AND new_message #> '{Action,StarGift,upgrade_stars}' IS NOT NULL;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- The up migration emits user-visible durable update facts. Rewinding pts or
|
||||
-- deleting events that may already have been delivered would create a hole in
|
||||
-- updates.getDifference, so rollback intentionally preserves those facts.
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
-- Migration 0100 corrected the durable Star Gift service-message JSON, but a
|
||||
-- TDesktop that had already cached the old message would otherwise keep using
|
||||
-- the conflated outer upgrade_stars field. Publish one durable edit event for
|
||||
-- every private message box whose paid-upgrade price was split by 0100.
|
||||
--
|
||||
-- This is deliberately a pts event, not a cache-only notification: history,
|
||||
-- updates.getDifference and online outbox delivery must all expose the same
|
||||
-- corrected message snapshot. The migration transaction keeps the message
|
||||
-- pts, user watermark, durable event and dispatch task atomic.
|
||||
DO $$
|
||||
DECLARE
|
||||
gift_box record;
|
||||
next_pts integer;
|
||||
event_date integer := EXTRACT(EPOCH FROM clock_timestamp())::integer;
|
||||
BEGIN
|
||||
FOR gift_box IN
|
||||
SELECT owner_user_id, box_id, peer_type, peer_id
|
||||
FROM public.message_boxes
|
||||
WHERE media #>> '{service_action,kind}' = 'star_gift'
|
||||
AND jsonb_typeof(media #> '{service_action,star_gift,upgrade_price_stars}') = 'number'
|
||||
AND (media #>> '{service_action,star_gift,upgrade_price_stars}')::bigint > 0
|
||||
ORDER BY owner_user_id, box_id
|
||||
LOOP
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
VALUES (gift_box.owner_user_id, 0)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
UPDATE public.user_update_watermarks
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
WHERE user_id = gift_box.owner_user_id
|
||||
RETURNING contiguous_pts INTO next_pts;
|
||||
|
||||
UPDATE public.message_boxes
|
||||
SET pts = next_pts
|
||||
WHERE owner_user_id = gift_box.owner_user_id
|
||||
AND box_id = gift_box.box_id;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id,
|
||||
pts,
|
||||
pts_count,
|
||||
date,
|
||||
event_type,
|
||||
message_box_id,
|
||||
peer_type,
|
||||
peer_id
|
||||
) VALUES (
|
||||
gift_box.owner_user_id,
|
||||
next_pts,
|
||||
1,
|
||||
event_date,
|
||||
'edit_message',
|
||||
gift_box.box_id,
|
||||
gift_box.peer_type,
|
||||
gift_box.peer_id
|
||||
);
|
||||
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id,
|
||||
pts,
|
||||
event_type,
|
||||
exclude_auth_key_id,
|
||||
exclude_session_id
|
||||
) VALUES (
|
||||
gift_box.owner_user_id,
|
||||
next_pts,
|
||||
'edit_message',
|
||||
0,
|
||||
0
|
||||
);
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
1
deploy/migrations/0102_star_gift_purchase_forms.down.sql
Normal file
1
deploy/migrations/0102_star_gift_purchase_forms.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS public.star_gift_purchase_forms;
|
||||
23
deploy/migrations/0102_star_gift_purchase_forms.up.sql
Normal file
23
deploy/migrations/0102_star_gift_purchase_forms.up.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
CREATE TABLE public.star_gift_purchase_forms (
|
||||
buyer_user_id bigint NOT NULL,
|
||||
form_id bigint NOT NULL,
|
||||
gift_id bigint NOT NULL REFERENCES public.star_gift_catalog(gift_id) ON DELETE RESTRICT,
|
||||
revision_id bigint NOT NULL REFERENCES public.star_gift_catalog_revisions(id) ON DELETE RESTRICT,
|
||||
recipient_peer_type text NOT NULL,
|
||||
recipient_peer_id bigint NOT NULL,
|
||||
include_upgrade boolean DEFAULT false NOT NULL,
|
||||
hide_name boolean DEFAULT false NOT NULL,
|
||||
message text DEFAULT '' NOT NULL,
|
||||
charge_stars bigint NOT NULL,
|
||||
issued_at integer NOT NULL,
|
||||
expires_at integer NOT NULL,
|
||||
CONSTRAINT star_gift_purchase_forms_pkey PRIMARY KEY (buyer_user_id, form_id),
|
||||
CONSTRAINT star_gift_purchase_form_shape_check CHECK (
|
||||
buyer_user_id > 0 AND form_id <> 0 AND gift_id > 0 AND revision_id > 0 AND
|
||||
recipient_peer_type IN ('user', 'channel') AND recipient_peer_id > 0 AND
|
||||
charge_stars > 0 AND issued_at > 0 AND expires_at = issued_at + 600 AND
|
||||
char_length(message) <= 128)
|
||||
);
|
||||
|
||||
CREATE INDEX star_gift_purchase_forms_expiry_idx
|
||||
ON public.star_gift_purchase_forms (expires_at, buyer_user_id, form_id);
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-- Projection repair events are durable account history and are intentionally
|
||||
-- not erased on rollback. Only remove the replay receipt column.
|
||||
ALTER TABLE public.star_gift_upgrade_commands
|
||||
DROP CONSTRAINT IF EXISTS star_gift_upgrade_command_source_edit_pts_check,
|
||||
DROP COLUMN IF EXISTS source_edit_pts;
|
||||
191
deploy/migrations/0103_star_gift_upgrade_message_links.up.sql
Normal file
191
deploy/migrations/0103_star_gift_upgrade_message_links.up.sql
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
-- A user-owned upgraded gift has one stable protocol identity: the original
|
||||
-- gift service-message id. The unique service message points back to it via
|
||||
-- saved_id, while the original message points forward to the box-local unique
|
||||
-- service message via upgrade_msg_id. Both projections must be durable pts
|
||||
-- edits so history, live delivery and updates.getDifference agree.
|
||||
|
||||
ALTER TABLE public.star_gift_upgrade_commands
|
||||
ADD COLUMN source_edit_pts integer DEFAULT 0 NOT NULL,
|
||||
ADD CONSTRAINT star_gift_upgrade_command_source_edit_pts_check CHECK (source_edit_pts >= 0);
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
gift record;
|
||||
source_root record;
|
||||
upgrade_root record;
|
||||
pair record;
|
||||
next_pts integer;
|
||||
event_date integer := EXTRACT(EPOCH FROM clock_timestamp())::integer;
|
||||
repaired_source_media jsonb;
|
||||
repaired_unique_media jsonb;
|
||||
private_media jsonb;
|
||||
BEGIN
|
||||
FOR gift IN
|
||||
SELECT id, owner_peer_id, msg_id, upgrade_msg_id
|
||||
FROM public.peer_star_gifts
|
||||
WHERE owner_peer_type = 'user'
|
||||
AND unique_gift_id IS NOT NULL
|
||||
AND lifecycle_status = 'active'
|
||||
AND msg_id > 0
|
||||
AND upgrade_msg_id > 0
|
||||
ORDER BY id
|
||||
LOOP
|
||||
SELECT private_message_id, message_sender_id
|
||||
INTO STRICT source_root
|
||||
FROM public.message_boxes
|
||||
WHERE owner_user_id = gift.owner_peer_id
|
||||
AND box_id = gift.msg_id
|
||||
AND NOT deleted;
|
||||
|
||||
SELECT private_message_id, message_sender_id
|
||||
INTO STRICT upgrade_root
|
||||
FROM public.message_boxes
|
||||
WHERE owner_user_id = gift.owner_peer_id
|
||||
AND box_id = gift.upgrade_msg_id
|
||||
AND NOT deleted;
|
||||
|
||||
FOR pair IN
|
||||
SELECT source_box.owner_user_id,
|
||||
source_box.box_id AS source_box_id,
|
||||
source_box.peer_type AS source_peer_type,
|
||||
source_box.peer_id AS source_peer_id,
|
||||
source_box.media AS source_media,
|
||||
unique_box.box_id AS unique_box_id,
|
||||
unique_box.peer_type AS unique_peer_type,
|
||||
unique_box.peer_id AS unique_peer_id,
|
||||
unique_box.media AS unique_media
|
||||
FROM public.message_boxes source_box
|
||||
JOIN public.message_boxes unique_box
|
||||
ON unique_box.owner_user_id = source_box.owner_user_id
|
||||
AND unique_box.message_sender_id = upgrade_root.message_sender_id
|
||||
AND unique_box.private_message_id = upgrade_root.private_message_id
|
||||
AND NOT unique_box.deleted
|
||||
WHERE source_box.message_sender_id = source_root.message_sender_id
|
||||
AND source_box.private_message_id = source_root.private_message_id
|
||||
AND NOT source_box.deleted
|
||||
ORDER BY source_box.owner_user_id
|
||||
LOOP
|
||||
IF pair.source_media #>> '{service_action,kind}' <> 'star_gift' THEN
|
||||
RAISE EXCEPTION 'saved gift % source box % has invalid service action', gift.id, pair.source_box_id;
|
||||
END IF;
|
||||
IF pair.unique_media #>> '{service_action,kind}' <> 'star_gift_unique' THEN
|
||||
RAISE EXCEPTION 'saved gift % unique box % has invalid service action', gift.id, pair.unique_box_id;
|
||||
END IF;
|
||||
|
||||
repaired_source_media := jsonb_set(
|
||||
pair.source_media,
|
||||
'{service_action,star_gift,upgrade_msg_id}',
|
||||
to_jsonb(pair.unique_box_id::bigint),
|
||||
true
|
||||
) #- '{service_action,star_gift,can_upgrade}';
|
||||
repaired_unique_media := jsonb_set(
|
||||
pair.unique_media,
|
||||
'{service_action,star_gift_unique,saved_id}',
|
||||
to_jsonb(pair.source_box_id::bigint),
|
||||
true
|
||||
);
|
||||
|
||||
IF pair.source_media IS DISTINCT FROM repaired_source_media THEN
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
VALUES (pair.owner_user_id, 0)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
UPDATE public.user_update_watermarks
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
WHERE user_id = pair.owner_user_id
|
||||
RETURNING contiguous_pts INTO next_pts;
|
||||
|
||||
UPDATE public.message_boxes
|
||||
SET media = repaired_source_media,
|
||||
pts = next_pts
|
||||
WHERE owner_user_id = pair.owner_user_id
|
||||
AND box_id = pair.source_box_id
|
||||
AND NOT deleted;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id, pts, pts_count, date, event_type,
|
||||
message_box_id, peer_type, peer_id
|
||||
) VALUES (
|
||||
pair.owner_user_id, next_pts, 1, event_date, 'edit_message',
|
||||
pair.source_box_id, pair.source_peer_type, pair.source_peer_id
|
||||
);
|
||||
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id, pts, event_type,
|
||||
exclude_auth_key_id, exclude_session_id
|
||||
) VALUES (pair.owner_user_id, next_pts, 'edit_message', 0, 0);
|
||||
|
||||
IF pair.owner_user_id = gift.owner_peer_id THEN
|
||||
UPDATE public.star_gift_upgrade_commands
|
||||
SET source_edit_pts = next_pts
|
||||
WHERE source_saved_gift_id = gift.id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF pair.unique_media IS DISTINCT FROM repaired_unique_media THEN
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
VALUES (pair.owner_user_id, 0)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
UPDATE public.user_update_watermarks
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
WHERE user_id = pair.owner_user_id
|
||||
RETURNING contiguous_pts INTO next_pts;
|
||||
|
||||
UPDATE public.message_boxes
|
||||
SET media = repaired_unique_media,
|
||||
pts = next_pts
|
||||
WHERE owner_user_id = pair.owner_user_id
|
||||
AND box_id = pair.unique_box_id
|
||||
AND NOT deleted;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id, pts, pts_count, date, event_type,
|
||||
message_box_id, peer_type, peer_id
|
||||
) VALUES (
|
||||
pair.owner_user_id, next_pts, 1, event_date, 'edit_message',
|
||||
pair.unique_box_id, pair.unique_peer_type, pair.unique_peer_id
|
||||
);
|
||||
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id, pts, event_type,
|
||||
exclude_auth_key_id, exclude_session_id
|
||||
) VALUES (pair.owner_user_id, next_pts, 'edit_message', 0, 0);
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
SELECT media INTO STRICT private_media
|
||||
FROM public.message_boxes
|
||||
WHERE message_sender_id = source_root.message_sender_id
|
||||
AND private_message_id = source_root.private_message_id
|
||||
AND NOT deleted
|
||||
ORDER BY (owner_user_id = message_sender_id) DESC, owner_user_id
|
||||
LIMIT 1;
|
||||
UPDATE public.private_messages
|
||||
SET media = private_media
|
||||
WHERE sender_user_id = source_root.message_sender_id
|
||||
AND id = source_root.private_message_id;
|
||||
|
||||
SELECT media INTO STRICT private_media
|
||||
FROM public.message_boxes
|
||||
WHERE message_sender_id = upgrade_root.message_sender_id
|
||||
AND private_message_id = upgrade_root.private_message_id
|
||||
AND NOT deleted
|
||||
ORDER BY (owner_user_id = message_sender_id) DESC, owner_user_id
|
||||
LIMIT 1;
|
||||
UPDATE public.private_messages
|
||||
SET media = private_media
|
||||
WHERE sender_user_id = upgrade_root.message_sender_id
|
||||
AND id = upgrade_root.private_message_id;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM public.star_gift_upgrade_commands
|
||||
WHERE source_saved_gift_id = gift.id AND source_edit_pts <= 0
|
||||
) THEN
|
||||
RAISE EXCEPTION 'saved gift % is missing its owner source edit receipt', gift.id;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
-- This migration repairs invalid persisted capabilities and intentionally does
|
||||
-- not restore them on downgrade: advertising Craft without an official crafted
|
||||
-- model would reintroduce a user-visible operation that can never succeed.
|
||||
SELECT 1;
|
||||
109
deploy/migrations/0104_star_gift_craft_capability.up.sql
Normal file
109
deploy/migrations/0104_star_gift_craft_capability.up.sql
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
-- Craft is a protocol capability, not a generic property of every collectible.
|
||||
-- A non-zero craft_chance_permille is valid only when the immutable official
|
||||
-- collectible revision contains at least one crafted model. Earlier versions
|
||||
-- advertised the default chance for every upgrade, including official sets
|
||||
-- such as Fresh Socks whose snapshot has no crafted model at all.
|
||||
--
|
||||
-- Repair the aggregate and every durable private-message projection together.
|
||||
-- Each visible message edit advances pts and is recoverable through both live
|
||||
-- outbox delivery and updates.getDifference.
|
||||
DO $$
|
||||
DECLARE
|
||||
gift_box record;
|
||||
next_pts integer;
|
||||
event_date integer := EXTRACT(EPOCH FROM clock_timestamp())::integer;
|
||||
repaired_media jsonb;
|
||||
BEGIN
|
||||
UPDATE public.unique_star_gifts unique_gift
|
||||
SET craft_chance_permille = 0,
|
||||
updated_at = now()
|
||||
WHERE unique_gift.craft_chance_permille > 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.star_gift_collectible_models model
|
||||
WHERE model.collectible_revision_id = unique_gift.collectible_revision_id
|
||||
AND model.crafted
|
||||
);
|
||||
|
||||
UPDATE public.peer_star_gifts saved_gift
|
||||
SET can_craft_at = 0
|
||||
FROM public.unique_star_gifts unique_gift
|
||||
WHERE unique_gift.id = saved_gift.unique_gift_id
|
||||
AND unique_gift.craft_chance_permille = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.star_gift_collectible_models model
|
||||
WHERE model.collectible_revision_id = unique_gift.collectible_revision_id
|
||||
AND model.crafted
|
||||
);
|
||||
|
||||
FOR gift_box IN
|
||||
SELECT box.owner_user_id,
|
||||
box.box_id,
|
||||
box.peer_type,
|
||||
box.peer_id,
|
||||
box.message_sender_id,
|
||||
box.private_message_id,
|
||||
box.media
|
||||
FROM public.message_boxes box
|
||||
JOIN public.unique_star_gifts unique_gift
|
||||
ON unique_gift.id = (box.media #>> '{service_action,star_gift_unique,gift,ID}')::bigint
|
||||
WHERE box.media #>> '{service_action,kind}' = 'star_gift_unique'
|
||||
AND jsonb_typeof(box.media #> '{service_action,star_gift_unique,gift,CraftChancePermille}') = 'number'
|
||||
AND (box.media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}')::integer > 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM public.star_gift_collectible_models model
|
||||
WHERE model.collectible_revision_id = unique_gift.collectible_revision_id
|
||||
AND model.crafted
|
||||
)
|
||||
ORDER BY box.owner_user_id, box.box_id
|
||||
LOOP
|
||||
repaired_media := gift_box.media
|
||||
#- '{service_action,star_gift_unique,gift,CraftChancePermille}'
|
||||
#- '{service_action,star_gift_unique,can_craft_at}';
|
||||
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
VALUES (gift_box.owner_user_id, 0)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
UPDATE public.user_update_watermarks
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
WHERE user_id = gift_box.owner_user_id
|
||||
RETURNING contiguous_pts INTO next_pts;
|
||||
|
||||
UPDATE public.message_boxes
|
||||
SET media = repaired_media,
|
||||
pts = next_pts
|
||||
WHERE owner_user_id = gift_box.owner_user_id
|
||||
AND box_id = gift_box.box_id
|
||||
AND NOT deleted;
|
||||
|
||||
UPDATE public.private_messages
|
||||
SET media = media
|
||||
#- '{service_action,star_gift_unique,gift,CraftChancePermille}'
|
||||
#- '{service_action,star_gift_unique,can_craft_at}',
|
||||
sender_snapshot = sender_snapshot
|
||||
#- '{message,Media,service_action,star_gift_unique,gift,CraftChancePermille}'
|
||||
#- '{message,Media,service_action,star_gift_unique,can_craft_at}'
|
||||
WHERE sender_user_id = gift_box.message_sender_id
|
||||
AND id = gift_box.private_message_id;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id, pts, pts_count, date, event_type,
|
||||
message_box_id, peer_type, peer_id
|
||||
) VALUES (
|
||||
gift_box.owner_user_id, next_pts, 1, event_date, 'edit_message',
|
||||
gift_box.box_id, gift_box.peer_type, gift_box.peer_id
|
||||
);
|
||||
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id, pts, event_type,
|
||||
exclude_auth_key_id, exclude_session_id
|
||||
) VALUES (
|
||||
gift_box.owner_user_id, next_pts, 'edit_message', 0, 0
|
||||
);
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
ALTER TABLE public.star_gift_craft_commands
|
||||
DROP CONSTRAINT IF EXISTS star_gift_craft_source_edit_shape_check,
|
||||
DROP COLUMN IF EXISTS source_edit_pts;
|
||||
|
||||
-- Burned/crafted lifecycle facts and emitted edit events are authoritative
|
||||
-- business history. Downgrade intentionally does not resurrect consumed gifts.
|
||||
SELECT 1;
|
||||
195
deploy/migrations/0105_star_gift_craft_projection.up.sql
Normal file
195
deploy/migrations/0105_star_gift_craft_projection.up.sql
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
-- Craft outcomes consume their inputs permanently. Keep the aggregate, every
|
||||
-- visible messageActionStarGiftUnique snapshot, pts/outbox delivery and command
|
||||
-- replay receipt in one state model.
|
||||
ALTER TABLE public.star_gift_craft_commands
|
||||
ADD COLUMN source_edit_pts integer[] DEFAULT ARRAY[]::integer[] NOT NULL;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
gift_box record;
|
||||
next_pts integer;
|
||||
event_date integer := EXTRACT(EPOCH FROM clock_timestamp())::integer;
|
||||
repaired_media jsonb;
|
||||
BEGIN
|
||||
-- A successful legacy command would require reconstructing the crafted
|
||||
-- model snapshot, not merely flipping flags. Fail fast instead of silently
|
||||
-- fabricating that projection; the development database must be rebuilt or
|
||||
-- repaired explicitly if such a row ever exists.
|
||||
IF EXISTS (SELECT 1 FROM public.star_gift_craft_commands WHERE success) THEN
|
||||
RAISE EXCEPTION 'cannot migrate legacy successful craft command without an exact crafted message projection';
|
||||
END IF;
|
||||
|
||||
-- upgrade_msg_id means the current owner's unique service-message
|
||||
-- projection, not permanently the first upgrade message. Ownership moves
|
||||
-- replace msg_id with the new transfer/resale/offer message; repair rows
|
||||
-- written before that invariant was enforced.
|
||||
UPDATE public.peer_star_gifts saved_gift
|
||||
SET upgrade_msg_id = saved_gift.msg_id
|
||||
WHERE saved_gift.owner_peer_type = 'user'
|
||||
AND saved_gift.unique_gift_id IS NOT NULL
|
||||
AND saved_gift.msg_id > 0
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM public.message_boxes box
|
||||
WHERE box.owner_user_id = saved_gift.owner_peer_id
|
||||
AND box.box_id = saved_gift.msg_id
|
||||
AND NOT box.deleted
|
||||
AND box.media #>> '{service_action,kind}' = 'star_gift_unique'
|
||||
AND (box.media #>> '{service_action,star_gift_unique,gift,ID}')::bigint = saved_gift.unique_gift_id
|
||||
);
|
||||
|
||||
UPDATE public.peer_star_gifts
|
||||
SET upgrade_msg_id = 0
|
||||
WHERE owner_peer_type = 'channel'
|
||||
AND unique_gift_id IS NOT NULL
|
||||
AND upgrade_msg_id <> 0;
|
||||
|
||||
UPDATE public.unique_star_gifts
|
||||
SET burned = true,
|
||||
craft_chance_permille = 0,
|
||||
offer_min_stars = 0,
|
||||
updated_at = now()
|
||||
WHERE burned;
|
||||
|
||||
UPDATE public.peer_star_gifts
|
||||
SET lifecycle_status = 'burned',
|
||||
unsaved = true,
|
||||
pinned_order = 0,
|
||||
transfer_stars = 0,
|
||||
can_export_at = 0,
|
||||
can_transfer_at = 0,
|
||||
can_resell_at = 0,
|
||||
drop_original_details_stars = 0,
|
||||
can_craft_at = 0
|
||||
WHERE lifecycle_status = 'burned';
|
||||
|
||||
FOR gift_box IN
|
||||
SELECT box.owner_user_id,
|
||||
box.box_id,
|
||||
box.peer_type,
|
||||
box.peer_id,
|
||||
box.message_sender_id,
|
||||
box.private_message_id,
|
||||
box.media
|
||||
FROM public.message_boxes box
|
||||
JOIN public.unique_star_gifts unique_gift
|
||||
ON unique_gift.id = (box.media #>> '{service_action,star_gift_unique,gift,ID}')::bigint
|
||||
WHERE box.media #>> '{service_action,kind}' = 'star_gift_unique'
|
||||
AND unique_gift.burned
|
||||
AND (
|
||||
COALESCE((box.media #>> '{service_action,star_gift_unique,gift,Burned}')::boolean, false) = false
|
||||
OR COALESCE((box.media #>> '{service_action,star_gift_unique,gift,CraftChancePermille}')::integer, 0) <> 0
|
||||
OR box.media #> '{service_action,star_gift_unique,saved}' IS NOT NULL
|
||||
OR box.media #> '{service_action,star_gift_unique,can_craft_at}' IS NOT NULL
|
||||
)
|
||||
ORDER BY box.owner_user_id, box.box_id
|
||||
LOOP
|
||||
repaired_media := jsonb_set(
|
||||
jsonb_set(
|
||||
gift_box.media
|
||||
#- '{service_action,star_gift_unique,gift,CraftChancePermille}'
|
||||
#- '{service_action,star_gift_unique,saved}'
|
||||
#- '{service_action,star_gift_unique,can_export_at}'
|
||||
#- '{service_action,star_gift_unique,transfer_stars}'
|
||||
#- '{service_action,star_gift_unique,resale_amount}'
|
||||
#- '{service_action,star_gift_unique,can_transfer_at}'
|
||||
#- '{service_action,star_gift_unique,can_resell_at}'
|
||||
#- '{service_action,star_gift_unique,drop_original_details_stars}'
|
||||
#- '{service_action,star_gift_unique,can_craft_at}',
|
||||
'{service_action,star_gift_unique,gift,Burned}', 'true'::jsonb, true),
|
||||
'{service_action,star_gift_unique,gift,OfferMinStars}', '0'::jsonb, true);
|
||||
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
VALUES (gift_box.owner_user_id, 0)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
UPDATE public.user_update_watermarks
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
WHERE user_id = gift_box.owner_user_id
|
||||
RETURNING contiguous_pts INTO next_pts;
|
||||
|
||||
UPDATE public.message_boxes
|
||||
SET media = repaired_media,
|
||||
pts = next_pts
|
||||
WHERE owner_user_id = gift_box.owner_user_id
|
||||
AND box_id = gift_box.box_id
|
||||
AND NOT deleted;
|
||||
|
||||
UPDATE public.private_messages
|
||||
SET media = jsonb_set(
|
||||
jsonb_set(
|
||||
media
|
||||
#- '{service_action,star_gift_unique,gift,CraftChancePermille}'
|
||||
#- '{service_action,star_gift_unique,saved}'
|
||||
#- '{service_action,star_gift_unique,can_export_at}'
|
||||
#- '{service_action,star_gift_unique,transfer_stars}'
|
||||
#- '{service_action,star_gift_unique,resale_amount}'
|
||||
#- '{service_action,star_gift_unique,can_transfer_at}'
|
||||
#- '{service_action,star_gift_unique,can_resell_at}'
|
||||
#- '{service_action,star_gift_unique,drop_original_details_stars}'
|
||||
#- '{service_action,star_gift_unique,can_craft_at}',
|
||||
'{service_action,star_gift_unique,gift,Burned}', 'true'::jsonb, true),
|
||||
'{service_action,star_gift_unique,gift,OfferMinStars}', '0'::jsonb, true),
|
||||
sender_snapshot = jsonb_set(
|
||||
jsonb_set(
|
||||
sender_snapshot
|
||||
#- '{message,Media,service_action,star_gift_unique,gift,CraftChancePermille}'
|
||||
#- '{message,Media,service_action,star_gift_unique,saved}'
|
||||
#- '{message,Media,service_action,star_gift_unique,can_export_at}'
|
||||
#- '{message,Media,service_action,star_gift_unique,transfer_stars}'
|
||||
#- '{message,Media,service_action,star_gift_unique,resale_amount}'
|
||||
#- '{message,Media,service_action,star_gift_unique,can_transfer_at}'
|
||||
#- '{message,Media,service_action,star_gift_unique,can_resell_at}'
|
||||
#- '{message,Media,service_action,star_gift_unique,drop_original_details_stars}'
|
||||
#- '{message,Media,service_action,star_gift_unique,can_craft_at}',
|
||||
'{message,Media,service_action,star_gift_unique,gift,Burned}', 'true'::jsonb, true),
|
||||
'{message,Media,service_action,star_gift_unique,gift,OfferMinStars}', '0'::jsonb, true)
|
||||
WHERE sender_user_id = gift_box.message_sender_id
|
||||
AND id = gift_box.private_message_id;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id, pts, pts_count, date, event_type,
|
||||
message_box_id, peer_type, peer_id
|
||||
) VALUES (
|
||||
gift_box.owner_user_id, next_pts, 1, event_date, 'edit_message',
|
||||
gift_box.box_id, gift_box.peer_type, gift_box.peer_id
|
||||
);
|
||||
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id, pts, event_type,
|
||||
exclude_auth_key_id, exclude_session_id
|
||||
) VALUES (
|
||||
gift_box.owner_user_id, next_pts, 'edit_message', 0, 0
|
||||
);
|
||||
END LOOP;
|
||||
|
||||
UPDATE public.star_gift_craft_commands command
|
||||
SET source_edit_pts = (
|
||||
SELECT array_agg(box.pts ORDER BY input.ordinality)::integer[] AS pts
|
||||
FROM unnest(command.input_unique_gift_ids) WITH ORDINALITY AS input(unique_gift_id, ordinality)
|
||||
JOIN public.unique_star_gifts unique_gift ON unique_gift.id = input.unique_gift_id
|
||||
JOIN public.peer_star_gifts saved_gift ON saved_gift.id = unique_gift.source_saved_gift_id
|
||||
JOIN public.message_boxes box
|
||||
ON box.owner_user_id = command.user_id
|
||||
AND box.box_id = saved_gift.upgrade_msg_id
|
||||
AND NOT box.deleted
|
||||
)
|
||||
WHERE NOT command.success;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM public.star_gift_craft_commands
|
||||
WHERE cardinality(source_edit_pts) <> cardinality(input_unique_gift_ids)
|
||||
OR array_position(source_edit_pts, 0) IS NOT NULL
|
||||
) THEN
|
||||
RAISE EXCEPTION 'craft command is missing an exact source message edit receipt';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
ALTER TABLE public.star_gift_craft_commands
|
||||
ADD CONSTRAINT star_gift_craft_source_edit_shape_check CHECK (
|
||||
cardinality(source_edit_pts) = cardinality(input_unique_gift_ids)
|
||||
AND array_position(source_edit_pts, 0) IS NULL
|
||||
);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
DROP INDEX IF EXISTS public.peer_star_gifts_owner_profile_order_idx;
|
||||
DROP INDEX IF EXISTS public.peer_star_gifts_owner_pinned_order_uniq;
|
||||
13
deploy/migrations/0106_star_gift_profile_pin_order.up.sql
Normal file
13
deploy/migrations/0106_star_gift_profile_pin_order.up.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
CREATE UNIQUE INDEX peer_star_gifts_owner_pinned_order_uniq
|
||||
ON public.peer_star_gifts(owner_peer_type, owner_peer_id, pinned_order)
|
||||
WHERE pinned_order > 0;
|
||||
|
||||
CREATE INDEX peer_star_gifts_owner_profile_order_idx
|
||||
ON public.peer_star_gifts(
|
||||
owner_peer_type,
|
||||
owner_peer_id,
|
||||
(pinned_order = 0),
|
||||
pinned_order,
|
||||
id DESC
|
||||
)
|
||||
WHERE lifecycle_status = 'active';
|
||||
25
deploy/migrations/0107_account_lifecycle.down.sql
Normal file
25
deploy/migrations/0107_account_lifecycle.down.sql
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
DROP TRIGGER IF EXISTS account_settings_ttl_trigger ON public.account_settings;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_account_settings_ttl_trigger();
|
||||
DROP TRIGGER IF EXISTS users_account_delete_at_trigger ON public.users;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_users_account_delete_at_trigger();
|
||||
DROP FUNCTION IF EXISTS public.telesrv_account_delete_at(timestamp with time zone, bigint, integer);
|
||||
|
||||
DROP TRIGGER IF EXISTS account_passwords_changed_at_trigger ON public.account_passwords;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_password_changed_at_trigger();
|
||||
ALTER TABLE public.account_passwords DROP COLUMN IF EXISTS password_changed_at;
|
||||
|
||||
ALTER TABLE public.account_settings
|
||||
DROP CONSTRAINT account_settings_account_ttl_days_check,
|
||||
ADD CONSTRAINT account_settings_account_ttl_days_check CHECK (account_ttl_days > 0);
|
||||
|
||||
DROP TABLE IF EXISTS public.account_deletion_notifications;
|
||||
DROP TABLE IF EXISTS public.account_deletion_requests;
|
||||
DROP INDEX IF EXISTS public.dialogs_user_peer_reverse_idx;
|
||||
|
||||
DROP INDEX IF EXISTS public.users_account_delete_due_idx;
|
||||
ALTER TABLE public.users DROP CONSTRAINT IF EXISTS users_deletion_state_check;
|
||||
ALTER TABLE public.users
|
||||
DROP COLUMN IF EXISTS account_delete_at,
|
||||
DROP COLUMN IF EXISTS deletion_reason,
|
||||
DROP COLUMN IF EXISTS deletion_source,
|
||||
DROP COLUMN IF EXISTS deleted_at;
|
||||
216
deploy/migrations/0107_account_lifecycle.up.sql
Normal file
216
deploy/migrations/0107_account_lifecycle.up.sql
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
-- Unified account deletion lifecycle. A deleted account remains as a minimal
|
||||
-- user tombstone so historical messages keep a stable sender id, while all
|
||||
-- reusable identity and profile fields are released atomically.
|
||||
ALTER TABLE public.users
|
||||
ADD COLUMN deleted_at timestamp with time zone,
|
||||
ADD COLUMN deletion_source text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN deletion_reason text DEFAULT '' NOT NULL,
|
||||
ADD COLUMN account_delete_at timestamp with time zone;
|
||||
|
||||
ALTER TABLE public.account_passwords
|
||||
ADD COLUMN password_changed_at timestamp with time zone;
|
||||
|
||||
ALTER TABLE public.account_settings
|
||||
DROP CONSTRAINT account_settings_account_ttl_days_check,
|
||||
ADD CONSTRAINT account_settings_account_ttl_days_check
|
||||
CHECK (account_ttl_days BETWEEN 1 AND 3650);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_password_changed_at_trigger()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
NEW.password_changed_at := CASE WHEN NEW.has_password THEN now() ELSE NULL END;
|
||||
ELSIF NEW.has_password IS DISTINCT FROM OLD.has_password
|
||||
OR NEW.srp_verifier IS DISTINCT FROM OLD.srp_verifier
|
||||
OR NEW.current_algo_salt1 IS DISTINCT FROM OLD.current_algo_salt1
|
||||
OR NEW.current_algo_salt2 IS DISTINCT FROM OLD.current_algo_salt2
|
||||
OR NEW.current_algo_g IS DISTINCT FROM OLD.current_algo_g
|
||||
OR NEW.current_algo_p IS DISTINCT FROM OLD.current_algo_p THEN
|
||||
NEW.password_changed_at := CASE WHEN NEW.has_password THEN now() ELSE NULL END;
|
||||
ELSE
|
||||
NEW.password_changed_at := OLD.password_changed_at;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;
|
||||
|
||||
UPDATE public.account_passwords
|
||||
SET password_changed_at = updated_at
|
||||
WHERE has_password = true AND password_changed_at IS NULL;
|
||||
|
||||
CREATE TRIGGER account_passwords_changed_at_trigger
|
||||
BEFORE INSERT OR UPDATE ON public.account_passwords
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_password_changed_at_trigger();
|
||||
|
||||
ALTER TABLE public.users
|
||||
ADD CONSTRAINT users_deletion_state_check CHECK (
|
||||
(deleted_at IS NULL AND deletion_source = '' AND deletion_reason = '')
|
||||
OR
|
||||
(deleted_at IS NOT NULL
|
||||
AND deletion_source IN (
|
||||
'manual',
|
||||
'forgot_password',
|
||||
'tos_decline',
|
||||
'password_reset_expiry',
|
||||
'account_ttl',
|
||||
'freeze_expiry'
|
||||
)
|
||||
AND account_delete_at IS NULL
|
||||
AND phone = ''
|
||||
AND first_name = ''
|
||||
AND last_name = ''
|
||||
AND username = ''
|
||||
AND country_code = ''
|
||||
AND about = ''
|
||||
AND verified = false
|
||||
AND support = false
|
||||
AND premium_expires_at IS NULL
|
||||
AND emoji_status_document_id = 0
|
||||
AND emoji_status_until = 0
|
||||
AND color_set = false
|
||||
AND color = 0
|
||||
AND color_background_emoji_id = 0
|
||||
AND profile_color_set = false
|
||||
AND profile_color = 0
|
||||
AND profile_color_background_emoji_id = 0
|
||||
AND birthday_day = 0
|
||||
AND birthday_month = 0
|
||||
AND birthday_year = 0
|
||||
AND personal_channel_id = 0
|
||||
AND last_seen_at = 0
|
||||
AND octet_length(deletion_reason) <= 1024)
|
||||
);
|
||||
|
||||
CREATE INDEX users_account_delete_due_idx
|
||||
ON public.users (account_delete_at, id)
|
||||
WHERE deleted_at IS NULL AND is_bot = false AND account_delete_at IS NOT NULL;
|
||||
|
||||
CREATE TABLE public.account_deletion_requests (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
user_id bigint NOT NULL REFERENCES public.users(id),
|
||||
requester_auth_key_id bigint NOT NULL,
|
||||
state text DEFAULT 'pending' NOT NULL,
|
||||
reason text DEFAULT '' NOT NULL,
|
||||
confirm_hash_digest bytea NOT NULL,
|
||||
requested_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
execute_at timestamp with time zone NOT NULL,
|
||||
completed_at timestamp with time zone,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT account_deletion_requests_state_check CHECK (
|
||||
(state = 'pending' AND completed_at IS NULL)
|
||||
OR (state IN ('cancelled', 'executed') AND completed_at IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT account_deletion_requests_reason_check CHECK (octet_length(reason) <= 1024),
|
||||
CONSTRAINT account_deletion_requests_hash_check CHECK (octet_length(confirm_hash_digest) = 32)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX account_deletion_requests_one_pending_user_idx
|
||||
ON public.account_deletion_requests(user_id) WHERE state = 'pending';
|
||||
CREATE UNIQUE INDEX account_deletion_requests_pending_hash_idx
|
||||
ON public.account_deletion_requests(confirm_hash_digest) WHERE state = 'pending';
|
||||
CREATE INDEX account_deletion_requests_due_idx
|
||||
ON public.account_deletion_requests(execute_at, id) WHERE state = 'pending';
|
||||
|
||||
-- updateUser is not a pts-bearing update. Keep a dedicated durable online-nudge
|
||||
-- queue so a crash between tombstone commit and best-effort fan-out is recovered.
|
||||
-- Offline clients converge from authoritative dialog/history hydration instead
|
||||
-- of an immortal retry queue or invented user pts events.
|
||||
CREATE TABLE public.account_deletion_notifications (
|
||||
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
target_user_id bigint NOT NULL REFERENCES public.users(id),
|
||||
deleted_user_id bigint NOT NULL REFERENCES public.users(id),
|
||||
status text DEFAULT 'pending' NOT NULL,
|
||||
attempts integer DEFAULT 0 NOT NULL,
|
||||
next_attempt_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
lease_until timestamp with time zone,
|
||||
last_error text DEFAULT '' NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT account_deletion_notifications_status_check
|
||||
CHECK (status IN ('pending', 'dispatching', 'delivered')),
|
||||
CONSTRAINT account_deletion_notifications_attempts_check CHECK (attempts >= 0),
|
||||
CONSTRAINT account_deletion_notifications_not_self_check CHECK (target_user_id <> deleted_user_id),
|
||||
UNIQUE (target_user_id, deleted_user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX account_deletion_notifications_ready_idx
|
||||
ON public.account_deletion_notifications(next_attempt_at, id)
|
||||
WHERE status = 'pending';
|
||||
|
||||
-- Account deletion discovers peers that have an inbound dialog row pointing at
|
||||
-- the deleted user. The ordinary dialog PK only covers the owner direction;
|
||||
-- keep the reverse lookup indexed so tombstoning one account never scans every
|
||||
-- user's dialog table.
|
||||
CREATE INDEX dialogs_user_peer_reverse_idx
|
||||
ON public.dialogs(peer_id, user_id)
|
||||
WHERE peer_type = 'user';
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_account_delete_at(
|
||||
p_created_at timestamp with time zone,
|
||||
p_last_seen_at bigint,
|
||||
p_ttl_days integer
|
||||
) RETURNS timestamp with time zone
|
||||
LANGUAGE sql IMMUTABLE AS $$
|
||||
SELECT GREATEST(
|
||||
p_created_at,
|
||||
CASE WHEN p_last_seen_at > 0 THEN to_timestamp(p_last_seen_at) ELSE p_created_at END
|
||||
) + make_interval(days => p_ttl_days)
|
||||
$$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_users_account_delete_at_trigger()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
v_ttl_days integer;
|
||||
BEGIN
|
||||
IF NEW.deleted_at IS NOT NULL OR NEW.is_bot THEN
|
||||
NEW.account_delete_at := NULL;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
SELECT account_ttl_days INTO v_ttl_days
|
||||
FROM public.account_settings WHERE user_id = NEW.id;
|
||||
NEW.account_delete_at := public.telesrv_account_delete_at(
|
||||
NEW.created_at,
|
||||
NEW.last_seen_at,
|
||||
COALESCE(v_ttl_days, 365)
|
||||
);
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER users_account_delete_at_trigger
|
||||
BEFORE INSERT OR UPDATE OF last_seen_at, deleted_at, is_bot
|
||||
ON public.users
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_users_account_delete_at_trigger();
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_account_settings_ttl_trigger()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
v_user_id bigint;
|
||||
v_ttl_days integer;
|
||||
BEGIN
|
||||
v_user_id := COALESCE(NEW.user_id, OLD.user_id);
|
||||
v_ttl_days := CASE WHEN TG_OP = 'DELETE' THEN 365 ELSE NEW.account_ttl_days END;
|
||||
UPDATE public.users
|
||||
SET account_delete_at = public.telesrv_account_delete_at(created_at, last_seen_at, v_ttl_days),
|
||||
updated_at = now()
|
||||
WHERE id = v_user_id AND deleted_at IS NULL AND is_bot = false;
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER account_settings_ttl_trigger
|
||||
AFTER INSERT OR UPDATE OF account_ttl_days OR DELETE
|
||||
ON public.account_settings
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_account_settings_ttl_trigger();
|
||||
|
||||
UPDATE public.users u
|
||||
SET account_delete_at = public.telesrv_account_delete_at(
|
||||
u.created_at,
|
||||
u.last_seen_at,
|
||||
COALESCE((
|
||||
SELECT s.account_ttl_days
|
||||
FROM public.account_settings s
|
||||
WHERE s.user_id = u.id
|
||||
), 365)
|
||||
)
|
||||
WHERE u.deleted_at IS NULL AND u.is_bot = false;
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE channel_messages DROP COLUMN IF EXISTS suggested_post;
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
ALTER TABLE channel_messages
|
||||
ADD COLUMN suggested_post jsonb NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
||||
-- A monoforum is a virtual per-saved-peer container, never an ordinary joined megagroup.
|
||||
DELETE FROM channel_dialogs d
|
||||
USING channels c
|
||||
WHERE d.channel_id = c.id AND c.monoforum;
|
||||
|
||||
DELETE FROM user_channel_member_index i
|
||||
USING channels c
|
||||
WHERE i.channel_id = c.id AND c.monoforum;
|
||||
|
||||
DELETE FROM channel_members m
|
||||
USING channels c
|
||||
WHERE m.channel_id = c.id AND c.monoforum;
|
||||
|
||||
UPDATE channels c
|
||||
SET participants_count = 0,
|
||||
admins_count = 0,
|
||||
updated_at = now()
|
||||
WHERE c.monoforum AND (c.participants_count <> 0 OR c.admins_count <> 0);
|
||||
|
||||
-- Older generic sends from non-admin users are deterministically their own saved-peer dialog.
|
||||
UPDATE channel_messages m
|
||||
SET saved_peer_type = 'user',
|
||||
saved_peer_id = m.sender_user_id
|
||||
FROM channels mono
|
||||
WHERE mono.id = m.channel_id
|
||||
AND mono.monoforum
|
||||
AND NOT m.deleted
|
||||
AND m.saved_peer_id = 0
|
||||
AND m.action = '{}'::jsonb
|
||||
AND m.sender_user_id <> 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM channel_members parent_member
|
||||
WHERE parent_member.channel_id = mono.linked_monoforum_id
|
||||
AND parent_member.user_id = m.sender_user_id
|
||||
AND parent_member.status = 'active'
|
||||
AND parent_member.role IN ('creator', 'admin')
|
||||
);
|
||||
|
||||
UPDATE channel_update_events e
|
||||
SET payload = jsonb_set(
|
||||
e.payload,
|
||||
'{message,SavedPeer}',
|
||||
jsonb_build_object('Type', 'user', 'ID', m.sender_user_id),
|
||||
true
|
||||
)
|
||||
FROM channel_messages m, channels mono
|
||||
WHERE mono.id = m.channel_id
|
||||
AND mono.monoforum
|
||||
AND e.channel_id = m.channel_id
|
||||
AND e.message_id = m.id
|
||||
AND m.saved_peer_type = 'user'
|
||||
AND m.saved_peer_id = m.sender_user_id
|
||||
AND COALESCE((e.payload #>> '{message,SavedPeer,ID}')::bigint, 0) = 0;
|
||||
|
||||
UPDATE channel_messages m
|
||||
SET send_snapshot = jsonb_set(
|
||||
m.send_snapshot,
|
||||
'{message,SavedPeer}',
|
||||
jsonb_build_object('Type', 'user', 'ID', m.sender_user_id),
|
||||
true
|
||||
)
|
||||
FROM channels mono
|
||||
WHERE mono.id = m.channel_id
|
||||
AND mono.monoforum
|
||||
AND m.random_id <> 0
|
||||
AND m.saved_peer_type = 'user'
|
||||
AND m.saved_peer_id = m.sender_user_id
|
||||
AND COALESCE((m.send_snapshot #>> '{message,SavedPeer,ID}')::bigint, 0) = 0;
|
||||
|
||||
-- Remove the impossible join service message without creating a pts gap: retain the event row as noop.
|
||||
UPDATE channel_update_events e
|
||||
SET event_type = 'noop', message_id = 0, sender_user_id = 0, user_ids = '[]'::jsonb, payload = '{}'::jsonb
|
||||
FROM channel_messages m, channels mono
|
||||
WHERE mono.id = m.channel_id
|
||||
AND mono.monoforum
|
||||
AND e.channel_id = m.channel_id
|
||||
AND e.message_id = m.id
|
||||
AND m.action->>'Type' = 'chat_joined';
|
||||
|
||||
UPDATE channel_messages m
|
||||
SET deleted = true
|
||||
FROM channels mono
|
||||
WHERE mono.id = m.channel_id
|
||||
AND mono.monoforum
|
||||
AND m.action->>'Type' = 'chat_joined';
|
||||
|
||||
UPDATE channels mono
|
||||
SET top_message_id = COALESCE((
|
||||
SELECT max(m.id)
|
||||
FROM channel_messages m
|
||||
WHERE m.channel_id = mono.id AND NOT m.deleted
|
||||
), 0),
|
||||
updated_at = now()
|
||||
WHERE mono.monoforum;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE public.channel_messages
|
||||
DROP CONSTRAINT IF EXISTS channel_messages_paid_message_stars_check,
|
||||
DROP COLUMN IF EXISTS paid_message_stars;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE public.channel_messages
|
||||
ADD COLUMN paid_message_stars bigint NOT NULL DEFAULT 0,
|
||||
ADD CONSTRAINT channel_messages_paid_message_stars_check CHECK (paid_message_stars >= 0);
|
||||
20
deploy/migrations/0110_bot_api_callback_queries.down.sql
Normal file
20
deploy/migrations/0110_bot_api_callback_queries.down.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
DELETE FROM public.bot_api_updates WHERE update_kind = 'callback_query';
|
||||
|
||||
DROP INDEX IF EXISTS public.bot_api_updates_callback_query_unique;
|
||||
DROP INDEX IF EXISTS public.bot_api_updates_message_source_unique;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
DROP CONSTRAINT bot_api_updates_callback_shape_check,
|
||||
DROP CONSTRAINT bot_api_updates_kind_check;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
ADD CONSTRAINT bot_api_updates_kind_check
|
||||
CHECK ((update_kind)::text = ANY (ARRAY['message'::text, 'edited_message'::text])),
|
||||
ADD CONSTRAINT bot_api_updates_source_unique
|
||||
UNIQUE (bot_user_id, update_kind, peer_type, peer_id, message_id, source_pts);
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
DROP COLUMN callback_data,
|
||||
DROP COLUMN callback_chat_instance,
|
||||
DROP COLUMN callback_user_id,
|
||||
DROP COLUMN callback_query_id;
|
||||
35
deploy/migrations/0110_bot_api_callback_queries.up.sql
Normal file
35
deploy/migrations/0110_bot_api_callback_queries.up.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
ALTER TABLE public.bot_api_updates
|
||||
ADD COLUMN callback_query_id bigint NOT NULL DEFAULT 0,
|
||||
ADD COLUMN callback_user_id bigint NOT NULL DEFAULT 0,
|
||||
ADD COLUMN callback_chat_instance bigint NOT NULL DEFAULT 0,
|
||||
ADD COLUMN callback_data bytea;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
DROP CONSTRAINT bot_api_updates_kind_check,
|
||||
DROP CONSTRAINT bot_api_updates_source_unique;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
ADD CONSTRAINT bot_api_updates_kind_check
|
||||
CHECK ((update_kind)::text = ANY (ARRAY['message'::text, 'edited_message'::text, 'callback_query'::text])),
|
||||
ADD CONSTRAINT bot_api_updates_callback_shape_check CHECK (
|
||||
(update_kind = 'callback_query'
|
||||
AND callback_query_id <> 0
|
||||
AND callback_user_id > 0
|
||||
AND callback_chat_instance <> 0
|
||||
AND COALESCE(octet_length(callback_data), 0) <= 64
|
||||
AND source_pts = 0)
|
||||
OR
|
||||
(update_kind IN ('message', 'edited_message')
|
||||
AND callback_query_id = 0
|
||||
AND callback_user_id = 0
|
||||
AND callback_chat_instance = 0
|
||||
AND callback_data IS NULL)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX bot_api_updates_message_source_unique
|
||||
ON public.bot_api_updates (bot_user_id, update_kind, peer_type, peer_id, message_id, source_pts)
|
||||
WHERE update_kind IN ('message', 'edited_message');
|
||||
|
||||
CREATE UNIQUE INDEX bot_api_updates_callback_query_unique
|
||||
ON public.bot_api_updates (bot_user_id, callback_query_id)
|
||||
WHERE update_kind = 'callback_query';
|
||||
6
deploy/migrations/0111_bot_api_polling_state.down.sql
Normal file
6
deploy/migrations/0111_bot_api_polling_state.down.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
DROP INDEX IF EXISTS public.bot_api_updates_created_retention_idx;
|
||||
|
||||
ALTER TABLE public.bot_api_update_states
|
||||
DROP CONSTRAINT IF EXISTS bot_api_update_states_allowed_updates_check,
|
||||
DROP COLUMN IF EXISTS cursor_initialized,
|
||||
DROP COLUMN IF EXISTS allowed_updates;
|
||||
12
deploy/migrations/0111_bot_api_polling_state.up.sql
Normal file
12
deploy/migrations/0111_bot_api_polling_state.up.sql
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
ALTER TABLE public.bot_api_update_states
|
||||
ADD COLUMN allowed_updates text[],
|
||||
ADD COLUMN cursor_initialized boolean NOT NULL DEFAULT false;
|
||||
|
||||
ALTER TABLE public.bot_api_update_states
|
||||
ADD CONSTRAINT bot_api_update_states_allowed_updates_check CHECK (
|
||||
allowed_updates IS NULL
|
||||
OR array_position(allowed_updates, NULL) IS NULL
|
||||
);
|
||||
|
||||
CREATE INDEX bot_api_updates_created_retention_idx
|
||||
ON public.bot_api_updates (created_at, id);
|
||||
4
deploy/migrations/0112_bot_api_poll_lease.down.sql
Normal file
4
deploy/migrations/0112_bot_api_poll_lease.down.sql
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE public.bot_api_update_states
|
||||
DROP CONSTRAINT IF EXISTS bot_api_update_states_poll_lease_check,
|
||||
DROP COLUMN IF EXISTS poll_expires_at,
|
||||
DROP COLUMN IF EXISTS poll_owner;
|
||||
10
deploy/migrations/0112_bot_api_poll_lease.up.sql
Normal file
10
deploy/migrations/0112_bot_api_poll_lease.up.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
ALTER TABLE public.bot_api_update_states
|
||||
ADD COLUMN poll_owner text NOT NULL DEFAULT '',
|
||||
ADD COLUMN poll_expires_at timestamptz;
|
||||
|
||||
ALTER TABLE public.bot_api_update_states
|
||||
ADD CONSTRAINT bot_api_update_states_poll_lease_check CHECK (
|
||||
(poll_owner = '' AND poll_expires_at IS NULL)
|
||||
OR
|
||||
(poll_owner <> '' AND poll_expires_at IS NOT NULL)
|
||||
);
|
||||
27
deploy/migrations/0113_bot_api_inline_callbacks.down.sql
Normal file
27
deploy/migrations/0113_bot_api_inline_callbacks.down.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
DELETE FROM public.bot_api_updates
|
||||
WHERE update_kind = 'callback_query' AND callback_inline_message_id <> 0;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
DROP CONSTRAINT bot_api_updates_callback_shape_check,
|
||||
DROP CONSTRAINT bot_api_updates_peer_type_check,
|
||||
DROP CONSTRAINT bot_api_updates_peer_id_check,
|
||||
DROP CONSTRAINT bot_api_updates_message_id_check;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
ADD CONSTRAINT bot_api_updates_peer_type_check CHECK (peer_type IN ('user', 'channel')),
|
||||
ADD CONSTRAINT bot_api_updates_peer_id_check CHECK (peer_id > 0),
|
||||
ADD CONSTRAINT bot_api_updates_message_id_check CHECK (message_id > 0),
|
||||
ADD CONSTRAINT bot_api_updates_callback_shape_check CHECK (
|
||||
(update_kind = 'callback_query' AND callback_query_id <> 0 AND callback_user_id > 0
|
||||
AND callback_chat_instance <> 0 AND COALESCE(octet_length(callback_data), 0) <= 64
|
||||
AND source_pts = 0)
|
||||
OR
|
||||
(update_kind IN ('message', 'edited_message') AND callback_query_id = 0
|
||||
AND callback_user_id = 0 AND callback_chat_instance = 0 AND callback_data IS NULL)
|
||||
);
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
DROP COLUMN callback_inline_access_hash,
|
||||
DROP COLUMN callback_inline_message_id,
|
||||
DROP COLUMN callback_inline_owner_id,
|
||||
DROP COLUMN callback_inline_dc_id;
|
||||
49
deploy/migrations/0113_bot_api_inline_callbacks.up.sql
Normal file
49
deploy/migrations/0113_bot_api_inline_callbacks.up.sql
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
ALTER TABLE public.bot_api_updates
|
||||
ADD COLUMN callback_inline_dc_id integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN callback_inline_owner_id bigint NOT NULL DEFAULT 0,
|
||||
ADD COLUMN callback_inline_message_id integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN callback_inline_access_hash bigint NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
DROP CONSTRAINT bot_api_updates_callback_shape_check,
|
||||
DROP CONSTRAINT bot_api_updates_peer_type_check,
|
||||
DROP CONSTRAINT bot_api_updates_peer_id_check,
|
||||
DROP CONSTRAINT bot_api_updates_message_id_check;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
ADD CONSTRAINT bot_api_updates_peer_type_check CHECK (
|
||||
peer_type IN ('user', 'channel')
|
||||
OR (update_kind = 'callback_query' AND peer_type = '')
|
||||
),
|
||||
ADD CONSTRAINT bot_api_updates_peer_id_check CHECK (
|
||||
peer_id > 0
|
||||
OR (update_kind = 'callback_query' AND peer_id = 0)
|
||||
),
|
||||
ADD CONSTRAINT bot_api_updates_message_id_check CHECK (
|
||||
message_id > 0
|
||||
OR (update_kind = 'callback_query' AND message_id = 0)
|
||||
),
|
||||
ADD CONSTRAINT bot_api_updates_callback_shape_check CHECK (
|
||||
(update_kind = 'callback_query'
|
||||
AND callback_query_id <> 0
|
||||
AND callback_user_id > 0
|
||||
AND callback_chat_instance <> 0
|
||||
AND COALESCE(octet_length(callback_data), 0) <= 64
|
||||
AND source_pts = 0
|
||||
AND (
|
||||
(peer_type IN ('user', 'channel') AND peer_id > 0 AND message_id > 0
|
||||
AND callback_inline_dc_id = 0 AND callback_inline_owner_id = 0
|
||||
AND callback_inline_message_id = 0 AND callback_inline_access_hash = 0)
|
||||
OR
|
||||
(peer_type = '' AND peer_id = 0 AND message_id = 0
|
||||
AND callback_inline_dc_id > 0 AND callback_inline_owner_id > 0
|
||||
AND callback_inline_message_id > 0 AND callback_inline_access_hash <> 0)
|
||||
))
|
||||
OR
|
||||
(update_kind IN ('message', 'edited_message')
|
||||
AND peer_type IN ('user', 'channel') AND peer_id > 0 AND message_id > 0
|
||||
AND callback_query_id = 0 AND callback_user_id = 0
|
||||
AND callback_chat_instance = 0 AND callback_data IS NULL
|
||||
AND callback_inline_dc_id = 0 AND callback_inline_owner_id = 0
|
||||
AND callback_inline_message_id = 0 AND callback_inline_access_hash = 0)
|
||||
);
|
||||
61
deploy/migrations/0114_collectible_emoji_status.down.sql
Normal file
61
deploy/migrations/0114_collectible_emoji_status.down.sql
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
DROP TRIGGER IF EXISTS unique_star_gifts_clear_invalid_emoji_status ON public.unique_star_gifts;
|
||||
DROP FUNCTION IF EXISTS public.telesrv_clear_invalid_collectible_emoji_status();
|
||||
|
||||
UPDATE public.documents d
|
||||
SET attributes = COALESCE((
|
||||
SELECT jsonb_agg(
|
||||
CASE
|
||||
WHEN item->>'kind' = 'custom_emoji' AND COALESCE((item->>'text_color')::boolean, false) THEN
|
||||
(item - 'text_color') || jsonb_build_object('kind', 'sticker')
|
||||
ELSE item
|
||||
END
|
||||
ORDER BY ord
|
||||
)
|
||||
FROM jsonb_array_elements(d.attributes) WITH ORDINALITY AS attrs(item, ord)
|
||||
), '[]'::jsonb)
|
||||
WHERE d.id IN (SELECT document_id FROM public.star_gift_collectible_patterns);
|
||||
|
||||
ALTER TABLE public.user_update_events DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
ALTER TABLE public.user_update_events ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
(event_type)::text = ANY (ARRAY[
|
||||
'new_message', 'read_history_inbox', 'read_history_outbox', 'read_message_contents',
|
||||
'edit_message', 'web_page', 'message_reactions', 'message_poll', 'draft_message', 'quick_replies',
|
||||
'new_quick_reply', 'delete_quick_reply', 'quick_reply_message', 'delete_quick_reply_messages',
|
||||
'contacts_reset', 'dialog_pinned', 'pinned_dialogs', 'pinned_messages', 'dialog_unread_mark',
|
||||
'peer_settings', 'peer_story_blocked', 'user_phone', 'delete_messages', 'dialog_filter',
|
||||
'dialog_filter_order', 'dialog_filters', 'folder_peers', 'channel_available_messages',
|
||||
'channel_view_forum_as_messages', 'channel_state', 'saved_dialog_pinned',
|
||||
'pinned_saved_dialogs', 'story', 'read_stories', 'sent_story_reaction',
|
||||
'new_story_reaction', 'noop', 'read_channel_discussion_inbox',
|
||||
'read_channel_discussion_outbox'
|
||||
]::text[])
|
||||
);
|
||||
ALTER TABLE public.user_update_events DROP COLUMN IF EXISTS emoji_status_payload;
|
||||
|
||||
ALTER TABLE public.users DROP CONSTRAINT IF EXISTS users_deletion_state_check;
|
||||
ALTER TABLE public.users DROP CONSTRAINT IF EXISTS users_emoji_status_shape_check;
|
||||
ALTER TABLE public.users
|
||||
DROP COLUMN IF EXISTS emoji_status_collectible,
|
||||
DROP COLUMN IF EXISTS emoji_status_collectible_id;
|
||||
|
||||
ALTER TABLE public.users
|
||||
ADD CONSTRAINT users_deletion_state_check CHECK (
|
||||
(deleted_at IS NULL AND deletion_source = '' AND deletion_reason = '')
|
||||
OR
|
||||
(deleted_at IS NOT NULL
|
||||
AND deletion_source IN (
|
||||
'manual', 'forgot_password', 'tos_decline', 'password_reset_expiry',
|
||||
'account_ttl', 'freeze_expiry'
|
||||
)
|
||||
AND account_delete_at IS NULL
|
||||
AND phone = '' AND first_name = '' AND last_name = '' AND username = ''
|
||||
AND country_code = '' AND about = '' AND verified = false AND support = false
|
||||
AND premium_expires_at IS NULL
|
||||
AND emoji_status_document_id = 0 AND emoji_status_until = 0
|
||||
AND color_set = false AND color = 0 AND color_background_emoji_id = 0
|
||||
AND profile_color_set = false AND profile_color = 0
|
||||
AND profile_color_background_emoji_id = 0
|
||||
AND birthday_day = 0 AND birthday_month = 0 AND birthday_year = 0
|
||||
AND personal_channel_id = 0 AND last_seen_at = 0
|
||||
AND octet_length(deletion_reason) <= 1024)
|
||||
);
|
||||
166
deploy/migrations/0114_collectible_emoji_status.up.sql
Normal file
166
deploy/migrations/0114_collectible_emoji_status.up.sql
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
-- Complete collectible emoji-status state: users persist the selected unique
|
||||
-- gift plus an immutable render snapshot, and the account update log persists
|
||||
-- the exact status payload for online dispatch/offline difference replay.
|
||||
ALTER TABLE public.users
|
||||
ADD COLUMN emoji_status_collectible_id bigint REFERENCES public.unique_star_gifts(id),
|
||||
ADD COLUMN emoji_status_collectible jsonb DEFAULT '{}'::jsonb NOT NULL;
|
||||
|
||||
ALTER TABLE public.users
|
||||
ADD CONSTRAINT users_emoji_status_shape_check CHECK (
|
||||
emoji_status_document_id >= 0
|
||||
AND emoji_status_until >= 0
|
||||
AND (emoji_status_document_id > 0 OR emoji_status_until = 0)
|
||||
AND (
|
||||
(
|
||||
emoji_status_collectible_id IS NULL
|
||||
AND emoji_status_collectible = '{}'::jsonb
|
||||
) OR (
|
||||
emoji_status_collectible_id IS NOT NULL
|
||||
AND emoji_status_collectible_id > 0
|
||||
AND emoji_status_document_id > 0
|
||||
AND jsonb_typeof(emoji_status_collectible) = 'object'
|
||||
AND emoji_status_collectible <> '{}'::jsonb
|
||||
AND emoji_status_collectible ? 'collectible_id'
|
||||
AND emoji_status_collectible ? 'document_id'
|
||||
AND emoji_status_collectible ? 'title'
|
||||
AND emoji_status_collectible ? 'slug'
|
||||
AND emoji_status_collectible ? 'pattern_document_id'
|
||||
AND (emoji_status_collectible->>'collectible_id')::bigint = emoji_status_collectible_id
|
||||
AND (emoji_status_collectible->>'document_id')::bigint = emoji_status_document_id
|
||||
AND (emoji_status_collectible->>'pattern_document_id')::bigint > 0
|
||||
AND length(emoji_status_collectible->>'title') > 0
|
||||
AND length(emoji_status_collectible->>'slug') > 0
|
||||
AND (emoji_status_collectible->>'center_color')::integer BETWEEN 0 AND 16777215
|
||||
AND (emoji_status_collectible->>'edge_color')::integer BETWEEN 0 AND 16777215
|
||||
AND (emoji_status_collectible->>'pattern_color')::integer BETWEEN 0 AND 16777215
|
||||
AND (emoji_status_collectible->>'text_color')::integer BETWEEN 0 AND 16777215
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE public.user_update_events
|
||||
ADD COLUMN emoji_status_payload jsonb DEFAULT '{}'::jsonb NOT NULL;
|
||||
|
||||
ALTER TABLE public.user_update_events DROP CONSTRAINT IF EXISTS user_update_events_type_check;
|
||||
ALTER TABLE public.user_update_events ADD CONSTRAINT user_update_events_type_check CHECK (
|
||||
(event_type)::text = ANY (ARRAY[
|
||||
'new_message', 'read_history_inbox', 'read_history_outbox', 'read_message_contents',
|
||||
'edit_message', 'web_page', 'message_reactions', 'message_poll', 'draft_message', 'quick_replies',
|
||||
'new_quick_reply', 'delete_quick_reply', 'quick_reply_message', 'delete_quick_reply_messages',
|
||||
'contacts_reset', 'dialog_pinned', 'pinned_dialogs', 'pinned_messages', 'dialog_unread_mark',
|
||||
'peer_settings', 'peer_story_blocked', 'user_phone', 'user_emoji_status', 'delete_messages',
|
||||
'dialog_filter', 'dialog_filter_order', 'dialog_filters', 'folder_peers',
|
||||
'channel_available_messages', 'channel_view_forum_as_messages', 'channel_state',
|
||||
'saved_dialog_pinned', 'pinned_saved_dialogs', 'story', 'read_stories',
|
||||
'sent_story_reaction', 'new_story_reaction', 'noop',
|
||||
'read_channel_discussion_inbox', 'read_channel_discussion_outbox'
|
||||
]::text[])
|
||||
);
|
||||
|
||||
-- Android applies the collectible backdrop's pattern_color only to a
|
||||
-- documentAttributeCustomEmoji with text_color=true. Existing imports stored
|
||||
-- these pattern documents as ordinary stickers, so repair them in place.
|
||||
UPDATE public.documents d
|
||||
SET attributes = COALESCE((
|
||||
SELECT jsonb_agg(
|
||||
CASE
|
||||
WHEN item->>'kind' = 'sticker' THEN
|
||||
jsonb_set(
|
||||
jsonb_set(item, '{kind}', '"custom_emoji"'::jsonb, false),
|
||||
'{text_color}', 'true'::jsonb, true
|
||||
)
|
||||
ELSE item
|
||||
END
|
||||
ORDER BY ord
|
||||
)
|
||||
FROM jsonb_array_elements(d.attributes) WITH ORDINALITY AS attrs(item, ord)
|
||||
), '[]'::jsonb)
|
||||
WHERE d.id IN (SELECT document_id FROM public.star_gift_collectible_patterns);
|
||||
|
||||
-- A transferred/exported/burned gift can no longer remain as the previous
|
||||
-- owner's status. Keep the durable user state valid even if the lifecycle
|
||||
-- mutation did not originate from an account RPC.
|
||||
CREATE OR REPLACE FUNCTION public.telesrv_clear_invalid_collectible_emoji_status()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
cleared_user_id bigint;
|
||||
cleared_pts integer;
|
||||
event_date integer;
|
||||
BEGIN
|
||||
event_date := EXTRACT(EPOCH FROM clock_timestamp())::integer;
|
||||
FOR cleared_user_id IN
|
||||
UPDATE public.users u
|
||||
SET emoji_status_document_id = 0,
|
||||
emoji_status_until = 0,
|
||||
emoji_status_collectible_id = NULL,
|
||||
emoji_status_collectible = '{}'::jsonb,
|
||||
updated_at = now()
|
||||
WHERE u.emoji_status_collectible_id = NEW.id
|
||||
AND (
|
||||
NEW.burned
|
||||
OR NEW.owner_address <> ''
|
||||
OR NEW.owner_peer_type IS DISTINCT FROM 'user'
|
||||
OR NEW.owner_peer_id IS DISTINCT FROM u.id
|
||||
)
|
||||
RETURNING u.id
|
||||
LOOP
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
VALUES (cleared_user_id, 0)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
UPDATE public.user_update_watermarks
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
WHERE user_id = cleared_user_id
|
||||
RETURNING contiguous_pts INTO cleared_pts;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id, pts, pts_count, date, event_type,
|
||||
peer_type, peer_id, emoji_status_payload
|
||||
) VALUES (
|
||||
cleared_user_id, cleared_pts, 1, event_date, 'user_emoji_status',
|
||||
'user', cleared_user_id, '{}'::jsonb
|
||||
);
|
||||
|
||||
-- No session is the origin of a lifecycle invalidation: every online
|
||||
-- device receives it, and offline devices replay the same event.
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id, pts, event_type, exclude_auth_key_id, exclude_session_id
|
||||
) VALUES (
|
||||
cleared_user_id, cleared_pts, 'user_emoji_status', 0, 0
|
||||
);
|
||||
END LOOP;
|
||||
RETURN NEW;
|
||||
END
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER unique_star_gifts_clear_invalid_emoji_status
|
||||
AFTER UPDATE OF owner_peer_type, owner_peer_id, owner_address, burned
|
||||
ON public.unique_star_gifts
|
||||
FOR EACH ROW EXECUTE FUNCTION public.telesrv_clear_invalid_collectible_emoji_status();
|
||||
|
||||
-- Deleted-user tombstones must not retain the new collectible facts.
|
||||
ALTER TABLE public.users DROP CONSTRAINT IF EXISTS users_deletion_state_check;
|
||||
ALTER TABLE public.users
|
||||
ADD CONSTRAINT users_deletion_state_check CHECK (
|
||||
(deleted_at IS NULL AND deletion_source = '' AND deletion_reason = '')
|
||||
OR
|
||||
(deleted_at IS NOT NULL
|
||||
AND deletion_source IN (
|
||||
'manual', 'forgot_password', 'tos_decline', 'password_reset_expiry',
|
||||
'account_ttl', 'freeze_expiry'
|
||||
)
|
||||
AND account_delete_at IS NULL
|
||||
AND phone = '' AND first_name = '' AND last_name = '' AND username = ''
|
||||
AND country_code = '' AND about = '' AND verified = false AND support = false
|
||||
AND premium_expires_at IS NULL
|
||||
AND emoji_status_document_id = 0 AND emoji_status_until = 0
|
||||
AND emoji_status_collectible_id IS NULL
|
||||
AND emoji_status_collectible = '{}'::jsonb
|
||||
AND color_set = false AND color = 0 AND color_background_emoji_id = 0
|
||||
AND profile_color_set = false AND profile_color = 0
|
||||
AND profile_color_background_emoji_id = 0
|
||||
AND birthday_day = 0 AND birthday_month = 0 AND birthday_year = 0
|
||||
AND personal_channel_id = 0 AND last_seen_at = 0
|
||||
AND octet_length(deletion_reason) <= 1024)
|
||||
);
|
||||
1
deploy/migrations/0115_bot_api_webhooks.down.sql
Normal file
1
deploy/migrations/0115_bot_api_webhooks.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS public.bot_api_webhooks;
|
||||
27
deploy/migrations/0115_bot_api_webhooks.up.sql
Normal file
27
deploy/migrations/0115_bot_api_webhooks.up.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
CREATE TABLE public.bot_api_webhooks (
|
||||
bot_user_id bigint PRIMARY KEY REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
url text NOT NULL,
|
||||
secret_token varchar(256) NOT NULL DEFAULT '',
|
||||
max_connections integer NOT NULL DEFAULT 40,
|
||||
allowed_updates text[],
|
||||
failure_count integer NOT NULL DEFAULT 0,
|
||||
last_error_date integer NOT NULL DEFAULT 0,
|
||||
last_error_message varchar(512) NOT NULL DEFAULT '',
|
||||
next_attempt_at timestamptz NOT NULL DEFAULT now(),
|
||||
delivery_owner text NOT NULL DEFAULT '',
|
||||
delivery_expires_at timestamptz,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT bot_api_webhooks_url_check CHECK (length(url) BETWEEN 1 AND 2048),
|
||||
CONSTRAINT bot_api_webhooks_max_connections_check CHECK (max_connections BETWEEN 1 AND 100),
|
||||
CONSTRAINT bot_api_webhooks_failure_count_check CHECK (failure_count >= 0),
|
||||
CONSTRAINT bot_api_webhooks_allowed_updates_check CHECK (
|
||||
allowed_updates IS NULL OR array_position(allowed_updates, NULL) IS NULL
|
||||
),
|
||||
CONSTRAINT bot_api_webhooks_delivery_lease_check CHECK (
|
||||
(delivery_owner = '' AND delivery_expires_at IS NULL)
|
||||
OR (delivery_owner <> '' AND delivery_expires_at IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX bot_api_webhooks_due_idx
|
||||
ON public.bot_api_webhooks (next_attempt_at, bot_user_id);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE public.webview_requested_buttons
|
||||
DROP COLUMN IF EXISTS peer_filter;
|
||||
2
deploy/migrations/0116_bot_requested_peer_filters.up.sql
Normal file
2
deploy/migrations/0116_bot_requested_peer_filters.up.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE public.webview_requested_buttons
|
||||
ADD COLUMN peer_filter jsonb NOT NULL DEFAULT '{}'::jsonb;
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
CREATE TEMP TABLE telesrv_pattern_document_repair_rollback ON COMMIT DROP AS
|
||||
SELECT old_document_id, new_document_id
|
||||
FROM public.star_gift_pattern_document_repairs;
|
||||
|
||||
CREATE TEMP TABLE telesrv_rollback_collectible_wearers ON COMMIT DROP AS
|
||||
SELECT u.id AS user_id, r.old_document_id, r.new_document_id
|
||||
FROM public.users u
|
||||
JOIN telesrv_pattern_document_repair_rollback r
|
||||
ON (u.emoji_status_collectible->>'pattern_document_id')::bigint = r.new_document_id
|
||||
WHERE u.emoji_status_collectible_id IS NOT NULL;
|
||||
|
||||
UPDATE public.users u
|
||||
SET emoji_status_collectible = jsonb_set(
|
||||
u.emoji_status_collectible,
|
||||
'{pattern_document_id}',
|
||||
to_jsonb(w.old_document_id),
|
||||
false
|
||||
),
|
||||
updated_at = now()
|
||||
FROM telesrv_rollback_collectible_wearers w
|
||||
WHERE u.id = w.user_id;
|
||||
|
||||
UPDATE public.user_update_events e
|
||||
SET emoji_status_payload = jsonb_set(
|
||||
e.emoji_status_payload,
|
||||
'{collectible,pattern_document_id}',
|
||||
to_jsonb(r.old_document_id),
|
||||
false
|
||||
)
|
||||
FROM telesrv_pattern_document_repair_rollback r
|
||||
WHERE e.event_type = 'user_emoji_status'
|
||||
AND (e.emoji_status_payload #>> '{collectible,pattern_document_id}')::bigint = r.new_document_id;
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
DISABLE TRIGGER star_gift_collectible_pattern_guard;
|
||||
UPDATE public.star_gift_collectible_patterns p
|
||||
SET document_id = r.old_document_id
|
||||
FROM telesrv_pattern_document_repair_rollback r
|
||||
WHERE p.document_id = r.new_document_id;
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
ENABLE TRIGGER star_gift_collectible_pattern_guard;
|
||||
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
SELECT user_id, 0 FROM telesrv_rollback_collectible_wearers
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
CREATE TEMP TABLE telesrv_collectible_pattern_rollback_events (
|
||||
user_id bigint PRIMARY KEY,
|
||||
pts integer NOT NULL
|
||||
) ON COMMIT DROP;
|
||||
|
||||
WITH bumped AS (
|
||||
UPDATE public.user_update_watermarks w
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
FROM telesrv_rollback_collectible_wearers wearer
|
||||
WHERE w.user_id = wearer.user_id
|
||||
RETURNING w.user_id, w.contiguous_pts
|
||||
)
|
||||
INSERT INTO telesrv_collectible_pattern_rollback_events (user_id, pts)
|
||||
SELECT user_id, contiguous_pts FROM bumped;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id, pts, pts_count, date, event_type,
|
||||
peer_type, peer_id, emoji_status_payload
|
||||
)
|
||||
SELECT
|
||||
c.user_id,
|
||||
c.pts,
|
||||
1,
|
||||
EXTRACT(EPOCH FROM clock_timestamp())::integer,
|
||||
'user_emoji_status',
|
||||
'user',
|
||||
c.user_id,
|
||||
jsonb_strip_nulls(jsonb_build_object(
|
||||
'document_id', u.emoji_status_document_id,
|
||||
'until', CASE WHEN u.emoji_status_until > 0 THEN u.emoji_status_until ELSE NULL END,
|
||||
'collectible', u.emoji_status_collectible
|
||||
))
|
||||
FROM telesrv_collectible_pattern_rollback_events c
|
||||
JOIN public.users u ON u.id = c.user_id;
|
||||
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id, pts, event_type, exclude_auth_key_id, exclude_session_id
|
||||
)
|
||||
SELECT user_id, pts, 'user_emoji_status', 0, 0
|
||||
FROM telesrv_collectible_pattern_rollback_events;
|
||||
|
||||
DELETE FROM public.file_blobs f
|
||||
USING telesrv_pattern_document_repair_rollback r
|
||||
WHERE f.location_key = 'doc:' || r.new_document_id::text
|
||||
OR f.location_key LIKE 'doc:' || r.new_document_id::text || ':%';
|
||||
|
||||
DROP TABLE public.star_gift_pattern_document_repairs;
|
||||
|
||||
DELETE FROM public.documents d
|
||||
USING telesrv_pattern_document_repair_rollback r
|
||||
WHERE d.id = r.new_document_id;
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
-- Telegram clients cache Document metadata by id indefinitely. Migration 0114
|
||||
-- corrected collectible pattern attributes in place, but an Android client that
|
||||
-- had already cached the old sticker shape would never observe that correction.
|
||||
-- Allocate a new immutable document identity for every pre-0117 pattern, keep a
|
||||
-- durable old/new map for audit and rollback, and point the published attribute
|
||||
-- at the clone. The blob bytes do not change, so the new location keys alias the
|
||||
-- same immutable backend object.
|
||||
CREATE TABLE public.star_gift_pattern_document_repairs (
|
||||
old_document_id bigint PRIMARY KEY,
|
||||
new_document_id bigint UNIQUE NOT NULL,
|
||||
repaired_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT star_gift_pattern_document_repairs_ids_check CHECK (
|
||||
old_document_id > 0 AND new_document_id > 0 AND old_document_id <> new_document_id
|
||||
)
|
||||
);
|
||||
|
||||
INSERT INTO public.star_gift_pattern_document_repairs (old_document_id, new_document_id)
|
||||
SELECT DISTINCT
|
||||
p.document_id,
|
||||
((('x' || substr(md5(p.document_id::text || ':collectible-pattern-custom-emoji:v1'), 1, 16))
|
||||
::bit(64)::bigint) & 9223372036854775807)
|
||||
FROM public.star_gift_collectible_patterns p;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM public.star_gift_pattern_document_repairs r
|
||||
JOIN public.documents d ON d.id = r.new_document_id
|
||||
) THEN
|
||||
RAISE EXCEPTION 'collectible pattern document repair id collision';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
INSERT INTO public.documents (
|
||||
id, access_hash, file_reference, date, mime_type, size, dc_id,
|
||||
attributes, thumbs, created_at
|
||||
)
|
||||
SELECT
|
||||
r.new_document_id,
|
||||
d.access_hash,
|
||||
d.file_reference,
|
||||
d.date,
|
||||
d.mime_type,
|
||||
d.size,
|
||||
d.dc_id,
|
||||
COALESCE((
|
||||
SELECT jsonb_agg(
|
||||
CASE
|
||||
WHEN item->>'kind' IN ('sticker', 'custom_emoji') THEN
|
||||
jsonb_set(
|
||||
jsonb_set(item, '{kind}', '"custom_emoji"'::jsonb, false),
|
||||
'{text_color}', 'true'::jsonb, true
|
||||
)
|
||||
ELSE item
|
||||
END
|
||||
ORDER BY ord
|
||||
)
|
||||
FROM jsonb_array_elements(d.attributes) WITH ORDINALITY AS attrs(item, ord)
|
||||
), '[]'::jsonb),
|
||||
d.thumbs,
|
||||
now()
|
||||
FROM public.star_gift_pattern_document_repairs r
|
||||
JOIN public.documents d ON d.id = r.old_document_id;
|
||||
|
||||
INSERT INTO public.file_blobs (
|
||||
location_key, backend, object_key, size, sha256, mime_type, created_at
|
||||
)
|
||||
SELECT
|
||||
'doc:' || r.new_document_id::text ||
|
||||
substr(f.location_key, length('doc:' || r.old_document_id::text) + 1),
|
||||
f.backend,
|
||||
f.object_key,
|
||||
f.size,
|
||||
f.sha256,
|
||||
f.mime_type,
|
||||
now()
|
||||
FROM public.star_gift_pattern_document_repairs r
|
||||
JOIN public.file_blobs f
|
||||
ON f.location_key = 'doc:' || r.old_document_id::text
|
||||
OR f.location_key LIKE 'doc:' || r.old_document_id::text || ':%';
|
||||
|
||||
-- This is a repair of a previously invalid document identity, not a mutation of
|
||||
-- the published appearance. Attribute id/name/animation/rarity remain intact.
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
DISABLE TRIGGER star_gift_collectible_pattern_guard;
|
||||
UPDATE public.star_gift_collectible_patterns p
|
||||
SET document_id = r.new_document_id
|
||||
FROM public.star_gift_pattern_document_repairs r
|
||||
WHERE p.document_id = r.old_document_id;
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
ENABLE TRIGGER star_gift_collectible_pattern_guard;
|
||||
|
||||
-- Capture active wearers before rewriting their immutable render snapshots.
|
||||
CREATE TEMP TABLE telesrv_repaired_collectible_wearers ON COMMIT DROP AS
|
||||
SELECT u.id AS user_id, r.old_document_id, r.new_document_id
|
||||
FROM public.users u
|
||||
JOIN public.star_gift_pattern_document_repairs r
|
||||
ON (u.emoji_status_collectible->>'pattern_document_id')::bigint = r.old_document_id
|
||||
WHERE u.emoji_status_collectible_id IS NOT NULL;
|
||||
|
||||
UPDATE public.users u
|
||||
SET emoji_status_collectible = jsonb_set(
|
||||
u.emoji_status_collectible,
|
||||
'{pattern_document_id}',
|
||||
to_jsonb(w.new_document_id),
|
||||
false
|
||||
),
|
||||
updated_at = now()
|
||||
FROM telesrv_repaired_collectible_wearers w
|
||||
WHERE u.id = w.user_id;
|
||||
|
||||
-- An offline client may still replay an older status event, so repair every
|
||||
-- durable snapshot. A fresh event is appended below for clients that already
|
||||
-- consumed the old pts and therefore need a new convergence edge.
|
||||
UPDATE public.user_update_events e
|
||||
SET emoji_status_payload = jsonb_set(
|
||||
e.emoji_status_payload,
|
||||
'{collectible,pattern_document_id}',
|
||||
to_jsonb(r.new_document_id),
|
||||
false
|
||||
)
|
||||
FROM public.star_gift_pattern_document_repairs r
|
||||
WHERE e.event_type = 'user_emoji_status'
|
||||
AND (e.emoji_status_payload #>> '{collectible,pattern_document_id}')::bigint = r.old_document_id;
|
||||
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
SELECT user_id, 0 FROM telesrv_repaired_collectible_wearers
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
CREATE TEMP TABLE telesrv_collectible_pattern_correction_events (
|
||||
user_id bigint PRIMARY KEY,
|
||||
pts integer NOT NULL
|
||||
) ON COMMIT DROP;
|
||||
|
||||
WITH bumped AS (
|
||||
UPDATE public.user_update_watermarks w
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
FROM telesrv_repaired_collectible_wearers wearer
|
||||
WHERE w.user_id = wearer.user_id
|
||||
RETURNING w.user_id, w.contiguous_pts
|
||||
)
|
||||
INSERT INTO telesrv_collectible_pattern_correction_events (user_id, pts)
|
||||
SELECT user_id, contiguous_pts FROM bumped;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id, pts, pts_count, date, event_type,
|
||||
peer_type, peer_id, emoji_status_payload
|
||||
)
|
||||
SELECT
|
||||
c.user_id,
|
||||
c.pts,
|
||||
1,
|
||||
EXTRACT(EPOCH FROM clock_timestamp())::integer,
|
||||
'user_emoji_status',
|
||||
'user',
|
||||
c.user_id,
|
||||
jsonb_strip_nulls(jsonb_build_object(
|
||||
'document_id', u.emoji_status_document_id,
|
||||
'until', CASE WHEN u.emoji_status_until > 0 THEN u.emoji_status_until ELSE NULL END,
|
||||
'collectible', u.emoji_status_collectible
|
||||
))
|
||||
FROM telesrv_collectible_pattern_correction_events c
|
||||
JOIN public.users u ON u.id = c.user_id;
|
||||
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id, pts, event_type, exclude_auth_key_id, exclude_session_id
|
||||
)
|
||||
SELECT user_id, pts, 'user_emoji_status', 0, 0
|
||||
FROM telesrv_collectible_pattern_correction_events;
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
CREATE TEMP TABLE telesrv_pattern_preview_repair_rollback ON COMMIT DROP AS
|
||||
SELECT old_document_id, new_document_id
|
||||
FROM public.star_gift_pattern_preview_document_repairs;
|
||||
|
||||
CREATE TEMP TABLE telesrv_pattern_preview_rollback_wearers ON COMMIT DROP AS
|
||||
SELECT u.id AS user_id, r.old_document_id, r.new_document_id
|
||||
FROM public.users u
|
||||
JOIN telesrv_pattern_preview_repair_rollback r
|
||||
ON (u.emoji_status_collectible->>'pattern_document_id')::bigint = r.new_document_id
|
||||
WHERE u.emoji_status_collectible_id IS NOT NULL;
|
||||
|
||||
UPDATE public.users u
|
||||
SET emoji_status_collectible = jsonb_set(
|
||||
u.emoji_status_collectible,
|
||||
'{pattern_document_id}',
|
||||
to_jsonb(w.old_document_id),
|
||||
false
|
||||
),
|
||||
updated_at = now()
|
||||
FROM telesrv_pattern_preview_rollback_wearers w
|
||||
WHERE u.id = w.user_id;
|
||||
|
||||
UPDATE public.user_update_events e
|
||||
SET emoji_status_payload = jsonb_set(
|
||||
e.emoji_status_payload,
|
||||
'{collectible,pattern_document_id}',
|
||||
to_jsonb(r.old_document_id),
|
||||
false
|
||||
)
|
||||
FROM telesrv_pattern_preview_repair_rollback r
|
||||
WHERE e.event_type = 'user_emoji_status'
|
||||
AND (e.emoji_status_payload #>> '{collectible,pattern_document_id}')::bigint = r.new_document_id;
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
DISABLE TRIGGER star_gift_collectible_pattern_guard;
|
||||
UPDATE public.star_gift_collectible_patterns p
|
||||
SET document_id = r.old_document_id
|
||||
FROM telesrv_pattern_preview_repair_rollback r
|
||||
WHERE p.document_id = r.new_document_id;
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
ENABLE TRIGGER star_gift_collectible_pattern_guard;
|
||||
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
SELECT user_id, 0 FROM telesrv_pattern_preview_rollback_wearers
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
CREATE TEMP TABLE telesrv_pattern_preview_rollback_events (
|
||||
user_id bigint PRIMARY KEY,
|
||||
pts integer NOT NULL
|
||||
) ON COMMIT DROP;
|
||||
|
||||
WITH bumped AS (
|
||||
UPDATE public.user_update_watermarks w
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
FROM telesrv_pattern_preview_rollback_wearers wearer
|
||||
WHERE w.user_id = wearer.user_id
|
||||
RETURNING w.user_id, w.contiguous_pts
|
||||
)
|
||||
INSERT INTO telesrv_pattern_preview_rollback_events (user_id, pts)
|
||||
SELECT user_id, contiguous_pts FROM bumped;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id, pts, pts_count, date, event_type,
|
||||
peer_type, peer_id, emoji_status_payload
|
||||
)
|
||||
SELECT
|
||||
c.user_id,
|
||||
c.pts,
|
||||
1,
|
||||
EXTRACT(EPOCH FROM clock_timestamp())::integer,
|
||||
'user_emoji_status',
|
||||
'user',
|
||||
c.user_id,
|
||||
jsonb_strip_nulls(jsonb_build_object(
|
||||
'document_id', u.emoji_status_document_id,
|
||||
'until', CASE WHEN u.emoji_status_until > 0 THEN u.emoji_status_until ELSE NULL END,
|
||||
'collectible', u.emoji_status_collectible
|
||||
))
|
||||
FROM telesrv_pattern_preview_rollback_events c
|
||||
JOIN public.users u ON u.id = c.user_id;
|
||||
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id, pts, event_type, exclude_auth_key_id, exclude_session_id
|
||||
)
|
||||
SELECT user_id, pts, 'user_emoji_status', 0, 0
|
||||
FROM telesrv_pattern_preview_rollback_events;
|
||||
|
||||
DELETE FROM public.file_blobs f
|
||||
USING telesrv_pattern_preview_repair_rollback r
|
||||
WHERE f.location_key = 'doc:' || r.new_document_id::text
|
||||
OR f.location_key LIKE 'doc:' || r.new_document_id::text || ':%';
|
||||
|
||||
DROP TABLE public.star_gift_pattern_preview_document_repairs;
|
||||
|
||||
DELETE FROM public.documents d
|
||||
USING telesrv_pattern_preview_repair_rollback r
|
||||
WHERE d.id = r.new_document_id;
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
-- DrKLO's profile-header path creates collectible patterns with
|
||||
-- CACHE_TYPE_ALERT_PREVIEW_STATIC. For application/x-tgsticker that client
|
||||
-- only loads the main TGS when Document.thumbs is non-empty; an empty list is
|
||||
-- routed to a null thumbnail location and the pattern remains a flat backdrop.
|
||||
--
|
||||
-- Document metadata is cached by id, so adding a thumb to an already published
|
||||
-- id would not repair existing Android installations. Clone every affected
|
||||
-- published pattern under a new immutable identity, add an inline PhotoPathSize
|
||||
-- placeholder, alias the unchanged main blob, and converge active/durable emoji
|
||||
-- status snapshots to the new id.
|
||||
CREATE TABLE public.star_gift_pattern_preview_document_repairs (
|
||||
old_document_id bigint PRIMARY KEY,
|
||||
new_document_id bigint UNIQUE NOT NULL,
|
||||
repaired_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT star_gift_pattern_preview_document_repairs_ids_check CHECK (
|
||||
old_document_id > 0 AND new_document_id > 0 AND old_document_id <> new_document_id
|
||||
)
|
||||
);
|
||||
|
||||
INSERT INTO public.star_gift_pattern_preview_document_repairs (old_document_id, new_document_id)
|
||||
SELECT DISTINCT
|
||||
p.document_id,
|
||||
((('x' || substr(md5(p.document_id::text || ':collectible-pattern-android-preview:v2'), 1, 16))
|
||||
::bit(64)::bigint) & 9223372036854775807)
|
||||
FROM public.star_gift_collectible_patterns p
|
||||
JOIN public.documents d ON d.id = p.document_id
|
||||
WHERE jsonb_array_length(d.thumbs) = 0;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM public.star_gift_pattern_preview_document_repairs r
|
||||
JOIN public.documents d ON d.id = r.new_document_id
|
||||
) OR EXISTS (
|
||||
SELECT 1
|
||||
FROM public.star_gift_pattern_preview_document_repairs r
|
||||
JOIN public.file_blobs f
|
||||
ON f.location_key = 'doc:' || r.new_document_id::text
|
||||
OR f.location_key LIKE 'doc:' || r.new_document_id::text || ':%'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'collectible pattern preview document repair id collision';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
INSERT INTO public.documents (
|
||||
id, access_hash, file_reference, date, mime_type, size, dc_id,
|
||||
attributes, thumbs, created_at
|
||||
)
|
||||
SELECT
|
||||
r.new_document_id,
|
||||
d.access_hash,
|
||||
d.file_reference,
|
||||
d.date,
|
||||
d.mime_type,
|
||||
d.size,
|
||||
d.dc_id,
|
||||
d.attributes,
|
||||
jsonb_build_array(jsonb_build_object(
|
||||
'kind', 'path',
|
||||
'type', 'j',
|
||||
'bytes', 'GQalBdxhTX54SARIBGNsfE4Imk4HooCjlLqhhYOHSIxMjEybVa1VkICfhqqRqquGigRYjgFNkXmHA0cGhwM='
|
||||
)),
|
||||
now()
|
||||
FROM public.star_gift_pattern_preview_document_repairs r
|
||||
JOIN public.documents d ON d.id = r.old_document_id;
|
||||
|
||||
INSERT INTO public.file_blobs (
|
||||
location_key, backend, object_key, size, sha256, mime_type, created_at
|
||||
)
|
||||
SELECT
|
||||
'doc:' || r.new_document_id::text ||
|
||||
substr(f.location_key, length('doc:' || r.old_document_id::text) + 1),
|
||||
f.backend,
|
||||
f.object_key,
|
||||
f.size,
|
||||
f.sha256,
|
||||
f.mime_type,
|
||||
now()
|
||||
FROM public.star_gift_pattern_preview_document_repairs r
|
||||
JOIN public.file_blobs f
|
||||
ON f.location_key = 'doc:' || r.old_document_id::text
|
||||
OR f.location_key LIKE 'doc:' || r.old_document_id::text || ':%';
|
||||
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
DISABLE TRIGGER star_gift_collectible_pattern_guard;
|
||||
UPDATE public.star_gift_collectible_patterns p
|
||||
SET document_id = r.new_document_id
|
||||
FROM public.star_gift_pattern_preview_document_repairs r
|
||||
WHERE p.document_id = r.old_document_id;
|
||||
ALTER TABLE public.star_gift_collectible_patterns
|
||||
ENABLE TRIGGER star_gift_collectible_pattern_guard;
|
||||
|
||||
CREATE TEMP TABLE telesrv_pattern_preview_repaired_wearers ON COMMIT DROP AS
|
||||
SELECT u.id AS user_id, r.old_document_id, r.new_document_id
|
||||
FROM public.users u
|
||||
JOIN public.star_gift_pattern_preview_document_repairs r
|
||||
ON (u.emoji_status_collectible->>'pattern_document_id')::bigint = r.old_document_id
|
||||
WHERE u.emoji_status_collectible_id IS NOT NULL;
|
||||
|
||||
UPDATE public.users u
|
||||
SET emoji_status_collectible = jsonb_set(
|
||||
u.emoji_status_collectible,
|
||||
'{pattern_document_id}',
|
||||
to_jsonb(w.new_document_id),
|
||||
false
|
||||
),
|
||||
updated_at = now()
|
||||
FROM telesrv_pattern_preview_repaired_wearers w
|
||||
WHERE u.id = w.user_id;
|
||||
|
||||
UPDATE public.user_update_events e
|
||||
SET emoji_status_payload = jsonb_set(
|
||||
e.emoji_status_payload,
|
||||
'{collectible,pattern_document_id}',
|
||||
to_jsonb(r.new_document_id),
|
||||
false
|
||||
)
|
||||
FROM public.star_gift_pattern_preview_document_repairs r
|
||||
WHERE e.event_type = 'user_emoji_status'
|
||||
AND (e.emoji_status_payload #>> '{collectible,pattern_document_id}')::bigint = r.old_document_id;
|
||||
|
||||
INSERT INTO public.user_update_watermarks (user_id, contiguous_pts)
|
||||
SELECT user_id, 0 FROM telesrv_pattern_preview_repaired_wearers
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
CREATE TEMP TABLE telesrv_pattern_preview_correction_events (
|
||||
user_id bigint PRIMARY KEY,
|
||||
pts integer NOT NULL
|
||||
) ON COMMIT DROP;
|
||||
|
||||
WITH bumped AS (
|
||||
UPDATE public.user_update_watermarks w
|
||||
SET contiguous_pts = contiguous_pts + 1,
|
||||
updated_at = now()
|
||||
FROM telesrv_pattern_preview_repaired_wearers wearer
|
||||
WHERE w.user_id = wearer.user_id
|
||||
RETURNING w.user_id, w.contiguous_pts
|
||||
)
|
||||
INSERT INTO telesrv_pattern_preview_correction_events (user_id, pts)
|
||||
SELECT user_id, contiguous_pts FROM bumped;
|
||||
|
||||
INSERT INTO public.user_update_events (
|
||||
user_id, pts, pts_count, date, event_type,
|
||||
peer_type, peer_id, emoji_status_payload
|
||||
)
|
||||
SELECT
|
||||
c.user_id,
|
||||
c.pts,
|
||||
1,
|
||||
EXTRACT(EPOCH FROM clock_timestamp())::integer,
|
||||
'user_emoji_status',
|
||||
'user',
|
||||
c.user_id,
|
||||
jsonb_strip_nulls(jsonb_build_object(
|
||||
'document_id', u.emoji_status_document_id,
|
||||
'until', CASE WHEN u.emoji_status_until > 0 THEN u.emoji_status_until ELSE NULL END,
|
||||
'collectible', u.emoji_status_collectible
|
||||
))
|
||||
FROM telesrv_pattern_preview_correction_events c
|
||||
JOIN public.users u ON u.id = c.user_id;
|
||||
|
||||
INSERT INTO public.dispatch_outbox (
|
||||
target_user_id, pts, event_type, exclude_auth_key_id, exclude_session_id
|
||||
)
|
||||
SELECT user_id, pts, 'user_emoji_status', 0, 0
|
||||
FROM telesrv_pattern_preview_correction_events;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE public.webview_requested_buttons
|
||||
DROP COLUMN IF EXISTS photo_requested,
|
||||
DROP COLUMN IF EXISTS username_requested,
|
||||
DROP COLUMN IF EXISTS name_requested;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE public.webview_requested_buttons
|
||||
ADD COLUMN name_requested boolean NOT NULL DEFAULT false,
|
||||
ADD COLUMN username_requested boolean NOT NULL DEFAULT false,
|
||||
ADD COLUMN photo_requested boolean NOT NULL DEFAULT false;
|
||||
13
deploy/migrations/0120_bot_api_ephemeral_payload.down.sql
Normal file
13
deploy/migrations/0120_bot_api_ephemeral_payload.down.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
DELETE FROM public.bot_api_updates
|
||||
WHERE ephemeral_payload IS NOT NULL;
|
||||
|
||||
DROP INDEX IF EXISTS public.bot_api_updates_ephemeral_version_unique;
|
||||
DROP INDEX IF EXISTS public.bot_api_updates_message_source_unique;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
DROP CONSTRAINT bot_api_updates_ephemeral_shape_check,
|
||||
DROP COLUMN ephemeral_payload;
|
||||
|
||||
CREATE UNIQUE INDEX bot_api_updates_message_source_unique
|
||||
ON public.bot_api_updates (bot_user_id, update_kind, peer_type, peer_id, message_id, source_pts)
|
||||
WHERE update_kind IN ('message', 'edited_message');
|
||||
61
deploy/migrations/0120_bot_api_ephemeral_payload.up.sql
Normal file
61
deploy/migrations/0120_bot_api_ephemeral_payload.up.sql
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
ALTER TABLE public.bot_api_updates
|
||||
ADD COLUMN ephemeral_payload jsonb;
|
||||
|
||||
ALTER TABLE public.bot_api_updates
|
||||
ADD CONSTRAINT bot_api_updates_ephemeral_shape_check CHECK (
|
||||
ephemeral_payload IS NULL
|
||||
OR (
|
||||
peer_type = 'channel'
|
||||
AND peer_id > 0
|
||||
AND message_id > 0
|
||||
AND source_pts = 0
|
||||
AND jsonb_typeof(ephemeral_payload) = 'object'
|
||||
AND jsonb_typeof(ephemeral_payload -> 'Message') = 'object'
|
||||
AND (ephemeral_payload #>> '{Message,ID}') IS NOT NULL
|
||||
AND (ephemeral_payload #>> '{Message,Peer,Type}') IS NOT NULL
|
||||
AND (ephemeral_payload #>> '{Message,Peer,ID}') IS NOT NULL
|
||||
AND (ephemeral_payload #>> '{Message,SenderUserID}') IS NOT NULL
|
||||
AND (ephemeral_payload #>> '{Message,ReceiverUserID}') IS NOT NULL
|
||||
AND (ephemeral_payload #>> '{Message,Version}') IS NOT NULL
|
||||
AND NOT ((ephemeral_payload -> 'Message') ?| ARRAY[
|
||||
'RandomID', 'OriginDevice', 'PayloadHash', 'CreatedAt', 'Deleted'
|
||||
])
|
||||
AND (
|
||||
NOT (ephemeral_payload ? 'ReplyTo')
|
||||
OR (
|
||||
jsonb_typeof(ephemeral_payload -> 'ReplyTo') = 'object'
|
||||
AND NOT ((ephemeral_payload -> 'ReplyTo') ?| ARRAY[
|
||||
'RandomID', 'OriginDevice', 'PayloadHash', 'CreatedAt', 'Deleted'
|
||||
])
|
||||
)
|
||||
)
|
||||
AND (ephemeral_payload #>> '{Message,ID}')::integer = message_id
|
||||
AND (ephemeral_payload #>> '{Message,Peer,Type}') = peer_type
|
||||
AND (ephemeral_payload #>> '{Message,Peer,ID}')::bigint = peer_id
|
||||
AND (
|
||||
(update_kind = 'callback_query'
|
||||
AND (ephemeral_payload #>> '{Message,SenderUserID}')::bigint = bot_user_id)
|
||||
OR
|
||||
(update_kind IN ('message', 'edited_message')
|
||||
AND (ephemeral_payload #>> '{Message,ReceiverUserID}')::bigint = bot_user_id)
|
||||
)
|
||||
AND (ephemeral_payload #>> '{Message,Version}')::bigint > 0
|
||||
)
|
||||
);
|
||||
|
||||
DROP INDEX public.bot_api_updates_message_source_unique;
|
||||
|
||||
CREATE UNIQUE INDEX bot_api_updates_message_source_unique
|
||||
ON public.bot_api_updates (bot_user_id, update_kind, peer_type, peer_id, message_id, source_pts)
|
||||
WHERE update_kind IN ('message', 'edited_message') AND ephemeral_payload IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX bot_api_updates_ephemeral_version_unique
|
||||
ON public.bot_api_updates (
|
||||
bot_user_id,
|
||||
update_kind,
|
||||
peer_type,
|
||||
peer_id,
|
||||
message_id,
|
||||
((ephemeral_payload #>> '{Message,Version}')::bigint)
|
||||
)
|
||||
WHERE update_kind IN ('message', 'edited_message') AND ephemeral_payload IS NOT NULL;
|
||||
1
deploy/migrations/0121_ephemeral_abuse_reports.down.sql
Normal file
1
deploy/migrations/0121_ephemeral_abuse_reports.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS public.ephemeral_abuse_reports;
|
||||
23
deploy/migrations/0121_ephemeral_abuse_reports.up.sql
Normal file
23
deploy/migrations/0121_ephemeral_abuse_reports.up.sql
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
CREATE TABLE public.ephemeral_abuse_reports (
|
||||
id bigserial PRIMARY KEY,
|
||||
reporter_user_id bigint NOT NULL CHECK (reporter_user_id > 0),
|
||||
channel_id bigint NOT NULL CHECK (channel_id > 0),
|
||||
ephemeral_message_id integer NOT NULL CHECK (ephemeral_message_id > 0),
|
||||
sender_user_id bigint NOT NULL CHECK (sender_user_id > 0),
|
||||
receiver_user_id bigint NOT NULL CHECK (receiver_user_id = reporter_user_id),
|
||||
report_option text NOT NULL CHECK (length(report_option) BETWEEN 1 AND 64),
|
||||
report_comment text NOT NULL DEFAULT '' CHECK (length(report_comment) <= 4096),
|
||||
comment_hash bytea NOT NULL CHECK (octet_length(comment_hash) = 32),
|
||||
payload_hash bytea NOT NULL CHECK (octet_length(payload_hash) = 32),
|
||||
evidence jsonb NOT NULL CHECK (jsonb_typeof(evidence) = 'object'),
|
||||
created_at timestamptz NOT NULL,
|
||||
CONSTRAINT ephemeral_abuse_reports_idempotency UNIQUE (
|
||||
reporter_user_id, channel_id, ephemeral_message_id, report_option, comment_hash
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX ephemeral_abuse_reports_created_at_idx
|
||||
ON public.ephemeral_abuse_reports (created_at DESC, id DESC);
|
||||
|
||||
CREATE INDEX ephemeral_abuse_reports_sender_created_idx
|
||||
ON public.ephemeral_abuse_reports (sender_user_id, created_at DESC, id DESC);
|
||||
20
deploy/migrations/0122_communities.down.sql
Normal file
20
deploy/migrations/0122_communities.down.sql
Normal 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;
|
||||
130
deploy/migrations/0122_communities.up.sql
Normal file
130
deploy/migrations/0122_communities.up.sql
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue