a cache for slack profile pictures and emojis

chore: docs and lint

dunkirk.sh c851c8d5 949d9a2b

verified
.188625fb7dd457ed-00000000.bun-build

This is a binary file and will not be displayed.

.188625fbf9fffb7a-00000000.bun-build

This is a binary file and will not be displayed.

+13 -7
README.md
··· 23 23 Your `.env` file should look like this: 24 24 25 25 ```bash 26 - SLACK_TOKEN=xoxb-123456789012-123456789012-123456789012-123456789012 26 + # Either SLACK_BOT_TOKEN or SLACK_TOKEN works (SLACK_BOT_TOKEN takes precedence) 27 + SLACK_BOT_TOKEN=xoxb-your-bot-token-here 27 28 SLACK_SIGNING_SECRET=12345678901234567890123456789012 28 29 NODE_ENV=production 30 + BEARER_TOKEN=your-secure-random-token-here # Required for admin endpoints 29 31 SENTRY_DSN="https://xxxxx@xxxx.ingest.us.sentry.io/123456" # Optional 30 32 DATABASE_PATH=/path/to/db.sqlite # Optional 31 33 PORT=3000 # Optional 34 + 35 + # Optional: Slack rate limiting (adjust if hitting rate limits) 36 + # SLACK_MAX_CONCURRENT=3 # Max concurrent requests (default: 3) 37 + # SLACK_MIN_TIME_MS=200 # Min ms between requests (default: 200) 32 38 ``` 33 39 34 40 The slack app can be created from the [`manifest.yaml`](./manifest.yaml) in this repo. It just needs the `emoji:read` and `users:read` scopes. ··· 128 134 // Analytics data access 129 135 const stats = await cache.getEssentialStats(7); 130 136 const chartData = await cache.getChartData(7); 131 - const userAgents = await cache.getUserAgents(7); 137 + const userAgents = await cache.getUserAgents(); 132 138 ``` 133 139 134 140 The final bit was at this point a bit of a ridiculous one. I didn't like how heavyweight the `bolt` or `slack-edge` packages were so I rolled my own slack api wrapper. It's again fully typed and designed to be as lightweight as possible. The background user update queue processes up to 3 users every 30 seconds to respect Slack's rate limits. 135 141 136 142 ```typescript 137 - const slack = new Slack( 138 - process.env.SLACK_TOKEN, 143 + const slack = new SlackWrapper( 144 + process.env.SLACK_BOT_TOKEN, 139 145 process.env.SLACK_SIGNING_SECRET, 140 146 ); 141 147 142 - const user = await slack.getUser("U062UG485EE"); 143 - const emojis = await slack.getEmoji(); 148 + const user = await slack.getUserInfo("U062UG485EE"); 149 + const emojis = await slack.getEmojiList(); 144 150 145 151 // Manually purge a specific user's cache using the API endpoint 146 152 const response = await fetch( ··· 169 175 ```typescript 170 176 // src/migrations/myNewMigration.ts 171 177 import { Database } from "bun:sqlite"; 172 - import { Migration } from "./types"; 178 + import type { Migration } from "./types"; 173 179 174 180 export const myNewMigration: Migration = { 175 181 version: "0.3.2", // Should match package.json version
+5 -5
src/cache.ts
··· 1216 1216 averageResponseTime: number; 1217 1217 }>; 1218 1218 averageResponseTime: number | null; 1219 - topUserAgents: Array<{ userAgent: string; count: number }>; 1219 + topUserAgents: Array<{ userAgent: string; hits: number }>; 1220 1220 latencyAnalytics: { 1221 1221 percentiles: { 1222 1222 p50: number | null; ··· 1418 1418 const topUserAgents = this.db 1419 1419 .query( 1420 1420 ` 1421 - SELECT user_agent as userAgent, hits as count 1421 + SELECT user_agent as userAgent, hits 1422 1422 FROM user_agent_stats 1423 1423 WHERE user_agent IS NOT NULL 1424 1424 ORDER BY hits DESC 1425 1425 LIMIT 50 1426 1426 `, 1427 1427 ) 1428 - .all() as Array<{ userAgent: string; count: number }>; 1428 + .all() as Array<{ userAgent: string; hits: number }>; 1429 1429 1430 1430 // Simplified latency analytics from bucket data 1431 1431 const percentiles = { ··· 1603 1603 peakDayRequests: peakDayData?.count || 0, 1604 1604 }, 1605 1605 dashboardMetrics: { 1606 - statsRequests: statsResult.count, 1607 - totalWithStats: totalCount + statsResult.count, 1606 + statsRequests: statsResult.count ?? 0, 1607 + totalWithStats: totalCount + (statsResult.count ?? 0), 1608 1608 }, 1609 1609 trafficOverview, 1610 1610 };
+2 -2
src/migrations/index.ts
··· 1 1 import { bucketAnalyticsMigration } from "./bucketAnalyticsMigration"; 2 2 import { endpointGroupingMigration } from "./endpointGroupingMigration"; 3 3 import { logGroupingMigration } from "./logGroupingMigration"; 4 - import { Migration } from "./types"; 4 + import type { Migration } from "./types"; 5 5 6 6 // Export all migrations 7 7 export const migrations = [ ··· 12 12 ]; 13 13 14 14 // Export the migration types 15 - export { Migration }; 15 + export type { Migration };
+7 -4
src/migrations/migrationManager.ts
··· 208 208 if (parts.length !== 3) return null; 209 209 210 210 const [major, minor, patch] = parts.map(Number); 211 + if (major === undefined || minor === undefined || patch === undefined) { 212 + return null; 213 + } 211 214 212 215 // If patch > 0, decrement patch 213 216 if (patch > 0) { 214 217 return `${major}.${minor}.${patch - 1}`; 215 218 } 216 219 // If minor > 0, decrement minor and set patch to 0 217 - else if (minor > 0) { 220 + if (minor > 0) { 218 221 return `${major}.${minor - 1}.0`; 219 222 } 220 223 // If major > 0, decrement major and set minor and patch to 0 221 - else if (major > 0) { 224 + if (major > 0) { 222 225 return `${major - 1}.0.0`; 223 226 } 224 227 ··· 236 239 const partsB = b.split(".").map(Number); 237 240 238 241 for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { 239 - const partA = i < partsA.length ? partsA[i] : 0; 240 - const partB = i < partsB.length ? partsB[i] : 0; 242 + const partA = partsA[i] ?? 0; 243 + const partB = partsB[i] ?? 0; 241 244 242 245 if (partA < partB) return -1; 243 246 if (partA > partB) return 1;