view who was fronting when a record was made
at main 13 kB view raw
1import { expect } from "@/lib/result"; 2import { FronterView, parseSocialAppPostUrl } from "@/lib/utils"; 3import { parseResourceUri } from "@atcute/lexicons"; 4 5const getAuthHeader = (headers: any): string | null => { 6 if (headers instanceof Headers) { 7 return headers.get("authorization"); 8 } else if (typeof headers === "object" && headers !== null) { 9 return headers["authorization"] || headers["Authorization"] || null; 10 } 11 return null; 12}; 13 14export default defineContentScript({ 15 matches: ["<all_urls>"], 16 runAt: "document_start", 17 world: "MAIN", 18 main: () => { 19 let respEventName: string | null = null; 20 21 const originalFetch = globalThis.fetch; 22 const overriddenFetch = async ( 23 ...args: [input: RequestInfo | URL, init?: RequestInit] 24 ) => { 25 const getRequestBody = async () => { 26 if (args[0] instanceof Request) { 27 if (args[0].bodyUsed) return null; 28 try { 29 const clone = args[0].clone(); 30 return await clone.text(); 31 } catch { 32 return null; 33 } 34 } else if (args[1]?.body) { 35 return typeof args[1].body === "string" 36 ? args[1].body 37 : JSON.stringify(args[1].body); 38 } 39 return null; 40 }; 41 const requestBody = await getRequestBody(); 42 const response = await originalFetch.apply(this, args); 43 44 if (respEventName === null) return response; 45 if (response.status !== 200) return response; 46 47 const body = await response.clone().text(); 48 49 const sendEvent = (detail: any) => { 50 // console.log("sending response event", detail); 51 window.dispatchEvent.call( 52 window, 53 new window.CustomEvent(`${respEventName}-isolated`, { 54 detail, 55 }), 56 ); 57 }; 58 const getAuthToken = () => { 59 let authHeader: string | null = null; 60 if (typeof args[0] === "string") { 61 if (args[1]?.headers) { 62 authHeader = getAuthHeader(args[1].headers); 63 } 64 } else if (args[0] instanceof Request) { 65 authHeader = getAuthHeader(args[0].headers); 66 } 67 return authHeader?.split(" ")[1] || null; 68 }; 69 const getRequestUrl = () => { 70 let url: string | null = null; 71 if (args[0] instanceof Request) { 72 url = args[0].url; 73 } else { 74 url = args[0].toString(); 75 } 76 return decodeURI(url); 77 }; 78 79 let detail: any = undefined; 80 if (response.url.includes("/xrpc/com.atproto.repo.applyWrites")) { 81 detail = { 82 type: "write", 83 body, 84 authToken: getAuthToken(), 85 }; 86 } else if (response.url.includes("/xrpc/com.atproto.repo.deleteRecord")) { 87 detail = { 88 type: "delete", 89 body: requestBody, 90 authToken: getAuthToken(), 91 }; 92 } else if (response.url.includes("/xrpc/com.atproto.repo.createRecord")) { 93 detail = { 94 type: "writeOne", 95 body, 96 authToken: getAuthToken(), 97 }; 98 } else if ( 99 response.url.includes("/xrpc/app.bsky.feed.getAuthorFeed") || 100 response.url.includes("/xrpc/app.bsky.feed.getTimeline") || 101 response.url.includes("/xrpc/app.bsky.feed.getFeed") 102 ) { 103 detail = { 104 type: "timeline", 105 body, 106 }; 107 } else if ( 108 response.url.includes("/xrpc/app.bsky.unspecced.getPostThreadV2") 109 ) { 110 detail = { 111 type: "thread", 112 body, 113 requestUrl: getRequestUrl(), 114 documentUrl: document.location.href, 115 }; 116 } else if (response.url.includes("/xrpc/app.bsky.feed.getPosts")) { 117 detail = { 118 type: "posts", 119 body, 120 }; 121 } else if ( 122 response.url.includes("/xrpc/app.bsky.notification.listNotifications") 123 ) { 124 detail = { 125 type: "notifications", 126 body, 127 }; 128 } else if (response.url.includes("/xrpc/app.bsky.feed.getLikes")) { 129 detail = { 130 type: "likes", 131 body, 132 }; 133 } else if (response.url.includes("/xrpc/app.bsky.feed.getRepostedBy")) { 134 detail = { 135 type: "reposts", 136 body, 137 }; 138 } 139 if (detail) { 140 sendEvent(detail); 141 } 142 143 return response; 144 }; 145 globalThis.fetch = overriddenFetch; 146 (globalThis as any).oldFetch = originalFetch; 147 148 const respEventSetup = new Promise<string>((resolve) => { 149 document.addEventListener( 150 "at-fronter-channel-setup", 151 (event) => { 152 event.stopImmediatePropagation(); 153 resolve((event as any).detail); 154 }, 155 { once: true, capture: true }, 156 ); 157 }); 158 respEventSetup.then((name) => (respEventName = name)); 159 160 const applyFronterName = ( 161 el: Element, 162 fronters: FronterView["members"], 163 ) => { 164 if (el.hasAttribute("data-fronter")) return false; 165 const s = fronters.map((f) => f.name).join(", "); 166 el.textContent += ` [f: ${s}]`; 167 el.setAttribute("data-fronter", s); 168 return true; 169 }; 170 const applyFrontersToPage = ( 171 fronters: Map<string, FronterView | null>, 172 pageChange: boolean, 173 ) => { 174 // console.log("applyFrontersToPage", fronters); 175 const match = parseSocialAppPostUrl(document.URL); 176 if (pageChange) { 177 console.log( 178 "page change so clearing all elements with data-fronter attribute", 179 ); 180 for (const el of document.querySelectorAll("[data-fronter]")) { 181 const previousFronter = el.getAttribute("data-fronter")!; 182 if (previousFronter !== "__set__") { 183 // remove fronter text 184 el.textContent = el.textContent.replace( 185 ` [f: ${previousFronter}]`, 186 "", 187 ); 188 } 189 el.removeAttribute("data-fronter"); 190 } 191 } 192 console.log("applyFrontersToPage", match, fronters); 193 if (fronters.size === 0) return; 194 const applyFronterToElement = (el: Element, fronter: FronterView) => { 195 let displayNameElement: Element | null = null; 196 if (fronter.type === "repost") { 197 displayNameElement = 198 el.parentElement?.parentElement?.parentElement?.parentElement 199 ?.parentElement?.firstElementChild?.nextElementSibling 200 ?.firstElementChild?.nextElementSibling?.firstElementChild 201 ?.firstElementChild?.nextElementSibling?.firstElementChild 202 ?.firstElementChild?.firstElementChild?.firstElementChild ?? null; 203 const actorIdentifier = displayNameElement?.parentElement 204 ?.getAttribute("href") 205 ?.split("/")[2]; 206 if ( 207 fronter.did !== actorIdentifier && 208 fronter.handle !== actorIdentifier 209 ) { 210 return; 211 } 212 // sanity check 213 if (displayNameElement?.tagName !== "SPAN") { 214 console.log( 215 `invalid display element tag ${displayNameElement?.tagName}, expected span:`, 216 displayNameElement, 217 ); 218 return; 219 } 220 } else if ( 221 fronter.type === "post" || 222 fronter.type === "thread_reply" || 223 fronter.type === "thread_post" || 224 (fronter.type === "notification" && 225 (fronter.reason === "reply" || fronter.reason === "quote")) 226 ) { 227 if (fronter.type === "thread_post" && fronter.depth === 0) { 228 if (match && match.rkey !== fronter.rkey) return; 229 if (el.ariaLabel !== fronter.displayName) return; 230 displayNameElement = 231 el.firstElementChild?.firstElementChild?.firstElementChild 232 ?.firstElementChild?.firstElementChild ?? null; 233 // sanity check 234 if (displayNameElement?.tagName !== "DIV") { 235 console.log( 236 `invalid display element tag ${displayNameElement?.tagName}, expected a:`, 237 displayNameElement, 238 ); 239 return; 240 } 241 } else { 242 displayNameElement = 243 el.parentElement?.firstElementChild?.firstElementChild 244 ?.firstElementChild?.firstElementChild ?? null; 245 // sanity check 246 if (displayNameElement?.tagName !== "A") { 247 console.log( 248 `invalid display element tag ${displayNameElement?.tagName}, expected a:`, 249 displayNameElement, 250 ); 251 return; 252 } 253 if (fronter.type === "post" && fronter.replyTo) { 254 const parsedReplyUri = expect(parseResourceUri(fronter.replyTo)); 255 const replyFronter = fronters.get( 256 `/profile/${parsedReplyUri.repo}/post/${parsedReplyUri.rkey}`, 257 ); 258 if (replyFronter && replyFronter.members?.length > 0) { 259 const replyDisplayNameElement = 260 el.parentElement?.parentElement?.parentElement 261 ?.firstElementChild?.nextElementSibling?.firstElementChild 262 ?.nextElementSibling?.firstElementChild?.firstElementChild 263 ?.firstElementChild?.firstElementChild ?? null; 264 if (replyDisplayNameElement) { 265 applyFronterName( 266 replyDisplayNameElement, 267 replyFronter.members, 268 ); 269 } 270 } 271 } 272 } 273 } else if (fronter.type === "notification") { 274 const multiOne = 275 el.firstElementChild?.nextElementSibling?.nextElementSibling 276 ?.firstElementChild?.firstElementChild?.nextElementSibling 277 ?.nextElementSibling?.firstElementChild?.firstElementChild 278 ?.firstElementChild ?? null; 279 const singleOne = 280 el.firstElementChild?.nextElementSibling?.nextElementSibling 281 ?.firstElementChild?.nextElementSibling?.nextElementSibling 282 ?.firstElementChild?.firstElementChild?.firstElementChild ?? null; 283 displayNameElement = multiOne ?? singleOne ?? null; 284 if (displayNameElement?.tagName !== "A") { 285 console.log( 286 `invalid display element tag ${displayNameElement?.tagName}, expected a:`, 287 displayNameElement, 288 ); 289 return; 290 } 291 const profileHref = displayNameElement?.getAttribute("href"); 292 if (profileHref) { 293 const actorIdentifier = profileHref.split("/").slice(2)[0]; 294 const isUser = 295 fronter.handle !== actorIdentifier && 296 fronter.did !== actorIdentifier; 297 if (isUser) displayNameElement = null; 298 } else displayNameElement = null; 299 } else if ( 300 fronter.type === "post_repost_entry" || 301 fronter.type === "post_like_entry" 302 ) { 303 // HACK: evil ass way to do this 304 if (el.ariaLabel !== `View ${fronter.displayName}'s profile`) return; 305 displayNameElement = 306 el.firstElementChild?.firstElementChild?.firstElementChild 307 ?.nextElementSibling?.firstElementChild?.firstElementChild ?? 308 null; 309 if (displayNameElement?.tagName !== "DIV") { 310 console.log( 311 `invalid display element tag ${displayNameElement?.tagName}, expected div:`, 312 displayNameElement, 313 ); 314 return; 315 } 316 } 317 if (!displayNameElement) return; 318 return applyFronterName(displayNameElement, fronter.members); 319 }; 320 for (const el of document.getElementsByTagName("a")) { 321 if (el.getAttribute("data-fronter")) continue; 322 const path = `/${el.href.split("/").slice(3).join("/")}`; 323 const elFronters = [fronters.get(path), fronters.get(`${path}#repost`)]; 324 for (const fronter of elFronters) { 325 if (!fronter || fronter.members?.length === 0) continue; 326 if (applyFronterToElement(el, fronter)) { 327 el.setAttribute("data-fronter", "__set__"); 328 } 329 } 330 } 331 }; 332 let postTabObserver: MutationObserver | null = null; 333 window.addEventListener("message", (event) => { 334 if (event.data.type !== "APPLY_CACHED_FRONTERS") return; 335 const applyFronters = () => { 336 console.log("applying cached fronters", event.data.fronters); 337 applyFrontersToPage(new Map(Object.entries(event.data.fronters)), true); 338 }; 339 // check if we are on profile so we can update fronters if the post tab is clicked on 340 const postTabElement = document.querySelector( 341 '[data-testid="profilePager-Posts"]', 342 ); 343 if (postTabElement) { 344 postTabObserver = new MutationObserver(applyFronters); 345 postTabObserver.observe(postTabElement, { attributes: true }); 346 } else if (postTabObserver) { 347 postTabObserver.disconnect(); 348 postTabObserver = null; 349 } 350 // update fronters on page 351 applyFronters(); 352 }); 353 window.addEventListener("message", (event) => { 354 if (event.data.type !== "APPLY_FRONTERS") return; 355 console.log(`received new fronters`, event.data.results); 356 applyFrontersToPage(new Map(Object.entries(event.data.results)), false); 357 }); 358 }, 359});