wacrm/supabase/migrations/023_chat_media.sql
Arnas Donauskas 312683ca05
feat(inbox): send photo, video, document & voice in chat (#213)
Agents can now attach a photo/video/document or record a voice note in
the inbox composer and send it live in a 1:1 thread — previously media
could only be received from customers or sent via the Flows builder.

- 023_chat_media.sql: new account-scoped `chat-media` Storage bucket
  (mirrors flow-media RLS from 020) plus Meta-accepted audio MIME types.
- meta-api: sendMediaMessage gains an `audio` kind (link-only — Meta
  rejects caption/filename on audio; WhatsApp renders OGG/Opus as a
  voice note).
- /api/whatsapp/send: media branch reusing the phone-variant retry loop.
- /api/whatsapp/transcode-audio: Node-runtime route remuxing the
  WebM/Opus that Chromium's MediaRecorder produces to OGG/Opus via
  ffmpeg-static (Firefox/Safari record accepted formats and skip it).
- composer: attach menu, file pickers, voice recorder + auto-transcode,
  preview with caption; thread: optimistic handleSendMedia.
- shared uploadAccountMedia helper (flows form refactored onto it).
- next.config: allow microphone=(self) (was fully blocked) + CSP
  media-src; document optional FFMPEG_PATH override.

Tests: meta-api audio payload shape, buildMediaPath, audio-transcode
helpers (14 new). typecheck/lint/build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:07:16 +03:00

123 lines
4.6 KiB
SQL

-- ============================================================
-- 023_chat_media.sql
--
-- Adds the `chat-media` Supabase Storage bucket used when an agent
-- sends a photo / video / document / voice note from the inbox
-- composer (issue #213). Today media can only be RECEIVED from
-- customers or sent via the Flows `send_media` node — never typed
-- and sent live in a 1:1 thread.
--
-- Mirrors the `flow-media` bucket (migration 016) and its
-- account-scoped storage RLS (migration 020), with two differences:
--
-- 1. A separate bucket so chat attachments and flow-builder media
-- stay conceptually distinct (and so a future per-bucket size /
-- retention policy can diverge without touching flows).
--
-- 2. The allowed MIME list adds the audio types Meta accepts for
-- outbound voice notes — audio/ogg (Opus), audio/mpeg, audio/aac,
-- audio/mp4, audio/amr. Browser recordings (WebM/Opus) are
-- transcoded to audio/ogg BEFORE upload, so WebM never lands
-- here and isn't allow-listed.
--
-- Path convention (same as flow-media post-020):
-- chat-media/account-<account_id>/<timestamp>-<basename>.<ext>
-- The bucket is public so Meta can fetch the URL without auth; writes
-- are scoped to account members via the path's first segment.
--
-- Size limit 16 MB — Meta's tightest universal cap (video). Documents
-- can technically be 100 MB on Meta, but we hold the universal cap to
-- match flow-media and keep one limit to reason about.
--
-- Idempotent — safe to re-run.
-- ============================================================
-- ============================================================
-- 1. chat-media storage bucket
-- ============================================================
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
VALUES (
'chat-media',
'chat-media',
TRUE,
16777216, -- 16 MB (Meta video cap; documents/images/audio fit under this)
ARRAY[
-- Images
'image/png', 'image/jpeg', 'image/webp',
-- Videos
'video/mp4', 'video/3gpp',
-- Documents
'application/pdf',
'application/vnd.ms-powerpoint',
'application/msword',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'text/plain',
-- Audio (voice notes) — only Meta-accepted outbound types. Browser
-- WebM/Opus is transcoded to audio/ogg before upload.
'audio/ogg',
'audio/mpeg',
'audio/aac',
'audio/mp4',
'audio/amr'
]
)
ON CONFLICT (id) DO UPDATE
SET
public = EXCLUDED.public,
file_size_limit = EXCLUDED.file_size_limit,
allowed_mime_types = EXCLUDED.allowed_mime_types;
-- ============================================================
-- 2. Storage RLS — account-scoped writes, public reads
--
-- Same predicate shape as migration 020's flow-media policies:
-- writes are allowed when the path's first segment is
-- `account-<account_id>` for an account the caller belongs to.
-- Reads are public (the bucket is public so Meta can fetch links).
--
-- Drop-then-create (Postgres has no CREATE POLICY IF NOT EXISTS).
-- ============================================================
DROP POLICY IF EXISTS "Chat media is publicly readable" ON storage.objects;
CREATE POLICY "Chat media is publicly readable"
ON storage.objects FOR SELECT
USING (bucket_id = 'chat-media');
DROP POLICY IF EXISTS "Members can upload chat media" ON storage.objects;
CREATE POLICY "Members can upload chat media"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'chat-media'
AND EXISTS (
SELECT 1 FROM public.profiles p
WHERE p.user_id = auth.uid()
AND ('account-' || p.account_id::text) = (storage.foldername(name))[1]
)
);
DROP POLICY IF EXISTS "Members can update chat media" ON storage.objects;
CREATE POLICY "Members can update chat media"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'chat-media'
AND EXISTS (
SELECT 1 FROM public.profiles p
WHERE p.user_id = auth.uid()
AND ('account-' || p.account_id::text) = (storage.foldername(name))[1]
)
);
DROP POLICY IF EXISTS "Members can delete chat media" ON storage.objects;
CREATE POLICY "Members can delete chat media"
ON storage.objects FOR DELETE
USING (
bucket_id = 'chat-media'
AND EXISTS (
SELECT 1 FROM public.profiles p
WHERE p.user_id = auth.uid()
AND ('account-' || p.account_id::text) = (storage.foldername(name))[1]
)
);