feat(db): message_attachments.view_once + mark_attachment_viewed RPC
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
-- View-once media: per-attachment opt-in. When a recipient opens the
|
||||
-- attachment for the first time we delete the storage object and replace
|
||||
-- it with a tombstone marker (kept_until / viewed_at). The encrypted
|
||||
-- attachment row stays so the bubble can render "Angesehen".
|
||||
|
||||
alter table public.message_attachments
|
||||
add column if not exists view_once boolean not null default false,
|
||||
add column if not exists viewed_at timestamptz null,
|
||||
add column if not exists viewed_by uuid null references auth.users(id) on delete set null;
|
||||
|
||||
-- Mark-viewed RPC: server-authoritative so the recipient can't replay the
|
||||
-- decrypted blob across devices. Atomic: only the FIRST viewer wins. The
|
||||
-- function returns the previous `viewed_at` so the client can distinguish
|
||||
-- first-open (got it) from already-viewed (too late, sorry).
|
||||
create or replace function public.mark_attachment_viewed(p_attachment_id uuid)
|
||||
returns jsonb
|
||||
language plpgsql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
declare
|
||||
caller uuid := auth.uid();
|
||||
att public.message_attachments%rowtype;
|
||||
msg public.messages%rowtype;
|
||||
prev timestamptz;
|
||||
begin
|
||||
if caller is null then raise exception 'not authenticated'; end if;
|
||||
|
||||
select * into att from public.message_attachments where id = p_attachment_id for update;
|
||||
if not found then raise exception 'attachment not found'; end if;
|
||||
if not att.view_once then return jsonb_build_object('view_once', false); end if;
|
||||
|
||||
select * into msg from public.messages where id = att.message_id;
|
||||
if not found then raise exception 'orphan attachment'; end if;
|
||||
if not public.is_conversation_member(msg.conversation_id) then
|
||||
raise exception 'not a member';
|
||||
end if;
|
||||
-- Sender opening their own view-once doesn't "burn" it — they sent it.
|
||||
if msg.sender_id = caller then
|
||||
return jsonb_build_object('view_once', true, 'viewed_at', att.viewed_at, 'self', true);
|
||||
end if;
|
||||
|
||||
if att.viewed_at is not null then
|
||||
return jsonb_build_object('view_once', true, 'viewed_at', att.viewed_at, 'already', true);
|
||||
end if;
|
||||
|
||||
update public.message_attachments
|
||||
set viewed_at = now(),
|
||||
viewed_by = caller
|
||||
where id = p_attachment_id
|
||||
returning viewed_at into prev;
|
||||
|
||||
return jsonb_build_object('view_once', true, 'viewed_at', prev);
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke execute on function public.mark_attachment_viewed(uuid) from public, anon;
|
||||
grant execute on function public.mark_attachment_viewed(uuid) to authenticated;
|
||||
Reference in New Issue
Block a user