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