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:
1 parent
0fd97e5d71
commit
6aa555cc6a
2 files changed
+20
-8
No files matched your search
@@ -530,7 +530,8 @@ PluginManager::instance()->onApiRoute(function (string $route, string $method) {
|
|||||||
|
|
||||||
// Client-generated UID, same convention as collab buckets.
|
// Client-generated UID, same convention as collab buckets.
|
||||||
$uid = bin2hex(random_bytes(8));
|
$uid = bin2hex(random_bytes(8));
|
||||||
$ch = curl_init(rtrim($r['url'], '/') . '/api/chat/rooms');
|
$relayUrl = rtrim($r['url'], '/') . '/api/chat/rooms';
|
||||||
|
$ch = curl_init($relayUrl);
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
CURLOPT_POST => true,
|
CURLOPT_POST => true,
|
||||||
@@ -547,7 +548,14 @@ PluginManager::instance()->onApiRoute(function (string $route, string $method) {
|
|||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
if ($status !== 200) {
|
if ($status !== 200) {
|
||||||
$err = json_decode($response, true);
|
$err = json_decode($response, true);
|
||||||
chatJson(['ok' => false, 'error' => $err['error'] ?? "relay HTTP $status"]);
|
$detail = $err['error'] ?? "relay HTTP $status";
|
||||||
|
// 404 on this path almost always means the relay is running a
|
||||||
|
// pre-group-chat build. Surface that explicitly so the user
|
||||||
|
// doesn't go hunting for data issues.
|
||||||
|
if ($status === 404) {
|
||||||
|
$detail = "relay does not know /api/chat/rooms — it's likely running old code. Restart the relay after pulling the latest commits.";
|
||||||
|
}
|
||||||
|
chatJson(['ok' => false, 'error' => $detail, 'url' => $relayUrl, 'relay_status' => $status]);
|
||||||
}
|
}
|
||||||
$data = json_decode($response, true) ?: [];
|
$data = json_decode($response, true) ?: [];
|
||||||
// Mirror locally right away so the next conversations list shows
|
// Mirror locally right away so the next conversations list shows
|
||||||
|
|||||||
@@ -295,14 +295,15 @@ if ($route === '/api/chat' && $method === 'POST') {
|
|||||||
$stored = clientEncrypt($plain, $interKey);
|
$stored = clientEncrypt($plain, $interKey);
|
||||||
|
|
||||||
if ($roomUid !== '') {
|
if ($roomUid !== '') {
|
||||||
// Group message: verify membership, single row with room_uid set
|
// Group message: verify membership. `recipient_id` is NOT NULL +
|
||||||
// and recipient_id=0 (sentinel — fan-out happens on GET via the
|
// has a tenants-FK, so we set it to the sender's own id as a
|
||||||
// chat_room_members join).
|
// 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 = $db->prepare('SELECT 1 FROM chat_room_members WHERE room_uid = :r AND tenant_id = :t');
|
||||||
$check->execute(['r' => $roomUid, 't' => $clientTenant['id']]);
|
$check->execute(['r' => $roomUid, 't' => $clientTenant['id']]);
|
||||||
if (!$check->fetchColumn()) jsonResponse(['error' => 'not a member of this room'], 403);
|
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)')
|
$db->prepare('INSERT INTO chat_messages (recipient_id, sender_id, sender_username, room_uid, body, persistent) VALUES (:me, :s, :sn, :ru, :b, 1)')
|
||||||
->execute(['s' => $clientTenant['id'], 'sn' => $clientTenant['username'], 'ru' => $roomUid, 'b' => $stored]);
|
->execute(['me' => $clientTenant['id'], 's' => $clientTenant['id'], 'sn' => $clientTenant['username'], 'ru' => $roomUid, 'b' => $stored]);
|
||||||
} else {
|
} else {
|
||||||
$recipient = getTenantByUsername($db, $recipientName);
|
$recipient = getTenantByUsername($db, $recipientName);
|
||||||
if (!$recipient) jsonResponse(['error' => "no such user: $recipientName"], 404);
|
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 ($route === '/api/chat/sent' && $method === 'GET') {
|
||||||
if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401);
|
if (!$clientTenant) jsonResponse(['error' => 'unauthorized'], 401);
|
||||||
$since = (int)($_GET['since'] ?? 0);
|
$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('
|
$stmt = $db->prepare('
|
||||||
SELECT id, recipient_id, fetched, seen, created_at
|
SELECT id, recipient_id, fetched, seen, created_at
|
||||||
FROM chat_messages
|
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
|
ORDER BY id ASC
|
||||||
LIMIT 1000
|
LIMIT 1000
|
||||||
');
|
');
|
||||||
|
|||||||
Reference in new issue
Block a user