101 lines
5.3 KiB
PL/PgSQL
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;
|