~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs-atirc

atirc channels in a browser page: WebRTC to atirc mesh nodes, no server, no token

0
0
main
72 lines · 3.6 KB Raw History
<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<title>atlibs-atirc live</title>
	<!-- The account's page against its real mesh (test/live.ts): find an agent on the owner's channel, ask it, read the answer. -->
	<script type="importmap">
	{
		"imports": {
			"age-encryption": "https://esm.sh/age-encryption@0.3.1",
			"@publicdomainrelay/atlibs-webrtc": "/node_modules/@publicdomainrelay/atlibs-webrtc/js/index.js",
			"@publicdomainrelay/atlibs-atirc": "/js/index.js"
		}
	}
	</script>
	<script type="module">
		import * as age from "age-encryption";
		import { atirc, channelOf } from "@publicdomainrelay/atlibs-atirc";
		import { WEBRTC_STATE, WebrtcHost } from "@publicdomainrelay/atlibs-webrtc";

		const q = new URLSearchParams(location.search);
		const [did, handle, ask] = ["did", "handle", "ask"].map((k) => q.get(k));
		// The mesh owner's channel (an agent account's owner is another handle).
		const channel = q.get("channel") || channelOf(handle);
		const post = (method, body) => fetch(`/xrpc/com.atproto.repo.${method}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
		const repo = {
			did,
			async getRecord({ repo, collection, rkey }) {
				const response = await fetch(`/xrpc/com.atproto.repo.getRecord?${new URLSearchParams({ repo, collection, rkey })}`);
				return response.ok ? response.json() : null;
			},
			putRecord: (input) => post("putRecord", input),
			deleteRecord: (input) => post("deleteRecord", input),
		};
		const log = (text) => fetch("/log", { method: "POST", body: text });
		const until = async (check, what, ms) => {
			for (const end = Date.now() + ms; Date.now() < end; await new Promise((r) => setTimeout(r, 500))) { const v = await check(); if (v) return v; }
			throw new Error(`timed out waiting for ${what}`);
		};
		try {
			// Each connection's phases, to the harness log.
			const host = (options) => new WebrtcHost({ ...options, store: new (class extends EventTarget {
				state = WEBRTC_STATE;
				emit(patch) {
					this.state = { ...this.state, ...patch };
					if (patch.phase) log(`webrtc ${patch.phase}${this.state.error ? `: ${this.state.error}` : ""}`);
					this.dispatchEvent(new Event("change"));
				}
			})() });
			const peers = (q.get("peers") || "").split(",").filter(Boolean).map((subhandle) => ({ did, subhandle }));
			const chat = atirc({ repo, handle, age, host, peers });
			const got = [];
			chat.onMessage((m) => (got.push(m), log(`${m.channel} ${m.identity}: ${m.text}`)));
			chat.onPresence((p) => log(`presence ${p.event} ${p.channel} ${p.identity}`));
			log("page up");
			const t0 = Date.now();
			await chat.join(channel);
			log(`joined ${channel} in ${Date.now() - t0} ms`);
			const agent = await until(async () => (await chat.peers(channel)).peers.find((p) => p.subhandle !== handle && p.subhandle.endsWith(`.${q.get("agentsOf") || handle}`)), "an agent on the channel", 300000);
			const asked = Date.now();
			// No nick prefix: an agent's monitor may drop lines naming itself as its own echoes.
			await chat.post(channel, ask);
			const answer = await until(() => got.find((m) => m.identity === agent.subhandle && Date.parse(m.ts ?? 0) >= asked - 5000 && !/online/.test(m.text)), "the agent's answer", 300000);
			await fetch("/result", { method: "POST", body: JSON.stringify({ ok: true, agent: agent.subhandle, answer: answer.text, seconds: (Date.now() - asked) / 1000, peers: (await chat.peers(channel)).peers.map((p) => p.subhandle) }) });
			chat.close();
		} catch (error) {
			await fetch("/result", { method: "POST", body: JSON.stringify({ ok: false, error: String(error?.stack ?? error) }) });
		}
	</script>
</head>
<body></body>
</html>