59 lines
3.8 KiB
PL/PgSQL
59 lines
3.8 KiB
PL/PgSQL
-- Room cookies are verified by the API; browser roles cannot access chat directly.
|
|
create table public.multiplayer_messages (
|
|
id bigint generated always as identity primary key,
|
|
room_id uuid not null references public.multiplayer_rooms(id) on delete cascade,
|
|
player_id uuid not null,
|
|
client_id uuid not null,
|
|
display_name text not null,
|
|
body text not null check (length(btrim(body)) between 1 and 500
|
|
and body !~ '[[:cntrl:]]' and body !~ U&'[\0080-\009F\202A-\202E\2066-\2069]'),
|
|
created_at timestamptz not null default clock_timestamp(),
|
|
foreign key(room_id,player_id) references public.multiplayer_players(room_id,id) on delete cascade,
|
|
unique(room_id,player_id,client_id)
|
|
);
|
|
create index multiplayer_messages_history on public.multiplayer_messages(room_id,id desc);
|
|
create index multiplayer_messages_rate on public.multiplayer_messages(room_id,player_id,created_at desc);
|
|
alter table public.multiplayer_messages enable row level security;
|
|
revoke all on public.multiplayer_messages from public,anon,authenticated;
|
|
revoke all on sequence public.multiplayer_messages_id_seq from public,anon,authenticated;
|
|
grant all on public.multiplayer_messages to service_role;
|
|
grant usage,select on sequence public.multiplayer_messages_id_seq to service_role;
|
|
|
|
create function public.multiplayer_chat(p_room uuid,p_access_hash text,p_text text default null,p_client_id uuid default null)
|
|
returns jsonb language plpgsql security invoker set search_path='' as $$
|
|
declare room public.multiplayer_rooms; player public.multiplayer_players;
|
|
begin
|
|
-- Match the room action lock order, including kicks/leaves/closure. Duplicate
|
|
-- sends and rate checks are serialized without touching game timers or guesses.
|
|
select * into room from public.multiplayer_rooms where id=p_room for update;
|
|
if not found then raise exception 'Room not found'; end if;
|
|
select * into player from public.multiplayer_players
|
|
where room_id=p_room and access_hash=p_access_hash and not has_left;
|
|
if not found then raise exception 'Chat permission requires joining this room'; end if;
|
|
if room.status='closed' or room.created_at < clock_timestamp()-interval '24 hours' then
|
|
raise exception 'Chat is unavailable in a closed or expired room';
|
|
end if;
|
|
if p_text is not null or p_client_id is not null then
|
|
if p_text is null or p_client_id is null or length(btrim(p_text)) not between 1 and 500
|
|
or p_text ~ '[[:cntrl:]]' or p_text ~ U&'[\0080-\009F\202A-\202E\2066-\2069]' then
|
|
raise exception 'Invalid chat message';
|
|
end if;
|
|
if not exists(select 1 from public.multiplayer_messages
|
|
where room_id=p_room and player_id=player.id and client_id=p_client_id) then
|
|
if exists(select 1 from public.multiplayer_messages where room_id=p_room and player_id=player.id
|
|
and created_at > clock_timestamp()-interval '1 second')
|
|
or (select count(*) from public.multiplayer_messages where room_id=p_room and player_id=player.id
|
|
and created_at > clock_timestamp()-interval '1 minute') >= 30 then
|
|
raise exception 'Too many chat messages. Wait a moment and try again.';
|
|
end if;
|
|
insert into public.multiplayer_messages(room_id,player_id,client_id,display_name,body)
|
|
values(p_room,player.id,p_client_id,player.display_name,btrim(p_text));
|
|
end if;
|
|
end if;
|
|
return jsonb_build_object('messages',coalesce((select jsonb_agg(jsonb_build_object(
|
|
'id',m.id::text,'playerId',m.player_id,'name',m.display_name,'text',m.body,'createdAt',m.created_at
|
|
) order by m.id) from (select * from public.multiplayer_messages where room_id=p_room order by id desc limit 100) m),'[]'::jsonb));
|
|
end; $$;
|
|
revoke all on function public.multiplayer_chat(uuid,text,text,uuid) from public,anon,authenticated;
|
|
grant execute on function public.multiplayer_chat(uuid,text,text,uuid) to service_role;
|