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};