a cache for slack profile pictures and emojis
at main 171 lines 4.6 kB view raw
1import { serve } from "bun"; 2import { getEmojiUrl } from "../utils/emojiHelper"; 3import { SlackCache } from "./cache"; 4import dashboard from "./dashboard.html"; 5import { buildRoutes, getSwaggerSpec } from "./lib/route-builder"; 6import { createApiRoutes } from "./routes/api-routes"; 7import { SlackWrapper } from "./slackWrapper"; 8import swagger from "./swagger.html"; 9 10// Initialize SlackWrapper and Cache 11const slackApp = new SlackWrapper(); 12const cache = new SlackCache( 13 process.env.DATABASE_PATH ?? "./data/cachet.db", 14 25, 15 async () => { 16 console.log("Fetching emojis from Slack"); 17 const emojis = await slackApp.getEmojiList(); 18 const emojiEntries = Object.entries(emojis) 19 .map(([name, url]) => { 20 if (typeof url === "string" && url.startsWith("alias:")) { 21 const aliasName = url.substring(6); 22 const aliasUrl = emojis[aliasName] ?? getEmojiUrl(aliasName); 23 24 if (!aliasUrl) { 25 console.warn(`Could not find alias for ${aliasName}`); 26 return null; 27 } 28 29 return { 30 name, 31 imageUrl: aliasUrl, 32 alias: aliasName, 33 }; 34 } 35 return { 36 name, 37 imageUrl: url, 38 alias: null, 39 }; 40 }) 41 .filter( 42 ( 43 entry, 44 ): entry is { name: string; imageUrl: string; alias: string | null } => 45 entry !== null, 46 ); 47 48 console.log("Batch inserting emojis"); 49 await cache.batchInsertEmojis(emojiEntries); 50 console.log("Finished batch inserting emojis"); 51 }, 52); 53 54// Inject SlackWrapper into cache for background user updates 55cache.setSlackWrapper(slackApp); 56 57// Create the typed API routes with injected dependencies 58const apiRoutes = createApiRoutes(cache, slackApp); 59 60// Build Bun-compatible routes and generate Swagger 61const typedRoutes = buildRoutes(apiRoutes); 62const generatedSwagger = getSwaggerSpec(); 63 64/** 65 * Add CORS headers to response 66 */ 67function addCorsHeaders(response: Response): Response { 68 const headers = new Headers(response.headers); 69 headers.set("Access-Control-Allow-Origin", "*"); 70 headers.set( 71 "Access-Control-Allow-Methods", 72 "GET, POST, PUT, DELETE, OPTIONS", 73 ); 74 headers.set( 75 "Access-Control-Allow-Headers", 76 "Content-Type, Authorization, X-Requested-With", 77 ); 78 headers.set("Access-Control-Max-Age", "86400"); 79 80 return new Response(response.body, { 81 status: response.status, 82 statusText: response.statusText, 83 headers, 84 }); 85} 86 87// Legacy routes (non-API) 88const legacyRoutes = { 89 "/dashboard": dashboard, 90 "/swagger": swagger, 91 "/swagger.json": async (_: Request) => { 92 const response = Response.json(generatedSwagger); 93 return addCorsHeaders(response); 94 }, 95 "/favicon.ico": async (_: Request) => { 96 const response = new Response(Bun.file("./favicon.ico")); 97 return addCorsHeaders(response); 98 }, 99 100 // Root route - redirect to dashboard for browsers 101 "/": async (request: Request) => { 102 // Handle OPTIONS preflight 103 if (request.method === "OPTIONS") { 104 return new Response(null, { 105 status: 204, 106 headers: { 107 "Access-Control-Allow-Origin": "*", 108 "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", 109 "Access-Control-Allow-Headers": 110 "Content-Type, Authorization, X-Requested-With", 111 "Access-Control-Max-Age": "86400", 112 }, 113 }); 114 } 115 116 const userAgent = request.headers.get("user-agent") || ""; 117 118 if ( 119 userAgent.toLowerCase().includes("mozilla") || 120 userAgent.toLowerCase().includes("chrome") || 121 userAgent.toLowerCase().includes("safari") 122 ) { 123 const response = new Response(null, { 124 status: 302, 125 headers: { Location: "/dashboard" }, 126 }); 127 return addCorsHeaders(response); 128 } 129 130 const response = new Response( 131 "Hello World from Cachet 😊\n\n---\nSee /swagger for docs\nSee /dashboard for analytics\n---", 132 ); 133 return addCorsHeaders(response); 134 }, 135}; 136 137// Merge all routes 138const allRoutes = { 139 ...legacyRoutes, 140 ...typedRoutes, 141}; 142 143// Start the server 144const server = serve({ 145 routes: allRoutes, 146 port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, 147 development: process.env.NODE_ENV === "dev", 148}); 149 150console.log(`🚀 Server running on http://localhost:${server.port}`); 151 152// Graceful shutdown handling 153const shutdown = () => { 154 console.log("Shutting down gracefully..."); 155 cache.endUptimeSession(); 156 process.exit(0); 157}; 158 159process.on("SIGINT", shutdown); 160process.on("SIGTERM", shutdown); 161 162// Prevent unhandled errors from crashing the process 163process.on("unhandledRejection", (reason) => { 164 console.error("Unhandled promise rejection:", reason); 165}); 166 167process.on("uncaughtException", (error) => { 168 console.error("Uncaught exception:", error); 169}); 170 171export { cache, slackApp };