Files
MineGeo/supabase/migrations/20261009133431_discord_announcements.sql

101 lines
5.3 KiB
PL/PgSQL

-- 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<clock_timestamp()-interval '24 hours' then
raise exception 'Only open lobbies can be published';
end if;
select count(*) into active from public.multiplayer_players where room_id=p_room and not has_left;
if active>=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<clock_timestamp() returning id
)
update public.discord_announcements e set state='sending',attempts=e.attempts+1,
lease_owner=p_worker,lease_until=clock_timestamp()+interval '3 minutes'
where e.id in (
select id from public.discord_announcements where kind=any(p_kinds)
and (p_target is null or target_id=p_target) and state in ('pending','sending')
and attempts<8 and next_attempt_at<=clock_timestamp()
and (lease_until is null or lease_until<clock_timestamp())
order by created_at,id limit 5 for update skip locked
) returning e.*;
$$;
create function public.retry_discord_announcements(p_reviewer uuid) returns integer
language plpgsql security invoker set search_path='' as $$
declare changed integer;
begin
if not exists(select 1 from public.profiles where id=p_reviewer and role='admin') then
raise exception 'Admin permission required';
end if;
update public.discord_announcements set state='pending',attempts=0,next_attempt_at=clock_timestamp(),last_error=null,
lease_owner=null,lease_until=null where state='failed' or (state='pending' and lease_owner is null)
or (state='sending' and lease_until<clock_timestamp());
get diagnostics changed=row_count;
return changed;
end; $$;
revoke all on function public.queue_new_map_discord(),public.publish_discord_party(uuid,text),
public.claim_discord_announcements(text,text[],uuid),public.retry_discord_announcements(uuid)
from public,anon,authenticated;
grant execute on function public.queue_new_map_discord(),public.publish_discord_party(uuid,text),
public.claim_discord_announcements(text,text[],uuid),public.retry_discord_announcements(uuid) to service_role;