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 { version } from "../package.json";
5import { SlackCache } from "./cache";
6import { SlackWrapper } from "./slackWrapper";
7import type { SlackUser } from "./slack";
8import { getEmojiUrl } from "../utils/emojiHelper";
9
10const slackApp = new SlackWrapper();
11
12const cache = new SlackCache(
13 process.env.DATABASE_PATH ?? "./data/cachet.db",
14 24,
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); // Remove 'alias:' prefix
22 const aliasUrl = emojis[aliasName] ?? getEmojiUrl(aliasName) ?? null;
23
24 if (aliasUrl === null) {
25 console.warn(`Could not find alias for ${aliasName}`);
26 return;
27 }
28
29 return {
30 name,
31 imageUrl: aliasUrl === null ? getEmojiUrl(aliasName) : 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 !== undefined,
46 );
47
48 console.log("Batch inserting emojis");
49
50 await cache.batchInsertEmojis(emojiEntries);
51
52 console.log("Finished batch inserting emojis");
53 },
54);
55
56const app = new Elysia()
57 .use(
58 logger({
59 mode: "combined",
60 }),
61 )
62 .use(
63 swagger({
64 documentation: {
65 info: {
66 version: version,
67 title: "Cachet",
68 description:
69 "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.",
70 contact: {
71 name: "Kieran Klukas",
72 email: "me@dunkirk.sh",
73 },
74 license: {
75 name: "AGPL 3.0",
76 url: "https://github.com/taciturnaxoltol/cachet/blob/master/LICENSE.md",
77 },
78 },
79 tags: [
80 {
81 name: "The Cache!",
82 description: "*must be read in an ominous voice*",
83 },
84 {
85 name: "Status",
86 description: "*Rather boring status endpoints :(*",
87 },
88 ],
89 },
90 }),
91 )
92 .get("/", ({ redirect, headers }) => {
93 // check if its a browser
94
95 if (
96 headers["user-agent"]?.toLowerCase().includes("mozilla") ||
97 headers["user-agent"]?.toLowerCase().includes("chrome") ||
98 headers["user-agent"]?.toLowerCase().includes("safari")
99 ) {
100 return redirect("/swagger", 302);
101 }
102
103 return "Hello World from Cachet 😊\n\n---\nSee /swagger for docs\n---";
104 })
105 .get(
106 "/health",
107 async ({ error }) => {
108 const slackConnection = await slackApp.testAuth();
109
110 const databaseConnection = await cache.healthCheck();
111
112 if (!slackConnection || !databaseConnection)
113 return error(500, {
114 http: false,
115 slack: slackConnection,
116 database: databaseConnection,
117 });
118
119 return {
120 http: true,
121 slack: true,
122 database: true,
123 };
124 },
125 {
126 tags: ["Status"],
127 response: {
128 200: t.Object({
129 http: t.Boolean(),
130 slack: t.Boolean(),
131 database: t.Boolean(),
132 }),
133 500: t.Object({
134 http: t.Boolean({
135 default: false,
136 }),
137 slack: t.Boolean({
138 default: false,
139 }),
140 database: t.Boolean({
141 default: false,
142 }),
143 }),
144 },
145 },
146 )
147 .get(
148 "/users/:user",
149 async ({ params, error }) => {
150 const user = await cache.getUser(params.user);
151
152 // if not found then check slack first
153 if (!user) {
154 let slackUser: SlackUser;
155 try {
156 slackUser = await slackApp.getUserInfo(params.user);
157 } catch (e) {
158 if (e instanceof Error && e.message === "user_not_found")
159 return error(404, { message: "User not found" });
160
161 return error(500, {
162 message: `Error fetching user from Slack: ${e}`,
163 });
164 }
165
166 await cache.insertUser(slackUser.id, slackUser.profile.image_original);
167
168 return {
169 id: slackUser.id,
170 expiration: new Date().toISOString(),
171 user: slackUser.id,
172 image: slackUser.profile.image_original,
173 };
174 }
175
176 return {
177 id: user.id,
178 expiration: user.expiration.toISOString(),
179 user: user.userId,
180 image: user.imageUrl,
181 };
182 },
183 {
184 tags: ["The Cache!"],
185 params: t.Object({
186 user: t.String(),
187 }),
188 response: {
189 404: t.Object({
190 message: t.String({
191 default: "User not found",
192 }),
193 }),
194 500: t.Object({
195 message: t.String({
196 default: "Error fetching user from Slack",
197 }),
198 }),
199 200: t.Object({
200 id: t.String({
201 default: "90750e24-c2f0-4c52-8681-e6176da6e7ab",
202 }),
203 expiration: t.String({
204 default: new Date().toISOString(),
205 }),
206 user: t.String({
207 default: "U12345678",
208 }),
209 image: t.String({
210 default:
211 "https://avatars.slack-edge.com/2024-11-30/8105375749571_53898493372773a01a1f_original.jpg",
212 }),
213 }),
214 },
215 },
216 )
217 .get(
218 "/users/:user/r",
219 async ({ params, error, redirect }) => {
220 const user = await cache.getUser(params.user);
221
222 // if not found then check slack first
223 if (!user) {
224 let slackUser: SlackUser;
225 try {
226 slackUser = await slackApp.getUserInfo(params.user);
227 } catch (e) {
228 if (e instanceof Error && e.message === "user_not_found")
229 return error(404, { message: "User not found" });
230
231 return error(500, {
232 message: `Error fetching user from Slack: ${e}`,
233 });
234 }
235
236 await cache.insertUser(slackUser.id, slackUser.profile.image_original);
237
238 return redirect(slackUser.profile.image_original, 302);
239 }
240
241 return redirect(user.imageUrl, 302);
242 },
243 {
244 tags: ["The Cache!"],
245 query: t.Object({
246 r: t.Optional(t.String()),
247 }),
248 params: t.Object({
249 user: t.String(),
250 }),
251 },
252 )
253 .get(
254 "/emojis",
255 async () => {
256 const emojis = await cache.listEmojis();
257
258 return emojis.map((emoji) => ({
259 id: emoji.id,
260 expiration: emoji.expiration.toISOString(),
261 name: emoji.name,
262 ...(emoji.alias ? { alias: emoji.alias } : {}),
263 image: emoji.imageUrl,
264 }));
265 },
266 {
267 tags: ["The Cache!"],
268 response: {
269 200: t.Array(
270 t.Object({
271 id: t.String({
272 default: "5427fe70-686f-4684-9da5-95d9ef4c1090",
273 }),
274 expiration: t.String({
275 default: new Date().toISOString(),
276 }),
277 name: t.String({
278 default: "blahaj-heart",
279 }),
280 alias: t.Optional(
281 t.String({
282 default: "blobhaj-heart",
283 }),
284 ),
285 image: t.String({
286 default:
287 "https://emoji.slack-edge.com/T0266FRGM/blahaj-heart/db9adf8229e9a4fb.png",
288 }),
289 }),
290 ),
291 },
292 },
293 )
294 .get(
295 "/emojis/:emoji",
296 async ({ params, error }) => {
297 const emoji = await cache.getEmoji(params.emoji);
298
299 if (!emoji) return error(404, { message: "Emoji not found" });
300
301 return {
302 id: emoji.id,
303 expiration: emoji.expiration.toISOString(),
304 name: emoji.name,
305 ...(emoji.alias ? { alias: emoji.alias } : {}),
306 image: emoji.imageUrl,
307 };
308 },
309 {
310 tags: ["The Cache!"],
311 params: t.Object({
312 emoji: t.String(),
313 }),
314 response: {
315 404: t.Object({
316 message: t.String({
317 default: "Emoji not found",
318 }),
319 }),
320 200: t.Object({
321 id: t.String({
322 default: "9ed0a560-928d-409c-89fc-10fe156299da",
323 }),
324 expiration: t.String({
325 default: new Date().toISOString(),
326 }),
327 name: t.String({
328 default: "orphmoji-yay",
329 }),
330 image: t.String({
331 default:
332 "https://emoji.slack-edge.com/T0266FRGM/orphmoji-yay/23a37f4af47092d3.png",
333 }),
334 }),
335 },
336 },
337 )
338 .get(
339 "/emojis/:emoji/r",
340 async ({ params, error, redirect }) => {
341 const emoji = await cache.getEmoji(params.emoji);
342
343 if (!emoji) return error(404, { message: "Emoji not found" });
344
345 return redirect(emoji.imageUrl, 302);
346 },
347 {
348 tags: ["The Cache!"],
349 params: t.Object({
350 emoji: t.String(),
351 }),
352 },
353 )
354 .listen(process.env.PORT ?? 3000);
355
356console.log(
357 `\n---\n\n🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port} at v${version}\n\n---\n`,
358);