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