a cache for slack profile pictures and emojis
1/**
2 * All route handler functions extracted for reuse
3 */
4
5import * as Sentry from "@sentry/bun";
6// These will be injected by the route system
7import type { SlackCache } from "../cache";
8import type { RouteHandlerWithAnalytics } from "../lib/analytics-wrapper";
9import type { SlackUser } from "../slack";
10import type { SlackWrapper } from "../slackWrapper";
11
12let cache!: SlackCache;
13let slackApp!: SlackWrapper;
14
15export function injectDependencies(
16 cacheInstance: SlackCache,
17 slackInstance: SlackWrapper,
18) {
19 cache = cacheInstance;
20 slackApp = slackInstance;
21}
22
23export const handleHealthCheck: RouteHandlerWithAnalytics = async (
24 request,
25 recordAnalytics,
26) => {
27 const url = new URL(request.url);
28 const detailed = url.searchParams.get("detailed") === "true";
29
30 if (detailed) {
31 const health = await cache.detailedHealthCheck();
32 const statusCode =
33 health.status === "unhealthy"
34 ? 503
35 : health.status === "degraded"
36 ? 200
37 : 200;
38 await recordAnalytics(statusCode);
39 return Response.json(health, { status: statusCode });
40 }
41
42 const isHealthy = await cache.healthCheck();
43 if (isHealthy) {
44 await recordAnalytics(200);
45 return Response.json({
46 status: "healthy",
47 cache: true,
48 uptime: process.uptime(),
49 });
50 } else {
51 await recordAnalytics(503);
52 return Response.json(
53 { status: "unhealthy", error: "Cache connection failed" },
54 { status: 503 },
55 );
56 }
57};
58
59export const handleGetUser: RouteHandlerWithAnalytics = async (
60 request,
61 recordAnalytics,
62) => {
63 const url = new URL(request.url);
64 const userId = url.pathname.split("/").pop() || "";
65 const user = await cache.getUser(userId);
66
67 if (!user || !user.imageUrl) {
68 let slackUser: SlackUser;
69 try {
70 slackUser = await slackApp.getUserInfo(userId);
71 } catch (e) {
72 if (e instanceof Error && e.message === "user_not_found") {
73 await recordAnalytics(404);
74 return Response.json({ message: "User not found" }, { status: 404 });
75 }
76
77 Sentry.withScope((scope) => {
78 scope.setExtra("url", request.url);
79 scope.setExtra("user", userId);
80 Sentry.captureException(e);
81 });
82
83 await recordAnalytics(500);
84 return Response.json(
85 { message: "Internal server error" },
86 { status: 500 },
87 );
88 }
89
90 await cache.insertUser(
91 slackUser.id,
92 slackUser.real_name || slackUser.name || "Unknown",
93 slackUser.profile?.pronouns || "",
94 slackUser.profile?.image_512 || slackUser.profile?.image_192 || "",
95 );
96
97 await recordAnalytics(200);
98 return Response.json({
99 id: slackUser.id,
100 userId: slackUser.id,
101 displayName: slackUser.real_name || slackUser.name || "Unknown",
102 pronouns: slackUser.profile?.pronouns || "",
103 imageUrl:
104 slackUser.profile?.image_512 || slackUser.profile?.image_192 || "",
105 });
106 }
107
108 await recordAnalytics(200);
109 return Response.json(user);
110};
111
112export const handleUserRedirect: RouteHandlerWithAnalytics = async (
113 request,
114 recordAnalytics,
115) => {
116 const url = new URL(request.url);
117 const parts = url.pathname.split("/");
118 const userId = parts[2] || "";
119 const user = await cache.getUser(userId);
120
121 if (!user || !user.imageUrl) {
122 let slackUser: SlackUser;
123 try {
124 slackUser = await slackApp.getUserInfo(userId.toUpperCase());
125 } catch (e) {
126 if (e instanceof Error && e.message === "user_not_found") {
127 console.warn(`⚠️ WARN user not found: ${userId}`);
128
129 await recordAnalytics(307);
130 return new Response(null, {
131 status: 307,
132 headers: {
133 Location:
134 "https://ca.slack-edge.com/T0266FRGM-U0266FRGP-g28a1f281330-512",
135 },
136 });
137 }
138
139 Sentry.withScope((scope) => {
140 scope.setExtra("url", request.url);
141 scope.setExtra("user", userId);
142 Sentry.captureException(e);
143 });
144
145 await recordAnalytics(500);
146 return Response.json(
147 { message: "Internal server error" },
148 { status: 500 },
149 );
150 }
151
152 await cache.insertUser(
153 slackUser.id,
154 slackUser.real_name || slackUser.name || "Unknown",
155 slackUser.profile?.pronouns || "",
156 slackUser.profile?.image_512 || slackUser.profile?.image_192 || "",
157 );
158
159 await recordAnalytics(302);
160 return new Response(null, {
161 status: 302,
162 headers: {
163 Location:
164 slackUser.profile?.image_512 || slackUser.profile?.image_192 || "",
165 },
166 });
167 }
168
169 await recordAnalytics(302);
170 return new Response(null, {
171 status: 302,
172 headers: { Location: user.imageUrl },
173 });
174};
175
176export const handlePurgeUser: RouteHandlerWithAnalytics = async (
177 request,
178 recordAnalytics,
179) => {
180 const authHeader = request.headers.get("authorization") || "";
181 if (authHeader !== `Bearer ${process.env.BEARER_TOKEN}`) {
182 await recordAnalytics(401);
183 return new Response("Unauthorized", { status: 401 });
184 }
185
186 const url = new URL(request.url);
187 const userId = url.pathname.split("/")[2] || "";
188 const result = await cache.purgeUserCache(userId);
189
190 await recordAnalytics(200);
191 return Response.json({
192 message: "User cache purged",
193 userId,
194 success: result,
195 });
196};
197
198export const handleListEmojis: RouteHandlerWithAnalytics = async (
199 _request,
200 recordAnalytics,
201) => {
202 const emojis = await cache.getAllEmojis();
203 await recordAnalytics(200);
204 return Response.json(emojis);
205};
206
207export const handleGetEmoji: RouteHandlerWithAnalytics = async (
208 request,
209 recordAnalytics,
210) => {
211 const url = new URL(request.url);
212 const emojiName = url.pathname.split("/").pop() || "";
213 const emoji = await cache.getEmoji(emojiName);
214
215 if (!emoji) {
216 await recordAnalytics(404);
217 return Response.json({ message: "Emoji not found" }, { status: 404 });
218 }
219
220 await recordAnalytics(200);
221 return Response.json(emoji);
222};
223
224export const handleEmojiRedirect: RouteHandlerWithAnalytics = async (
225 request,
226 recordAnalytics,
227) => {
228 const url = new URL(request.url);
229 const parts = url.pathname.split("/");
230 const emojiName = parts[2] || "";
231 const emoji = await cache.getEmoji(emojiName);
232
233 if (!emoji) {
234 await recordAnalytics(404);
235 return Response.json({ message: "Emoji not found" }, { status: 404 });
236 }
237
238 await recordAnalytics(302);
239 return new Response(null, {
240 status: 302,
241 headers: { Location: emoji.imageUrl },
242 });
243};
244
245export const handleResetCache: RouteHandlerWithAnalytics = async (
246 request,
247 recordAnalytics,
248) => {
249 const authHeader = request.headers.get("authorization") || "";
250 if (authHeader !== `Bearer ${process.env.BEARER_TOKEN}`) {
251 await recordAnalytics(401);
252 return new Response("Unauthorized", { status: 401 });
253 }
254 const result = await cache.purgeAll();
255 await recordAnalytics(200);
256 return Response.json(result);
257};
258
259export const handleGetEssentialStats: RouteHandlerWithAnalytics = async (
260 request,
261 recordAnalytics,
262) => {
263 const url = new URL(request.url);
264 const params = new URLSearchParams(url.search);
265 const daysParam = params.get("days");
266 const days = daysParam ? parseInt(daysParam, 10) : 7;
267
268 const stats = await cache.getEssentialStats(days);
269 await recordAnalytics(200);
270 return Response.json(stats);
271};
272
273export const handleGetChartData: RouteHandlerWithAnalytics = async (
274 request,
275 recordAnalytics,
276) => {
277 const url = new URL(request.url);
278 const params = new URLSearchParams(url.search);
279 const daysParam = params.get("days");
280 const days = daysParam ? parseInt(daysParam, 10) : 7;
281
282 const chartData = await cache.getChartData(days);
283 await recordAnalytics(200);
284 return Response.json(chartData);
285};
286
287export const handleGetUserAgents: RouteHandlerWithAnalytics = async (
288 request,
289 recordAnalytics,
290) => {
291 const url = new URL(request.url);
292 const params = new URLSearchParams(url.search);
293 const daysParam = params.get("days");
294 const days = daysParam ? parseInt(daysParam, 10) : 7;
295
296 const userAgents = await cache.getUserAgents(days);
297 await recordAnalytics(200);
298 return Response.json(userAgents);
299};
300
301export const handleGetStats: RouteHandlerWithAnalytics = async (
302 request,
303 recordAnalytics,
304) => {
305 const url = new URL(request.url);
306 const params = new URLSearchParams(url.search);
307 const daysParam = params.get("days");
308 const days = daysParam ? parseInt(daysParam, 10) : 7;
309
310 const [essentialStats, chartData, userAgents] = await Promise.all([
311 cache.getEssentialStats(days),
312 cache.getChartData(days),
313 cache.getUserAgents(days),
314 ]);
315
316 await recordAnalytics(200);
317 return Response.json({
318 ...essentialStats,
319 chartData,
320 userAgents,
321 });
322};