fix(chat): group-message FK violation + receipt-cursor stall

Group inserts were using recipient_id=0 as a "no recipient" sentinel,
which fails the tenants FK (foreign_keys is ON) and 500s on every
group send. Use the sender's own id as the placeholder instead;
group GET queries already key off room_uid so there's no self-message
artefact from the value.

Also exclude group rows from /api/chat/sent so the client's
last_sent_relay_id cursor doesn't stall on group sends that never
reach seen=1 (group receipts aren't tracked in v1).

Client-side: surface a clearer error when a room create hits a 404
on the relay — almost always means the relay is running pre-group
code and needs a restart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-15 20:57:18 +02:00
1 parent 0fd97e5d71
commit 6aa555cc6a
2 files changed
+20 -8

No files matched your search

+10 -6
View File
@@ -295,14 +295,15 @@ if ($route === '/api/chat' && $method === 'POST') {
$stored = clientEncrypt($plain, $interKey);
if ($roomUid !== '') {
// Group message: verify membership, single row with room_uid set
// and recipient_id=0 (sentinel — fan-out happens on GET via the
// chat_room_members join).
// Group message: verify membership. `recipient_id` is NOT NULL +
// has a tenants-FK, so we set it to the sender's own id as a
// placeholder — GET queries key off room_uid, never recipient_id,
// for group rows so this doesn't create a self-message artefact.
$check = $db->prepare('SELECT 1 FROM chat_room_members WHERE room_uid = :r AND tenant_id = :t');
$check->execute(['r' => $roomUid, 't' => $clientTenant['id']]);
if (!$check->fetchColumn()) jsonResponse(['error' => 'not a member of this room'], 403);
$db->prepare('INSERT INTO chat_messages (recipient_id, sender_id, sender_username, room_uid, body, persistent) VALUES (0, :s, :sn, :ru, :b, 1)')
->execute(['s' => $clientTenant['id'], 'sn' => $clientTenant['username'], 'ru' => $roomUid, 'b' => $stored]);
$db->prepare('INSERT INTO chat_messages (recipient_id, sender_id, sender_username, room_uid, body, persistent) VALUES (:me, :s, :sn, :ru, :b, 1)')
->execute(['me' => $clientTenant['id'], 's' => $clientTenant['id'], 'sn' => $clientTenant['username'], 'ru' => $roomUid, 'b' => $stored]);
} else {
$recipient = getTenantByUsername($db, $recipientName);
if (!$recipient) jsonResponse(['error' => "no such user: $recipientName"], 404);
@@ -463,10 +464,13 @@ if ($route === '/api/chat/backfill' && $method === 'GET') {
if ($route === '/api/chat/sent' && $method === 'GET') {
if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401);
$since = (int)($_GET['since'] ?? 0);
// 1:1 only — group messages use the same sender_id but have no
// per-recipient receipt state, so including them would stall the
// cursor on rows that never reach seen=1.
$stmt = $db->prepare('
SELECT id, recipient_id, fetched, seen, created_at
FROM chat_messages
WHERE sender_id = :s AND id > :since
WHERE sender_id = :s AND room_uid IS NULL AND id > :since
ORDER BY id ASC
LIMIT 1000
');