a cache for slack profile pictures and emojis
1import { Elysia, t } from "elysia";
2import { logger } from "@tqman/nice-logger";
3import { swagger } from "@elysiajs/swagger";
4import { cors } from "@elysiajs/cors";
5import { version } from "../package.json";
6import { SlackCache } from "./cache";
7import { SlackWrapper } from "./slackWrapper";
8import type { SlackUser } from "./slack";
9import { getEmojiUrl } from "../utils/emojiHelper";
10import * as Sentry from "@sentry/bun";
11
12if (process.env.SENTRY_DSN) {
13 console.log("Sentry DSN provided, error monitoring is enabled");
14 Sentry.init({
15 dsn: process.env.SENTRY_DSN, // Replace with your Sentry DSN
16 tracesSampleRate: 1.0, // Adjust this value for performance monitoring
17 });
18} else {
19 console.warn("Sentry DSN not provided, error monitoring is disabled");
20}
21
22const slackApp = new SlackWrapper();
23
24const cache = new SlackCache(
25 process.env.DATABASE_PATH ?? "./data/cachet.db",
26 24,
27 async () => {
28 console.log("Fetching emojis from Slack");
29 const emojis = await slackApp.getEmojiList();
30 const emojiEntries = Object.entries(emojis)
31 .map(([name, url]) => {
32 if (typeof url === "string" && url.startsWith("alias:")) {
33 const aliasName = url.substring(6); // Remove 'alias:' prefix
34 const aliasUrl = emojis[aliasName] ?? getEmojiUrl(aliasName) ?? null;
35
36 if (aliasUrl === null) {
37 console.warn(`Could not find alias for ${aliasName}`);
38 return;
39 }
40
41 return {
42 name,
43 imageUrl: aliasUrl === null ? getEmojiUrl(aliasName) : aliasUrl,
44 alias: aliasName,
45 };
46 }
47 return {
48 name,
49 imageUrl: url,
50 alias: null,
51 };
52 })
53 .filter(
54 (
55 entry,
56 ): entry is { name: string; imageUrl: string; alias: string | null } =>
57 entry !== undefined,
58 );
59
60 console.log("Batch inserting emojis");
61
62 await cache.batchInsertEmojis(emojiEntries);
63
64 console.log("Finished batch inserting emojis");
65 },
66);
67
68const app = new Elysia()
69 .use(
70 logger({
71 mode: "combined",
72 }),
73 )
74 .use(cors())
75 .use(
76 swagger({
77 documentation: {
78 info: {
79 version: version,
80 title: "Cachet",
81 description:
82 "Hi 👋\n\nThis is a pretty simple API that acts as a middleman caching layer between slack and the outside world. There may be authentication in the future, but for now, it's just a simple cache.\n\nThe `/r` endpoints are redirects to the actual image URLs, so you can use them as direct image links.",
83 contact: {
84 name: "Kieran Klukas",
85 email: "me@dunkirk.sh",
86 },
87 license: {
88 name: "AGPL 3.0",
89 url: "https://github.com/taciturnaxoltol/cachet/blob/master/LICENSE.md",
90 },
91 },
92 tags: [
93 {
94 name: "The Cache!",
95 description: "*must be read in an ominous voice*",
96 },
97 {
98 name: "Status",
99 description: "*Rather boring status endpoints :(*",
100 },
101 ],
102 },
103 }),
104 )
105 .onError(({ code, error }) => {
106 console.error(error);
107 Sentry.captureException(error);
108 if (code === "VALIDATION") {
109 return error.message;
110 }
111 })
112 .get("/", ({ redirect, headers }) => {
113 // check if its a browser
114
115 if (
116 headers["user-agent"]?.toLowerCase().includes("mozilla") ||
117 headers["user-agent"]?.toLowerCase().includes("chrome") ||
118 headers["user-agent"]?.toLowerCase().includes("safari")
119 ) {
120 return redirect("/swagger", 302);
121 }
122
123 return "Hello World from Cachet 😊\n\n---\nSee /swagger for docs\n---";
124 })
125 .get(
126 "/health",
127 async ({ error }) => {
128 const slackConnection = await slackApp.testAuth();
129
130 const databaseConnection = await cache.healthCheck();
131
132 if (!slackConnection || !databaseConnection)
133 return error(500, {
134 http: false,
135 slack: slackConnection,
136 database: databaseConnection,
137 });
138
139 return {
140 http: true,
141 slack: true,
142 database: true,
143 };
144 },
145 {
146 tags: ["Status"],
147 response: {
148 200: t.Object({
149 http: t.Boolean(),
150 slack: t.Boolean(),
151 database: t.Boolean(),
152 }),
153 500: t.Object({
154 http: t.Boolean({
155 default: false,
156 }),
157 slack: t.Boolean({
158 default: false,
159 }),
160 database: t.Boolean({
161 default: false,
162 }),
163 }),
164 },
165 },
166 )
167 .get(
168 "/users/:user",
169 async ({ params, error }) => {
170 const user = await cache.getUser(params.user);
171
172 // if not found then check slack first
173 if (!user) {
174 let slackUser: SlackUser;
175 try {
176 slackUser = await slackApp.getUserInfo(params.user);
177 } catch (e) {
178 if (e instanceof Error && e.message === "user_not_found")
179 return error(404, { message: "User not found" });
180
181 Sentry.captureException(e);
182
183 if (e instanceof Error)
184 console.warn(
185 `\x1b[38;5;214m ⚠️ WARN\x1b[0m error on fetching user from slack: \x1b[38;5;208m${e.message}\x1b[0m`,
186 );
187
188 return error(500, {
189 message: `Error fetching user from Slack: ${e}`,
190 });
191 }
192
193 await cache.insertUser(slackUser.id, slackUser.profile.image_original);
194
195 return {
196 id: slackUser.id,
197 expiration: new Date().toISOString(),
198 user: slackUser.id,
199 image: slackUser.profile.image_original,
200 };
201 }
202
203 return {
204 id: user.id,
205 expiration: user.expiration.toISOString(),
206 user: user.userId,
207 image: user.imageUrl,
208 };
209 },
210 {
211 tags: ["The Cache!"],
212 params: t.Object({
213 user: t.String(),
214 }),
215 response: {
216 404: t.Object({
217 message: t.String({
218 default: "User not found",
219 }),
220 }),
221 500: t.Object({
222 message: t.String({
223 default: "Error fetching user from Slack",
224 }),
225 }),
226 200: t.Object({
227 id: t.String({
228 default: "90750e24-c2f0-4c52-8681-e6176da6e7ab",
229 }),
230 expiration: t.String({
231 default: new Date().toISOString(),
232 }),
233 user: t.String({
234 default: "U12345678",
235 }),
236 image: t.String({
237 default:
238 "https://avatars.slack-edge.com/2024-11-30/8105375749571_53898493372773a01a1f_original.jpg",
239 }),
240 }),
241 },
242 },
243 )
244 .get(
245 "/users/:user/r",
246 async ({ params, error, redirect }) => {
247 const user = await cache.getUser(params.user);
248
249 // if not found then check slack first
250 if (!user) {
251 let slackUser: SlackUser;
252 try {
253 slackUser = await slackApp.getUserInfo(params.user);
254 } catch (e) {
255 if (e instanceof Error && e.message === "user_not_found")
256 return error(404, { message: "User not found" });
257
258 Sentry.captureException(e);
259
260 if (e instanceof Error)
261 console.warn(
262 `\x1b[38;5;214m ⚠️ WARN\x1b[0m error on fetching user from slack: \x1b[38;5;208m${e.message}\x1b[0m`,
263 );
264
265 return error(500, {
266 message: `Error fetching user from Slack: ${e}`,
267 });
268 }
269
270 await cache.insertUser(slackUser.id, slackUser.profile.image_original);
271
272 return redirect(slackUser.profile.image_original, 302);
273 }
274
275 return redirect(user.imageUrl, 302);
276 },
277 {
278 tags: ["The Cache!"],
279 query: t.Object({
280 r: t.Optional(t.String()),
281 }),
282 params: t.Object({
283 user: t.String(),
284 }),
285 },
286 )
287 .get(
288 "/emojis",
289 async () => {
290 const emojis = await cache.listEmojis();
291
292 return emojis.map((emoji) => ({
293 id: emoji.id,
294 expiration: emoji.expiration.toISOString(),
295 name: emoji.name,
296 ...(emoji.alias ? { alias: emoji.alias } : {}),
297 image: emoji.imageUrl,
298 }));
299 },
300 {
301 tags: ["The Cache!"],
302 response: {
303 200: t.Array(
304 t.Object({
305 id: t.String({
306 default: "5427fe70-686f-4684-9da5-95d9ef4c1090",
307 }),
308 expiration: t.String({
309 default: new Date().toISOString(),
310 }),
311 name: t.String({
312 default: "blahaj-heart",
313 }),
314 alias: t.Optional(
315 t.String({
316 default: "blobhaj-heart",
317 }),
318 ),
319 image: t.String({
320 default:
321 "https://emoji.slack-edge.com/T0266FRGM/blahaj-heart/db9adf8229e9a4fb.png",
322 }),
323 }),
324 ),
325 },
326 },
327 )
328 .get(
329 "/emojis/:emoji",
330 async ({ params, error }) => {
331 const emoji = await cache.getEmoji(params.emoji);
332
333 if (!emoji) return error(404, { message: "Emoji not found" });
334
335 return {
336 id: emoji.id,
337 expiration: emoji.expiration.toISOString(),
338 name: emoji.name,
339 ...(emoji.alias ? { alias: emoji.alias } : {}),
340 image: emoji.imageUrl,
341 };
342 },
343 {
344 tags: ["The Cache!"],
345 params: t.Object({
346 emoji: t.String(),
347 }),
348 response: {
349 404: t.Object({
350 message: t.String({
351 default: "Emoji not found",
352 }),
353 }),
354 200: t.Object({
355 id: t.String({
356 default: "9ed0a560-928d-409c-89fc-10fe156299da",
357 }),
358 expiration: t.String({
359 default: new Date().toISOString(),
360 }),
361 name: t.String({
362 default: "orphmoji-yay",
363 }),
364 image: t.String({
365 default:
366 "https://emoji.slack-edge.com/T0266FRGM/orphmoji-yay/23a37f4af47092d3.png",
367 }),
368 }),
369 },
370 },
371 )
372 .get(
373 "/emojis/:emoji/r",
374 async ({ params, error, redirect }) => {
375 const emoji = await cache.getEmoji(params.emoji);
376
377 if (!emoji) return error(404, { message: "Emoji not found" });
378
379 return redirect(emoji.imageUrl, 302);
380 },
381 {
382 tags: ["The Cache!"],
383 params: t.Object({
384 emoji: t.String(),
385 }),
386 },
387 )
388 .listen(process.env.PORT ?? 3000);
389
390console.log(
391 `\n---\n\n🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port} at v${version}\n\n---\n`,
392);