Add account based multiplayer hosting and bug report retention
This commit is contained in:
80
supabase/migrations/20261010053310_bug_report_retention.sql
Normal file
80
supabase/migrations/20261010053310_bug_report_retention.sql
Normal file
@@ -0,0 +1,80 @@
|
||||
alter table public.bug_reports drop constraint bug_reports_state_check;
|
||||
alter table public.bug_reports add constraint bug_reports_state_check check(state in ('draft','pending','open','done'));
|
||||
alter table public.bug_reports
|
||||
add column closed_at timestamptz,
|
||||
add column delete_after timestamptz,
|
||||
add column linear_done_synced boolean not null default false,
|
||||
add column cleanup_run_id text,
|
||||
add column cleanup_lease_until timestamptz,
|
||||
add column cleanup_error text,
|
||||
add column purge_started_at timestamptz,
|
||||
add column status_checked_at timestamptz;
|
||||
create index bug_reports_cleanup on public.bug_reports(delete_after) where state='done';
|
||||
|
||||
create function public.set_bug_report_done(p_report uuid,p_done boolean,p_closed_at timestamptz default null)
|
||||
returns boolean language plpgsql security invoker set search_path='' as $$
|
||||
declare report public.bug_reports; finished timestamptz;
|
||||
begin
|
||||
select * into report from public.bug_reports where id=p_report and state<>'draft' for update;
|
||||
if not found or report.purge_started_at is not null then return false; end if;
|
||||
if p_done then
|
||||
finished=coalesce(report.closed_at,least(coalesce(p_closed_at,clock_timestamp()),clock_timestamp()));
|
||||
update public.bug_reports set state='done',closed_at=finished,delete_after=finished+interval '3 days',updated_at=clock_timestamp()
|
||||
where id=p_report;
|
||||
elsif report.state='done' then
|
||||
update public.bug_reports set state='open',closed_at=null,delete_after=null,linear_done_synced=false,
|
||||
cleanup_run_id=null,cleanup_lease_until=null,cleanup_error=null,updated_at=clock_timestamp() where id=p_report;
|
||||
end if;
|
||||
return true;
|
||||
end; $$;
|
||||
|
||||
create function public.claim_bug_cleanup(p_report uuid,p_worker text)
|
||||
returns setof public.bug_reports language sql security invoker set search_path='' as $$
|
||||
update public.bug_reports set cleanup_run_id=p_worker,cleanup_lease_until=clock_timestamp()+interval '3 minutes'
|
||||
where id=p_report and state='done' and closed_at is not null
|
||||
and (cleanup_run_id is null or cleanup_run_id like 'starting:%')
|
||||
and (cleanup_lease_until is null or cleanup_lease_until<clock_timestamp()) returning *;
|
||||
$$;
|
||||
create function public.begin_bug_purge(p_report uuid,p_closed_at timestamptz)
|
||||
returns boolean language plpgsql security invoker set search_path='' as $$
|
||||
begin
|
||||
update public.bug_reports set purge_started_at=coalesce(purge_started_at,clock_timestamp())
|
||||
where id=p_report and state='done' and closed_at=p_closed_at and delete_after<=clock_timestamp() and linear_done_synced;
|
||||
return found;
|
||||
end; $$;
|
||||
create function public.finish_bug_purge(p_report uuid,p_closed_at timestamptz)
|
||||
returns boolean language plpgsql security invoker set search_path='' as $$
|
||||
begin
|
||||
delete from public.bug_reports where id=p_report and state='done' and closed_at=p_closed_at
|
||||
and delete_after<=clock_timestamp() and purge_started_at is not null;
|
||||
return found;
|
||||
end; $$;
|
||||
|
||||
-- A closed conversation cannot gain new messages while deletion is in progress.
|
||||
create function public.guard_done_bug_message() returns trigger
|
||||
language plpgsql security invoker set search_path='' as $$
|
||||
begin
|
||||
perform 1 from public.bug_reports where id=new.report_id and purge_started_at is null
|
||||
and (new.kind<>'user' or state<>'done') for share;
|
||||
if not found then raise exception 'This bug report is Done'; end if;
|
||||
return new;
|
||||
end; $$;
|
||||
create trigger guard_done_bug_message before insert on public.bug_report_messages
|
||||
for each row execute function public.guard_done_bug_message();
|
||||
revoke all on function public.set_bug_report_done(uuid,boolean,timestamptz),public.claim_bug_cleanup(uuid,text),
|
||||
public.begin_bug_purge(uuid,timestamptz),public.finish_bug_purge(uuid,timestamptz),public.guard_done_bug_message()
|
||||
from public,anon,authenticated;
|
||||
grant execute on function public.set_bug_report_done(uuid,boolean,timestamptz),public.claim_bug_cleanup(uuid,text),
|
||||
public.begin_bug_purge(uuid,timestamptz),public.finish_bug_purge(uuid,timestamptz),public.guard_done_bug_message() to service_role;
|
||||
|
||||
create function public.receive_bug_close(p_report uuid,p_external uuid) returns boolean
|
||||
language plpgsql security invoker set search_path='' as $$
|
||||
begin
|
||||
perform 1 from public.bug_reports where id=p_report and state<>'draft' and purge_started_at is null for update;
|
||||
if not found or exists(select 1 from public.bug_report_messages where external_id=p_external) then return false; end if;
|
||||
insert into public.bug_report_messages(report_id,kind,body,author_name,delivered,external_id)
|
||||
values(p_report,'system','Report closed.','Support',true,p_external);
|
||||
return public.set_bug_report_done(p_report,true,null);
|
||||
end; $$;
|
||||
revoke all on function public.receive_bug_close(uuid,uuid) from public,anon,authenticated;
|
||||
grant execute on function public.receive_bug_close(uuid,uuid) to service_role;
|
||||
@@ -0,0 +1,65 @@
|
||||
-- Hosting always belongs to an account. The browser cannot choose the host name.
|
||||
create or replace function public.create_map_room(p_dataset uuid,p_difficulty text,p_name text,p_limit integer,p_access_hash text,p_owner uuid)
|
||||
returns uuid language plpgsql security invoker set search_path='' as $$
|
||||
declare rid uuid; revision uuid; pid uuid:=gen_random_uuid(); rev public.revisions; vp jsonb; n integer:=0; account_name text;
|
||||
begin
|
||||
select display_name into account_name from public.profiles where id=p_owner;
|
||||
if not found then raise exception 'Log in to create a multiplayer room'; end if;
|
||||
if p_limit is null or p_limit not between 2 and 16 then raise exception 'Choose between 2 and 16 players'; end if;
|
||||
if p_difficulty is null or p_difficulty not in ('easy','normal','hard') then raise exception 'Invalid difficulty'; end if;
|
||||
revision:=public.playable_map_revision(p_dataset,p_owner);
|
||||
select * into rev from public.revisions where id=revision;
|
||||
if not found then raise exception 'Map unavailable'; end if;
|
||||
if coalesce(jsonb_array_length(rev.manifest->'viewpoints'),0)<5 then raise exception 'Map has fewer than five viewpoints'; end if;
|
||||
insert into public.multiplayer_rooms(revision_id,host_player_id,difficulty,player_limit) values(rev.id,pid,p_difficulty,p_limit) returning id into rid;
|
||||
insert into public.multiplayer_players(id,room_id,access_hash,display_name,account_id) values(pid,rid,p_access_hash,account_name,p_owner);
|
||||
for vp in select value from jsonb_array_elements(rev.manifest->'viewpoints') order by random() limit 5 loop
|
||||
insert into public.multiplayer_rounds(room_id,round_number,viewpoint) values(rid,n,vp); n:=n+1;
|
||||
end loop;
|
||||
return rid;
|
||||
end; $$;
|
||||
|
||||
-- Link signed-in joins within the room lock, before a concurrent host can leave.
|
||||
create function public.join_multiplayer_room(p_room uuid,p_access_hash text,p_owner uuid,p_name text)
|
||||
returns jsonb language plpgsql security invoker set search_path='' as $$
|
||||
declare account_name text; result jsonb;
|
||||
begin
|
||||
perform 1 from public.multiplayer_rooms where id=p_room for update;
|
||||
if p_owner is not null then
|
||||
select display_name into account_name from public.profiles where id=p_owner;
|
||||
if not found then raise exception 'Account not found'; end if;
|
||||
else account_name:=p_name;
|
||||
end if;
|
||||
result:=public.multiplayer_action(p_room,p_access_hash,'join',jsonb_build_object('name',account_name));
|
||||
if p_owner is not null then
|
||||
update public.multiplayer_players set account_id=p_owner
|
||||
where room_id=p_room and access_hash=p_access_hash and account_id is null and not has_left;
|
||||
end if;
|
||||
return result;
|
||||
end; $$;
|
||||
|
||||
-- A departing host can hand the party to another signed-in player, never a guest.
|
||||
create function public.ensure_account_party_host() returns trigger
|
||||
language plpgsql security invoker set search_path='' as $$
|
||||
declare replacement uuid;
|
||||
begin
|
||||
if new.host_player_id is not distinct from old.host_player_id or new.status='closed' then return new; end if;
|
||||
if exists(select 1 from public.multiplayer_players p join public.profiles a on a.id=p.account_id
|
||||
where p.id=new.host_player_id and p.room_id=new.id and not p.has_left) then return new; end if;
|
||||
select p.id into replacement from public.multiplayer_players p join public.profiles a on a.id=p.account_id
|
||||
where p.room_id=new.id and not p.has_left order by p.joined_at,p.id limit 1;
|
||||
if replacement is null then
|
||||
new.status:='closed'; new.transition_at:=null;
|
||||
else new.host_player_id:=replacement;
|
||||
end if;
|
||||
return new;
|
||||
end; $$;
|
||||
create trigger ensure_account_party_host before update of host_player_id on public.multiplayer_rooms
|
||||
for each row execute function public.ensure_account_party_host();
|
||||
|
||||
-- Capture a close even when its status was set by the host-transfer trigger.
|
||||
create trigger close_transferred_party_discord after update of host_player_id on public.multiplayer_rooms
|
||||
for each row when (new.status='closed') execute function public.close_party_discord();
|
||||
|
||||
revoke all on function public.join_multiplayer_room(uuid,text,uuid,text),public.ensure_account_party_host() from public,anon,authenticated;
|
||||
grant execute on function public.join_multiplayer_room(uuid,text,uuid,text),public.ensure_account_party_host() to service_role;
|
||||
Reference in New Issue
Block a user