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