aboutsummaryrefslogtreecommitdiff
path: root/api/chat/poll.php
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-10-23 19:29:31 +0200
committerAgando <roduccomm@gmail.com>2026-10-23 19:29:31 +0200
commit0d2365ab202ba5c9b1d0604467af0594dc7360de (patch)
treeba00766715c07e621641e16e8a9dbc0d7802a3b9 /api/chat/poll.php
parentb0f90494fa00e787f8e9d34368febf2e7e26e8a6 (diff)
feat(chat): token-scoped chat endpoints, contract, host CLI, old POST retired (v4.6.71 phase B)
- api/chat/{lib,post,poll}.php: thread = sha256(token)[0:16]; flock read-modify-write; tmp+rename; ms epoch cursor; per-thread + per-IP windows with retry_after_ms; 401/413/507/405 carry a machine code, only rate_limited is retryable - _docs/chat-contract.md: frozen shapes, incl. why the cursor is ms and not ts - scripts/chat.py: list/show/reply, UTC, locked, atomic replace - api/debug/agent-replies.php POST -> 410 Gone with pointers (one write path) Measured: 401 no-token, 201+id, poll 1 msg, poll>since 1 new, data/chat 403, 4001 chars 413 (codepoints), 429 with Retry-After 576, round trip via chat.py.
Diffstat (limited to 'api/chat/poll.php')
-rw-r--r--api/chat/poll.php27
1 files changed, 27 insertions, 0 deletions
diff --git a/api/chat/poll.php b/api/chat/poll.php
new file mode 100644
index 0000000..601beca
--- /dev/null
+++ b/api/chat/poll.php
@@ -0,0 +1,27 @@
+<?php
+// api/chat/poll.php — read back the caller's own thread. The thread comes from the
+// token, never from a query parameter: no id to guess, no IDOR, no listing.
+// Contract: _docs/chat-contract.md
+require_once __DIR__ . '/lib.php';
+// Note: poll is deliberately NOT rate limited. A 30 s poller is 120 GETs/hour;
+// sharing the write window would make the client throttle itself into silence.
+
+$token = chat_token();
+$thread = chat_thread($token);
+$since = isset($_GET['since']) ? (int)$_GET['since'] : 0; // epoch seconds, numeric
+
+$all = chat_read($thread);
+$messages = $since > 0
+ ? array_values(array_filter($all, fn($m) => (int)($m['epoch'] ?? 0) > $since))
+ : $all;
+$lastAll = $all ? end($all) : null;
+
+json_response([
+ 'success' => true,
+ 'thread' => $thread,
+ 'messages' => array_slice($messages, -50),
+ // `awaiting` replaces `unread`: the server keeps no read state, so an unread
+ // counter could never clear. Who spoke last is knowable.
+ 'awaiting' => (bool)($lastAll && ($lastAll['author'] ?? '') !== 'hermes'),
+ 'cursor' => $lastAll ? (int)($lastAll['epoch'] ?? 0) : 0,
+]);