view who was fronting when a record was made

Compare changes

Choose any two refs to compare.

+5
README.md
··· 2 2 3 3 web extension to show which member(s) of a system was fronting when an ATProto record was created. it also implements showing fronter names on posts and other places, currently only for social-app (bsky.app & forks). 4 4 5 + #### installing 6 + 7 + - for firefox, download [here](https://dev.gaze.systems/x/at-fronter_firefox.xpi) 8 + - for chrome, download [here](https://dev.gaze.systems/x/at-fronter_chrome.crx) 9 + 5 10 #### building 6 11 7 12 install dependencies with `pnpm i` and run `pnpm build` for chrome and `pnpm build:firefox` for firefox.
+1
env.d.ts
··· 1 1 /// <reference types="@atcute/atproto" /> 2 + /// <reference types="@atcute/bluesky" />
+2 -1
package.json
··· 2 2 "name": "at-fronter", 3 3 "description": "view who was fronting when a record was made", 4 4 "private": true, 5 - "version": "0.0.1", 5 + "version": "0.0.8", 6 6 "type": "module", 7 7 "scripts": { 8 8 "dev": "wxt", ··· 25 25 }, 26 26 "dependencies": { 27 27 "@atcute/atproto": "^3.1.1", 28 + "@atcute/bluesky": "^3.2.2", 28 29 "@atcute/client": "^4.0.3", 29 30 "@atcute/identity": "^1.0.3", 30 31 "@atcute/identity-resolver": "^1.1.3",
+11
pnpm-lock.yaml
··· 11 11 '@atcute/atproto': 12 12 specifier: ^3.1.1 13 13 version: 3.1.3 14 + '@atcute/bluesky': 15 + specifier: ^3.2.2 16 + version: 3.2.2 14 17 '@atcute/client': 15 18 specifier: ^4.0.3 16 19 version: 4.0.3 ··· 66 69 67 70 '@atcute/atproto@3.1.3': 68 71 resolution: {integrity: sha512-+5u0l+8E7h6wZO7MM1HLXIPoUEbdwRtr28ZRTgsURp+Md9gkoBj9e5iMx/xM8F2Exfyb65J5RchW/WlF2mw/RQ==} 72 + 73 + '@atcute/bluesky@3.2.2': 74 + resolution: {integrity: sha512-L8RrMNeRLGvSHMq2KDIAGXrpuNGA87YOXpXHY1yhmovVCjQ5n55FrR6JoQaxhprdXdKKQiefxNwQQQybDrfgFQ==} 69 75 70 76 '@atcute/client@4.0.3': 71 77 resolution: {integrity: sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==} ··· 1935 1941 1936 1942 '@atcute/atproto@3.1.3': 1937 1943 dependencies: 1944 + '@atcute/lexicons': 1.1.1 1945 + 1946 + '@atcute/bluesky@3.2.2': 1947 + dependencies: 1948 + '@atcute/atproto': 3.1.3 1938 1949 '@atcute/lexicons': 1.1.1 1939 1950 1940 1951 '@atcute/client@4.0.3':
+421 -72
src/entrypoints/background.ts
··· 1 - import { PersistentCache } from "@/lib/cache"; 2 1 import { expect } from "@/lib/result"; 3 2 import { 4 3 type Fronter, 5 4 fronterGetSocialAppHrefs, 6 - fronterGetSocialAppHref, 7 5 getFronter, 8 6 getSpFronters, 9 - memberUriString, 10 7 putFronter, 11 8 frontersCache, 9 + parseSocialAppPostUrl, 10 + displayNameCache, 11 + deleteFronter, 12 + getPkFronters, 13 + FronterView, 14 + docResolver, 12 15 } from "@/lib/utils"; 13 - import { parseResourceUri, ResourceUri } from "@atcute/lexicons"; 16 + import { 17 + AppBskyFeedLike, 18 + AppBskyFeedPost, 19 + AppBskyFeedRepost, 20 + AppBskyNotificationListNotifications, 21 + } from "@atcute/bluesky"; 22 + import { feedViewPostSchema } from "@atcute/bluesky/types/app/feed/defs"; 23 + import { getAtprotoHandle } from "@atcute/identity"; 24 + import { is, parseResourceUri, ResourceUri } from "@atcute/lexicons"; 25 + import { 26 + AtprotoDid, 27 + Handle, 28 + parseCanonicalResourceUri, 29 + } from "@atcute/lexicons/syntax"; 14 30 15 31 export default defineBackground({ 16 32 persistent: true, ··· 51 67 browser.tabs.onUpdated.addListener(deleteOld); 52 68 }; 53 69 70 + const handleDelete = async ( 71 + data: any, 72 + authToken: string | null, 73 + sender: globalThis.Browser.runtime.MessageSender, 74 + ) => { 75 + if (!authToken) return; 76 + const deleted = await deleteFronter( 77 + data.repo, 78 + data.collection, 79 + data.rkey, 80 + authToken, 81 + ); 82 + if (!deleted.ok) { 83 + console.error("failed to delete fronter:", deleted.error); 84 + } 85 + }; 54 86 const handleWrite = async ( 55 87 items: any[], 56 88 authToken: string | null, ··· 58 90 ) => { 59 91 if (!authToken) return; 60 92 const frontersArray = await storage.getItem<string[]>("sync:fronters"); 61 - let members: Parameters<typeof putFronter>["1"] = frontersArray ?? []; 93 + let members: Parameters<typeof putFronter>["1"] = 94 + frontersArray?.map((n) => ({ name: n, uri: undefined })) ?? []; 62 95 if (members.length === 0) { 63 - const pkFronters = await storage.getItem<string[]>("sync:pk-fronter"); 64 - if (pkFronters) { 65 - members = pkFronters.map((id) => ({ type: "pk", memberId: id })); 66 - } else { 67 - members = await getSpFronters(); 68 - } 96 + members = await getPkFronters(); 97 + } 98 + if (members.length === 0) { 99 + members = await getSpFronters(); 69 100 } 70 101 // dont write if no names is specified or no sp/pk fronters are fetched 71 102 if (members.length === 0) return; 72 - const results = []; 103 + const results: FronterView[] = []; 73 104 for (const result of items) { 74 105 const resp = await putFronter(result.uri, members, authToken); 75 106 if (resp.ok) { 76 107 const parsedUri = await cacheFronter(result.uri, resp.value); 77 108 results.push({ 109 + type: 110 + parsedUri.collection === "app.bsky.feed.repost" 111 + ? "repost" 112 + : parsedUri.collection === "app.bsky.feed.like" 113 + ? "like" 114 + : "post", 78 115 rkey: parsedUri.rkey!, 79 116 ...resp.value, 80 117 }); ··· 82 119 console.error(`fronter write: ${resp.error}`); 83 120 } 84 121 } 122 + if (results.length === 0) return; 85 123 // hijack timeline fronter message because when a write is made it is either on the timeline 86 124 // or its a reply to a depth === 0 post on a threaded view, which is the same as a timeline post 87 125 browser.tabs.sendMessage(sender.tab?.id!, { 88 - type: "TIMELINE_FRONTER", 89 - results: new Map( 126 + type: "APPLY_FRONTERS", 127 + results: Object.fromEntries( 90 128 results.flatMap((fronter) => 91 - fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [ 92 - href, 93 - fronter, 94 - ]), 129 + fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]), 95 130 ), 96 131 ), 97 132 }); 98 133 }; 99 - const handleTimeline = async ( 100 - feed: any[], 134 + const handleNotifications = async ( 135 + items: any, 101 136 sender: globalThis.Browser.runtime.MessageSender, 102 137 ) => { 103 - const handlePost = async (post: any) => { 104 - const cachedFronter = await frontersCache.get(post.uri); 105 - if (cachedFronter === null) return; 106 - const promise = cachedFronter 107 - ? Promise.resolve(cachedFronter) 108 - : getFronter(post.uri).then(async (fronter) => { 109 - if (!fronter.ok) { 110 - await frontersCache.set(post.uri, null); 111 - return; 112 - } 113 - return fronter.value; 114 - }); 115 - return promise.then(async (fronter) => { 116 - if (!fronter) return; 117 - const parsedUri = await cacheFronter(post.uri, fronter); 118 - return { 119 - rkey: parsedUri.rkey!, 120 - ...fronter, 121 - }; 122 - }); 138 + const fetchReply = async ( 139 + uri: ResourceUri, 140 + ): Promise<FronterView | undefined> => { 141 + const cachedFronter = await frontersCache.get(uri); 142 + const fronter = 143 + (cachedFronter ?? null) || 144 + (await getFronter(uri).then((fronter) => { 145 + if (!fronter.ok) { 146 + frontersCache.set(uri, null); 147 + return null; 148 + } 149 + return fronter.value; 150 + })); 151 + if (!fronter) return; 152 + const parsedUri = await cacheFronter(uri, fronter); 153 + return { 154 + type: "post", 155 + rkey: parsedUri.rkey!, 156 + ...fronter, 157 + }; 123 158 }; 124 - const allPromises = feed.flatMap((item) => { 125 - const promises = [handlePost(item.post)]; 126 - if (item.reply?.parent) { 127 - promises.push(handlePost(item.reply.parent)); 159 + const handleNotif = async ( 160 + item: AppBskyNotificationListNotifications.Notification, 161 + ): Promise<FronterView | undefined> => { 162 + let postUrl: ResourceUri | null = null; 163 + const fronterUrl: ResourceUri = item.uri; 164 + if ( 165 + item.reason === "subscribed-post" || 166 + item.reason === "quote" || 167 + item.reason === "reply" 168 + ) 169 + postUrl = item.uri; 170 + if (item.reason === "repost" || item.reason === "repost-via-repost") 171 + postUrl = (item.record as AppBskyFeedRepost.Main).subject.uri; 172 + if (item.reason === "like" || item.reason === "like-via-repost") 173 + postUrl = (item.record as AppBskyFeedLike.Main).subject.uri; 174 + if (!postUrl) return; 175 + const cachedFronter = await frontersCache.get(fronterUrl); 176 + let fronter = 177 + (cachedFronter ?? null) || 178 + (await getFronter(fronterUrl).then((fronter) => { 179 + if (!fronter.ok) { 180 + frontersCache.set(fronterUrl, null); 181 + return null; 182 + } 183 + return fronter.value; 184 + })); 185 + if (!fronter) return; 186 + if (item.reason === "reply") 187 + fronter.replyTo = ( 188 + item.record as AppBskyFeedPost.Main 189 + ).reply?.parent.uri; 190 + const parsedUri = await cacheFronter(fronterUrl, fronter); 191 + const postParsedUri = expect(parseCanonicalResourceUri(postUrl)); 192 + let handle: Handle | undefined = undefined; 193 + try { 194 + handle = 195 + getAtprotoHandle( 196 + await docResolver.resolve(postParsedUri.repo as AtprotoDid), 197 + ) ?? undefined; 198 + } catch (err) { 199 + console.error(`failed to get handle for ${postParsedUri.repo}:`, err); 128 200 } 129 - if (item.reply?.root) { 130 - promises.push(handlePost(item.reply.root)); 201 + return { 202 + type: "notification", 203 + reason: item.reason, 204 + rkey: parsedUri.rkey!, 205 + subject: { 206 + did: postParsedUri.repo as AtprotoDid, 207 + rkey: postParsedUri.rkey, 208 + handle, 209 + }, 210 + ...fronter, 211 + }; 212 + }; 213 + const allPromises = []; 214 + for (const item of items.notifications ?? []) { 215 + if (!is(AppBskyNotificationListNotifications.notificationSchema, item)) 216 + continue; 217 + console.log("Handling notification:", item); 218 + allPromises.push(handleNotif(item)); 219 + if (item.reason === "reply" && item.record) { 220 + const parentUri = (item.record as AppBskyFeedPost.Main).reply?.parent 221 + .uri; 222 + if (parentUri) allPromises.push(fetchReply(parentUri)); 131 223 } 132 - return promises; 224 + } 225 + const results = new Map( 226 + (await Promise.allSettled(allPromises)) 227 + .filter((result) => result.status === "fulfilled") 228 + .flatMap((result) => result.value ?? []) 229 + .flatMap((fronter) => 230 + fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]), 231 + ), 232 + ); 233 + if (results.size === 0) return; 234 + browser.tabs.sendMessage(sender.tab?.id!, { 235 + type: "APPLY_FRONTERS", 236 + results: Object.fromEntries(results), 133 237 }); 238 + }; 239 + const handleTimeline = async ( 240 + feed: any[], 241 + sender: globalThis.Browser.runtime.MessageSender, 242 + ) => { 243 + const allPromises = feed.flatMap( 244 + (item): Promise<FronterView | undefined>[] => { 245 + if (!is(feedViewPostSchema, item)) return []; 246 + const handleUri = async ( 247 + uri: ResourceUri, 248 + type: "repost" | "post", 249 + ) => { 250 + const cachedFronter = await frontersCache.get(uri); 251 + if (cachedFronter === null) return; 252 + const promise = cachedFronter 253 + ? Promise.resolve(cachedFronter) 254 + : getFronter(uri).then(async (fronter) => { 255 + if (!fronter.ok) { 256 + await frontersCache.set(uri, null); 257 + return; 258 + } 259 + return fronter.value; 260 + }); 261 + return await promise.then( 262 + async (fronter): Promise<FronterView | undefined> => { 263 + if (!fronter) return; 264 + if (type === "repost") { 265 + const parsedPostUri = expect( 266 + parseCanonicalResourceUri(item.post.uri), 267 + ); 268 + fronter = { 269 + subject: { 270 + did: parsedPostUri.repo as AtprotoDid, 271 + rkey: parsedPostUri.rkey, 272 + handle: 273 + item.post.author.handle === "handle.invalid" 274 + ? undefined 275 + : item.post.author.handle, 276 + }, 277 + ...fronter, 278 + }; 279 + } else if ( 280 + uri === item.post.uri && 281 + item.reply?.parent.$type === "app.bsky.feed.defs#postView" 282 + ) { 283 + fronter = { 284 + replyTo: item.reply?.parent.uri, 285 + ...fronter, 286 + }; 287 + } else if ( 288 + uri === item.reply?.parent.uri && 289 + item.reply?.parent.$type === "app.bsky.feed.defs#postView" 290 + ) { 291 + fronter = { 292 + replyTo: (item.reply.parent.record as AppBskyFeedPost.Main) 293 + .reply?.parent.uri, 294 + ...fronter, 295 + }; 296 + } 297 + const parsedUri = await cacheFronter(uri, fronter); 298 + return { 299 + type, 300 + rkey: parsedUri.rkey!, 301 + ...fronter, 302 + }; 303 + }, 304 + ); 305 + }; 306 + const promises: ReturnType<typeof handleUri>[] = []; 307 + promises.push(handleUri(item.post.uri, "post")); 308 + if (item.reply?.parent) { 309 + promises.push(handleUri(item.reply.parent.uri, "post")); 310 + if (item.reply?.parent.$type === "app.bsky.feed.defs#postView") { 311 + const grandparentUri = ( 312 + item.reply.parent.record as AppBskyFeedPost.Main 313 + ).reply?.parent.uri; 314 + if (grandparentUri) 315 + promises.push(handleUri(grandparentUri, "post")); 316 + } 317 + } 318 + if (item.reply?.root) { 319 + promises.push(handleUri(item.reply.root.uri, "post")); 320 + } 321 + if ( 322 + item.reason && 323 + item.reason.$type === "app.bsky.feed.defs#reasonRepost" && 324 + item.reason.uri 325 + ) { 326 + promises.push(handleUri(item.reason.uri, "repost")); 327 + } 328 + return promises; 329 + }, 330 + ); 134 331 const results = new Map( 135 332 (await Promise.allSettled(allPromises)) 136 333 .filter((result) => result.status === "fulfilled") 137 334 .flatMap((result) => result.value ?? []) 138 335 .flatMap((fronter) => 139 - fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [ 140 - href, 141 - fronter, 142 - ]), 336 + fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]), 143 337 ), 144 338 ); 339 + if (results.size === 0) return; 145 340 browser.tabs.sendMessage(sender.tab?.id!, { 146 - type: "TIMELINE_FRONTER", 147 - results, 341 + type: "APPLY_FRONTERS", 342 + results: Object.fromEntries(results), 148 343 }); 149 344 // console.log("sent timeline fronters", results); 150 345 }; 151 346 const handleThread = async ( 152 - { data: { body } }: any, 347 + { 348 + data: { body, requestUrl, documentUrl }, 349 + }: { data: { body: string; requestUrl: string; documentUrl: string } }, 153 350 sender: globalThis.Browser.runtime.MessageSender, 154 351 ) => { 352 + // check if this request was made for fetching replies 353 + // if anchor is not the same as current document url, that is the case 354 + // which means the depth of the returned posts are invalid to us 355 + let isReplyThreadFetch = false; 356 + const parsedDocumentUri = parseSocialAppPostUrl(documentUrl); 357 + const anchorUri = new URL(requestUrl).searchParams.get("anchor"); 358 + // console.log( 359 + // "parsedDocumentUri", 360 + // parsedDocumentUri, 361 + // "anchorUri", 362 + // anchorUri, 363 + // ); 364 + if (parsedDocumentUri && anchorUri) { 365 + const parsedAnchorUri = expect(parseResourceUri(anchorUri)); 366 + isReplyThreadFetch = parsedDocumentUri.rkey !== parsedAnchorUri.rkey; 367 + } 368 + // console.log("isReplyThreadFetch", isReplyThreadFetch); 155 369 const data: any = JSON.parse(body); 156 370 const promises = (data.thread as any[]).flatMap((item) => { 157 371 return frontersCache.get(item.uri).then(async (cachedFronter) => { ··· 165 379 } 166 380 return fronter.value; 167 381 }); 168 - return promise.then(async (fronter) => { 169 - if (!fronter) return; 170 - const parsedUri = await cacheFronter(item.uri, fronter); 171 - if (item.depth === 0) await setTabFronter(item.uri, fronter); 172 - return { 173 - rkey: parsedUri.rkey!, 174 - displayName: item.value.post.author.displayName, 175 - depth: item.depth, 176 - ...fronter, 177 - }; 178 - }); 382 + return promise.then( 383 + async (fronter): Promise<FronterView | undefined> => { 384 + if (!fronter) return; 385 + const parsedUri = await cacheFronter(item.uri, fronter); 386 + if (isReplyThreadFetch) 387 + return { 388 + type: "thread_reply", 389 + rkey: parsedUri.rkey!, 390 + ...fronter, 391 + }; 392 + if (item.depth === 0) await setTabFronter(item.uri, fronter); 393 + const displayName = item.value.post.author.displayName; 394 + // cache display name for later use 395 + if (fronter.handle) 396 + await displayNameCache.set(fronter.handle, displayName); 397 + await displayNameCache.set(fronter.did, displayName); 398 + return { 399 + type: "thread_post", 400 + rkey: parsedUri.rkey!, 401 + displayName, 402 + depth: item.depth, 403 + ...fronter, 404 + }; 405 + }, 406 + ); 179 407 }); 180 408 }); 181 409 const results = new Map( ··· 183 411 .filter((result) => result.status === "fulfilled") 184 412 .flatMap((result) => result.value ?? []) 185 413 .flatMap((fronter) => 186 - fronterGetSocialAppHrefs(fronter, fronter.rkey, fronter.depth).map( 187 - (href) => [href, fronter], 188 - ), 414 + fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]), 189 415 ), 190 416 ); 417 + if (results.size === 0) return; 191 418 browser.tabs.sendMessage(sender.tab?.id!, { 192 - type: "THREAD_FRONTER", 193 - results, 419 + type: "APPLY_FRONTERS", 420 + results: Object.fromEntries(results), 194 421 }); 195 422 // console.log("sent thread fronters", results); 196 423 }; 424 + const handleInteractions = async ( 425 + data: any, 426 + sender: globalThis.Browser.runtime.MessageSender, 427 + collection: string, 428 + actors: { did: AtprotoDid; displayName: string }[], 429 + ) => { 430 + const postUri = data.uri as ResourceUri; 431 + const fetchInteractions = async (cursor?: string) => { 432 + const resp = await fetch( 433 + `https://constellation.microcosm.blue/links?target=${postUri}&collection=${collection}&path=.subject.uri&limit=100${cursor ? `&cursor=${cursor}` : ""}`, 434 + ); 435 + if (!resp.ok) return; 436 + const data = await resp.json(); 437 + return { 438 + total: data.total as number, 439 + records: data.linking_records.map( 440 + (record: any) => 441 + `at://${record.did}/${record.collection}/${record.rkey}` as ResourceUri, 442 + ) as ResourceUri[], 443 + cursor: data.cursor as string, 444 + }; 445 + }; 446 + let interactions = await fetchInteractions(); 447 + if (!interactions) return; 448 + let allRecords: (typeof interactions)["records"] = []; 449 + while (allRecords.length < interactions.total) { 450 + allRecords.push(...interactions.records); 451 + if (!interactions.cursor) break; 452 + interactions = await fetchInteractions(interactions.cursor); 453 + if (!interactions) break; 454 + } 455 + 456 + const actorMap = new Map( 457 + actors.map((actor) => [actor.did, actor.displayName]), 458 + ); 459 + const allPromises = allRecords.map( 460 + async (recordUri): Promise<FronterView | undefined> => { 461 + const cachedFronter = await frontersCache.get(recordUri); 462 + let fronter = 463 + (cachedFronter ?? null) || 464 + (await getFronter(recordUri).then((fronter) => { 465 + if (!fronter.ok) { 466 + frontersCache.set(recordUri, null); 467 + return null; 468 + } 469 + return fronter.value; 470 + })); 471 + if (!fronter) return; 472 + const parsedUri = await cacheFronter(recordUri, fronter); 473 + const displayName = 474 + actorMap.get(fronter.did) ?? 475 + (await displayNameCache.get(fronter.did)); 476 + if (!displayName) return; 477 + return { 478 + type: 479 + collection === "app.bsky.feed.repost" 480 + ? "post_repost_entry" 481 + : "post_like_entry", 482 + rkey: parsedUri.rkey!, 483 + displayName, 484 + ...fronter, 485 + }; 486 + }, 487 + ); 488 + 489 + const results = new Map( 490 + (await Promise.allSettled(allPromises)) 491 + .filter((result) => result.status === "fulfilled") 492 + .flatMap((result) => result.value ?? []) 493 + .flatMap((fronter) => 494 + fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]), 495 + ), 496 + ); 497 + if (results.size === 0) return; 498 + browser.tabs.sendMessage(sender.tab?.id!, { 499 + type: "APPLY_FRONTERS", 500 + results: Object.fromEntries(results), 501 + }); 502 + }; 503 + const handleReposts = async ( 504 + data: any, 505 + sender: globalThis.Browser.runtime.MessageSender, 506 + ) => 507 + handleInteractions( 508 + data, 509 + sender, 510 + "app.bsky.feed.repost", 511 + data.repostedBy.map((by: any) => ({ 512 + did: by.did, 513 + displayName: by.displayName, 514 + })), 515 + ); 516 + const handleLikes = async ( 517 + data: any, 518 + sender: globalThis.Browser.runtime.MessageSender, 519 + ) => 520 + handleInteractions( 521 + data, 522 + sender, 523 + "app.bsky.feed.like", 524 + data.likes.map((by: any) => ({ 525 + did: by.actor.did, 526 + displayName: by.actor.displayName, 527 + })), 528 + ); 197 529 198 530 browser.runtime.onMessage.addListener(async (message, sender) => { 199 531 if (message.type !== "RESPONSE_CAPTURED") return; 200 - // console.log("handling response event", message); 532 + console.log("handling response", message.data); 201 533 switch (message.data.type as string) { 534 + case "delete": 535 + await handleDelete( 536 + JSON.parse(message.data.body), 537 + message.data.authToken, 538 + sender, 539 + ); 540 + break; 202 541 case "write": 203 542 await handleWrite( 204 543 JSON.parse(message.data.body).results, ··· 206 545 sender, 207 546 ); 208 547 break; 209 - case "writeOne": 548 + case "writeOne": { 210 549 await handleWrite( 211 550 [JSON.parse(message.data.body)], 212 551 message.data.authToken, 213 552 sender, 214 553 ); 215 554 break; 555 + } 216 556 case "posts": 217 557 await handleTimeline( 218 558 (JSON.parse(message.data.body) as any[]).map((post) => ({ post })), ··· 224 564 break; 225 565 case "thread": 226 566 await handleThread(message, sender); 567 + break; 568 + case "notifications": 569 + await handleNotifications(JSON.parse(message.data.body), sender); 570 + break; 571 + case "reposts": 572 + await handleReposts(JSON.parse(message.data.body), sender); 573 + break; 574 + case "likes": 575 + await handleLikes(JSON.parse(message.data.body), sender); 227 576 break; 228 577 } 229 578 });
+218 -47
src/entrypoints/content.ts
··· 1 - import { decodeStorageKey } from "@/lib/cache"; 2 1 import { expect } from "@/lib/result"; 3 - import { 4 - Fronter, 5 - fronterGetSocialAppHref, 6 - fronterGetSocialAppHrefs, 7 - parseSocialAppPostUrl, 8 - } from "@/lib/utils"; 9 - import { parseResourceUri, ResourceUri } from "@atcute/lexicons"; 2 + import { FronterView, parseSocialAppPostUrl } from "@/lib/utils"; 3 + import { parseResourceUri } from "@atcute/lexicons"; 10 4 11 5 const getAuthHeader = (headers: any): string | null => { 12 6 if (headers instanceof Headers) { ··· 28 22 const overriddenFetch = async ( 29 23 ...args: [input: RequestInfo | URL, init?: RequestInit] 30 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(); 31 42 const response = await originalFetch.apply(this, args); 32 43 33 44 if (respEventName === null) return response; ··· 55 66 } 56 67 return authHeader?.split(" ")[1] || null; 57 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 + }; 58 78 59 79 let detail: any = undefined; 60 80 if (response.url.includes("/xrpc/com.atproto.repo.applyWrites")) { 61 81 detail = { 62 82 type: "write", 63 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, 64 90 authToken: getAuthToken(), 65 91 }; 66 92 } else if (response.url.includes("/xrpc/com.atproto.repo.createRecord")) { ··· 84 110 detail = { 85 111 type: "thread", 86 112 body, 113 + requestUrl: getRequestUrl(), 114 + documentUrl: document.location.href, 87 115 }; 88 116 } else if (response.url.includes("/xrpc/app.bsky.feed.getPosts")) { 89 117 detail = { 90 118 type: "posts", 91 119 body, 92 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 + }; 93 138 } 94 139 if (detail) { 95 140 sendEvent(detail); ··· 112 157 }); 113 158 respEventSetup.then((name) => (respEventName = name)); 114 159 115 - const applyFronterName = (el: Element, fronters: Fronter["members"]) => { 116 - if (el.getAttribute("data-fronter")) return; 160 + const applyFronterName = ( 161 + el: Element, 162 + fronters: FronterView["members"], 163 + ) => { 164 + if (el.hasAttribute("data-fronter")) return false; 117 165 const s = fronters.map((f) => f.name).join(", "); 118 166 el.textContent += ` [f: ${s}]`; 119 167 el.setAttribute("data-fronter", s); 168 + return true; 120 169 }; 121 - const applyFrontersToPage = (fronters: Map<string, any>) => { 170 + const applyFrontersToPage = ( 171 + fronters: Map<string, FronterView | null>, 172 + pageChange: boolean, 173 + ) => { 122 174 // console.log("applyFrontersToPage", fronters); 123 175 const match = parseSocialAppPostUrl(document.URL); 124 - // console.log(match, fronters); 125 - for (const el of document.querySelectorAll("[data-fronter]")) { 126 - const previousFronter = el.getAttribute("data-fronter")!; 127 - // remove fronter text 128 - el.textContent = el.textContent.replace(` [f: ${previousFronter}]`, ""); 129 - el.removeAttribute("data-fronter"); 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 + } 130 191 } 192 + console.log("applyFrontersToPage", match, fronters); 131 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 + }; 132 320 for (const el of document.getElementsByTagName("a")) { 321 + if (el.getAttribute("data-fronter")) continue; 133 322 const path = `/${el.href.split("/").slice(3).join("/")}`; 134 - const fronter = fronters.get(path); 135 - if (!fronter || fronter.members.length === 0) continue; 136 - const isFocusedPost = fronter.depth === 0; 137 - if (isFocusedPost && match && match.rkey !== fronter.rkey) continue; 138 - if (isFocusedPost && el.ariaLabel !== fronter.displayName) continue; 139 - const displayNameElement = isFocusedPost 140 - ? (el.firstElementChild?.firstElementChild?.firstElementChild 141 - ?.firstElementChild?.firstElementChild ?? null) 142 - : (el.parentElement?.firstElementChild?.firstElementChild 143 - ?.firstElementChild?.firstElementChild ?? null); 144 - if (!displayNameElement) continue; 145 - // console.log(path, fronter, displayNameElement); 146 - applyFronterName(displayNameElement, fronter.members); 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 + } 147 330 } 148 331 }; 149 332 let postTabObserver: MutationObserver | null = null; 150 333 window.addEventListener("message", (event) => { 151 334 if (event.data.type !== "APPLY_CACHED_FRONTERS") return; 152 335 const applyFronters = () => { 153 - const fronters = event.data.fronters as Map<string, Fronter | null>; 154 - const updated = new Map( 155 - fronters.entries().flatMap(([storageKey, fronter]) => { 156 - if (!fronter) return []; 157 - const uri = decodeStorageKey(storageKey); 158 - const rkey = expect(parseResourceUri(uri)).rkey!; 159 - return fronterGetSocialAppHrefs(fronter, rkey).map((href) => [ 160 - href, 161 - fronter, 162 - ]); 163 - }), 164 - ); 165 - // console.log("applying cached fronters"); 166 - applyFrontersToPage(updated); 336 + console.log("applying cached fronters", event.data.fronters); 337 + applyFrontersToPage(new Map(Object.entries(event.data.fronters)), true); 167 338 }; 168 339 // check if we are on profile so we can update fronters if the post tab is clicked on 169 340 const postTabElement = document.querySelector( ··· 180 351 applyFronters(); 181 352 }); 182 353 window.addEventListener("message", (event) => { 183 - if (!["TIMELINE_FRONTER", "THREAD_FRONTER"].includes(event.data.type)) 184 - return; 185 - applyFrontersToPage(event.data.results as Map<string, any>); 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); 186 357 }); 187 358 }, 188 359 });
+64 -7
src/entrypoints/isolated.content.ts
··· 1 - import { Fronter, frontersCache, parseSocialAppPostUrl } from "@/lib/utils"; 2 - import { ResourceUri } from "@atcute/lexicons"; 1 + import { decodeStorageKey } from "@/lib/cache"; 2 + import { expect } from "@/lib/result"; 3 + import { 4 + displayNameCache, 5 + Fronter, 6 + fronterGetSocialAppHref, 7 + fronterGetSocialAppHrefs, 8 + frontersCache, 9 + FronterView, 10 + parseSocialAppPostUrl, 11 + } from "@/lib/utils"; 12 + import { parseResourceUri, ResourceUri } from "@atcute/lexicons"; 3 13 4 14 export default defineContentScript({ 5 15 matches: ["<all_urls>"], ··· 31 41 data, 32 42 }); 33 43 }); 34 - const messageTypes = ["TAB_FRONTER", "THREAD_FRONTER", "TIMELINE_FRONTER"]; 44 + const bgMessageTypes = ["APPLY_FRONTERS"]; 35 45 browser.runtime.onMessage.addListener((message) => { 36 - if (!messageTypes.includes(message.type)) return; 46 + if (!bgMessageTypes.includes(message.type)) return; 37 47 window.postMessage(message); 38 48 }); 39 - window.addEventListener("popstate", async (event) => { 49 + const updateOnUrlChange = async () => { 50 + const fronters = await frontersCache.getAll(); 51 + const updated = new Map<string, FronterView | null>(); 52 + for (const [storageKey, fronter] of fronters.entries()) { 53 + const uri = decodeStorageKey(storageKey); 54 + const parsedUri = expect(parseResourceUri(uri)); 55 + if (!fronter) { 56 + updated.set( 57 + fronterGetSocialAppHref(parsedUri.repo, parsedUri.rkey!), 58 + null, 59 + ); 60 + continue; 61 + } 62 + const view: FronterView = { 63 + type: 64 + parsedUri.collection === "app.bsky.feed.repost" ? "repost" : "post", 65 + rkey: parsedUri.rkey!, 66 + ...fronter, 67 + }; 68 + for (const href of fronterGetSocialAppHrefs(view)) { 69 + updated.set(href, view); 70 + } 71 + } 72 + // add entry for current page 73 + const match = parseSocialAppPostUrl(document.location.href); 74 + if (match && !updated.has(`/profile/${match.actorIdentifier}`)) { 75 + const maybeFronter = updated.get( 76 + `/profile/${match.actorIdentifier}/post/${match.rkey}`, 77 + ); 78 + if (maybeFronter) { 79 + const displayName = await displayNameCache.get(match.actorIdentifier); 80 + if (displayName) { 81 + const view: FronterView = { 82 + ...maybeFronter, 83 + type: "thread_post", 84 + depth: 0, 85 + displayName, 86 + rkey: match.rkey, 87 + }; 88 + updated.set(`/profile/${maybeFronter.did}`, view); 89 + if (maybeFronter.handle) { 90 + updated.set(`/profile/${maybeFronter.handle}`, view); 91 + } 92 + } 93 + } 94 + } 40 95 window.postMessage({ 41 96 type: "APPLY_CACHED_FRONTERS", 42 - fronters: await frontersCache.getAll(), 97 + fronters: Object.fromEntries(updated), 43 98 }); 44 99 // check for tab fronter for the current "post" 45 100 await checkFronter(document.location.href); 46 - }); 101 + }; 102 + window.addEventListener("popstate", updateOnUrlChange); 103 + ctx.addEventListener(window, "wxt:locationchange", updateOnUrlChange); 47 104 48 105 // setup response "channel" 49 106 document.dispatchEvent(
+33 -19
src/entrypoints/popup/App.svelte
··· 13 13 let queryError = $state(""); 14 14 let isQuerying = $state(false); 15 15 let fronters = $state<string[]>([]); 16 - let pkFronters = $state<string[]>([]); 16 + let pkSystemId = $state<string>(""); 17 17 let spToken = $state(""); 18 18 let isFromCurrentTab = $state(false); 19 19 ··· 51 51 storage.setItem("sync:fronters", newFronters); 52 52 }; 53 53 54 - const updatePkFronters = (newPkFronters: string[]) => { 55 - pkFronters = newPkFronters; 56 - storage.setItem("sync:pk-fronter", newPkFronters); 54 + const updatePkSystem = (event: any) => { 55 + pkSystemId = (event.target as HTMLInputElement).value; 56 + storage.setItem("sync:pk-system", pkSystemId); 57 57 }; 58 58 59 59 const updateSpToken = (event: any) => { ··· 80 80 fronters = frontersArray; 81 81 } 82 82 83 - const pkFrontersArray = 84 - await storage.getItem<string[]>("sync:pk-fronter"); 85 - if (pkFrontersArray && Array.isArray(pkFrontersArray)) { 86 - pkFronters = pkFrontersArray; 83 + const pkSystem = await storage.getItem<string>("sync:pk-system"); 84 + if (pkSystem) { 85 + pkSystemId = pkSystem; 87 86 } 88 87 89 88 const token = await storage.getItem<string>("sync:sp_token"); ··· 187 186 >{fronter.name}</a 188 187 > 189 188 {:else} 190 - {fronter.name} 191 - {/if} 192 - {#if i < queryResult.fronters.length - 1}, 189 + {fronter.name + 190 + (i < 191 + queryResult.fronters 192 + .length - 193 + 1 194 + ? ", " 195 + : "")} 193 196 {/if} 194 197 {/each} 195 198 </div> ··· 228 231 </span> 229 232 </div> 230 233 </div> 231 - <FronterList 232 - bind:fronters={pkFronters} 233 - onUpdate={updatePkFronters} 234 - label="PK FRONTERS" 235 - placeholder="enter_member_ids" 236 - note="PluralKit member IDs, overrides SP fronters" 237 - fetchNames={true} 238 - /> 234 + <div class="config-card"> 235 + <div class="config-row"> 236 + <span class="config-label">PK SYSTEM</span> 237 + <input 238 + type="password" 239 + placeholder="enter_pk_system_id" 240 + oninput={updatePkSystem} 241 + bind:value={pkSystemId} 242 + class="config-input" 243 + class:has-value={pkSystemId} 244 + /> 245 + </div> 246 + <div class="config-note"> 247 + <span class="note-text"> 248 + when set, pulls fronters from PluralKit (fronters 249 + must be public) 250 + </span> 251 + </div> 252 + </div> 239 253 <FronterList 240 254 bind:fronters 241 255 onUpdate={updateFronters}
+140 -23
src/lib/utils.ts
··· 10 10 GenericUri, 11 11 Handle, 12 12 isHandle, 13 + Nsid, 13 14 RecordKey, 14 15 type AtprotoDid, 15 16 type ResourceUri, ··· 26 27 } from "@atcute/identity-resolver"; 27 28 import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity"; 28 29 import { PersistentCache } from "./cache"; 30 + import { AppBskyNotificationListNotifications } from "@atcute/bluesky"; 31 + 32 + export type Subject = { 33 + handle?: Handle; 34 + did: AtprotoDid; 35 + rkey: RecordKey; 36 + }; 29 37 30 38 export type Fronter = { 31 39 members: { ··· 34 42 }[]; 35 43 handle: Handle | null; 36 44 did: AtprotoDid; 45 + subject?: Subject; 46 + replyTo?: ResourceUri; 37 47 }; 38 48 49 + export type FronterView = Fronter & { rkey: RecordKey } & ( 50 + | { 51 + type: "thread_reply"; 52 + } 53 + | { 54 + type: "thread_post"; 55 + displayName: string; 56 + depth: number; 57 + } 58 + | { 59 + type: "post"; 60 + } 61 + | { 62 + type: "like"; 63 + } 64 + | { 65 + type: "repost"; 66 + } 67 + | { 68 + type: "notification"; 69 + reason: InferOutput<AppBskyNotificationListNotifications.notificationSchema>["reason"]; 70 + } 71 + | { 72 + type: "post_repost_entry"; 73 + displayName: string; 74 + } 75 + | { 76 + type: "post_like_entry"; 77 + displayName: string; 78 + } 79 + ); 80 + export type FronterType = FronterView["type"]; 81 + 39 82 export const fronterSchema = v.record( 40 83 v.string(), 41 84 v.object({ ··· 53 96 54 97 export type MemberUri = 55 98 | { type: "at"; recordUri: ResourceUri } 56 - | { type: "pk"; memberId: string } 99 + | { type: "pk"; systemId: string; memberId: string } 57 100 | { type: "sp"; systemId: string; memberId: string }; 58 101 59 102 export const parseMemberId = (memberId: GenericUri): MemberUri => { ··· 61 104 switch (uri.protocol) { 62 105 case "pk:": { 63 106 const split = uri.pathname.split("/").slice(1); 64 - return { type: "pk", memberId: split[0] }; 107 + return { type: "pk", systemId: split[0], memberId: split[1] }; 65 108 } 66 109 case "sp:": { 67 110 const split = uri.pathname.split("/").slice(1); ··· 141 184 } 142 185 }; 143 186 144 - export const getFronterNames = async (members: (string | MemberUri)[]) => { 187 + export const getFronterNames = async ( 188 + members: { name?: string; uri?: MemberUri }[], 189 + ) => { 145 190 const promises = await Promise.allSettled( 146 191 members.map(async (m): Promise<Fronter["members"][0] | null> => { 147 - if (typeof m === "string") 148 - return Promise.resolve({ uri: undefined, name: m }); 149 - const name = await fetchMember(m); 150 - return name ? { uri: m, name } : null; 192 + if (!m.uri) return Promise.resolve({ uri: undefined, name: m.name! }); 193 + if (m.name) return Promise.resolve({ uri: m.uri, name: m.name }); 194 + const name = await fetchMember(m.uri); 195 + return name ? { uri: m.uri, name } : null; 151 196 }), 152 197 ); 153 198 return promises ··· 155 200 .flatMap((p) => p.value ?? []); 156 201 }; 157 202 158 - const handleResolver = new CompositeHandleResolver({ 203 + export const handleResolver = new CompositeHandleResolver({ 159 204 strategy: "race", 160 205 methods: { 161 206 dns: new DohJsonHandleResolver({ ··· 164 209 http: new WellKnownHandleResolver(), 165 210 }, 166 211 }); 167 - const docResolver = new CompositeDidDocumentResolver({ 212 + export const docResolver = new CompositeDidDocumentResolver({ 168 213 methods: { 169 214 plc: new PlcDidDocumentResolver(), 170 215 web: new WebDidDocumentResolver(), ··· 243 288 244 289 export const putFronter = async ( 245 290 subject: FronterSchema["subject"], 246 - members: (string | MemberUri)[], 291 + members: { name?: string; uri?: MemberUri }[], 247 292 authToken: string, 248 293 ): Promise<Result<Fronter, string>> => { 249 294 const parsedRecordUri = parseResourceUri(subject); ··· 290 335 }); 291 336 }; 292 337 293 - export const getSpFronters = async (): Promise<MemberUri[]> => { 338 + export const deleteFronter = async ( 339 + did: AtprotoDid, 340 + collection: Nsid, 341 + rkey: RecordKey, 342 + authToken: string, 343 + ): Promise<Result<boolean, string>> => { 344 + // make client 345 + const atpClient = await getAtpClient(did); 346 + 347 + // delete 348 + let maybeRecord = await atpClient.post("com.atproto.repo.deleteRecord", { 349 + input: { 350 + repo: did, 351 + collection: fronterSchema.object.shape.$type.expected, 352 + rkey: `${collection}_${rkey}`, 353 + }, 354 + headers: { authorization: `Bearer ${authToken}` }, 355 + }); 356 + if (!maybeRecord.ok) 357 + return err(maybeRecord.data.message ?? maybeRecord.data.error); 358 + 359 + return ok(true); 360 + }; 361 + 362 + export const getSpFronters = async (): Promise< 363 + Parameters<typeof putFronter>["1"] 364 + > => { 294 365 const spToken = await storage.getItem<string>("sync:sp_token"); 295 366 if (!spToken) return []; 296 367 const resp = await fetch(`https://api.apparyllis.com/v1/fronters`, { ··· 301 372 if (!resp.ok) return []; 302 373 const spFronters = (await resp.json()) as any[]; 303 374 return spFronters.map((fronter) => ({ 304 - type: "sp", 305 - memberId: fronter.content.member, 306 - systemId: fronter.content.uid, 375 + name: undefined, 376 + uri: { 377 + type: "sp", 378 + memberId: fronter.content.member, 379 + systemId: fronter.content.uid, 380 + }, 381 + })); 382 + }; 383 + 384 + export const getPkFronters = async (): Promise< 385 + Parameters<typeof putFronter>["1"] 386 + > => { 387 + const pkSystemId = await storage.getItem<string>("sync:pk-system"); 388 + if (!pkSystemId) return []; 389 + const resp = await fetch( 390 + `https://api.pluralkit.me/v2/systems/${pkSystemId}/fronters`, 391 + ); 392 + if (!resp.ok) return []; 393 + const pkFronters = await resp.json(); 394 + return (pkFronters.members as any[]).map((member) => ({ 395 + name: member.display_name ?? member.name, 396 + uri: { 397 + type: "pk", 398 + memberId: member.id, 399 + systemId: member.system, 400 + }, 307 401 })); 308 402 }; 309 403 310 - export const fronterGetSocialAppHrefs = ( 311 - fronter: Fronter, 312 - rkey: RecordKey, 313 - depth?: number, 314 - ) => { 404 + export const fronterGetSocialAppHrefs = (view: FronterView) => { 405 + if (view.type === "repost" && view.subject) { 406 + const subject = view.subject; 407 + const handle = subject?.handle; 408 + return [ 409 + handle ? [`${fronterGetSocialAppHref(handle, subject.rkey)}#repost`] : [], 410 + `${fronterGetSocialAppHref(subject.did, subject.rkey)}#repost`, 411 + ].flat(); 412 + } else if (view.type === "notification" && view.subject) { 413 + const subject = view.subject; 414 + const handle = subject?.handle; 415 + return [ 416 + handle ? [`${fronterGetSocialAppHref(handle, subject.rkey)}`] : [], 417 + `${fronterGetSocialAppHref(subject.did, subject.rkey)}`, 418 + ].flat(); 419 + } else if ( 420 + view.type === "post_repost_entry" || 421 + view.type === "post_like_entry" 422 + ) { 423 + return [ 424 + view.handle ? [`/profile/${view.handle}`] : [], 425 + `/profile/${view.did}`, 426 + ].flat(); 427 + } 428 + const depth = view.type === "thread_post" ? view.depth : undefined; 315 429 return [ 316 - fronter.handle 317 - ? [fronterGetSocialAppHref(fronter.handle, rkey, depth)] 318 - : [], 319 - fronterGetSocialAppHref(fronter.did, rkey, depth), 430 + view.handle ? [fronterGetSocialAppHref(view.handle, view.rkey, depth)] : [], 431 + fronterGetSocialAppHref(view.did, view.rkey, depth), 320 432 ].flat(); 321 433 }; 322 434 ··· 334 446 const [website, actorIdentifier, rkey] = match; 335 447 return { actorIdentifier, rkey }; 336 448 }; 449 + 450 + export const displayNameCache = new PersistentCache<string>( 451 + "displayNameCache", 452 + 1, 453 + );
+1 -1
wxt.config.ts
··· 19 19 ], 20 20 host_permissions: ["<all_urls>"], 21 21 content_security_policy: { 22 - extension_pages: "script-src 'self' 'unsafe-eval'; object-src 'self'", 22 + extension_pages: "script-src 'self'; object-src 'self'", 23 23 }, 24 24 }, 25 25 });