that channel's messages // POST action=send channel= text=… [color=#rrggbb] [font=mono|sans|serif] // POST action=delete channel= id= local only // POST action=read channel= mark seen up to now // // DESIGN PRINCIPLES // Read marks are local and per channel. "Unread" is a fact about this operator at this // machine, not something to replicate — the partner has their own idea of what they have seen. // // OPERATIONAL SAFEGUARDS // POST for every mutation, so Unraid's CSRF guard applies. See README-unraid.md. // // Channel ids are resolved against vv_nc_channels() inside the store layer, so a request // cannot name a file this host is not a member of. // // Colour and font are validated to a hex triplet and a three-item list before they are stored, // because they are interpolated into a style attribute when rendered. // // Sending reports the record even when no partner could be reached. The message is written // locally and spooled for retry; saying "failed" over something that is stored and queued // would be the wrong claim. // // DEPENDS ON // include/node_chat.php // ═══════════════════════════════════════════════════════════════════════════════════════════════ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/node_chat.php'; define('VV_NC_READ_DB', VV_NC_DIR . '/.read.json'); function vv_nc_read_marks(): array { $j = @json_decode((string)@file_get_contents(VV_NC_READ_DB), true); return is_array($j) ? $j : []; } function vv_nc_set_read(string $ch, int $ts): void { $m = vv_nc_read_marks(); $m[$ch] = $ts; @mkdir(VV_NC_DIR, 0755, true); @file_put_contents(VV_NC_READ_DB, json_encode($m), LOCK_EX); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $marks = vv_nc_read_marks(); $ch = $_GET['channel'] ?? ''; if ($ch !== '') { if (!vv_nc_channel($ch)) { echo json_encode(['ok' => false, 'error' => 'Unknown channel']); exit; } echo json_encode([ 'ok' => true, 'me' => vv_nc_me(), 'channel' => $ch, 'messages' => vv_nc_read($ch, 200), 'last_read'=> (int)($marks[$ch] ?? 0), ]); exit; } $chans = []; foreach (vv_nc_channels() as $c) { $since = (int)($marks[$c['id']] ?? 0); $unread = 0; foreach (vv_nc_read($c['id'], 200) as $m) { if ((int)($m['ts'] ?? 0) > $since && ($m['from'] ?? '') !== vv_nc_me()) $unread++; } $c['unread'] = $unread; $chans[] = $c; } // Hostnames alongside the ids, so the card can show "unRAID-Jayred36" or "HOST2" without a // second request — the mapping is master.conf's and the page should not be guessing it. echo json_encode(['ok' => true, 'me' => vv_nc_me(), 'channels' => $chans, 'hostnames' => vv_known_hosts()]); exit; } $action = $_POST['action'] ?? ''; $ch = $_POST['channel'] ?? ''; if (!vv_nc_channel($ch)) { echo json_encode(['ok' => false, 'error' => 'Unknown channel']); exit; } if ($action === 'send') { $text = (string)($_POST['text'] ?? ''); if (trim($text) === '') { echo json_encode(['ok' => false, 'error' => 'Nothing to send']); exit; } $msg = vv_nc_send($ch, $text, [ 'color' => (string)($_POST['color'] ?? ''), 'font' => (string)($_POST['font'] ?? ''), 'size' => (string)($_POST['size'] ?? ''), 'bold' => !empty($_POST['bold']), 'italic' => !empty($_POST['italic']), 'underline' => !empty($_POST['underline']), ]); if (!$msg) { echo json_encode(['ok' => false, 'error' => 'Could not store message']); exit; } // Queued is not failed — say which, so a partner being asleep reads as pending rather than // as an error the operator should act on. $queued = count(glob(VV_NC_SPOOL . '/*/' . $msg['id'] . '.json') ?: []); echo json_encode(['ok' => true, 'msg' => $msg, 'queued' => $queued]); exit; } if ($action === 'delete') { $id = preg_replace('/[^a-f0-9]/i', '', (string)($_POST['id'] ?? '')); if ($id === '') { echo json_encode(['ok' => false, 'error' => 'No message id']); exit; } echo json_encode(['ok' => vv_nc_delete_local($ch, $id)]); exit; } if ($action === 'read') { vv_nc_set_read($ch, time()); echo json_encode(['ok' => true]); exit; } echo json_encode(['ok' => false, 'error' => 'Unknown action']);