icons/bsky-favicon.png
icons/bsky-favicon.png
This is a binary file and will not be displayed.
icons/microcosm-favicon.png
icons/microcosm-favicon.png
This is a binary file and will not be displayed.
+1190
-117
index.html
+1190
-117
index.html
···
3
3
<head>
4
4
<meta charset="utf-8">
5
5
<meta name="viewport" content="width=device-width"/>
6
+
<title>atproto PDS & account debugger</title>
7
+
<meta name="description" content="Quick diagnostics for PDS hosts, handles, relay connections, handles, DIDs, ..." />
8
+
9
+
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
6
10
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
7
11
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css"/>
8
12
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
9
13
10
14
<script type="module">
11
-
import { Client, ClientResponseError, ok, simpleFetchHandler } from 'https://esm.sh/@atcute/client@4.1.1';
15
+
import {
16
+
Client,
17
+
ClientResponseError,
18
+
ok,
19
+
simpleFetchHandler,
20
+
} from 'https://esm.sh/@atcute/client@4.1.1';
21
+
import {
22
+
DohJsonHandleResolver,
23
+
WellKnownHandleResolver,
24
+
} from 'https://esm.sh/@atcute/identity-resolver@1.2.1';
25
+
12
26
window.SimpleQuery = service => {
13
27
const client = new Client({ handler: simpleFetchHandler({ service }) });
14
28
return (...args) => ok(client.get(...args));
···
18
32
return (...args) => ok(client.post(...args));
19
33
};
20
34
window.isXrpcErr = e => e instanceof ClientResponseError;
35
+
36
+
window.isBeforeNow = iso => new Date(iso) < new Date();
37
+
38
+
window.dnsResolver = new DohJsonHandleResolver({
39
+
dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query',
40
+
});
41
+
window.httpResolver = new WellKnownHandleResolver();
42
+
21
43
window.slingshot = window.SimpleQuery('https://slingshot.microcosm.blue');
22
44
window.relays = [
23
45
{
24
-
name: 'Bluesky production',
46
+
name: 'Bluesky',
47
+
icon: './icons/bsky-favicon.png',
25
48
hostname: 'bsky.network',
49
+
note: 'current',
50
+
missingApis: {
51
+
['com.atproto.sync.getHostStatus']: 'missing API (old relay code)',
52
+
['com.atproto.sync.getRepoStatus']: 'missing API (old relay code)',
53
+
},
26
54
},
27
55
{
28
-
name: 'Bluesky sync1.1 East',
29
-
hostname: 'relay1.us-east.bsky.network',
56
+
name: 'Microcosm Montreal',
57
+
icon: './icons/microcosm-favicon.png',
58
+
hostname: 'relay.fire.hose.cam',
30
59
},
31
60
{
32
-
name: 'Bluesky sync1.1 West',
33
-
hostname: 'relay1.us-west.bsky.network',
61
+
name: 'Microcosm France',
62
+
icon: './icons/microcosm-favicon.png',
63
+
hostname: 'relay3.fr.hose.cam',
64
+
},
65
+
{
66
+
name: 'Upcloud',
67
+
icon: 'https://upcloud.com/media/android-chrome-512x512-2-150x150.png',
68
+
hostname: 'relay.upcloud.world',
34
69
},
35
70
{
36
71
name: 'Blacksky',
72
+
icon: 'https://blacksky.community/static/favicon-32x32.png',
37
73
hostname: 'atproto.africa',
74
+
missingApis: {
75
+
['com.atproto.sync.getHostStatus']: 'API not yet deployed',
76
+
['com.atproto.sync.getRepoStatus']: 'API not implemented',
77
+
},
38
78
},
39
79
{
40
-
name: 'Microcosm Montreal',
41
-
hostname: 'relay.fire.hose.cam',
80
+
name: 'Bluesky East',
81
+
icon: './icons/bsky-favicon.png',
82
+
note: 'future',
83
+
hostname: 'relay1.us-east.bsky.network',
42
84
},
43
85
{
44
-
name: 'Microcosm France',
45
-
hostname: 'relay3.fr.hose.cam',
86
+
name: 'Bluesky West',
87
+
icon: './icons/bsky-favicon.png',
88
+
note: 'future',
89
+
hostname: 'relay1.us-west.bsky.network',
46
90
},
47
91
];
92
+
93
+
window.regionalModAccounts = [ // https://github.com/mary-ext/atproto-scraping?tab=readme-ov-file#bluesky-labelers
94
+
'https://mod-br.bsky.app',
95
+
'https://mod-de.bsky.app',
96
+
'https://mod-in.bsky.app',
97
+
'https://mod-ru.bsky.app',
98
+
'https://mod-tr.bsky.app',
99
+
];
100
+
101
+
window.bskyAccountDeathLabels = {
102
+
['needs-review']: 'Automated action, cleared by manual review from Bluesky moderation team. Your content can ve accessed via direct links on Bluesky, but invisible in feeds and replies to posts.',
103
+
['!suspend']: 'Moderation action from Bluesky moderation team. Makes your content inaccessible on the Bluesky app.',
104
+
['!takedown']: 'Moderation action from Bluesky moderation team. Makes your content inaccessible on the Bluesky app.',
105
+
['!hide']: 'Almost always used with !takedown, makes your content inaccessible on the Bluesky app.',
106
+
// other labels shouldn't cause problems that make you think your pds is broken
107
+
// 'spam': just hides replies by default + makes your posts click-through
108
+
// 'intolerant', etc: similar to spam
109
+
};
110
+
111
+
if (window.blehYeahReady) blehYeahReady();
112
+
else window.yeahBlehIsReady = true;
48
113
</script>
49
114
115
+
<style>
116
+
body:not(.ready) .hide-until-ready,
117
+
body.ready .show-until-ready {
118
+
display: none;
119
+
}
120
+
</style>
121
+
50
122
<script>
51
123
document.addEventListener('alpine:init', () => {
124
+
if (window.yeahBlehIsReady) {
125
+
document.body.classList.add('ready');
126
+
} else {
127
+
window.blehYeahReady = () => document.body.classList.add('ready');
128
+
}
129
+
52
130
Alpine.data('debug', () => ({
53
131
// form input
54
132
identifier: '',
···
61
139
pds: null,
62
140
did: null,
63
141
handle: null,
142
+
143
+
async goto(identifier) {
144
+
this.identifier = identifier;
145
+
await this.diagnose();
146
+
},
64
147
65
148
async diagnose() {
66
149
this.identifierLoading = true;
67
150
this.identifierError = null;
68
151
this.pds = null;
69
152
this.did = null;
70
-
if (this.identifier.startsWith('https://')) {
153
+
this.handle = null;
154
+
this.identifier = this.identifier.trim();
155
+
if (this.identifier === '') {
156
+
// do nothing
157
+
} else if (this.identifier.startsWith('https://')) {
71
158
this.pds = this.identifier;
72
159
} else {
73
160
if (this.identifier.startsWith('at://')) {
···
75
162
}
76
163
if (this.identifier.startsWith('did:')) {
77
164
this.did = this.identifier;
78
-
} else {
79
165
let data;
80
166
try {
81
167
data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
82
168
params: { identifier: this.identifier },
83
169
});
170
+
this.pds = data.pds;
171
+
this.handle = data.handle;
172
+
} catch (e) {
173
+
if (window.isXrpcErr(e)) {
174
+
this.identifierError = e.error;
175
+
if (e.message) this.description += ` ${e.description}`;
176
+
} else {
177
+
this.identifierError = 'Failed to resolve identifier, see console for error.';
178
+
console.error(e);
179
+
}
180
+
}
181
+
} else {
182
+
this.handle = this.identifier.toLowerCase();
183
+
let data;
184
+
try {
185
+
data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
186
+
params: { identifier: this.identifier.toLowerCase() },
187
+
});
84
188
this.did = data.did;
85
189
this.pds = data.pds;
86
190
} catch (e) {
···
98
202
},
99
203
}));
100
204
205
+
Alpine.data('pdsCheck', pds => ({
206
+
loadingDesc: false,
207
+
error: null,
208
+
description: null,
209
+
accounts: [],
210
+
accountsComplete: false,
211
+
version: null,
212
+
213
+
async init() {
214
+
await this.update(pds);
215
+
},
216
+
217
+
async update(pds) {
218
+
this.loadingDesc = true;
219
+
this.error = null;
220
+
this.description = null;
221
+
this.accounts = [];
222
+
this.accountsComplete = false;
223
+
this.version = null;
224
+
225
+
if (!pds) {
226
+
this.loadingDesc = false;
227
+
return;
228
+
}
229
+
230
+
let query = window.SimpleQuery(pds);
231
+
try {
232
+
this.description = await query('com.atproto.server.describeServer');
233
+
} catch (e) {
234
+
if (window.isXrpcErr(e)) {
235
+
this.error = e.error;
236
+
} else {
237
+
this.error = 'Failed to reach (see console)';
238
+
console.error(e);
239
+
}
240
+
}
241
+
let health
242
+
try {
243
+
health = await query('_health');
244
+
this.version = health.version;
245
+
} catch (e) {
246
+
if (window.isXrpcErr(e)) {
247
+
this.error = e.error;
248
+
} else {
249
+
this.error = 'Failed to reach (see console)';
250
+
console.error(e);
251
+
}
252
+
}
253
+
let accountsRes;
254
+
try {
255
+
accountsRes = await query('com.atproto.sync.listRepos', {
256
+
params: { limit: 100 },
257
+
});
258
+
this.accounts = accountsRes.repos;
259
+
260
+
// weird thing with the ref pds: it *always* has a cursor on the first page
261
+
if (accountsRes.cursor) {
262
+
// so grab a second page just to see if there really is a second page
263
+
try {
264
+
const secondPage = await query('com.atproto.sync.listRepos', {
265
+
params: { limit: 1, cursor: accountsRes.cursor },
266
+
});
267
+
this.accountsComplete = !secondPage.cursor || secondPage.repos.length == 0;
268
+
} catch (e) {
269
+
// we're in a niche spot. ignore errors and look at the original (faulty) cursor
270
+
this.accountsComplete = !accountsRes.cursor; // ๐คทโโ๏ธ
271
+
}
272
+
} else {
273
+
this.accountsComplete = true;
274
+
}
275
+
276
+
} catch (e) {
277
+
if (window.isXrpcErr(e)) {
278
+
this.error = e.error;
279
+
} else {
280
+
this.error = 'Failed to reach (see console)';
281
+
console.error(e);
282
+
}
283
+
}
284
+
this.loadingDesc = false;
285
+
},
286
+
}));
287
+
101
288
Alpine.data('relayCheckHost', (pds, relay) => ({
102
289
loading: false,
103
290
error: null,
104
291
status: null,
292
+
expectedErrorInfo: null,
105
293
reqCrawlStatus: null,
106
294
reqCrawlError: null,
107
295
108
296
async init() {
109
-
await this.check();
297
+
await this.check(pds, relay);
110
298
},
111
299
112
-
async check() {
300
+
async check(pds, relay) {
113
301
this.loading = true;
114
302
this.error = null;
115
303
this.status = null;
116
-
let query = window.SimpleQuery(`https://${relay.hostname}`);
304
+
this.expectedError = false;
305
+
const query = window.SimpleQuery(`https://${relay.hostname}`);
117
306
const hostname = pds.split('://')[1];
118
307
let data;
119
308
try {
···
122
311
});
123
312
this.status = data.status;
124
313
} catch(e) {
125
-
if (window.isXrpcErr(e)) {
314
+
if (relay.missingApis?.['com.atproto.sync.getHostStatus']) {
315
+
this.error = 'Can\'t check';
316
+
this.expectedErrorInfo = relay.missingApis?.['com.atproto.sync.getHostStatus'];
317
+
} else if (window.isXrpcErr(e)) {
126
318
this.error = e.error;
127
319
} else {
128
320
this.error = 'Failed to check (see console)';
···
134
326
this.reqCrawlError = null;
135
327
},
136
328
137
-
async requestCrawl() {
329
+
async requestCrawl(pds, relay) {
138
330
this.reqCrawlStatus = "loading";
139
331
const proc = window.SimpleProc(`https://${relay.hostname}`);
140
332
const hostname = pds.split('://')[1];
···
153
345
}
154
346
this.reqCrawlStatus = "done";
155
347
},
156
-
}))
348
+
}));
349
+
350
+
Alpine.data('checkHandle', handle => ({
351
+
loading: false,
352
+
dnsDid: null,
353
+
dnsErr: null,
354
+
httpDid: null,
355
+
httpErr: null,
356
+
357
+
async init() {
358
+
await this.updateHandle(handle);
359
+
},
360
+
async updateHandle(handle) {
361
+
this.loading = true;
362
+
this.dnsDid = null;
363
+
this.dnsErr = null;
364
+
this.httpDid = null;
365
+
this.httpErr = null;
366
+
try {
367
+
this.dnsDid = await window.dnsResolver.resolve(handle);
368
+
} catch (e) {
369
+
this.dnsErr = e.name;
370
+
}
371
+
try {
372
+
this.httpDid = await window.httpResolver.resolve(handle);
373
+
} catch (e) {
374
+
this.httpErr = e.name;
375
+
}
376
+
this.loading = false;
377
+
},
378
+
}));
379
+
380
+
Alpine.data('didToHandle', did => ({
381
+
loading: false,
382
+
error: null,
383
+
handle: null,
384
+
async load() {
385
+
loading = true;
386
+
error = null;
387
+
handle = null;
388
+
let data;
389
+
try {
390
+
data = await window.slingshot('com.bad-example.identity.resolveMiniDoc', {
391
+
params: { identifier: did },
392
+
});
393
+
this.handle = data.handle;
394
+
} catch (e) {
395
+
if (window.isXrpcErr(e)) {
396
+
this.error = e.error;
397
+
} else {
398
+
this.error = 'failed (see console)';
399
+
console.error(e);
400
+
}
401
+
}
402
+
loading = false;
403
+
}
404
+
}));
405
+
406
+
Alpine.data('didRepoState', (did, pds) => ({
407
+
loading: false,
408
+
error: null,
409
+
state: null,
410
+
411
+
async init() {
412
+
await this.checkRepoState(did, pds);
413
+
},
414
+
async checkRepoState(did, pds) {
415
+
this.loading = true;
416
+
this.error = null;
417
+
this.state = null;
418
+
419
+
if (!did || !pds) {
420
+
this.loading = false;
421
+
return;
422
+
}
423
+
const query = window.SimpleQuery(pds);
424
+
try {
425
+
this.state = await query('com.atproto.sync.getRepoStatus', {
426
+
params: { did },
427
+
});
428
+
} catch (e) {
429
+
if (window.isXrpcErr(e)) {
430
+
this.error = e.error;
431
+
} else {
432
+
this.error = 'failed (see console)';
433
+
console.error(e);
434
+
}
435
+
}
436
+
this.loading = false;
437
+
},
438
+
}));
439
+
440
+
Alpine.data('repoSize', () => ({
441
+
lading: false,
442
+
error: null,
443
+
size: null,
444
+
445
+
async loadRepoForSize(did, pds) {
446
+
this.loading = true;
447
+
448
+
if (!did || !pds) {
449
+
this.loading = false;
450
+
return;
451
+
}
452
+
const query = window.SimpleQuery(pds);
453
+
try {
454
+
const res = await query('com.atproto.sync.getRepo', {
455
+
params: { did },
456
+
as: 'blob',
457
+
});
458
+
let bytes = res.size;
459
+
let mbs = bytes / Math.pow(2, 20);
460
+
this.size = mbs.toFixed(1);
461
+
} catch (e) {
462
+
if (window.isXrpcErr(e)) {
463
+
this.error = e.error;
464
+
} else {
465
+
this.error = 'failed (see console)';
466
+
console.error(e);
467
+
}
468
+
}
469
+
this.loading = false;
470
+
},
471
+
}));
472
+
473
+
Alpine.data('relayCheckRepo', (did, relay) => ({
474
+
loading: false,
475
+
error: null,
476
+
status: null,
477
+
expectedErrorInfo: null,
478
+
479
+
async init() {
480
+
await this.check(did, relay);
481
+
},
482
+
483
+
async check(did, relay) {
484
+
this.loading = true;
485
+
this.error = null;
486
+
this.status = null;
487
+
this.expectedErrorInfo = null;
488
+
489
+
const query = window.SimpleQuery(`https://${relay.hostname}`);
490
+
try {
491
+
this.status = await query('com.atproto.sync.getRepoStatus', {
492
+
params: { did },
493
+
});
494
+
} catch(e) {
495
+
if (relay.missingApis?.['com.atproto.sync.getRepoStatus']) {
496
+
this.error = 'Can\'t check';
497
+
this.expectedErrorInfo = relay.missingApis?.['com.atproto.sync.getRepoStatus'];
498
+
} else if (window.isXrpcErr(e)) {
499
+
this.error = e.error;
500
+
} else {
501
+
this.error = 'Failed to check (see console)';
502
+
console.error(e);
503
+
}
504
+
}
505
+
506
+
this.loading = false;
507
+
},
508
+
509
+
revStatus(repoRev) {
510
+
if (
511
+
!repoRev ||
512
+
!(this.status && this.status.rev)
513
+
) return null;
514
+
515
+
if (this.status.rev < repoRev) return 'behind';
516
+
if (this.status.rev === repoRev) return 'current';
517
+
if (this.status.rev > repoRev) return 'ahead';
518
+
}
519
+
}));
520
+
521
+
Alpine.data('modLabels', did => ({
522
+
loading: false,
523
+
error: null,
524
+
regionalErrors: [],
525
+
labels: [],
526
+
527
+
async init() {
528
+
this.loading = true;
529
+
this.error = null;
530
+
this.regionalErrors = [];
531
+
this.labels = [];
532
+
533
+
const query = window.SimpleQuery('https://mod.bsky.app');
534
+
535
+
try {
536
+
const res = await query('com.atproto.label.queryLabels', {
537
+
params: { uriPatterns: [did] },
538
+
});
539
+
this.labels = res.labels ?? [];
540
+
// TODO: handle cursors?
541
+
542
+
for (const region of window.regionalModAccounts) {
543
+
// intentionally no await, these come in async
544
+
// (...and could get messy if we start re-checking labels before they're done)
545
+
this.checkRegionLabels(region);
546
+
}
547
+
} catch (e) {
548
+
if (window.isXrpcErr(e)) {
549
+
this.error = e.error;
550
+
} else {
551
+
this.error = 'Failed to check (see console)';
552
+
console.error(e);
553
+
}
554
+
}
555
+
this.loading = false;
556
+
},
557
+
558
+
async checkRegionLabels(labeler) {
559
+
const query = window.SimpleQuery(labeler);
560
+
try {
561
+
const res = await query('com.atproto.label.queryLabels', {
562
+
params: { uriPatterns: [did] },
563
+
});
564
+
if (res?.labels?.length > 0) this.labels.push(...res.labels);
565
+
} catch (e) {
566
+
if (window.isXrpcErr(e)) {
567
+
this.regionalErrors.push(`${labeler}: ${e.error}`);
568
+
} else {
569
+
this.regionalErrors.push(`Failed to check ${labeler} (see console)`);
570
+
console.error(`labeler: ${labeler}`, e);
571
+
}
572
+
}
573
+
}
574
+
}));
575
+
576
+
Alpine.data('pdsHistory', (did, currentPds) => ({
577
+
loading: false,
578
+
error: null,
579
+
history: [],
580
+
581
+
async init() {
582
+
this.loading = true;
583
+
this.error = null;
584
+
this.history = [];
585
+
try {
586
+
const res = await fetch(`https://plc.directory/${did}/log/audit`);
587
+
if (res.ok) {
588
+
const log = await res.json();
589
+
let prev = null;
590
+
for (op of log) {
591
+
let opPds = null;
592
+
const services = op.operation.services;
593
+
if (services) {
594
+
const app = services.atproto_pds;
595
+
if (app) {
596
+
opPds = app.endpoint;
597
+
}
598
+
}
599
+
if (opPds === prev) continue;
600
+
prev = opPds;
601
+
this.history.push({
602
+
pds: opPds,
603
+
date: op.createdAt,
604
+
});
605
+
}
606
+
this.history.reverse();
607
+
if (this.history[0]) this.history[0].current = true;
608
+
} else {
609
+
this.error = `${res.status}: ${await res.text()}`;
610
+
}
611
+
} catch (e) {
612
+
this.error = 'failed to get history';
613
+
console.error(e);
614
+
}
615
+
this.loading = false;
616
+
},
617
+
}));
618
+
619
+
Alpine.data('handleHistory', (did, currentHandle) => ({
620
+
loading: false,
621
+
error: null,
622
+
history: [],
623
+
624
+
async init() {
625
+
this.loading = true;
626
+
this.error = null;
627
+
this.history = [];
628
+
try {
629
+
const res = await fetch(`https://plc.directory/${did}/log/audit`);
630
+
if (res.ok) {
631
+
const log = await res.json();
632
+
let prev = null;
633
+
for (op of log) {
634
+
let opHandle = null;
635
+
if (op.operation.alsoKnownAs) {
636
+
for (aka of op.operation.alsoKnownAs) {
637
+
if (aka.startsWith("at://")) {
638
+
opHandle = aka.slice("at://".length);
639
+
break;
640
+
}
641
+
}
642
+
}
643
+
if (opHandle === prev) continue;
644
+
prev = opHandle;
645
+
this.history.push({
646
+
handle: opHandle,
647
+
date: op.createdAt,
648
+
});
649
+
}
650
+
this.history.reverse();
651
+
} else {
652
+
this.error = `${res.status}: ${await res.text()}`;
653
+
}
654
+
} catch (e) {
655
+
this.error = 'failed to get history';
656
+
console.error(e);
657
+
}
658
+
this.loading = false;
659
+
},
660
+
}));
157
661
})
158
662
</script>
159
663
</head>
160
-
<body x-data="debug">
161
-
<div class="hero bg-base-200">
664
+
<body x-data="debug" class="bg-base-200">
665
+
<div class="hero bg-base-200 p-8">
162
666
<div class="hero-content flex-col">
163
-
<h1>PDS Debugger</h1>
667
+
<h1 class="text-2xl mb-8">PDS Debugger</h1>
668
+
<p class="show-until-ready"><em>Loading…</em></p>
669
+
<form class="hide-until-ready" @submit.prevent="await diagnose()">
670
+
<label class="text-sm text-primary" for="identifier">
671
+
atproto handle, DID, or HTTPS PDS URL
672
+
</label>
673
+
<br/>
674
+
<div class="join">
675
+
<input
676
+
id="identifier"
677
+
class="input join-item"
678
+
x-model="identifier"
679
+
:disabled="identifierLoading"
680
+
autofocus
681
+
/>
682
+
<button
683
+
class="btn btn-primary join-item"
684
+
type="submit"
685
+
>go</button>
686
+
</div>
687
+
</form>
688
+
</div>
689
+
</div>
690
+
691
+
<div class="w-full max-w-lg mx-auto">
692
+
<template x-if="identifierError">
693
+
<p>uh oh: <span x-text="identifierError"></span></p>
694
+
</template>
164
695
165
-
<p>Work in progress!</p>
166
-
<details class="text-xs">
167
-
<summary>Would be nice</summary>
168
-
<ul>
169
-
<li>anything that actually works</li>
170
-
<li>firehose listener for missing pds events</li>
171
-
<li>jetstream listener for missing pds events</li>
172
-
<li>check relays for account status</li>
173
-
<li>check relays for pds state</li>
174
-
<li>plc: check old pds hosts for active account state</li>
175
-
</ul>
176
-
</details>
177
-
<details class="text-xs">
178
-
<summary>Limitations</summary>
179
-
<ul>
180
-
<li>it's all client-side</li>
181
-
</ul>
182
-
</details>
696
+
<template x-if="pds != null">
183
697
184
-
<div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl">
698
+
<div class="card bg-base-100 w-full max-w-2xl shrink-0 shadow-2xl m-4">
185
699
<div class="card-body">
186
-
<form @submit.prevent="await diagnose()">
187
-
<label>
188
-
Enter an atproto handle, DID, or HTTPS PDS URL
189
-
<input
190
-
class="input"
191
-
x-model="identifier"
192
-
:disabled="identifierLoading"
193
-
autofocus
194
-
/>
195
-
</label>
196
-
</form>
700
+
<h2 class="card-title">
701
+
<span class="badge badge-secondary">PDS</span>
702
+
<span x-text="pds"></span>
703
+
</h2>
704
+
705
+
<div
706
+
x-data="pdsCheck(pds)"
707
+
x-init="$watch('pds', v => update(v))"
708
+
>
709
+
<h3 class="text-lg mt-3">
710
+
Server
711
+
<span
712
+
x-show="description !== null"
713
+
class="badge badge-sm badge-soft badge-success"
714
+
>online</span>
715
+
</h3>
716
+
<p x-show="loadingDesc">Loading…</p>
717
+
<p x-show="error" class="text-warning" x-text="error"></p>
718
+
<template x-if="description !== null">
719
+
<div>
720
+
<div class="overflow-x-auto">
721
+
<table class="table table-xs">
722
+
<tbody>
723
+
<tr>
724
+
<td class="text-sm">
725
+
Open registration:
726
+
<span
727
+
x-text="!description.inviteCodeRequired"
728
+
></span>
729
+
</td>
730
+
</tr>
731
+
<tr>
732
+
<td class="text-sm">
733
+
Version:
734
+
<code
735
+
class="text-xs"
736
+
x-text="version"
737
+
></code>
738
+
</td>
739
+
</tr>
740
+
</tbody>
741
+
</table>
742
+
</div>
743
+
<h4 class="font-bold">
744
+
Accounts
745
+
</h4>
746
+
<div class="overflow-x-auto overflow-y-auto max-h-26">
747
+
<table class="table table-xs">
748
+
<tbody>
749
+
<template x-for="account in accounts">
750
+
<tr>
751
+
<td>
752
+
<code>
753
+
<a
754
+
href="#"
755
+
class="link"
756
+
x-text="account.did"
757
+
@click.prevent="goto(account.did)"
758
+
></a>
759
+
</code>
760
+
</td>
761
+
<td>
762
+
<span
763
+
x-show="account.active"
764
+
class="badge badge-sm badge-soft badge-success"
765
+
>
766
+
active
767
+
</span>
768
+
<span
769
+
x-show="!account.active"
770
+
x-text="account.status"
771
+
class="badge badge-sm badge-soft badge-warning"
772
+
></span>
773
+
</td>
774
+
<td
775
+
x-data="didToHandle(account.did)"
776
+
x-intersect:enter.once="load"
777
+
>
778
+
<span x-show="loading">Loading…</span>
779
+
<span x-show="error !== null" x-text="error"></span>
780
+
<a
781
+
href="#"
782
+
class="link"
783
+
@click.prevent="goto(handle)"
784
+
x-show="handle !== null"
785
+
x-text="`@${handle}`"
786
+
></a>
787
+
</td>
788
+
</tr>
789
+
</template>
790
+
<template x-if="!loadingDesc && !accountsComplete">
791
+
<tr>
792
+
<td colspan="2" class="text-xs text-warning-content">
793
+
(more accounts not shown)
794
+
</td>
795
+
</tr>
796
+
</template>
797
+
</tbody>
798
+
</table>
799
+
</div>
800
+
</div>
801
+
</template>
802
+
</div>
803
+
804
+
<h3 class="text-lg mt-3">Relay host status</h3>
805
+
<div class="overflow-x-auto">
806
+
<table class="table table-xs">
807
+
<tbody>
808
+
<template x-for="relay in window.relays">
809
+
<tr
810
+
x-data="relayCheckHost(pds, relay)"
811
+
x-init="$watch('pds', pds => check(pds, relay))"
812
+
>
813
+
<td class="text-sm">
814
+
<div class="tooltip tooltip-right" :data-tip="relay.hostname">
815
+
<img
816
+
class="inline-block h-4 w-4"
817
+
:src="relay.icon"
818
+
alt=""
819
+
/>
820
+
<span x-text="relay.name"></span>
821
+
<span
822
+
x-show="!!relay.note"
823
+
x-text="relay.note"
824
+
class="badge badge-soft badge-neutral badge-xs"
825
+
></span>
826
+
</div>
827
+
</td>
828
+
<td>
829
+
<template x-if="loading">
830
+
<em>loading…</em>
831
+
</template>
832
+
<template x-if="error">
833
+
<div
834
+
class="text-xs"
835
+
:class="expectedErrorInfo
836
+
? 'text-info tooltip tooltip-left cursor-help'
837
+
: 'text-warning'"
838
+
:data-tip="expectedErrorInfo"
839
+
>
840
+
<span x-text="error"></span>
841
+
<span
842
+
x-show="!!expectedErrorInfo"
843
+
class="badge badge-soft badge-info badge-xs"
844
+
>i</span>
845
+
</div>
846
+
</template>
847
+
<template x-if="status">
848
+
<span
849
+
x-text="status"
850
+
class="badge badge-sm"
851
+
:class="status === 'active' && 'badge-soft badge-success'"
852
+
></span>
853
+
</template>
854
+
</td>
855
+
<td>
856
+
<div x-show="status !== 'active' && !expectedErrorInfo">
857
+
<button
858
+
x-show="reqCrawlStatus !== 'done'"
859
+
class="btn btn-xs btn-ghost whitespace-nowrap"
860
+
:disabled="reqCrawlStatus === 'loading'"
861
+
@click="requestCrawl(pds, relay)"
862
+
>
863
+
request crawl
864
+
</button>
865
+
<span
866
+
x-show="reqCrawlError !== null"
867
+
x-text="reqCrawlError"
868
+
class="text-xs text-warning"
869
+
></span>
870
+
<button
871
+
x-show="reqCrawlError === null && reqCrawlStatus === 'done'"
872
+
class="btn btn-xs btn-soft btn-primary whitespace-nowrap"
873
+
@click="check"
874
+
>
875
+
refresh
876
+
</button>
877
+
</div>
878
+
</td>
879
+
</tr>
880
+
</template>
881
+
</tbody>
882
+
</table>
883
+
</div>
197
884
</div>
198
885
</div>
886
+
</template>
199
887
200
-
<template x-if="identifierError">
201
-
<p>uh oh: <span x-text="identifierError"></span></p>
202
-
</template>
888
+
<template x-if="did != null">
889
+
<div class="card bg-base-100 w-full max-w-2xl shrink-0 shadow-2xl m-4">
890
+
<div class="card-body">
891
+
<h2 class="card-title">
892
+
<span class="badge badge-secondary">DID</span>
893
+
<code x-text="did"></code>
894
+
</h2>
895
+
<template x-if="pds != null">
896
+
<div x-data="didRepoState(did, pds)">
897
+
<h3 class="text-lg mt-3">
898
+
Repo
899
+
<span
900
+
x-show="state && state.active"
901
+
class="badge badge-sm badge-soft badge-success"
902
+
>active</span>
903
+
</h3>
904
+
<div class="overflow-x-auto">
905
+
<table class="table table-xs">
906
+
<tbody>
907
+
<tr>
908
+
<td class="text-sm">
909
+
Rev:
910
+
<code x-text="state && state.rev"></code>
911
+
</td>
912
+
</tr>
913
+
<tr>
914
+
<td class="text-sm">
915
+
Size:
916
+
<span x-data="repoSize">
917
+
<template x-if="loading">
918
+
<em>loading…</em>
919
+
</template>
920
+
<template x-if="error">
921
+
<span class="text-xs text-warning" x-text="error"></span>
922
+
</template>
923
+
<template x-if="size">
924
+
<code>
925
+
<span x-text="size"></span> MiB
926
+
</code>
927
+
</template>
928
+
<template x-if="!size && !error && !loading">
929
+
<button
930
+
class="btn btn-xs btn-soft btn-primary"
931
+
@click.prevent="loadRepoForSize(did, pds)"
932
+
>load</button>
933
+
</template>
934
+
</span>
935
+
</td>
936
+
</tr>
937
+
<tr>
938
+
<td class="text-sm">
939
+
PDS:
940
+
<a
941
+
href="#"
942
+
class="link"
943
+
@click.prevent="goto(pds)"
944
+
x-text="pds"
945
+
></a>
946
+
</td>
947
+
</tr>
948
+
<!--<tr>
949
+
<td
950
+
class="text-sm"
951
+
x-data="repoMonitor(did, pds)"
952
+
>
953
+
<button
954
+
class="btn btn-xs btn-success"
955
+
>Start live monitoring</button>
956
+
</td>
957
+
</tr>-->
958
+
</tbody>
959
+
</table>
960
+
</div>
961
+
962
+
<h3 class="text-lg mt-3">
963
+
Relay repo status
964
+
</h3>
965
+
<div class="overflow-x-auto">
966
+
<table class="table table-xs">
967
+
<tbody>
968
+
<template x-for="relay in window.relays">
969
+
<tr
970
+
x-data="relayCheckRepo(did, relay)"
971
+
x-init="$watch('pds', pds => check(did, relay))"
972
+
>
973
+
<td class="text-sm">
974
+
<div class="tooltip tooltip-right" :data-tip="relay.hostname">
975
+
<img
976
+
class="inline-block h-4 w-4"
977
+
:src="relay.icon"
978
+
alt=""
979
+
/>
980
+
<span x-text="relay.name"></span>
981
+
<span
982
+
x-show="!!relay.note"
983
+
x-text="relay.note"
984
+
class="badge badge-neutral badge-soft badge-xs"
985
+
></span>
986
+
</div>
987
+
</td>
988
+
<template x-if="loading">
989
+
<td>
990
+
<em>loading…</em>
991
+
</td>
992
+
</template>
993
+
<template x-if="error">
994
+
<td>
995
+
<div
996
+
class="text-xs"
997
+
:class="expectedErrorInfo
998
+
? 'text-info tooltip tooltip-left cursor-help'
999
+
: 'text-warning'"
1000
+
:data-tip="expectedErrorInfo"
1001
+
>
1002
+
<span x-text="error"></span>
1003
+
<span
1004
+
x-show="!!expectedErrorInfo"
1005
+
class="badge badge-soft badge-info badge-xs"
1006
+
>i</span>
1007
+
</div>
1008
+
</td>
1009
+
</template>
1010
+
<template x-if="status">
1011
+
<td>
1012
+
<span
1013
+
x-show="status.active"
1014
+
class="badge badge-sm badge-soft badge-success"
1015
+
>
1016
+
active
1017
+
</span>
1018
+
<span
1019
+
x-show="!status.active"
1020
+
x-text="status.status"
1021
+
class="badge badge-sm badge-soft badge-warning"
1022
+
></span>
1023
+
</td>
1024
+
</template>
1025
+
<template x-if="revStatus(state && state.rev)">
1026
+
<td x-data="{ asdf: revStatus(state.rev) }">
1027
+
<span
1028
+
x-show="asdf === 'current'"
1029
+
class="badge badge-sm badge-soft badge-success"
1030
+
>current</span>
1031
+
<span
1032
+
x-show="asdf === 'behind'"
1033
+
class="badge badge-sm badge-soft badge-warning tooltip tooltip-left"
1034
+
:data-tip="status.rev"
1035
+
>behind</span>
1036
+
<span
1037
+
x-show="asdf === 'ahead'"
1038
+
class="badge badge-sm badge-soft badge-success tooltip tooltip-left"
1039
+
:data-tip="`Account may have updated between checks? ${status.rev}`"
1040
+
>ahead</span>
1041
+
</td>
1042
+
</template>
1043
+
<template x-if="!revStatus(state && state.rev)">
1044
+
<td></td>
1045
+
</template>
1046
+
</tr>
1047
+
</template>
1048
+
</tbody>
1049
+
</table>
1050
+
</div>
203
1051
204
-
<template x-if="pds != null">
205
-
<div class="card bg-base-100 w-full max-w-lg shrink-0 shadow-2xl">
206
-
<div class="card-body">
207
-
<h2 class="card-title">
208
-
<span class="badge badge-secondary">PDS</span>
209
-
<span x-text="pds"></span>
210
-
</h2>
1052
+
<div x-data="modLabels(did)">
1053
+
<h3 class="text-lg mt-3">
1054
+
Labels
1055
+
</h3>
1056
+
<div class="overflow-x-auto">
1057
+
<table class="table table-xs">
1058
+
<tbody>
1059
+
<template x-if="loading">
1060
+
<tr>
1061
+
<td>Loading…</td>
1062
+
</tr>
1063
+
</template>
1064
+
<template x-if="error">
1065
+
<tr>
1066
+
<td>Error: <span x-text="error"></span></td>
1067
+
</tr>
1068
+
</template>
1069
+
<template x-if="!loading && !error && labels.length === 0">
1070
+
<tr>
1071
+
<td class="text-xs">
1072
+
<em>No Bluesky moderation labels found</em>
1073
+
</td>
1074
+
</tr>
1075
+
</template>
1076
+
<template x-for="label in labels">
1077
+
<template x-if="!!label">
1078
+
<tr x-data="{ expired: isBeforeNow(label.exp) }">
1079
+
<td>
1080
+
<span x-show="label.neg">removed</span>
1081
+
<code
1082
+
x-text="label.cts.split('T')[0]"
1083
+
:title="label.cts"
1084
+
></code>
1085
+
</td>
1086
+
<td>
1087
+
<template x-if="!!label.exp">
1088
+
<span x-text="expired ? 'expired' : 'expires'"></span>
1089
+
<code
1090
+
x-text="label.exp.split('T')[0]"
1091
+
:title="label.exp"
1092
+
></code>
1093
+
</template>
1094
+
</td>
1095
+
<td>
1096
+
<code
1097
+
x-text="label.val"
1098
+
class="badge badge-sm badge-soft"
1099
+
:class="(label.neg || expired)
1100
+
? 'badge-neutral line-through'
1101
+
: !!window.bskyAccountDeathLabels[label.val]
1102
+
? 'badge-warning'
1103
+
: 'badge-info'"
1104
+
:title="label.neg
1105
+
? 'label negated'
1106
+
: expired
1107
+
? 'label expired'
1108
+
: window.bskyAccountDeathLabels[label.val] ?? ''"
1109
+
></code>
1110
+
</td>
1111
+
<td
1112
+
x-data="didToHandle(label.src)"
1113
+
x-intersect:enter.once="load"
1114
+
>
1115
+
<span x-show="loading">Loading…</span>
1116
+
<span x-show="error !== null" x-text="error"></span>
1117
+
<a
1118
+
href="#"
1119
+
class="link"
1120
+
@click.prevent="goto(handle)"
1121
+
x-show="handle !== null"
1122
+
x-text="`@${handle}`"
1123
+
></a>
1124
+
</td>
1125
+
</tr>
1126
+
</template>
1127
+
</template>
1128
+
</tbody>
1129
+
</table>
1130
+
</div>
1131
+
<template x-for="error in regionalErrors">
1132
+
<p
1133
+
x-text="error"
1134
+
class="text-xs text-warning"
1135
+
></p>
1136
+
</template>
1137
+
</div>
211
1138
212
-
<h3 class="text-lg">Relay host status</h3>
213
-
<div class="overflow-x-auto">
214
-
<table class="table table-xs">
215
-
<tbody>
216
-
<template x-for="relay in window.relays">
217
-
<tr x-data="relayCheckHost(pds, relay)">
218
-
<td x-text="relay.name" class="text-sm"></td>
219
-
<td>
1139
+
<template x-if="did.startsWith('did:plc:')">
1140
+
<div x-data="pdsHistory(did, pds)">
1141
+
<h3 class="text-lg mt-3">
1142
+
PLC PDS history
1143
+
</h3>
1144
+
<div class="overflow-x-auto">
1145
+
<table class="table table-xs">
1146
+
<tbody>
220
1147
<template x-if="loading">
221
-
<em>loading…</em>
1148
+
<tr>
1149
+
<td>Loading…</td>
1150
+
</tr>
222
1151
</template>
223
1152
<template x-if="error">
224
-
<span
225
-
x-text="error"
226
-
class="text-xs text-warning"
227
-
></span>
1153
+
<tr>
1154
+
<td>Error: <span x-text="error"></span></td>
1155
+
</tr>
228
1156
</template>
229
-
<template x-if="status">
230
-
<span
231
-
x-text="status"
232
-
class="badge badge-sm"
233
-
:class="status === 'active' && 'badge-soft badge-success'"
234
-
></span>
1157
+
<template x-if="!loading && !error && history.length === 0">
1158
+
<tr>
1159
+
<td class="text-sm">
1160
+
<em>no previous PDS</em>
1161
+
</td>
1162
+
</tr>
235
1163
</template>
236
-
</td>
237
-
<td>
238
-
<div x-show="status !== 'active'">
239
-
<button
240
-
x-show="reqCrawlStatus !== 'done'"
241
-
class="btn btn-xs btn-ghost whitespace-nowrap"
242
-
:disabled="reqCrawlStatus === 'loading'"
243
-
@click="requestCrawl"
244
-
>
245
-
request crawl
246
-
</button>
247
-
<span
248
-
x-show="reqCrawlError !== null"
249
-
x-text="reqCrawlError"
250
-
class="text-xs text-warning"
251
-
></span>
252
-
<button
253
-
x-show="reqCrawlError === null && reqCrawlStatus === 'done'"
254
-
class="btn btn-xs btn-soft btn-primary whitespace-nowrap"
255
-
@click="check"
256
-
>
257
-
refresh
258
-
</button>
259
-
</div>
260
-
</td>
261
-
</tr>
262
-
</template>
263
-
</tbody>
264
-
</table>
1164
+
<template x-for="event in history">
1165
+
<tr x-data="didRepoState(did, event.pds)">
1166
+
<td>
1167
+
<code x-text="event.date.split('T')[0]"></code>
1168
+
</td>
1169
+
<td>
1170
+
<a
1171
+
href="#"
1172
+
class="link"
1173
+
@click.prevent="goto(event.pds)"
1174
+
x-text="event.pds"
1175
+
></a>
1176
+
</td>
1177
+
<template x-if="event.current">
1178
+
<td>
1179
+
<span
1180
+
x-show="state && !state.active"
1181
+
x-text="state && state.status"
1182
+
class="badge badge-sm badge-soft badge-warning"
1183
+
></span>
1184
+
<span
1185
+
x-show="state && state.active"
1186
+
class="badge badge-sm badge-soft badge-success"
1187
+
>current</span>
1188
+
</td>
1189
+
</template>
1190
+
<template x-if="!event.current">
1191
+
<td>
1192
+
<span
1193
+
x-show="state && !state.active"
1194
+
x-text="state && state.status"
1195
+
class="badge badge-sm badge-soft badge-success"
1196
+
></span>
1197
+
<span
1198
+
x-show="state && state.active"
1199
+
class="badge badge-sm badge-soft badge-warning"
1200
+
>active</span>
1201
+
</td>
1202
+
</template>
1203
+
</tr>
1204
+
</template>
1205
+
</tbody>
1206
+
</table>
1207
+
</div>
1208
+
</div>
1209
+
</template>
265
1210
</div>
266
-
</div>
1211
+
</template>
267
1212
</div>
268
-
</template>
1213
+
</div>
1214
+
</template>
269
1215
270
-
<template x-if="did != null">
271
-
<div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl">
272
-
<div class="card-body">
273
-
<p x-text="`DID: ${did}`"></p>
1216
+
<template x-if="handle !== null">
1217
+
<div class="card bg-base-100 w-full max-w-2xl shrink-0 shadow-2xl m-4">
1218
+
<div
1219
+
x-data="checkHandle(handle)"
1220
+
x-init="$watch('handle', h => updateHandle(h))"
1221
+
class="card-body"
1222
+
>
1223
+
<h2 class="card-title">
1224
+
<span class="badge badge-secondary">Handle</span>
1225
+
<span x-text="handle"></span>
1226
+
</h2>
1227
+
1228
+
<h3 class="text-lg mt-3">
1229
+
Resolution
1230
+
</h3>
1231
+
<p x-show="loading" class="text-i">Loading…</p>
1232
+
<div x-show="!loading" class="overflow-x-auto">
1233
+
<table class="table table-xs">
1234
+
<tbody>
1235
+
<tr>
1236
+
<td class="text-sm">DNS</td>
1237
+
<td class="text-sm">
1238
+
<code x-text="dnsDid"></code>
1239
+
</td>
1240
+
<td>
1241
+
<div
1242
+
class="badge badge-sm badge-soft badge-neutral"
1243
+
x-show="dnsErr !== null"
1244
+
x-text="dnsErr"
1245
+
></div>
1246
+
</td>
1247
+
</tr>
1248
+
<tr>
1249
+
<td class="text-sm">Http</td>
1250
+
<td class="text-sm">
1251
+
<code x-text="httpDid"></code>
1252
+
</td>
1253
+
<td>
1254
+
<div
1255
+
class="badge badge-sm badge-soft badge-neutral"
1256
+
x-show="httpErr !== null"
1257
+
x-text="httpErr"
1258
+
></div>
1259
+
</td>
1260
+
</tr>
1261
+
</tbody>
1262
+
</table>
274
1263
</div>
1264
+
1265
+
<template x-if="did !== null && did.startsWith('did:plc:')">
1266
+
1267
+
<div x-data="handleHistory(did, handle)">
1268
+
<h3 class="text-lg mt-3">
1269
+
PLC handle history
1270
+
</h3>
1271
+
<div class="overflow-x-auto">
1272
+
<table class="table table-xs">
1273
+
<tbody>
1274
+
<template x-if="loading">
1275
+
<tr>
1276
+
<td>Loading…</td>
1277
+
</tr>
1278
+
</template>
1279
+
<template x-if="error">
1280
+
<tr>
1281
+
<td>Error: <span x-text="error"></span></td>
1282
+
</tr>
1283
+
</template>
1284
+
<template x-if="!loading && !error && history.length === 0">
1285
+
<tr>
1286
+
<td class="text-sm">
1287
+
<em>no previous handle</em>
1288
+
</td>
1289
+
</tr>
1290
+
</template>
1291
+
<template x-for="event in history">
1292
+
<tr>
1293
+
<td>
1294
+
<code x-text="event.date.split('T')[0]"></code>
1295
+
</td>
1296
+
<td>
1297
+
<a
1298
+
href="#"
1299
+
class="link"
1300
+
@click.prevent="goto(event.handle)"
1301
+
x-text="event.handle"
1302
+
></a>
1303
+
</td>
1304
+
</tr>
1305
+
</template>
1306
+
</tbody>
1307
+
</table>
1308
+
</div>
1309
+
</div>
1310
+
1311
+
</template>
275
1312
</div>
276
-
</template>
277
-
</div>
1313
+
</div>
1314
+
</template>
278
1315
</div>
1316
+
1317
+
1318
+
1319
+
<div class="footer text-xs sm:footer-horizontal text-neutral mt-32 p-8 max-w-2xl mx-auto">
1320
+
<nav>
1321
+
<h3 class="footer-title mt-3">Current limitations</h3>
1322
+
<p>PDS hosts without CORS will fail tests.</p>
1323
+
<p>Bluesky relay is missing API endpoints.</p>
1324
+
<p>Blacksky relay is also missing API endpoints.</p>
1325
+
<p>The requestCrawl button is not well tested.</p>
1326
+
1327
+
<h3 class="footer-title mt-3">Future features</h3>
1328
+
<p>Firehose listener</p>
1329
+
<p>URL routing</p>
1330
+
<p>Less strict identity resolution</p>
1331
+
</nav>
1332
+
1333
+
<nav>
1334
+
<h3 class="footer-title mt-3">Places</h3>
1335
+
<p><a href="https://tangled.org/microcosm.blue/pds-debug">Source code (tangled.org)</a></p>
1336
+
<p><a href="https://discord.gg/Vwamex5UFS">Discord (microcosm)</a></p>
1337
+
<p><a href="https://pdsmoover.com/">PDS Moover</a></p>
1338
+
<p><a href="https://microcosm.blue">microcosm</a></p>
1339
+
1340
+
<h3 class="footer-title mt-3">Made by</h3>
1341
+
<p>
1342
+
<a href="https://bsky.app/profile/did:plc:hdhoaan3xa3jiuq4fg4mefid">fig</a>
1343
+
<a href="https://github.com/sponsors/uniphil">(sponsor)</a>
1344
+
</p>
1345
+
<p>
1346
+
<a href="https://bsky.app/profile/did:plc:rnpkyqnmsw4ipey6eotbdnnf">bailey</a>
1347
+
<a href="https://github.com/sponsors/fatfingers23">(sponsor)</a>
1348
+
</p>
1349
+
</nav>
1350
+
</div>
1351
+
279
1352
</body>
280
1353
</html>
+5
readme.md
+5
readme.md
+13
useful-accounts.txt
+13
useful-accounts.txt
···
1
+
some accounts that show things useful for testing the debugger
2
+
3
+
4
+
Labels
5
+
6
+
- did:plc:bnwrgnvwkg2n5cbvk4xodb3h | !hide | no other labels
7
+
- did:plc:qhl3vg5tmwey536z2fil2lrh | !hide | from moderation-tr.bsky.app
8
+
- did:plc:fsmaoqqnm6knqh4cuphb4jow | !hide, ~!takedown | takedown negated
9
+
- did:plc:iv3yod6zf2j4zaakq6qyiz46 | !takedown |
10
+
- did:plc:nwrcwcrhpkgrqqvkg3lmaqky | ~needs-review, ~!takedown | both negated
11
+
- did:plc:2tinwgqvf4asiwh36ii6ko7l | needs-review | expired
12
+
- did:plc:5plqrpw3x6j5wzaosssqams7 | spam | no other labels
13
+
- did:plc:tqww7jdpqx5tb3w435fugmxi | intolerant |