a cache for slack profile pictures and emojis
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186import { serve } from "bun";import faviconFile from "../favicon.ico";import { getEmojiUrl } from "../utils/emojiHelper";import { SlackCache } from "./cache";import { config } from "./config";import dashboard from "./dashboard.html";import { addCorsHeaders, corsPreflightResponse } from "./lib/cors";import { createFallbackHandler, internalErrorResponse,} from "./lib/http-errors";import { swaggerGenerator } from "./lib/swagger-generator";import { createApiRoutes } from "./routes/api-routes";import { SlackWrapper } from "./slackWrapper";import swagger from "./swagger.html";import type { RouteDefinition } from "./types/routes";
// Initialize SlackWrapper and Cacheconst slackApp = new SlackWrapper({ signingSecret: config.slack.signingSecret, botToken: config.slack.botToken, maxConcurrent: config.slack.maxConcurrent, minTimeMs: config.slack.minTimeMs, requestTimeoutMs: config.slack.requestTimeoutMs,});const cache = new SlackCache(config.databasePath, 25, async () => { console.log("Fetching emojis from Slack"); const emojis = await slackApp.getEmojiList(); // A Map keeps aliases named `constructor` from resolving off Object.prototype const emojisByName = new Map(Object.entries(emojis)); const emojiEntries = Object.entries(emojis) .map(([name, url]) => { if (typeof url === "string" && url.startsWith("alias:")) { const aliasName = url.substring(6); const aliasUrl = emojisByName.get(aliasName) ?? getEmojiUrl(aliasName);
if (!aliasUrl) { console.warn(`Could not find alias for ${aliasName}`); return null; }
return { name, imageUrl: aliasUrl, alias: aliasName, }; } return { name, imageUrl: url, alias: null, }; }) .filter( ( entry, ): entry is { name: string; imageUrl: string; alias: string | null } => entry !== null, );
console.log("Batch inserting emojis"); await cache.batchInsertEmojis(emojiEntries); console.log("Finished batch inserting emojis");});
// Inject SlackWrapper into cache for background user updatescache.setSlackWrapper(slackApp);
// Create the typed API routes with injected dependenciesconst apiRoutes = createApiRoutes(cache, slackApp);
// Generate Swagger and convert typed routes to Bun formatswaggerGenerator.addRoutes(apiRoutes as Record<string, RouteDefinition>);const generatedSwagger = swaggerGenerator.getSpec();
const typedRoutes: Record< string, Record<string, (request: Request) => Promise<Response> | Response>> = {};for (const [path, routeConfig] of Object.entries( apiRoutes as Record<string, RouteDefinition>,)) { const bunRoute: Record< string, (request: Request) => Promise<Response> | Response > = {}; for (const [method, typedRoute] of Object.entries(routeConfig)) { if (typedRoute && "handler" in typedRoute) { bunRoute[method] = typedRoute.handler; } } typedRoutes[path] = bunRoute;}
// Legacy routes (non-API)const legacyRoutes = { "/dashboard": dashboard, "/swagger": swagger, "/swagger.json": async (_: Request) => { const response = Response.json(generatedSwagger); return addCorsHeaders(response); }, "/favicon.ico": async (_: Request) => { const response = new Response(Bun.file(faviconFile)); return addCorsHeaders(response); },
// Root route - redirect to dashboard for browsers "/": async (request: Request) => { // Handle OPTIONS preflight if (request.method === "OPTIONS") { return corsPreflightResponse(); }
const userAgent = request.headers.get("user-agent") || "";
if ( userAgent.toLowerCase().includes("mozilla") || userAgent.toLowerCase().includes("chrome") || userAgent.toLowerCase().includes("safari") ) { const response = new Response(null, { status: 302, headers: { Location: "/dashboard" }, }); return addCorsHeaders(response); }
const response = new Response( "Hello World from Cachet 😊\n\n---\nSee /swagger for docs\nSee /dashboard for analytics\n---", ); return addCorsHeaders(response); },};
// Merge all routesconst allRoutes = { ...legacyRoutes, ...typedRoutes,};
const fallbackHandler = createFallbackHandler(allRoutes);
// Start the serverconst server = serve({ routes: allRoutes, fetch(request) { if (request.method === "OPTIONS") return corsPreflightResponse(); return addCorsHeaders(fallbackHandler(request)); }, error(error) { console.error("Unhandled request error:", error); return addCorsHeaders(internalErrorResponse()); }, port: config.port, development: config.development,});
console.log(`🚀 Server running on http://localhost:${server.port}`);
// Graceful shutdown handlinglet shuttingDown = false;const shutdown = () => { if (shuttingDown) return; shuttingDown = true; console.log("Shutting down gracefully..."); server.stop(); cache.close(); console.log("Shutdown complete"); process.exit(0);};
process.on("SIGINT", shutdown);process.on("SIGTERM", shutdown);
process.on("unhandledRejection", (reason) => { console.error("Unhandled promise rejection:", reason);});
process.on("uncaughtException", (error) => { console.error("Uncaught exception:", error); process.exit(1);});
export { cache, slackApp };