11, "wing" => "Realtime", "title" => "Private streams stay private", "promise" => "A live stream meant for one person can't be read by someone who guesses its name.", "block" => "Add `auth` to a channel in ratcloud.conf, and the app decides who may listen: nginx asks it first, with RC_CHANNEL and RC_CHANNEL_ID.", "files" => ["public/index.php", "lib/robot.php"], "channels" => [ "mine" => fn(string $id, ?array $me): bool => $me !== null && $id === "u" . $me["id"], ], "show" => function (?array $me): string { $own = $me ? '

Your stream is mine/u' . (int)$me["id"] . '.

' : '

Sign in to get a stream of your own.

'; $guess = $me ? (int)$me["id"] + 1 : 1; return $own . '

'; }, "api" => function (string $do, ?array $me, array $in, bool $post): ?array { if ($do !== "ping" || !$post) return null; if (!$me) return ["error" => "sign in to have a stream", "status" => 401]; return ["published" => publish("mine", "u" . $me["id"], ["text" => "ping for @{$me['username']}", "at" => gmdate("c")])]; }, // The robot hears its own stream; nobody else, signed in or not, may listen to it. "check" => function (): array { require_once ZOO_ROOT . "/lib/robot.php"; $host = "https://" . env("RC_HOST"); $b = robot_at_zoo(); $id = "u" . robot_user()["id"]; [$status, $msgs, $ms] = $b->listen(["$host/rc/sub/mine/$id"], fn() => $b->post("$host/api/11/ping"), fn($m) => count(array_filter($m[0], fn($x) => is_array($x) && str_starts_with($x["text"] ?? "", "ping"))) > 0, 2000); if ($status[0] !== 200) return [false, "the robot was refused its own stream ($status[0])"]; if (!array_filter($msgs[0], fn($x) => is_array($x) && str_starts_with($x["text"] ?? "", "ping"))) return [false, "the robot's own stream opened but its ping never came"]; $tries = [ "a visitor, on the robot's stream" => [new Browser(), "$host/rc/sub/mine/$id"], "the robot, on someone else's" => [$b, "$host/rc/sub/mine/u" . (robot_user()["id"] + 1)], "the robot, on a stream with no name" => [$b, "$host/rc/sub/mine"], ]; foreach ($tries as $who => [$br, $url]) { [$s] = $br->listen([$url], fn() => null, fn() => true, 0); if ($s[0] !== 403) return [false, "$who: answered {$s[0]}, not refused"]; } return [true, "the robot heard its own ping ({$ms}ms); a visitor and the robot on someone else's stream were refused (403)"]; }, "script" => <<<'JS' (() => { const list = document.getElementById("pings"); if (list && zoo.me) { const es = new EventSource("/rc/sub/mine/u" + zoo.me); es.onmessage = (m) => { const j = JSON.parse(m.data); list.insertAdjacentHTML("afterbegin", "
  • " + zoo.esc(j.text) + " ยท " + zoo.ago(j.at) + "
  • "); }; document.getElementById("ping").addEventListener("click", () => zoo.call("/api/11/ping", {})); } document.getElementById("eavesdrop")?.addEventListener("click", async (ev) => { const out = document.getElementById("eaves-out"); const ctl = new AbortController(); try { const r = await fetch("/rc/sub/mine/" + ev.target.dataset.guess, { headers: { Accept: "text/event-stream" }, signal: ctl.signal }); out.innerHTML = r.status === 403 ? 'refused' : 'connected (' + r.status + ')'; } catch (e) { out.textContent = "error: " + e.message; } ctl.abort(); }); })(); JS, ];