import { createPost, formatPost, listMentions, login, type Mention } from "./bluesky"; import { getRandomTracks } from "./navidrome"; import { claimReply, recordReplyUri, releaseReply } from "./replies"; import { loadCredentials, loadImage, requireEnv } from "./shared"; const DAY_MS = 86_400_000; /** Far enough back that a ping survives a long outage. */ const LOOKBACK_DAYS = 3; /** Limits the damage from a burst of new accounts. Any extra people wait for the next run. */ const MAX_REPLIES_PER_RUN = 20; /** A few tracks, in case some are dropped for missing details or cover art. */ const SAMPLE_SIZE = 5; /** Takes oldest-first mentions and keeps each person's newest, since they get one reply. */ function newestPerPerson(mentions: Mention[]): Mention[] { const newest = new Map(); for (const mention of mentions) { newest.delete(mention.authorDid); newest.set(mention.authorDid, mention); } return [...newest.values()]; } /** * Replies to new pings with a random track, once per person every 6 hours. * These tracks skip the post history, so pings can't use up the scheduled posts' choices. */ export async function handler() { const table = requireEnv("REPLIES_TABLE"); const { navidrome, bluesky } = await loadCredentials(); const session = await login(bluesky); const mentions = await listMentions(session, new Date(Date.now() - LOOKBACK_DAYS * DAY_MS)); const people = newestPerPerson(mentions); let replied = 0; let failures = 0; for (const mention of people) { if (replied >= MAX_REPLIES_PER_RUN) break; const claimed = await claimReply( table, mention.authorDid, mention.authorHandle, mention.parent.uri, mention.indexedAt, ); if (!claimed) continue; let uri: string; try { const [track] = await getRandomTracks(navidrome, SAMPLE_SIZE); if (!track) throw new Error("Navidrome returned no usable track"); uri = await createPost(session, formatPost(track), await loadImage(navidrome, track), { root: mention.root, parent: mention.parent, }); } catch (error) { // Release the claim so the next run can try again, and keep going for everyone else. failures++; console.error(`Could not reply to ${mention.parent.uri}`, error); await releaseReply(table, mention.authorDid).catch((releaseError) => console.error("Could not release the reply claim", releaseError), ); continue; } replied++; await recordReplyUri(table, mention.authorDid, uri).catch((error) => console.error("Replied, but could not record the reply URI", error), ); } const summary = { mentions: mentions.length, people: people.length, replied, failures }; console.log(JSON.stringify(summary)); if (failures > 0) throw new Error(`${failures} of ${people.length} replies failed`); return summary; }