+5
README.md
+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
env.d.ts
+2
-1
package.json
+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
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
+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
+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
+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
+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
+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
+
);