Relay firehose browser tools: https://compare.hose.cam
1import { useState, useCallback, useEffect } from 'react';
2import { MiniDoc, Collection, Did } from './types';
3import { listRecords, resolveMiniDoc, getRepoStatus } from './microcosm';
4import LilUser from './LilUser';
5import knownRelays from '../knownRelays.json';
6
7const INCLUDED_RELAYS = [
8 // most relays don't send cors headers rn
9 'wss://relay.xero.systems',
10 'wss://relay1.us-east.bsky.network',
11 'wss://relay1.us-west.bsky.network',
12 'wss://relay.fire.hose.cam',
13 'wss://relay3.fr.hose.cam',
14
15 // these relays are ineligible for other reasons:
16 // 'wss://atproto.africa', // rsky-relay does not implement getRepoStatus (and doesn't have this bug)
17 // 'wss://bsky.network', // old bgs codes does not have getRepoStatus
18];
19
20
21async function checkRelayStatuses(repo: Did) {
22 const deactivateds = [];
23 const missings = [];
24 const fails = [];
25 for (const url of INCLUDED_RELAYS) {
26 const u = new URL(url);
27 u.protocol = u.protocol.replace('ws', 'http');
28 let repoStatus;
29 try {
30 repoStatus = await getRepoStatus(u, repo);
31 } catch (e) {}
32 if (repoStatus === 'notfound') {
33 missings.push(u.hostname);
34 continue;
35 }
36 if (!repoStatus) {
37 fails.push(u.hostname);
38 continue;
39 }
40 if (!repoStatus.active) {
41 console.log('rs', repoStatus);
42 deactivateds.push(u.hostname);
43 }
44 }
45 return { deactivateds, missings, fails };
46}
47
48function FailSummary({ oof, children }) {
49 const badRelays = {};
50 const { deactivateds, missings, fails } = oof;
51
52 deactivateds.forEach(u => badRelays[u] = 'deactivated');
53 missings.forEach(u => badRelays[u] = 'not crawling');
54 fails.forEach(u => badRelays[u] = 'check failed');
55
56 return (
57 <p style={{ fontSize: '0.8em', textAlign: 'right', margin: '0' }}>
58 {Object.keys(badRelays).map(k => (<>
59 <code>{k}</code>: <span style={{ color: "#f64" }}>{badRelays[k]}</span><br />
60 </>))}
61 <strong>pds:</strong> <code>{oof.doc.pds.hostname}</code> (<span style={{ color: "#7f6"}}>active</span>)<br/>
62 </p>
63 )
64}
65
66function Results({ actives }) {
67 const hasFails = [];
68 let oks = 0;
69 actives.forEach(a => {
70 if (a.deactivateds.length > 0 || a.missings.length > 0 || a.fails.length > 0) {
71 hasFails.push(a);
72 } else {
73 oks += 1;
74 }
75 })
76 return (
77 <>
78 <p>{oks} account{oks !== 1 && 's'} on alternative PDSs checked out ok.</p>
79 {hasFails.length > 0 &&
80 <>
81 <h3>{hasFails.length} account{hasFails.length !== 1 && 's'} found with relay problems</h3>
82 {hasFails.map(f => (
83 <div key={f.doc.did.val} style={{ margin: "0.5rem 0" }}>
84 <LilUser doc={f.doc}>
85 <FailSummary oof={f} />
86 </LilUser>
87 </div>
88 ))}
89 </>
90 }
91 </>
92 );
93}
94
95function CheckFollowers({ doc }: {
96 doc: MiniDoc,
97}) {
98 const [seenDids, setSeenDids] = useState({});
99 const [actives, setActives] = useState([]);
100 const [actuallyDeactivated, setActuallyDeactivated] = useState([]);
101 const [mushrooms, setMushrooms] = useState([]);
102 const [failures, setFailures] = useState([]);
103
104 const checkFollowing = useCallback(async subject => {
105 if (seenDids[subject]) return;
106 else setSeenDids(s => ({ ...s, [subject]: true }));
107
108 let doc;
109 try {
110 doc = await resolveMiniDoc(subject);
111 } catch {}
112 if (!doc) {
113 setFailures(fs => [...fs, { subject, reason: 'resolution' }]);
114 return;
115 }
116 if (doc.pds.hostname.endsWith(".host.bsky.network")) {
117 setMushrooms(ms => [...ms, doc]);
118 return;
119 }
120 let repoStatus;
121 try {
122 repoStatus = await getRepoStatus(doc.pds, doc.did);
123 } catch (e) {}
124 if (repoStatus === 'notfound') {
125 setFailures(fs => [...fs, { subject, reason: 'notfound' } ]);
126 return;
127 }
128 if (!repoStatus) {
129 setFailures(fs => [...fs, { subject, reason: 'pds getRepoStatus' } ]);
130 return;
131 }
132 if (!repoStatus.active) {
133 setActuallyDeactivated(ads => [...ads, doc]);
134 return;
135 }
136 const { deactivateds, missings, fails } = await checkRelayStatuses(doc.did);
137 setActives(as => [...as, { doc, deactivateds, missings, fails }]);
138 }, []);
139
140 useEffect(() => {
141 let cancel = false;
142 (async() => {
143 // check ourselves first
144 checkFollowing(doc.did.val);
145
146 const gen = listRecords(doc.pds, doc.did, new Collection('app.bsky.graph.follow'));
147
148 for await (const record of gen) {
149 if (cancel) break;
150 checkFollowing(record.subject);
151 }
152 })();
153 return () => cancel = true;
154 }, [doc.did.val, doc.pds]);
155
156 return (
157 <div style={{ marginBottom: "4em" }}>
158 <h2>Checking following ({Object.keys(seenDids).length})…</h2>
159 <p>Of your follows, {failures.length} failed resolution, {mushrooms.length} are on bsky mushroom PDSs, and {actuallyDeactivated.length} are actually deactivated.</p>
160 <Results actives={actives} />
161
162 <div style={{ textAlign: "left" }}>
163 <h3 style={{ margin: "3em 0 0" }}>What these results mean</h3>
164
165 <h4 style={{ marginBottom: "0", color: "#f64" }}>Deactivated</h4>
166 <p>The relay has become desynchronized with this account, incorrectly marking it as not <code>active</code>. All commits from this account will be blocked by the relay; none will be broadcast to relay consumers.</p>
167
168 <h4 style={{ marginBottom: "0", color: "#f64" }}>Not crawling</h4>
169 <p>The relay doesn't know about this account—perhaps it as never crawled its PDS. No content from this account will be discovered by the relay, so relay consumers won't see it.</p>
170
171 <h4 style={{ marginBottom: "0", color: "#f64" }}>Check failed</h4>
172 <p>This account seems active, but something went wrong when checking its status with the relay. It might be fine!</p>
173
174 <h3 style={{ margin: "3em 0 0" }}>Which relays are checked?</h3>
175
176 <ul>
177 {INCLUDED_RELAYS.map(u => (
178 <li key={u}><code>{new URL(u).hostname}</code></li>
179 ))}
180 </ul>
181
182 <h4 style={{ marginBottom: "0" }}>Excluded relays</h4>
183
184 <ul>
185 <li><code>atproto.africa</code> does not store repo status, so it can't get desynchronized, and won't drop commits.</li>
186 <li><code>bsky.network</code>, running the old BGS code, does not implement <code>com.atproto.sync.getRepoStatus</code>.</li>
187 <li>All other known relays do not allow CORS XRPC requests, so we can't check from your browser.</li>
188 </ul>
189
190 <p>Accounts on Bluesky's mushroom PDSs are not checked because accounts seem to mainly desynchronize when migrating PDSs. Since accounts can now be migrated into the mushrooms, perhaps they should be checked too?</p>
191 </div>
192 </div>
193 );
194}
195
196export default CheckFollowers;