1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
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<Config, Payload> = {
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 };
},
};
|