Style validation moves to one shared function so the sender and receiver cannot drift on what a style is.
120 lines
5.2 KiB
PHP
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']);
|