+26
-11
src/entrypoints/background.ts
+26
-11
src/entrypoints/background.ts
···
1
1
import { expect } from "@/lib/result";
2
2
import {
3
-
Fronter,
3
+
type Fronter,
4
4
fronterGetSocialAppHref,
5
5
getFronter,
6
+
getSpFronters,
7
+
memberUriString,
6
8
putFronter,
7
9
} from "@/lib/utils";
8
10
import { parseResourceUri, ResourceUri } from "@atcute/lexicons";
···
15
17
let fronters = new Map<ResourceUri, Fronter | null>();
16
18
const cacheFronter = (uri: ResourceUri, fronter: Fronter) => {
17
19
const parsedUri = expect(parseResourceUri(uri));
18
-
fronters.set(uri, fronter);
19
20
fronters.set(
20
21
`at://${fronter.did}/${parsedUri.collection!}/${parsedUri.rkey!}`,
21
22
fronter,
···
49
50
};
50
51
51
52
const handleWrite = async (
52
-
{ data: { body, authToken } }: any,
53
+
items: any[],
54
+
authToken: string | null,
53
55
sender: globalThis.Browser.runtime.MessageSender,
54
56
) => {
55
57
const fronter = await storage.getItem<string>("sync:fronter");
56
-
if (!fronter) return;
58
+
const spFronters = (await getSpFronters()).map((m) => memberUriString(m));
57
59
if (!authToken) return;
58
-
const data: any = JSON.parse(body);
59
-
// console.log("will put fronter", fronter, "for records", data.results);
60
60
const results = [];
61
-
for (const result of data.results) {
62
-
const resp = await putFronter(result.uri, fronter, authToken);
61
+
for (const result of items) {
62
+
const resp = await putFronter(
63
+
{ name: fronter ?? "", subject: result.uri, member: spFronters },
64
+
authToken,
65
+
);
63
66
if (resp.ok) {
64
67
const parsedUri = cacheFronter(result.uri, resp.value);
65
68
results.push({
66
69
rkey: parsedUri.rkey!,
67
70
...resp.value,
68
71
});
72
+
} else {
73
+
console.error(`fronter write: ${resp.error}`);
69
74
}
70
75
}
71
76
browser.tabs.sendMessage(sender.tab?.id!, {
···
178
183
// console.log("handling response event", message);
179
184
switch (message.data.type as string) {
180
185
case "write":
181
-
await handleWrite(message, sender);
186
+
await handleWrite(
187
+
JSON.parse(message.data.body).results,
188
+
message.data.authToken,
189
+
sender,
190
+
);
191
+
break;
192
+
case "writeOne":
193
+
await handleWrite(
194
+
[JSON.parse(message.data.body)],
195
+
message.data.authToken,
196
+
sender,
197
+
);
182
198
break;
183
199
case "posts":
184
-
const posts = JSON.parse(message.data.body) as any[];
185
200
await handleTimeline(
186
-
posts.map((post) => ({ post })),
201
+
(JSON.parse(message.data.body) as any[]).map((post) => ({ post })),
187
202
sender,
188
203
);
189
204
break;
+20
-9
src/entrypoints/content.ts
+20
-9
src/entrypoints/content.ts
···
38
38
}),
39
39
);
40
40
};
41
-
42
-
let detail: any;
43
-
if (response.url.includes("/xrpc/com.atproto.repo.applyWrites")) {
41
+
const getAuthToken = () => {
44
42
let authHeader: string | null = null;
45
43
if (typeof args[0] === "string") {
46
44
if (args[1]?.headers) {
···
49
47
} else if (args[0] instanceof Request) {
50
48
authHeader = getAuthHeader(args[0].headers);
51
49
}
50
+
return authHeader?.split(" ")[1] || null;
51
+
};
52
52
53
+
let detail: any = undefined;
54
+
if (response.url.includes("/xrpc/com.atproto.repo.applyWrites")) {
53
55
detail = {
54
56
type: "write",
55
57
body,
56
-
authToken: authHeader?.split(" ")[1] || null,
58
+
authToken: getAuthToken(),
59
+
};
60
+
} else if (response.url.includes("/xrpc/com.atproto.repo.createRecord")) {
61
+
detail = {
62
+
type: "writeOne",
63
+
body,
64
+
authToken: getAuthToken(),
57
65
};
58
66
} else if (
59
67
response.url.includes("/xrpc/app.bsky.feed.getAuthorFeed") ||
···
77
85
body,
78
86
};
79
87
}
80
-
sendEvent(detail);
88
+
if (detail) {
89
+
sendEvent(detail);
90
+
}
81
91
82
92
return response;
83
93
};
···
96
106
});
97
107
respEventSetup.then((name) => (respEventName = name));
98
108
99
-
const applyFronterName = (el: Element, fronterName: string) => {
109
+
const applyFronterName = (el: Element, fronterNames: string[]) => {
100
110
if (el.getAttribute("data-fronter")) return;
101
-
el.textContent += ` [f: ${fronterName}]`;
102
-
el.setAttribute("data-fronter", fronterName);
111
+
const s = fronterNames.join(", ");
112
+
el.textContent += ` [f: ${s}]`;
113
+
el.setAttribute("data-fronter", s);
103
114
};
104
115
const applyFrontersToPage = (fronters: Map<string, any>) => {
105
116
for (const el of document.getElementsByTagName("a")) {
···
114
125
: (el.parentElement?.firstElementChild?.firstElementChild
115
126
?.firstElementChild?.firstElementChild ?? null);
116
127
if (!displayNameElement) continue;
117
-
applyFronterName(displayNameElement, fronter.fronterName);
128
+
applyFronterName(displayNameElement, fronter.names);
118
129
}
119
130
};
120
131
window.addEventListener("message", (event) => {
+5
-9
src/entrypoints/isolated.content.ts
+5
-9
src/entrypoints/isolated.content.ts
···
30
30
const respEventName = Math.random().toString(36).slice(2);
31
31
window.addEventListener(`${respEventName}-isolated`, async (event) => {
32
32
const data = (event as any).detail;
33
-
// console.log("passing response event to bg", data);
34
-
await browser.runtime
35
-
.sendMessage({
36
-
type: "RESPONSE_CAPTURED",
37
-
data,
38
-
})
39
-
.catch(() => {
40
-
console.log("background script not ready");
41
-
});
33
+
// console.log("passing response event to bg", event);
34
+
await browser.runtime.sendMessage({
35
+
type: "RESPONSE_CAPTURED",
36
+
data,
37
+
});
42
38
});
43
39
const messageTypes = [
44
40
"TAB_FRONTER",
+34
-18
src/entrypoints/popup/App.svelte
+34
-18
src/entrypoints/popup/App.svelte
···
2
2
import { expect } from "@/lib/result";
3
3
import { getFronter } from "@/lib/utils";
4
4
import { isResourceUri } from "@atcute/lexicons";
5
-
import type {
6
-
AtprotoDid,
7
-
Handle,
8
-
ResourceUri,
9
-
} from "@atcute/lexicons/syntax";
5
+
import type { ResourceUri } from "@atcute/lexicons/syntax";
10
6
11
7
let recordAtUri = $state("");
12
8
let queryResult = $state("");
13
9
let isQuerying = $state(false);
14
10
let fronterName = $state("");
11
+
let spToken = $state("");
15
12
16
-
const makeOutput = (fronterName: string, handle: Handle | null) => {
17
-
return `HANDLE: ${handle ?? "handle.invalid"}\nFRONTER: ${fronterName}`;
13
+
const makeOutput = (fronter: any) => {
14
+
return `HANDLE: ${fronter.handle ?? "handle.invalid"}<br>FRONTER: ${fronter.fronterName}`;
18
15
};
19
16
20
17
const queryRecord = async (recordUri: ResourceUri) => {
···
26
23
try {
27
24
if (!isResourceUri(recordUri)) throw "INVALID_RESOURCE_URI";
28
25
const result = expect(await getFronter(recordUri));
29
-
queryResult =
30
-
makeOutput(result.fronterName, result.handle) ||
31
-
"NO_FRONTER_FOUND";
26
+
queryResult = makeOutput(result) || "NO_FRONTER_FOUND";
32
27
} catch (error) {
33
28
queryResult = `ERROR: ${error}`;
34
29
} finally {
···
39
34
const updateFronter = (event: any) => {
40
35
fronterName = (event.target as HTMLInputElement).value;
41
36
storage.setItem("sync:fronter", fronterName);
37
+
};
38
+
39
+
const updateSpToken = (event: any) => {
40
+
spToken = (event.target as HTMLInputElement).value;
41
+
storage.setItem("sync:sp_token", spToken);
42
42
};
43
43
44
44
const handleKeyPress = (event: KeyboardEvent) => {
···
58
58
fronterName = fronter;
59
59
}
60
60
61
+
const token = await storage.getItem<string>("sync:sp_token");
62
+
if (token) {
63
+
spToken = token;
64
+
}
65
+
61
66
const tabs = await browser.tabs.query({
62
67
active: true,
63
68
currentWindow: true,
64
69
});
65
-
const tabFronter = await storage.getItem<{
66
-
fronterName: string;
67
-
recordUri: ResourceUri;
68
-
handle: Handle | null;
69
-
did: AtprotoDid;
70
-
}>(`local:tab-${tabs[0].id!}-fronter`);
70
+
const tabFronter = await storage.getItem<any>(
71
+
`local:tab-${tabs[0].id!}-fronter`,
72
+
);
71
73
if (tabFronter) {
72
-
queryResult = makeOutput(tabFronter.fronterName, tabFronter.handle);
74
+
queryResult = makeOutput(tabFronter);
73
75
recordAtUri = tabFronter.recordUri;
74
76
}
75
77
});
···
142
144
"ERROR:",
143
145
)}
144
146
>
145
-
{queryResult}
147
+
{@html queryResult}
146
148
</div>
147
149
{:else}
148
150
<div class="placeholder-text">
···
170
172
bind:value={fronterName}
171
173
class="config-input"
172
174
class:has-value={fronterName}
175
+
/>
176
+
</div>
177
+
</div>
178
+
179
+
<div class="config-row">
180
+
<span class="config-label">SP_TOKEN</span>
181
+
<div class="config-input-wrapper">
182
+
<input
183
+
type="password"
184
+
placeholder="enter_simply_plural_token"
185
+
oninput={updateSpToken}
186
+
bind:value={spToken}
187
+
class="config-input"
188
+
class:has-value={spToken}
173
189
/>
174
190
</div>
175
191
</div>
+125
-11
src/lib/utils.ts
+125
-11
src/lib/utils.ts
···
7
7
import {
8
8
ActorIdentifier,
9
9
Did,
10
+
GenericUri,
10
11
Handle,
11
12
isHandle,
12
13
RecordKey,
···
26
27
import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity";
27
28
28
29
export type Fronter = {
29
-
fronterName: string;
30
+
memberUris: MemberUri[];
31
+
names: string[];
30
32
handle: Handle | null;
31
33
did: AtprotoDid;
32
34
};
···
47
49
$type: v.literal("systems.gaze.atfronter.fronter"),
48
50
name: v.string(),
49
51
subject: v.resourceUriString(),
52
+
member: v.array(v.genericUriString()), // identifier(s) for pk or sp or etc. (maybe member record in the future?)
50
53
}),
51
54
);
52
55
56
+
type MemberUri =
57
+
| { type: "at"; recordUri: ResourceUri }
58
+
| { type: "pk"; systemId: string; memberId: string }
59
+
| { type: "sp"; systemId: string; memberId: string };
60
+
61
+
export const parseMemberId = (memberId: GenericUri): MemberUri => {
62
+
const uri = new URL(memberId);
63
+
switch (uri.protocol) {
64
+
case "pk:": {
65
+
const split = uri.pathname.split("/").slice(1);
66
+
return { type: "pk", systemId: split[0], memberId: split[1] };
67
+
}
68
+
case "sp:": {
69
+
const split = uri.pathname.split("/").slice(1);
70
+
return { type: "sp", systemId: split[0], memberId: split[1] };
71
+
}
72
+
case "at:": {
73
+
return { type: "at", recordUri: memberId as ResourceUri };
74
+
}
75
+
default: {
76
+
throw new Error(`Invalid member ID: ${memberId}`);
77
+
}
78
+
}
79
+
};
80
+
export const memberUriString = (memberUri: MemberUri): GenericUri => {
81
+
switch (memberUri.type) {
82
+
case "pk": {
83
+
return `pk://api.pluralkit.com/${memberUri.systemId}/${memberUri.memberId}`;
84
+
}
85
+
case "sp": {
86
+
return `sp://api.apparyllis.com/${memberUri.systemId}/${memberUri.memberId}`;
87
+
}
88
+
case "at": {
89
+
return memberUri.recordUri;
90
+
}
91
+
}
92
+
};
93
+
94
+
let memberCache = new Map<string, any>();
95
+
export const fetchMember = async (
96
+
memberUri: MemberUri,
97
+
): Promise<string | undefined> => {
98
+
switch (memberUri.type) {
99
+
case "sp": {
100
+
const s = memberUriString(memberUri);
101
+
const cached = memberCache.get(s);
102
+
if (cached) return cached.content.name;
103
+
const token = await storage.getItem<string>("sync:sp_token");
104
+
if (!token) return;
105
+
const resp = await fetch(
106
+
`https://api.apparyllis.com/v1/member/${memberUri.systemId}/${memberUri.memberId}`,
107
+
{
108
+
headers: {
109
+
authorization: token,
110
+
},
111
+
},
112
+
);
113
+
if (!resp.ok) return;
114
+
const member = await resp.json();
115
+
memberCache.set(s, member);
116
+
return member.content.name;
117
+
}
118
+
}
119
+
};
120
+
121
+
export const getFronterNames = async (
122
+
name: string,
123
+
memberUris: MemberUri[],
124
+
) => {
125
+
let fronterNames = [name];
126
+
if (memberUris.length > 0) {
127
+
fronterNames = (
128
+
await Promise.allSettled(memberUris.map((m) => fetchMember(m)))
129
+
)
130
+
.filter((p) => p.status === "fulfilled")
131
+
.flatMap((p) => p.value ?? []);
132
+
}
133
+
return fronterNames;
134
+
};
135
+
53
136
const handleResolver = new CompositeHandleResolver({
54
137
strategy: "race",
55
138
methods: {
···
114
197
const maybeTyped = safeParse(fronterSchema, maybeRecord.data.value);
115
198
if (!maybeTyped.ok) return err(maybeTyped.message);
116
199
200
+
let memberUris, fronterNames;
201
+
try {
202
+
memberUris = maybeTyped.value.member.map((m) => parseMemberId(m));
203
+
fronterNames = await getFronterNames(maybeTyped.value.name, memberUris);
204
+
} catch (error) {
205
+
return err(`error fetching fronter names: ${error}`);
206
+
}
207
+
117
208
return ok({
118
-
fronterName: maybeTyped.value.name,
209
+
memberUris,
210
+
names: fronterNames,
119
211
handle,
120
212
did,
121
213
});
122
214
};
123
215
124
-
export const putFronter = async <Uri extends ResourceUri>(
125
-
recordUri: Uri,
126
-
name: string,
216
+
export const putFronter = async (
217
+
record: Omit<InferOutput<typeof fronterSchema>, "$type">,
127
218
authToken: string,
128
219
): Promise<Result<Fronter, string>> => {
129
-
const parsedRecordUri = parseResourceUri(recordUri);
220
+
const parsedRecordUri = parseResourceUri(record.subject);
130
221
if (!parsedRecordUri.ok) return err(parsedRecordUri.error);
131
222
const { repo, collection, rkey } = parsedRecordUri.value;
132
223
···
142
233
repo: did,
143
234
collection: fronterSchema.object.shape.$type.expected,
144
235
rkey: `${collection}_${rkey}`,
145
-
record: {
146
-
name,
147
-
subject: `at://${did}/${collection}/${rkey}`,
148
-
},
236
+
record,
149
237
validate: false,
150
238
},
151
239
headers: { authorization: `Bearer ${authToken}` },
···
153
241
if (!maybeRecord.ok)
154
242
return err(maybeRecord.data.message ?? maybeRecord.data.error);
155
243
244
+
let memberUris, fronterNames;
245
+
try {
246
+
memberUris = record.member.map((m) => parseMemberId(m));
247
+
fronterNames = await getFronterNames(record.name, memberUris);
248
+
} catch (error) {
249
+
return err(`error fetching fronter names: ${error}`);
250
+
}
251
+
156
252
return ok({
157
253
did,
158
254
handle,
159
-
fronterName: name,
255
+
names: fronterNames,
256
+
memberUris,
160
257
});
161
258
};
259
+
260
+
export const getSpFronters = async (): Promise<MemberUri[]> => {
261
+
const spToken = await storage.getItem<string>("sync:sp_token");
262
+
if (!spToken) return [];
263
+
const resp = await fetch(`https://api.apparyllis.com/v1/fronters`, {
264
+
headers: {
265
+
authorization: spToken,
266
+
},
267
+
});
268
+
if (!resp.ok) return [];
269
+
const spFronters = (await resp.json()) as any[];
270
+
return spFronters.map((fronter) => ({
271
+
type: "sp",
272
+
memberId: fronter.content.member,
273
+
systemId: fronter.content.uid,
274
+
}));
275
+
};