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);