···11import { App, staticFiles } from "fresh";
22import { type State } from "./utils.ts";
33-import { streamplaceWS } from "./utils/websocket.ts";
33+import { isHandle } from "@atcute/lexicons/syntax";
44+import { resolveHandle } from "./utils/atcuteUtils.ts";
55+import { filterByStreamplace, getBacklinks } from "./utils/constellationUtils.ts";
46import StreamplaceBot from "./utils/streamplaceBot.ts";
77+import { streamplaceWS } from "./utils/websocket.ts";
58import { ATPROTO_PASSWORD, ATPROTO_USERNAME, PDS_HOST_URL } from "./env.ts";
69710export const app = new App<State>();
81199-export const streamplaceBot = new StreamplaceBot(
1010- "did:plc:o6xucog6fghiyrvp7pyqxcs3", //this is me, make more flexible later
1111- {
1212- username: ATPROTO_USERNAME!,
1313- password: ATPROTO_PASSWORD!,
1414- pdsHostUrl: PDS_HOST_URL!,
1515- },
1212+// Get bot following
1313+const botDid = isHandle(ATPROTO_USERNAME)
1414+ ? await resolveHandle(ATPROTO_USERNAME)
1515+ : ATPROTO_USERNAME;
1616+export const botInstances: Map<Did, StreamplaceBot> = new Map();
1717+const backlinks = await getBacklinks(
1818+ botDid,
1919+ "app.bsky.graph.follow",
2020+ ".subject",
2121+ 100,
1622);
1717-streamplaceBot.init();
2323+const filteredBacklinks = await filterByStreamplace(backlinks);
2424+for (const backlink of filteredBacklinks) {
2525+ const streamplaceBot = new StreamplaceBot(
2626+ backlink,
2727+ {
2828+ username: ATPROTO_USERNAME!,
2929+ password: ATPROTO_PASSWORD!,
3030+ pdsHostUrl: PDS_HOST_URL!,
3131+ },
3232+ );
3333+ streamplaceBot.init();
3434+ botInstances.set(backlink, streamplaceBot);
3535+}
1836streamplaceWS.start();
1937app.use(staticFiles());
2038
+46-3
utils/atcuteUtils.ts
···142142 return await handleResolver.resolve(handle);
143143}
144144145145+// com.atproto.repo.describeRepo wrapper
146146+export async function describeRepo(did: Did, pdsHostUrl: string): Promise<
147147+ {
148148+ collections: `${string}.${string}.${string}`[];
149149+ did: `did:${string}:${string}`;
150150+ didDoc: Record<string, unknown>;
151151+ handle: `${string}.${string}`;
152152+ handleIsCorrect: boolean;
153153+ } | null
154154+> {
155155+ const rpc = new Client({
156156+ handler: simpleFetchHandler({
157157+ service: pdsHostUrl,
158158+ }),
159159+ });
160160+161161+ const response = await rpc.get("com.atproto.repo.describeRepo", {
162162+ params: { repo: did },
163163+ });
164164+165165+ if (response.ok) {
166166+ return response.data;
167167+ } else {
168168+ switch (response.data.error) {
169169+ case "InvalidRequest":
170170+ // handle or account doesn't exist
171171+ console.log(`describeRepo: ${did} does not exist.`);
172172+ break;
173173+ case "RepoTakendown":
174174+ // account was taken down
175175+ console.log(`describeRepo: ${did} was taken down.`);
176176+ break;
177177+ case "AccountDeactivated":
178178+ // account deactivated by user
179179+ console.log(`describeRepo: ${did} is deactivated.`);
180180+ break;
181181+ }
182182+ }
183183+ return null;
184184+}
185185+145186// Get DID document
146187export async function getDidDocument(did: Did): Promise<DidDocument> {
147188 if (!isAtprotoDid(did)) {
···206247 params: params,
207248 });
208249 if (!record.ok) {
209209- console.log(
210210- `No record of collection ${params.collection} and rkey ${params.rkey} found in ${params.repo}.`,
211211- );
250250+ // as this project scales, there will be a lot of this
251251+ // maybe I should let users fine tune their logging
252252+ // console.log(
253253+ // `No record of collection ${params.collection} and rkey ${params.rkey} found in ${params.repo}.`,
254254+ // );
212255 return undefined;
213256 }
214257
+28
utils/constellationUtils.ts
···11+import { describeRepo, listRecords } from "./atcuteUtils.ts";
22+import { didResolver } from "./didResolver.ts";
33+14// Microcosm backlinks
25interface ConstellationResponse {
36 total: number;
···3740 throw error;
3841 }
3942}
4343+4444+export async function filterByStreamplace(
4545+ backlinks: ConstellationResponse,
4646+): Promise<Did[]> {
4747+ const activeDids: Did[] = [];
4848+4949+ // Doing in batches probably necessary for larger following
5050+ for (const follower of backlinks.linking_records) {
5151+ const profile = await didResolver.resolve(follower.did);
5252+ const repoDescription = await describeRepo(
5353+ follower.did,
5454+ profile.pdsEndpoint,
5555+ );
5656+ if (!repoDescription) continue;
5757+ const livestreams = await listRecords(profile.pdsEndpoint, {
5858+ repo: follower.did,
5959+ // Should this be "place.stream.livestream" instead?
6060+ collection: "place.stream.chat.profile",
6161+ });
6262+ if (livestreams.records?.length > 0) {
6363+ activeDids.push(follower.did);
6464+ }
6565+ }
6666+ return activeDids;
6767+}