a cache for slack profile pictures and emojis
at main 8.3 kB view raw
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};