view who was fronting when a record was made
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});