this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: move back to sqlite and implement verification in a routing / handler manner

+215 -120
+51
bun.lock
··· 4 4 "": { 5 5 "name": "takes", 6 6 "dependencies": { 7 + "@libsql/client": "^0.15.4", 7 8 "@sentry/bun": "^9.10.1", 8 9 "@types/pg": "^8.11.13", 9 10 "bottleneck": "^2.19.5", ··· 79 80 80 81 "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.3", "", { "os": "win32", "cpu": "x64" }, "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg=="], 81 82 83 + "@libsql/client": ["@libsql/client@0.15.4", "", { "dependencies": { "@libsql/core": "^0.15.4", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.6", "promise-limit": "^2.7.0" } }, "sha512-m8a7giWlhLdfKVIZFd3UlBptWTS+H0toSOL09BxbqzBeFHwuVC+5ewyi4LMBxoy2TLNQGE4lO8cwpsTWmu695w=="], 84 + 85 + "@libsql/core": ["@libsql/core@0.15.4", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-NMvh6xnn3vrcd7DNehj0HiJcRWB2a8hHhJUTkOBej3Pf3KB21HOmdOUjXxJ5pGbjWXh4ezQBmHtF5ozFhocXaA=="], 86 + 87 + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3rex2cdEihYY5HIxOvn66CxdvEtjQDXyrX+EggaV3Qf4vZOWeow+jIqVjZoNLmfb7AwJuSL088G645zyMitcbQ=="], 88 + 89 + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-OEJPm0G+s+MZhBpf5TNTj9ODRPwIiBjtrHJoYNsGm0fPZM62wdHryLAGqu6gl4IuQ+e4GzC3c5c8J0lu4u6p+A=="], 90 + 91 + "@libsql/hrana-client": ["@libsql/hrana-client@0.7.0", "", { "dependencies": { "@libsql/isomorphic-fetch": "^0.3.1", "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw=="], 92 + 93 + "@libsql/isomorphic-fetch": ["@libsql/isomorphic-fetch@0.3.1", "", {}, "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw=="], 94 + 95 + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], 96 + 97 + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-86PusgAOzxu8e/8YCxwdbi8/6HkBRWW49FCXgAqVjlDaNC/08jSQy7XnIZuRqHYRNg9uXk3fZ+Nug/6kbEqH5Q=="], 98 + 99 + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-KSNrF4wd7xqQQYx12PrF808QhX5oL+EwvzippvDs/NNJWSM035AwCG0z2t7tqNVTZWaN6MVuRGbMlhQDCFj66w=="], 100 + 101 + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-gMHAKtn2UuIJdFU1DeHD2CgIEhT5OZFJtyyTFXNVs2A4VZmjlOmss+QVANO59cO4/3E4MnJQ1KyJF4fbBNp9fw=="], 102 + 103 + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-qh6nLCZ+YI6fKRCR525Ryw/LHd7vNvi/A7hhPtlwewYkxUumNsz7t9kwTlhXFlYmBNmOYP/FfaP+vj5CHvZvrQ=="], 104 + 105 + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.7", "", { "os": "win32", "cpu": "x64" }, "sha512-66c5bN2UF6rnuvzxgRLRkrAXMlv2aNYivPjMa175M0IVbFB3XQl1joOL2hO4/HxijL5xHf7xYJxs7X6HvpiG7Q=="], 106 + 107 + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], 108 + 82 109 "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], 83 110 84 111 "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="], ··· 171 198 172 199 "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], 173 200 201 + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 202 + 174 203 "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], 175 204 176 205 "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], ··· 185 214 186 215 "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="], 187 216 217 + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], 218 + 188 219 "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 220 + 221 + "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], 189 222 190 223 "drizzle-kit": ["drizzle-kit@0.31.0", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.2", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-pcKVT+GbfPA+bUovPIilgVOoq+onNBo/YQBG86sf3/GFHkN6lRJPm1l7dKN0IMAk57RQoIm4GUllRrasLlcaSg=="], 191 224 ··· 195 228 196 229 "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], 197 230 231 + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], 232 + 233 + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], 234 + 198 235 "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], 199 236 200 237 "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], ··· 207 244 208 245 "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], 209 246 247 + "js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="], 248 + 249 + "libsql": ["libsql@0.5.7", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.7", "@libsql/darwin-x64": "0.5.7", "@libsql/linux-arm64-gnu": "0.5.7", "@libsql/linux-arm64-musl": "0.5.7", "@libsql/linux-x64-gnu": "0.5.7", "@libsql/linux-x64-musl": "0.5.7", "@libsql/win32-x64-msvc": "0.5.7" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-YbWW3YsGZl6PrBRNAtUVuMLqKRqYcYPa/QqhQCuR67SlcOHIhJ11ksTLAHxXkDRJs6kSrzr9JgGJGJQ91e1Bww=="], 250 + 210 251 "module-details-from-path": ["module-details-from-path@1.0.3", "", {}, "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A=="], 211 252 212 253 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 254 + 255 + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], 256 + 257 + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], 213 258 214 259 "obuf": ["obuf@1.1.2", "", {}, "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="], 215 260 ··· 243 288 244 289 "postgres-range": ["postgres-range@1.1.4", "", {}, "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w=="], 245 290 291 + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], 292 + 246 293 "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], 247 294 248 295 "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], ··· 268 315 "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 269 316 270 317 "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 318 + 319 + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], 320 + 321 + "ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="], 271 322 272 323 "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], 273 324
+2 -11
drizzle.config.ts
··· 1 1 import type { Config } from "drizzle-kit"; 2 2 3 - // Parse connection string from environment variable 4 - const databaseUrl = process.env.DATABASE_URL || ""; 5 - const url = new URL(databaseUrl); 6 - 7 3 export default { 8 4 schema: "./src/libs/schema.ts", 9 5 out: "./migrations", 10 - dialect: "postgresql", 6 + dialect: "sqlite", 11 7 dbCredentials: { 12 - host: url.hostname, 13 - port: Number.parseInt(url.port), 14 - user: url.username, 15 - password: url.password, 16 - database: url.pathname.slice(1), 17 - ssl: url.searchParams.get("sslmode") === "require", 8 + url: "./local.db", 18 9 }, 19 10 } satisfies Config;
+1
package.json
··· 21 21 "typescript": "^5" 22 22 }, 23 23 "dependencies": { 24 + "@libsql/client": "^0.15.4", 24 25 "@sentry/bun": "^9.10.1", 25 26 "@types/pg": "^8.11.13", 26 27 "bottleneck": "^2.19.5",
+60 -67
src/features/handler/linking.ts
··· 4 4 import { generatePassphrase } from "../../libs/words"; 5 5 import { getUser } from "../../libs/hackernews"; 6 6 7 - export async function linkUserSetup() { 8 - try { 9 - slackApp.command( 10 - "/hn-alerts-link", 11 - () => Promise.resolve(), 12 - async ({ payload, context }) => { 13 - const userInput = payload.text?.trim() || null; 14 - let hnUsername = userInput; 7 + // Helper functions for each command action 8 + async function handleLinkRequest( 9 + userId: string, 10 + userInput: string | null, 11 + ): Promise<string> { 12 + let hnUsername = userInput; 15 13 16 - const userFromDB = await db 17 - .select() 18 - .from(usersTable) 19 - .where(eq(usersTable.id, payload.user_id)) 20 - .then((user) => user[0]); 14 + // Extract username from URL if provided 15 + if (userInput?.includes("news.ycombinator.com/user?id=")) { 16 + try { 17 + const cleanedInput = userInput.replace(/[<>]/g, ""); 18 + const username = new URL(cleanedInput).searchParams.get("id"); 19 + if (username) hnUsername = username; 20 + } catch (e) { 21 + console.log("Failed to parse URL, using raw input", e); 22 + } 23 + } 21 24 22 - if (userFromDB) { 23 - if (!userFromDB.verified) { 24 - const res = await getUser( 25 - userFromDB.hackernewsUsername as string, 26 - ).then((user) => 27 - user?.about?.includes(userFromDB.challenge as string), 28 - ); 25 + if (!hnUsername) { 26 + return "Please provide your Hacker News username: `/hn-alerts-link your_username`"; 27 + } 29 28 30 - if (!res) { 31 - await context.respond({ 32 - text: `Your Hacker News account is not verified. Add \`${userFromDB.challenge}\` to your <https://news.ycombinator.com/user?id=${userFromDB.hackernewsUsername}|profile>.`, 33 - response_type: "ephemeral", 34 - }); 35 - return; 36 - } 29 + const verificationPhrase = generatePassphrase(3); 37 30 38 - await db.update(usersTable).set({ verified: true }); 31 + await db.insert(usersTable).values({ 32 + id: userId, 33 + hackernewsUsername: hnUsername, 34 + challenge: verificationPhrase, 35 + }); 36 + 37 + return `Please verify your Hacker News username: <https://news.ycombinator.com/user?id=${hnUsername}|\`${hnUsername}\`> by adding the verification phrase: \`${verificationPhrase}\`. When you're done, type \`/hn-alerts-link verify\` to complete the process.`; 38 + } 39 + 40 + async function handleVerify( 41 + userId: string, 42 + hackernewsUsername: string, 43 + challenge: string, 44 + ): Promise<string> { 45 + const res = await getUser(hackernewsUsername as string).then((user) => 46 + user?.about?.includes(challenge as string), 47 + ); 39 48 40 - await context.respond({ 41 - text: "Your Hacker News account has been verified :yay:", 42 - response_type: "ephemeral", 43 - }); 44 - return; 45 - } 49 + if (!res) { 50 + return `Your Hacker News account is not verified. Add \`${challenge}\` to your <https://news.ycombinator.com/user?id=${hackernewsUsername}|profile>.`; 51 + } 46 52 47 - await context.respond({ 48 - text: "You are already linked to a Hacker News account.", 49 - response_type: "ephemeral", 50 - }); 51 - return; 52 - } 53 + await db 54 + .update(usersTable) 55 + .set({ verified: true }) 56 + .where(eq(usersTable.id, userId)); 53 57 54 - // Extract username from URL if provided 55 - if (userInput?.includes("news.ycombinator.com/user?id=")) { 56 - try { 57 - const cleanedInput = userInput.replace(/[<>]/g, ""); 58 - const username = new URL(cleanedInput).searchParams.get("id"); 59 - if (username) hnUsername = username; 60 - } catch (e) { 61 - console.log("Failed to parse URL, using raw input", e); 62 - } 63 - } 58 + return "Your Hacker News account has been verified :yay:"; 59 + } 64 60 65 - const verificationPhrase = generatePassphrase(3); 61 + async function handleUnlink(userId: string): Promise<string> { 62 + await db.delete(usersTable).where(eq(usersTable.id, userId)); 66 63 67 - await db.insert(usersTable).values({ 68 - id: payload.user_id, 69 - hackernewsUsername: hnUsername, 70 - challenge: verificationPhrase, 71 - }); 64 + return "Your Hacker News account has been unlinked successfully."; 65 + } 72 66 73 - await context.respond({ 74 - text: hnUsername 75 - ? `Please verify your Hacker News username: <https://news.ycombinator.com/user?id=${hnUsername}|\`${hnUsername}\`> by adding the verification phrase: \`${verificationPhrase}\`. When you're done, type \`/hn-alerts-link\` to complete the process.` 76 - : "Please provide your Hacker News username: `/hn-alerts-link your_username`", 77 - response_type: "ephemeral", 78 - }); 79 - }, 80 - ); 81 - } catch (error) { 82 - console.error("Error setting up linking", error); 83 - } 67 + function handleHelp(): string { 68 + return ( 69 + "Available commands:\n" + 70 + "• `/hn-alerts-link your_username` - Link your account\n" + 71 + "• `/hn-alerts-link verify` - Verify your Hacker News account\n" + 72 + "• `/hn-alerts-link unlink` - Remove your linked account\n" + 73 + "• `/hn-alerts-link help` - Show this help message" 74 + ); 84 75 } 76 + 77 + export { handleVerify, handleUnlink, handleHelp, handleLinkRequest };
+2 -2
src/features/index.ts
··· 1 - import { linkUserSetup } from "./handler/linking"; 1 + import { commandSetup } from "./routing/commad"; 2 2 3 3 export default async function setup() { 4 - linkUserSetup(); 4 + commandSetup(); 5 5 }
+86
src/features/routing/commad.ts
··· 1 + import { eq } from "drizzle-orm"; 2 + import { slackApp, db } from "../../index"; 3 + import { users as usersTable } from "../../libs/schema"; 4 + import { 5 + handleVerify, 6 + handleUnlink, 7 + handleHelp, 8 + handleLinkRequest, 9 + } from "../handler/linking"; 10 + 11 + export async function commandSetup() { 12 + try { 13 + slackApp.command( 14 + "/hn-alerts-link", 15 + () => Promise.resolve(), 16 + async ({ payload, context }) => { 17 + const input = payload.text?.trim() || ""; 18 + const userId = payload.user_id; 19 + const command = input.split(" ")[0]?.toLowerCase() ?? ""; 20 + 21 + const userFromDB = await db 22 + .select() 23 + .from(usersTable) 24 + .where(eq(usersTable.id, userId)) 25 + .then((user) => user[0]); 26 + 27 + let responseText = ""; 28 + 29 + // Handle commands using a switch statement 30 + switch (command) { 31 + case "verify": 32 + if (!userFromDB) { 33 + responseText = 34 + "You don't have a pending verification. Use `/hn-alerts-link your_username` first."; 35 + } else if (userFromDB.verified) { 36 + responseText = "Your account is already verified."; 37 + } else { 38 + responseText = await handleVerify( 39 + userId, 40 + userFromDB.hackernewsUsername as string, 41 + userFromDB.challenge as string, 42 + ); 43 + } 44 + break; 45 + 46 + case "unlink": 47 + if (!userFromDB) { 48 + responseText = "You don't have a linked Hacker News account."; 49 + } else { 50 + responseText = await handleUnlink(userId); 51 + } 52 + break; 53 + 54 + case "help": 55 + responseText = handleHelp(); 56 + break; 57 + 58 + default: 59 + // If the user is already linked and verified 60 + if (userFromDB?.verified) { 61 + responseText = `You are already linked to the <https://news.ycombinator.com/user?id=${userFromDB.hackernewsUsername}|\`${userFromDB.hackernewsUsername}\`> Hacker News account. Use \`/hn-alerts-link unlink\` to remove the link.`; 62 + } 63 + // If there's a pending verification 64 + else if (userFromDB) { 65 + responseText = await handleVerify( 66 + userId, 67 + userFromDB.hackernewsUsername as string, 68 + userFromDB.challenge as string, 69 + ); 70 + } 71 + // Handle new link request (when no command specified, treat input as username) 72 + else { 73 + responseText = await handleLinkRequest(userId, input); 74 + } 75 + } 76 + 77 + await context.respond({ 78 + text: responseText, 79 + response_type: "ephemeral", 80 + }); 81 + }, 82 + ); 83 + } catch (error) { 84 + console.error("Error setting up linking", error); 85 + } 86 + }
+10 -9
src/libs/db.ts
··· 1 - import { drizzle } from "drizzle-orm/node-postgres"; 2 - import { Pool } from "pg"; 1 + import { drizzle } from "drizzle-orm/bun-sqlite"; 2 + import { Database } from "bun:sqlite"; 3 3 import * as schema from "./schema"; 4 4 5 - const pool = new Pool({ 6 - connectionString: process.env.DATABASE_URL, 7 - }); 5 + // Use environment variable for the database path in production 6 + const dbPath = process.env.DATABASE_PATH || "./local.db"; 8 7 9 - export const db = drizzle(pool, { schema }); 8 + // Create a SQLite database instance using Bun's built-in driver 9 + const sqlite = new Database(dbPath); 10 10 11 - // Set up triggers when initializing the database 12 - schema.setupTriggers(pool).catch(console.error); 11 + // Create a Drizzle instance with the database and schema 12 + export const db = drizzle(sqlite, { schema }); 13 13 14 - export { pool, schema }; 14 + // Export the sqlite instance and schema for use in other files 15 + export { sqlite, schema };
+3 -31
src/libs/schema.ts
··· 1 - import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"; 2 - import type { Pool } from "pg"; 1 + import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; 3 2 4 3 // Define the users table 5 - export const users = pgTable("users", { 4 + export const users = sqliteTable("users", { 6 5 id: text("id").primaryKey(), 7 6 hackernewsUsername: text("hackernews_username"), 8 7 challenge: text("challenge"), 9 - verified: boolean("verified").default(false), 10 - createdAt: timestamp("created_at") 11 - .$defaultFn(() => new Date()) 12 - .notNull(), 13 - updatedAt: timestamp("updated_at") 14 - .$defaultFn(() => new Date()) 15 - .notNull(), 8 + verified: integer("verified", { mode: "boolean" }).default(false).notNull(), 16 9 }); 17 - 18 - export async function setupTriggers(pool: Pool) { 19 - await pool.query(` 20 - -- Create or replace the update function 21 - CREATE OR REPLACE FUNCTION update_user_updated_at() 22 - RETURNS TRIGGER AS $$ 23 - BEGIN 24 - NEW.updated_at = NOW(); 25 - RETURN NEW; 26 - END; 27 - $$ LANGUAGE plpgsql; 28 - 29 - -- Drop trigger if exists and create a new one 30 - DROP TRIGGER IF EXISTS update_user_updated_at_trigger ON users; 31 - 32 - CREATE TRIGGER update_user_updated_at_trigger 33 - BEFORE UPDATE ON users 34 - FOR EACH ROW 35 - EXECUTE FUNCTION update_user_updated_at(); 36 - `); 37 - }