a cache for slack profile pictures and emojis
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 };