A small web tool for showing what Bluesky moderation labels are assigned to a profile

if post is missing, try to figure out why

Changed files
+71 -8
+1 -1
index.html
··· 10 10 font-src 'self'; 11 11 script-src-attr 'none'; 12 12 style-src-attr 'none'; 13 - connect-src https://blue.mackuba.eu https://api.bsky.app; 13 + connect-src https:; 14 14 base-uri 'none'; 15 15 form-action 'self';"> 16 16
+70 -7
scanner.js
··· 7 7 ]; 8 8 9 9 class URLError extends Error {} 10 + class AccountError extends Error {} 11 + class PostTakenDownError extends Error {} 10 12 11 13 document.addEventListener("DOMContentLoaded", function() { 12 14 initScanner(); ··· 102 104 resultField.innerText = '👾 Unable to resolve handle'; 103 105 return; 104 106 } 105 - } else if (error.json.error == 'AccountDeactivated') { 107 + } else if (error.json.error == 'AccountDeactivated' || error.json.error == 'RepoDeactivated') { 106 108 resultField.innerText = '😶‍🌫️ Account is deactivated'; 107 109 return; 110 + } else if (error.json.error == 'RecordNotFound') { 111 + resultField.innerText = '🚫 Post not found'; 112 + return; 113 + } else if (error.json.error == 'RepoNotFound') { 114 + resultField.innerText = '🚫 Account was deleted'; 115 + return; 108 116 } 109 117 } 110 118 ··· 112 120 return; 113 121 } else if (error instanceof URLError) { 114 122 resultField.innerText = `⚠️ ${error.message}`; 123 + return; 124 + } else if (error instanceof PostTakenDownError) { 125 + resultField.innerText = `🚫 Post was taken down on the AppView`; 126 + return; 127 + } else if (error instanceof AccountError) { 128 + resultField.innerText = '🚫 Account not found'; 115 129 return; 116 130 } 117 131 ··· 140 154 } 141 155 142 156 async function scanURL(string) { 143 - let atURI, note; 157 + let atURI, note, userDID, rkey; 158 + 159 + let match = string.match(/^at:\/\/(did:[^/]+)\/app\.bsky\.feed\.post\/([\w]+)$/); 144 160 145 - if (string.match(/^at:\/\/did:[^/]+\/app\.bsky\.feed\.post\/[\w]+$/)) { 161 + if (match) { 146 162 atURI = string; 163 + userDID = match[1]; 164 + rkey = match[2]; 147 165 } else { 148 166 let url = new URL(string); 149 167 ··· 167 185 let match = url.pathname.match(/^\/profile\/([^/]+)\/post\/([\w]+)\/?$/); 168 186 169 187 if (match && match[1].startsWith('did:')) { 170 - atURI = `at://${match[1]}/app.bsky.feed.post/${match[2]}`; 188 + userDID = match[1]; 189 + rkey = match[2]; 171 190 } else if (match) { 172 191 let json = await appView.getRequest('com.atproto.identity.resolveHandle', { handle: match[1] }); 173 - atURI = `at://${json.did}/app.bsky.feed.post/${match[2]}`; 192 + userDID = json.did; 193 + rkey = match[2]; 174 194 } else { 175 195 throw new URLError('Unknown URL'); 176 196 } 197 + 198 + atURI = `at://${userDID}/app.bsky.feed.post/${rkey}`; 177 199 } 178 200 } 179 201 180 - let userDID = atURI.split('/')[2]; 181 202 let batches = []; 182 203 183 204 for (let i = 0; i < labellers.length; i += batchSize) { ··· 188 209 let results = await Promise.all(batches); 189 210 190 211 if (results.every(x => !x)) { 191 - throw '🚫 Post not found.'; 212 + // post not found, look it up on the origin PDS 213 + 214 + let post = await loadPostFromPDS(userDID, rkey); 215 + 216 + // post found, so it was taken down on the AppView 217 + 218 + throw new PostTakenDownError(); 192 219 } 193 220 194 221 let labels = results.flatMap(x => x.labels).filter(x => (x.src != userDID)); 195 222 return { labels, note }; 223 + } 224 + 225 + async function loadPostFromPDS(did, rkey) { 226 + let didDocument = await fetchDidDocument(did); 227 + 228 + if (didDocument.message?.startsWith("DID not registered:")) { 229 + throw new AccountError("Account not found"); 230 + } 231 + 232 + let pds = didDocument.service?.find(x => x.id == "#atproto_pds")?.serviceEndpoint; 233 + 234 + if (!pds) { 235 + throw new AccountError("Invalid DID document"); 236 + } 237 + 238 + let pdsSky = new Minisky(pds); 239 + let repo = await pdsSky.getRequest('com.atproto.repo.describeRepo', { repo: did }); 240 + 241 + return await pdsSky.getRequest('com.atproto.repo.getRecord', { 242 + repo: did, 243 + collection: 'app.bsky.feed.post', 244 + rkey: rkey 245 + }); 246 + } 247 + 248 + async function fetchDidDocument(did) { 249 + if (did.startsWith('did:plc:')) { 250 + let response = await fetch(`https://plc.directory/${did}`); 251 + return await response.json(); 252 + } else if (did.startsWith('did:web:')) { 253 + let hostname = did.split(':')[2]; 254 + let response = await fetch(`https://${hostname}/.well-known/did.json`); 255 + return await response.json(); 256 + } else { 257 + throw new AccountError("DID not found"); 258 + } 196 259 } 197 260 198 261 async function checkProfileWithLabellers(handle, batch) {