Daily Bluesky bot for AT Mot. Invites players and congratulates yesterday's solvers.
0

Configure Feed

Select the types of activity you want to include in your feed.

at trunk 2.8 kB View raw
1/** 2 * Posting side: create an app-password session against the bot's PDS, check we 3 * haven't already posted today (by scanning our own recent posts — stateless), 4 * and publish the post. 5 */ 6import { Client, simpleFetchHandler, type FetchHandler } from '@atcute/client'; 7import { resolveActor } from './identity.js'; 8import type { ComposedPost } from './compose.js'; 9 10export interface BotSession { 11 client: Client; 12 did: string; 13} 14 15/** Log in with an app password; returns a client whose requests carry the bearer token. */ 16export async function createBotSession(identifier: string, appPassword: string): Promise<BotSession> { 17 const actor = await resolveActor(identifier); 18 const pds = actor.pds; 19 20 const loginClient = new Client({ handler: simpleFetchHandler({ service: pds }) }); 21 const res = await loginClient.post('com.atproto.server.createSession', { 22 input: { identifier, password: appPassword }, 23 }); 24 if (!res.ok) throw new Error(`createSession failed: ${JSON.stringify(res.data)}`); 25 26 const accessJwt = res.data.accessJwt; 27 const base = simpleFetchHandler({ service: pds }); 28 const authed: FetchHandler = (pathname, init) => { 29 const headers = new Headers(init.headers ?? {}); 30 headers.set('authorization', `Bearer ${accessJwt}`); 31 return base(pathname, { ...init, headers }); 32 }; 33 34 return { client: new Client({ handler: authed }), did: res.data.did }; 35} 36 37/** True if any of the bot's recent posts already contains `marker` (today's puzzle token). */ 38export async function alreadyPosted(session: BotSession, marker: string): Promise<boolean> { 39 const res = await session.client.get('com.atproto.repo.listRecords', { 40 params: { 41 repo: session.did as `did:${string}:${string}`, 42 collection: 'app.bsky.feed.post' as `${string}.${string}.${string}`, 43 limit: 20, 44 }, 45 }); 46 if (!res.ok) return false; // can't confirm — allow the post rather than skip forever 47 const records = res.data.records as Array<{ value?: { text?: string } }>; 48 return records.some((r) => typeof r.value?.text === 'string' && r.value.text.includes(marker)); 49} 50 51/** Create the post; returns its at:// uri. */ 52export async function publishPost(session: BotSession, post: ComposedPost): Promise<string> { 53 const res = await session.client.post('com.atproto.repo.createRecord', { 54 input: { 55 repo: session.did as `did:${string}:${string}`, 56 collection: 'app.bsky.feed.post' as `${string}.${string}.${string}`, 57 record: { 58 $type: 'app.bsky.feed.post', 59 text: post.text, 60 langs: post.langs, 61 facets: post.facets, 62 createdAt: new Date().toISOString(), 63 } as unknown as Record<string, unknown>, 64 }, 65 }); 66 if (!res.ok) throw new Error(`createRecord failed: ${JSON.stringify(res.data)}`); 67 return res.data.uri; 68}