Files
Varaverk/Plugin/unraid/api/node_chat.php
T
Gmer4Lfe d88fe381db Mesh chat: autoscroll toggle, hostname/slot toggle, and a formatting row behind one button
Style validation moves to one shared function so the sender and receiver cannot drift on what a style is.
2026-08-17 16:51:17 -04:00

120 lines
5.2 KiB
PHP

<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// The Partnership tab's mesh chat: read a channel, post to it, forget a message on this
// machine, and mark a channel read.
//
// REQUEST
// GET channels + this host's id + unread counts
// GET ?channel=<id> that channel's messages
// POST action=send channel=<id> text=… [color=#rrggbb] [font=mono|sans|serif]
// POST action=delete channel=<id> id=<msgid> local only
// POST action=read channel=<id> 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']);