···11+import { useState, useCallback, useEffect } from 'react';
22+import { MiniDoc, Collection, Did } from './types';
33+import { listRecords, resolveMiniDoc, getRepoStatus } from './microcosm';
44+import LilUser from './LilUser';
55+import knownRelays from '../knownRelays.json';
66+77+const INCLUDED_RELAYS = [
88+ // most relays don't send cors headers rn
99+ 'wss://relay.xero.systems',
1010+ 'wss://relay1.us-east.bsky.network',
1111+ 'wss://relay1.us-west.bsky.network',
1212+1313+ // these relays are ineligible for other reasons:
1414+ // 'wss://atproto.africa', // rsky-relay does not implement getRepoStatus (and doesn't have this bug)
1515+ // 'wss://bsky.network', // old bgs codes does not have getRepoStatus
1616+];
1717+1818+1919+async function checkRelayStatuses(repo: Did) {
2020+ const deactivateds = [];
2121+ const missings = [];
2222+ const fails = [];
2323+ for (const url of INCLUDED_RELAYS) {
2424+ const u = new URL(url);
2525+ u.protocol = u.protocol.replace('ws', 'http');
2626+ let repoStatus;
2727+ try {
2828+ repoStatus = await getRepoStatus(u, repo);
2929+ } catch (e) {}
3030+ if (repoStatus === 'notfound') {
3131+ missings.push(u.hostname);
3232+ continue;
3333+ }
3434+ if (!repoStatus) {
3535+ fails.push(u.hostname);
3636+ continue;
3737+ }
3838+ if (!repoStatus.active) {
3939+ console.log('rs', repoStatus);
4040+ deactivateds.push(u.hostname);
4141+ }
4242+ }
4343+ return { deactivateds, missings, fails };
4444+}
4545+4646+function FailSummary({ oof, children }) {
4747+ const badRelays = {};
4848+ const { deactivateds, missings, fails } = oof;
4949+5050+ deactivateds.forEach(u => badRelays[u] = 'deactivated');
5151+ missings.forEach(u => badRelays[u] = 'not crawling');
5252+ fails.forEach(u => badRelays[u] = 'check failed');
5353+5454+ return (
5555+ <p style={{ fontSize: '0.8em', textAlign: 'right', margin: '0' }}>
5656+ {Object.keys(badRelays).map(k => (<>
5757+ <code>{k}</code>: <span style={{ color: "#f64" }}>{badRelays[k]}</span><br />
5858+ </>))}
5959+ <strong>pds:</strong> <code>{oof.doc.pds.hostname}</code> (<span style={{ color: "#7f6"}}>active</span>)<br/>
6060+ </p>
6161+ )
6262+}
6363+6464+function Results({ actives }) {
6565+ const hasFails = [];
6666+ let oks = 0;
6767+ actives.forEach(a => {
6868+ if (a.deactivateds.length > 0 || a.missings.length > 0 || a.fails.length > 0) {
6969+ hasFails.push(a);
7070+ } else {
7171+ oks += 1;
7272+ }
7373+ })
7474+ return (
7575+ <>
7676+ <p>{oks} account{oks !== 1 && 's'} on alternative PDSs checked out ok.</p>
7777+ {hasFails.length > 0 &&
7878+ <>
7979+ <h3>{hasFails.length} account{hasFails.length !== 1 && 's'} found with relay problems</h3>
8080+ {hasFails.map(f => (
8181+ <div key={f.doc.did.val} style={{ margin: "0.5rem 0" }}>
8282+ <LilUser doc={f.doc}>
8383+ <FailSummary oof={f} />
8484+ </LilUser>
8585+ </div>
8686+ ))}
8787+ </>
8888+ }
8989+ </>
9090+ );
9191+}
9292+9393+function CheckFollowers({ doc }: {
9494+ doc: MiniDoc,
9595+}) {
9696+ const [seenDids, setSeenDids] = useState({});
9797+ const [actives, setActives] = useState([]);
9898+ const [actuallyDeactivated, setActuallyDeactivated] = useState([]);
9999+ const [mushrooms, setMushrooms] = useState([]);
100100+ const [failures, setFailures] = useState([]);
101101+102102+ const checkFollowing = useCallback(async subject => {
103103+ if (seenDids[subject]) return;
104104+ else setSeenDids(s => ({ ...s, [subject]: true }));
105105+106106+ let doc;
107107+ try {
108108+ doc = await resolveMiniDoc(subject);
109109+ } catch {}
110110+ if (!doc) {
111111+ setFailures(fs => [...fs, { subject, reason: 'resolution' }]);
112112+ return;
113113+ }
114114+ if (doc.pds.hostname.endsWith(".host.bsky.network")) {
115115+ setMushrooms(ms => [...ms, doc]);
116116+ return;
117117+ }
118118+ let repoStatus;
119119+ try {
120120+ repoStatus = await getRepoStatus(doc.pds, doc.did);
121121+ } catch (e) {}
122122+ if (repoStatus === 'notfound') {
123123+ setFailures(fs => [...fs, { subject, reason: 'notfound' } ]);
124124+ return;
125125+ }
126126+ if (!repoStatus) {
127127+ setFailures(fs => [...fs, { subject, reason: 'pds getRepoStatus' } ]);
128128+ return;
129129+ }
130130+ if (!repoStatus.active) {
131131+ setActuallyDeactivated(ads => [...ads, doc]);
132132+ return;
133133+ }
134134+ const { deactivateds, missings, fails } = await checkRelayStatuses(doc.did);
135135+ setActives(as => [...as, { doc, deactivateds, missings, fails }]);
136136+ }, []);
137137+138138+ useEffect(() => {
139139+ let cancel = false;
140140+ (async() => {
141141+ // check ourselves first
142142+ checkFollowing(doc.did.val);
143143+144144+ const gen = listRecords(doc.pds, doc.did, new Collection('app.bsky.graph.follow'));
145145+146146+ for await (const record of gen) {
147147+ if (cancel) break;
148148+ checkFollowing(record.subject);
149149+ }
150150+ })();
151151+ return () => cancel = true;
152152+ }, [doc.did.val, doc.pds]);
153153+154154+ return (
155155+ <div style={{ marginBottom: "4em" }}>
156156+ <h2>Checking following ({Object.keys(seenDids).length})…</h2>
157157+ <p>Of your follows, {failures.length} failed resolution, {mushrooms.length} are on bsky mushroom PDSs, and {actuallyDeactivated.length} are actually deactivated.</p>
158158+ <Results actives={actives} />
159159+160160+ <div style={{ textAlign: "left" }}>
161161+ <h3 style={{ margin: "3em 0 0" }}>What these results mean</h3>
162162+163163+ <h4 style={{ marginBottom: "0", color: "#f64" }}>Deactivated</h4>
164164+ <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>
165165+166166+ <h4 style={{ marginBottom: "0", color: "#f64" }}>Not crawling</h4>
167167+ <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>
168168+169169+ <h4 style={{ marginBottom: "0", color: "#f64" }}>Check failed</h4>
170170+ <p>This account seems active, but something went wrong when checking its status with the relay. It might be fine!</p>
171171+172172+ <h3 style={{ margin: "3em 0 0" }}>Which relays are checked?</h3>
173173+174174+ <ul>
175175+ {INCLUDED_RELAYS.map(u => (
176176+ <li key={u}><code>{new URL(u).hostname}</code></li>
177177+ ))}
178178+ </ul>
179179+180180+ <h4 style={{ marginBottom: "0" }}>Excluded relays</h4>
181181+182182+ <ul>
183183+ <li><code>atproto.africa</code> does not store repo status, so it can't get desynchronized, and won't drop commits.</li>
184184+ <li><code>bsky.network</code>, running the old BGS code, does not implement <code>com.atproto.sync.getRepoStatus</code>.</li>
185185+ <li>All other known relays do not allow CORS XRPC requests, so we can't check from your browser.</li>
186186+ </ul>
187187+188188+ <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>
189189+ </div>
190190+ </div>
191191+ );
192192+}
193193+194194+export default CheckFollowers;
+36
src/deactivated/Deactivated.tsx
···11+import { useState } from 'react';
22+import { MiniDoc } from './types';
33+import { resolveMiniDoc } from './microcosm';
44+import LilUser from './LilUser';
55+import AccountInput from './AccountInput';
66+import CheckFollowers from './CheckFollowers';
77+88+function Deactivated() {
99+ const [doc, setDoc] = useState(null);
1010+1111+ return (
1212+ <div style={{
1313+ maxWidth: "800px",
1414+ }}>
1515+ <h1>Oops deactivated checker</h1>
1616+ <p>This is a relay debugging tool to check if relays are blocking accounts you follow due to desynchronized <code>active</code> state. This can happen when accounts migrate to an alternative PDS host.</p>
1717+1818+ {doc
1919+ ? <LilUser doc={doc}>
2020+ <button
2121+ style={{color: "#f90"}}
2222+ title="clear"
2323+ onClick={() => setDoc(null)}
2424+ >×</button>
2525+ </LilUser>
2626+ : <AccountInput onSet={setDoc} />
2727+ }
2828+2929+ {doc && <CheckFollowers doc={doc} />}
3030+3131+ <p><small>False positive note: it's possible for a relay to set an account as <code>deactivated</code> on purpose, but this moderation action is extremely rare.</small></p>
3232+ </div>
3333+ );
3434+}
3535+3636+export default Deactivated;