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 font-src 'self'; 11 script-src-attr 'none'; 12 style-src-attr 'none'; 13 - connect-src https://blue.mackuba.eu https://api.bsky.app; 14 base-uri 'none'; 15 form-action 'self';"> 16
··· 10 font-src 'self'; 11 script-src-attr 'none'; 12 style-src-attr 'none'; 13 + connect-src https:; 14 base-uri 'none'; 15 form-action 'self';"> 16
+70 -7
scanner.js
··· 7 ]; 8 9 class URLError extends Error {} 10 11 document.addEventListener("DOMContentLoaded", function() { 12 initScanner(); ··· 102 resultField.innerText = '👾 Unable to resolve handle'; 103 return; 104 } 105 - } else if (error.json.error == 'AccountDeactivated') { 106 resultField.innerText = '😶‍🌫️ Account is deactivated'; 107 return; 108 } 109 } 110 ··· 112 return; 113 } else if (error instanceof URLError) { 114 resultField.innerText = `⚠️ ${error.message}`; 115 return; 116 } 117 ··· 140 } 141 142 async function scanURL(string) { 143 - let atURI, note; 144 145 - if (string.match(/^at:\/\/did:[^/]+\/app\.bsky\.feed\.post\/[\w]+$/)) { 146 atURI = string; 147 } else { 148 let url = new URL(string); 149 ··· 167 let match = url.pathname.match(/^\/profile\/([^/]+)\/post\/([\w]+)\/?$/); 168 169 if (match && match[1].startsWith('did:')) { 170 - atURI = `at://${match[1]}/app.bsky.feed.post/${match[2]}`; 171 } else if (match) { 172 let json = await appView.getRequest('com.atproto.identity.resolveHandle', { handle: match[1] }); 173 - atURI = `at://${json.did}/app.bsky.feed.post/${match[2]}`; 174 } else { 175 throw new URLError('Unknown URL'); 176 } 177 } 178 } 179 180 - let userDID = atURI.split('/')[2]; 181 let batches = []; 182 183 for (let i = 0; i < labellers.length; i += batchSize) { ··· 188 let results = await Promise.all(batches); 189 190 if (results.every(x => !x)) { 191 - throw '🚫 Post not found.'; 192 } 193 194 let labels = results.flatMap(x => x.labels).filter(x => (x.src != userDID)); 195 return { labels, note }; 196 } 197 198 async function checkProfileWithLabellers(handle, batch) {
··· 7 ]; 8 9 class URLError extends Error {} 10 + class AccountError extends Error {} 11 + class PostTakenDownError extends Error {} 12 13 document.addEventListener("DOMContentLoaded", function() { 14 initScanner(); ··· 104 resultField.innerText = '👾 Unable to resolve handle'; 105 return; 106 } 107 + } else if (error.json.error == 'AccountDeactivated' || error.json.error == 'RepoDeactivated') { 108 resultField.innerText = '😶‍🌫️ Account is deactivated'; 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; 116 } 117 } 118 ··· 120 return; 121 } else if (error instanceof URLError) { 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'; 129 return; 130 } 131 ··· 154 } 155 156 async function scanURL(string) { 157 + let atURI, note, userDID, rkey; 158 + 159 + let match = string.match(/^at:\/\/(did:[^/]+)\/app\.bsky\.feed\.post\/([\w]+)$/); 160 161 + if (match) { 162 atURI = string; 163 + userDID = match[1]; 164 + rkey = match[2]; 165 } else { 166 let url = new URL(string); 167 ··· 185 let match = url.pathname.match(/^\/profile\/([^/]+)\/post\/([\w]+)\/?$/); 186 187 if (match && match[1].startsWith('did:')) { 188 + userDID = match[1]; 189 + rkey = match[2]; 190 } else if (match) { 191 let json = await appView.getRequest('com.atproto.identity.resolveHandle', { handle: match[1] }); 192 + userDID = json.did; 193 + rkey = match[2]; 194 } else { 195 throw new URLError('Unknown URL'); 196 } 197 + 198 + atURI = `at://${userDID}/app.bsky.feed.post/${rkey}`; 199 } 200 } 201 202 let batches = []; 203 204 for (let i = 0; i < labellers.length; i += batchSize) { ··· 209 let results = await Promise.all(batches); 210 211 if (results.every(x => !x)) { 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(); 219 } 220 221 let labels = results.flatMap(x => x.labels).filter(x => (x.src != userDID)); 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 + } 259 } 260 261 async function checkProfileWithLabellers(handle, batch) {