Mesh chat store: append-replicated log over the existing SSH trust
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
<?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;
|
||||
}
|
||||
echo json_encode(['ok' => true, 'me' => vv_nc_me(), 'channels' => $chans]);
|
||||
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'] ?? ''),
|
||||
]);
|
||||
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']);
|
||||
Reference in New Issue
Block a user