···11+# Handle or DID of the account you want to use as bot
22+BOT_IDENTIFIER=bot.timtinkers.online
33+# App password for the account
44+BOT_PASSWORD=xxxx-xxxx-xxxx-xxxx
55+# URL for the Jetstream instance
66+JETSTREAM_URL=wss://jetstream1.us-east.bsky.network/subscribe
77+# If set to true, this will fetch the followers of the bot account
88+# and the bot will operate in their Streamplace chats as well.
99+# The bot will always spin up for itself, but you can set this to
1010+# true if you want the bot to serve multiple streamers at once.
1111+FOLLOWER_MODE=false
+48-6
env.ts
···11import { load } from "@std/dotenv";
22+import { isDid, isHandle } from "@atcute/lexicons/syntax";
33+import { getDidDocument, getPDS, resolveHandle } from "./utils/atcuteUtils.ts";
2435await load({ export: true });
4655-export const PDS_HOST_URL = Deno.env.get("PDS_HOST_URL")!;
66-export const ATPROTO_USERNAME = Deno.env.get("ATPROTO_USERNAME")! as
77- | Did
88- | Handle;
99-export const ATPROTO_PASSWORD = Deno.env.get("ATPROTO_PASSWORD")!;
1010-export const JETSTREAM_URL = Deno.env.get("JETSTREAM_URL")!;
77+function requiredEnv(key: string): string {
88+ const value = Deno.env.get(key);
99+ if (!value) {
1010+ throw new Error(
1111+ `❎ Environment variable ${key} is required but not set`,
1212+ );
1313+ }
1414+ return value;
1515+}
1616+1717+function validateBotIdentifier(identifier: string): Did | Handle {
1818+ if (isDid(identifier) || isHandle(identifier)) {
1919+ return identifier as Did | Handle;
2020+ }
2121+ throw new Error(
2222+ `❎ BOT_IDENTIFIER must be a valid DID or Handle, got: ${identifier}`,
2323+ );
2424+}
2525+2626+async function getPDSfromIdentifier(identifier: Did | Handle) {
2727+ const did = isDid(identifier)
2828+ ? identifier
2929+ : await resolveHandle(identifier);
3030+ const doc = await getDidDocument(did);
3131+ const pds = getPDS(doc);
3232+ if (!pds) {
3333+ throw new Error(
3434+ `❎ No valid PDS endpoint found for given BOT_IDENTIFIER ${identifier}`,
3535+ );
3636+ }
3737+ return pds;
3838+}
3939+4040+export const BOT_CREDENTIALS = {
4141+ identifier: validateBotIdentifier(requiredEnv("BOT_IDENTIFIER")),
4242+ password: requiredEnv("BOT_PASSWORD"),
4343+};
4444+export const BOT_SERVICE = await getPDSfromIdentifier(
4545+ BOT_CREDENTIALS.identifier,
4646+);
4747+export const JETSTREAM_URL = (Deno.env.get("JETSTREAM_URL") ||
4848+ "wss://jetstream1.us-east.bsky.network/subscribe") +
4949+ "?wantedCollections=place.stream.chat.message";
5050+export const FOLLOWER_MODE = Deno.env.get("FOLLOWER_MODE") === "true"
5151+ ? true
5252+ : false;
+33-21
main.ts
···22import { type State } from "./utils.ts";
33import { isHandle } from "@atcute/lexicons/syntax";
44import { resolveHandle } from "./utils/atcuteUtils.ts";
55-import { filterByStreamplace, getBacklinks } from "./utils/constellationUtils.ts";
55+import {
66+ filterByStreamplace,
77+ getBacklinks,
88+} from "./utils/constellationUtils.ts";
69import StreamplaceBot from "./utils/streamplaceBot.ts";
710import { streamplaceWS } from "./utils/websocket.ts";
88-import { ATPROTO_PASSWORD, ATPROTO_USERNAME, PDS_HOST_URL } from "./env.ts";
1111+import { BOT_CREDENTIALS, BOT_SERVICE, FOLLOWER_MODE } from "./env.ts";
9121013export const app = new App<State>();
11141212-// Get bot following
1313-const botDid = isHandle(ATPROTO_USERNAME)
1414- ? await resolveHandle(ATPROTO_USERNAME)
1515- : ATPROTO_USERNAME;
1615export const botInstances: Map<Did, StreamplaceBot> = new Map();
1717-const backlinks = await getBacklinks(
1616+1717+// Initialize for self
1818+const botDid = isHandle(BOT_CREDENTIALS.identifier)
1919+ ? await resolveHandle(BOT_CREDENTIALS.identifier)
2020+ : BOT_CREDENTIALS.identifier;
2121+const streamplaceBot = new StreamplaceBot(
1822 botDid,
1919- "app.bsky.graph.follow",
2020- ".subject",
2121- 100,
2323+ BOT_CREDENTIALS,
2424+ BOT_SERVICE,
2225);
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- },
2626+streamplaceBot.init();
2727+botInstances.set(botDid, streamplaceBot);
2828+2929+if (FOLLOWER_MODE) {
3030+ // Get bot followers
3131+ const backlinks = await getBacklinks(
3232+ botDid,
3333+ "app.bsky.graph.follow",
3434+ ".subject",
3535+ 100, // I should fetch all of them, but I also don't know if I can handle that many
3236 );
3333- streamplaceBot.init();
3434- botInstances.set(backlink, streamplaceBot);
3737+ const filteredBacklinks = await filterByStreamplace(backlinks);
3838+ for (const backlinkDid of filteredBacklinks) {
3939+ const streamplaceBot = new StreamplaceBot(
4040+ backlinkDid,
4141+ BOT_CREDENTIALS,
4242+ BOT_SERVICE,
4343+ );
4444+ streamplaceBot.init();
4545+ botInstances.set(backlinkDid, streamplaceBot);
4646+ }
3547}
3648streamplaceWS.start();
3749app.use(staticFiles());