-- Durable, server-only announcements. One event per public map or shared party. create table public.discord_announcements ( id uuid primary key default gen_random_uuid(), kind text not null check (kind in ('map','party')), target_id uuid not null, state text not null default 'pending' check (state in ('pending','sending','sent','skipped','failed')), attempts integer not null default 0 check (attempts>=0), next_attempt_at timestamptz not null default clock_timestamp(), lease_owner text, lease_until timestamptz, channel_id text, message_id text, last_error text, created_at timestamptz not null default clock_timestamp(), sent_at timestamptz, unique(kind,target_id) ); create index discord_announcements_due on public.discord_announcements(next_attempt_at,created_at) where state in ('pending','sending'); alter table public.discord_announcements enable row level security; revoke all on public.discord_announcements from public,anon,authenticated; grant all on public.discord_announcements to service_role; create policy "Server manages Discord delivery" on public.discord_announcements for all to service_role using (true) with check (true); create function public.queue_new_map_discord() returns trigger language plpgsql security invoker set search_path='' as $$ begin if new.state='published' and old.state<>'published' and old.revision_id is null and new.parent_dataset_id is null and new.imported_from_id is null then insert into public.discord_announcements(kind,target_id) values('map',new.id) on conflict(kind,target_id) do nothing; end if; return new; end; $$; create trigger queue_new_map_discord after update of state on public.datasets for each row execute function public.queue_new_map_discord(); create function public.publish_discord_party(p_room uuid,p_access_hash text) returns uuid language plpgsql security invoker set search_path='' as $$ declare room public.multiplayer_rooms; event_id uuid; active integer; begin select * into room from public.multiplayer_rooms where id=p_room for update; if not found then raise exception 'Room not found'; end if; if not exists(select 1 from public.multiplayer_players where room_id=p_room and id=room.host_player_id and access_hash=p_access_hash and not has_left) then raise exception 'Only the room owner can publish this party'; end if; if room.status<>'lobby' or room.created_at=room.player_limit then raise exception 'This room is full'; end if; if not exists(select 1 from public.revisions r join public.datasets d on d.id=r.dataset_id where r.id=room.revision_id and d.state='published' and d.parent_dataset_id is null and d.imported_from_id is null) then raise exception 'Only parties on published maps can be shared on Discord'; end if; insert into public.discord_announcements(kind,target_id) values('party',p_room) on conflict(kind,target_id) do nothing; select id into event_id from public.discord_announcements where kind='party' and target_id=p_room; return event_id; end; $$; create function public.claim_discord_announcements(p_worker text,p_kinds text[],p_target uuid default null) returns setof public.discord_announcements language sql security invoker set search_path='' as $$ with exhausted as ( update public.discord_announcements set state='failed',lease_owner=null,lease_until=null, last_error='Discord delivery could not be confirmed. Check the channel before retrying.' where state='sending' and attempts>=8 and lease_until