account-firehose.html
edited
1<!doctype html>
2<html lang="en">
3 <head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Account status</title>
7
8 <script type="importmap">
9 {
10 "imports": {
11 "@atcute/cbor": "https://unpkg.com/@atcute/cbor/dist/index.js",
12 "@atcute/cid": "https://unpkg.com/@atcute/cid/dist/index.js",
13 "@atcute/multibase": "https://unpkg.com/@atcute/multibase/dist/index.js",
14 "@atcute/uint8array": "https://unpkg.com/@atcute/uint8array/dist/index.js",
15 "#bases/base16": "https://unpkg.com/@atcute/multibase/dist/bases/base16-web.js",
16 "#bases/base64": "https://unpkg.com/@atcute/multibase/dist/bases/base64-web.js"
17 }
18 }
19 </script>
20 <script type="module">
21 const params = new URLSearchParams(new URL(window.location).search);
22 window.user = params.get("did") ?? "did:plc:4zht3z4caxwrw3dlsybodywc";
23 window.pds = params.get("pds") ?? "katproto.girlonthemoon.xyz";
24
25 window.handle = await (
26 window.user.startsWith("did:plc")
27 ? fetch(`https://plc.directory/${window.user}`)
28 : fetch(`https://${window.user}/.well-known/did.json`)
29 )
30 .then((res) => res.json())
31 .then((res) =>
32 res.alsoKnownAs
33 ? (res.alsoKnownAs[0] ?? "handle.invalid")
34 : "handle.invalid"
35 );
36
37 document.querySelector("title").innerHTML =
38 `${window.handle} • ${window.user}`;
39 </script>
40
41 <style>
42 html {
43 background: repeating-linear-gradient(#fff1, #0000 5px, #fff1 10px),
44 black;
45 min-height: 100%;
46 font-family:
47 "Monaspace neon var",
48 -moz-fixed,
49 monospace;
50 font-weight: bold;
51 }
52
53 body,
54 body * {
55 /* override some resource://content-accessible/plaintext.css styles in ff */
56 white-space: pre !important;
57 width: max-content;
58 text-shadow: 0 0 5px lime;
59 color: lime;
60
61 font-size: 10px;
62
63 &::selection {
64 color: black;
65 background-color: lime;
66 }
67 }
68
69 ul {
70 margin: 0;
71 padding: 0;
72 list-style-type: "> ";
73 padding-inline-start: 2ch;
74 }
75 </style>
76 <script type="module">
77 import * as CBOR from "@atcute/cbor";
78 import * as CID from "@atcute/cid";
79
80 const events = document.createElement("ul");
81
82 document.body.innerHTML = `${window.user} ${window.handle} ${window.pds}\n`;
83 document.body.append(events);
84
85 const ws = new WebSocket(
86 "wss://" + window.pds + "/xrpc/com.atproto.sync.subscribeRepos"
87 );
88
89 ws.addEventListener("open", () =>
90 console.info(
91 "connected to wss://" +
92 window.pds +
93 "/xrpc/com.atproto.sync.subscribeRepos"
94 )
95 );
96 ws.addEventListener("error", (err) => console.warn(err));
97 ws.addEventListener("close", () => console.error("Connection closed :("));
98
99 ws.addEventListener(
100 "message",
101 async (/** @type {{ data: Blob }} */ { data }) => {
102 const [header, remainder] = CBOR.decodeFirst(await data.bytes());
103 const [payload] = CBOR.decodeFirst(remainder);
104
105 /** @type {{
106 * t: "#commit",
107 * repo: string,
108 * rev: string,
109 * commit: { bytes: Uint8Array },
110 * blocks: Uint8Array,
111 * ops: {
112 * action: "create" | "update" | "delete",
113 * path: string,
114 * cid: { bytes: Uint8Array }
115 * }[]
116 * } | {
117 * t: "#sync",
118 * did: string,
119 * rev: string,
120 * blocks: Uint8Array
121 * } | {
122 * t: "#identity",
123 * did: string,
124 * handle: string
125 * } | {
126 * t: "#account",
127 * did: string,
128 * active: boolean,
129 * status: string,
130 * } | {
131 * t: "#info",
132 * name: string,
133 * message: string,
134 * }} */
135 const msg = {
136 ...header,
137 ...payload,
138 };
139
140 switch (msg.t) {
141 case "#commit": {
142 console.log("#commit", msg.repo, msg.rev);
143 break;
144 }
145 case "#sync": {
146 console.log("#sync", msg.did, msg.rev);
147 break;
148 }
149 case "#account": {
150 console.log("#account", msg.did, msg.status);
151 break;
152 }
153 case "#identity": {
154 console.log("#account", msg.did, msg.handle);
155 break;
156 }
157 case "#info": {
158 console.log("#info", msg.name, msg.message);
159 break;
160 }
161 }
162
163 // skip other users
164 if (
165 (msg.t === "#commit" && msg.repo !== window.user) ||
166 ((msg.t === "#sync" ||
167 msg.t === "#account" ||
168 msg.t === "#identity") &&
169 msg.did !== window.user)
170 )
171 return;
172
173 const li = document.createElement("li");
174 const pre = document.createElement("pre");
175
176 try {
177 pre.innerText = (
178 msg.t === "#commit"
179 ? [
180 `#commit {`,
181 ` "repo": "${payload.repo}",`,
182 ` "rev": "${payload.rev}",`,
183 ` "commit": "${CID.toString(CID.decode(payload.commit.bytes))}",`,
184 ` "ops": [`,
185 payload.ops
186 .map((op) =>
187 [
188 ` {`,
189 ` "action": "${op.action}",`,
190 ` "path": "${op.path}",`,
191 ` "cid": ${op.cid ? `"${CID.toString(CID.decode(op.cid.bytes))}"` : "null"}`,
192 `}`,
193 ].join("\n ")
194 )
195 .join(",\n"),
196 ` ],`,
197 `}`,
198 ]
199 : msg.t === "#sync"
200 ? [
201 `#sync {`,
202 ` "did": "${msg.did}",`,
203 ` "rev": "${msg.rev}",`,
204 `}`,
205 ]
206 : msg.t === "#account"
207 ? [
208 `#account {`,
209 ` "did": "${msg.did}",`,
210 ` "active": ${msg.active},`,
211 ` "status": "${msg.status}",`,
212 `}`,
213 ]
214 : msg.t === "#identity"
215 ? [
216 `#identity {`,
217 ` "did": "${msg.did}",`,
218 ` "handle": "${msg.handle}",`,
219 `}`,
220 ]
221 : msg.t === "#info"
222 ? [
223 `#info { "name": "${msg.name}", "message": "${msg.message}" }`,
224 ]
225 : ["#unknown {}"]
226 ).join("\n");
227 } catch (err) {
228 console.error(err, msg, data);
229 }
230
231 if (pre.innerText === "") return;
232
233 li.append(pre);
234 events.append(li);
235 }
236 );
237 </script>
238 </head>
239 <body></body>
240</html>