fancy live pfps
1const ATPROTO_STATUS_URI =
2 "at://did:plc:krxbvxvis5skq7jj6eot23ul/fm.teal.alpha.actor.status/self";
3const SLACK_TOKENS = [
4 process.env.SLACK_TOKEN,
5 process.env.SLACK_TOKEN_2,
6].filter(Boolean) as string[];
7const POLL_INTERVAL = 30_000; // 30s
8const PORT = parseInt(process.env.PORT || "3000", 10);
9
10// --- AT Protocol helpers ---
11
12async function resolveDidToPds(did: string): Promise<string | null> {
13 if (did.startsWith("did:plc:")) {
14 const res = await fetch(`https://plc.directory/${did}`);
15 const doc = await res.json();
16 return doc.service?.find((s: any) => s.id === "#atproto_pds")
17 ?.serviceEndpoint;
18 } else if (did.startsWith("did:web:")) {
19 const domain = did.slice(8);
20 const res = await fetch(`https://${domain}/.well-known/did.json`);
21 const doc = await res.json();
22 return doc.service?.find((s: any) => s.id === "#atproto_pds")
23 ?.serviceEndpoint;
24 }
25 return null;
26}
27
28async function fetchAtUriRecord(atUri: string): Promise<any | null> {
29 const match = atUri.match(/^at:\/\/([^/]+)\/([^/]+)\/([^/]+)$/);
30 if (!match) return null;
31 const [, repo, collection, rkey] = match;
32 const pds = await resolveDidToPds(repo);
33 if (!pds) return null;
34 const url = `${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(repo)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}`;
35 const res = await fetch(url);
36 return res.ok ? res.json() : null;
37}
38
39// --- State ---
40
41type PfpState = "default" | "headphones" | "zz";
42
43let currentState: PfpState = "default";
44const lastSlackUpdate = new Map<string, string>();
45
46// --- Music detection ---
47
48async function checkNowPlaying(): Promise<boolean> {
49 try {
50 const data = await fetchAtUriRecord(ATPROTO_STATUS_URI);
51 if (!data?.value?.item) return false;
52 const expiry = new Date(data.value.expiry).getTime();
53 return Date.now() <= expiry + 5 * 60_000;
54 } catch {
55 return false;
56 }
57}
58
59// --- Image selection ---
60
61function getHour(): number {
62 return new Date().getHours();
63}
64
65function getImagePath(hour: number, state: PfpState): string {
66 const h = hour.toString().padStart(2, "0");
67 const suffix = state === "default" ? "" : `_${state}`;
68 return `./imgs/${h}${suffix}.png`;
69}
70
71function determineState(isPlaying: boolean): PfpState {
72 if (isPlaying) return "headphones";
73 const hour = getHour();
74 if (hour >= 0 && hour < 7) return "zz";
75 return "default";
76}
77
78// --- Slack ---
79
80async function updateSlackPfp(token: string, label: string, imagePath: string) {
81 if (lastSlackUpdate.get(token) === imagePath) return;
82
83 const file = Bun.file(imagePath);
84 const blob = await file.arrayBuffer();
85
86 const form = new FormData();
87 form.append("image", new Blob([blob], { type: "image/png" }), "pfp.png");
88
89 const res = await fetch("https://slack.com/api/users.setPhoto", {
90 method: "POST",
91 headers: { Authorization: `Bearer ${token}` },
92 body: form,
93 });
94
95 const data = await res.json();
96 if (data.ok) {
97 lastSlackUpdate.set(token, imagePath);
98 console.log(`[slack:${label}] updated pfp to ${imagePath}`);
99 } else {
100 console.error(`[slack:${label}] failed to update pfp:`, data.error);
101 }
102}
103
104// --- Poll loop ---
105
106async function tick() {
107 const isPlaying = await checkNowPlaying();
108 const state = determineState(isPlaying);
109 const hour = getHour();
110 const imagePath = getImagePath(hour, state);
111
112 currentState = state;
113
114 await Promise.all(
115 SLACK_TOKENS.map((token, i) =>
116 updateSlackPfp(token, i === 0 ? "primary" : `workspace-${i + 1}`, imagePath)
117 )
118 );
119}
120
121tick();
122setInterval(tick, POLL_INTERVAL);
123
124// --- Server ---
125
126Bun.serve({
127 port: PORT,
128 routes: {
129 "/pfp": async () => {
130 const hour = getHour();
131 const imagePath = getImagePath(hour, currentState);
132 const file = Bun.file(imagePath);
133 return new Response(file, {
134 headers: {
135 "Content-Type": "image/png",
136 "Cache-Control": "no-cache, no-store, must-revalidate",
137 },
138 });
139 },
140 "/status": () => {
141 return Response.json({
142 state: currentState,
143 hour: getHour(),
144 image: getImagePath(getHour(), currentState),
145 });
146 },
147 },
148 fetch() {
149 return new Response("Not found", { status: 404 });
150 },
151});
152
153console.log(`livepfp running on http://localhost:${PORT}`);
154console.log(` GET /pfp → current profile picture`);
155console.log(` GET /status → current state as JSON`);