import type { SourceModule } from "../registry"; import { BlueskyConfig, BlueskyPayload, type BlueskyConfig as Config, type BlueskyPayload as Payload, } from "@/lib/schemas/sources/bluesky"; const XRPC = "https://bsky.social/xrpc"; const KEEP = new Set(["reply", "mention", "quote"]); export const blueskyModule: SourceModule = { kind: "bluesky", label: "Bluesky", keyless: false, defaultRefreshSeconds: 600, configSchema: BlueskyConfig, payloadSchema: BlueskyPayload, isConfigured: (_c, env) => Boolean(env.BLUESKY_HANDLE && env.BLUESKY_APP_PASSWORD), async fetch({ config, env, signal }) { const identifier = env.BLUESKY_HANDLE; const password = env.BLUESKY_APP_PASSWORD; if (!identifier || !password) throw new Error("Set BLUESKY_HANDLE + BLUESKY_APP_PASSWORD"); const sessRes = await fetch(`${XRPC}/com.atproto.server.createSession`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ identifier, password }), signal, }); if (!sessRes.ok) throw new Error(`Bluesky auth ${sessRes.status}`); const { accessJwt } = (await sessRes.json()) as { accessJwt: string }; const notifRes = await fetch( `${XRPC}/app.bsky.notification.listNotifications?limit=40`, { headers: { Authorization: `Bearer ${accessJwt}` }, signal }, ); if (!notifRes.ok) throw new Error(`Bluesky ${notifRes.status}`); const j = (await notifRes.json()) as { notifications: Array<{ reason: string; author: { handle: string; displayName?: string }; record?: { text?: string }; indexedAt: string; }>; }; const notifications = j.notifications .filter((n) => KEEP.has(n.reason)) .slice(0, config.limit) .map((n) => ({ reason: n.reason, author: n.author.displayName || `@${n.author.handle}`, text: (n.record?.text ?? "").slice(0, 200), createdAt: n.indexedAt, })); return { notifications }; }, };