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