a fun bot for the hc slack
1
fork

Configure Feed

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

feat: add basic takes functionality

dunkirk.sh af3a0793 81da9860

verified
+1060 -1
+27
CLAUDE.md
··· 1 + # Development Guide for Takes 2 + 3 + ## Commands 4 + - `bun run dev` - Start development server with watch mode 5 + - `bun run db:generate` - Generate database migrations with Drizzle 6 + - `bun run db:migrate` - Apply database migrations 7 + - `bun run db:studio` - Start Drizzle Studio on port 3001 8 + - `npx biome lint .` - Lint codebase 9 + - `npx biome format . --write` - Format codebase 10 + 11 + ## Code Style 12 + - **Formatting**: Use tabs for indentation, double quotes for strings (enforced by Biome) 13 + - **Imports**: Organize imports automatically with Biome 14 + - **Types**: Use TypeScript with strict mode enabled; prefer explicit return types 15 + - **Error Handling**: Use try/catch blocks with specific error types when possible 16 + - **Naming**: 17 + - camelCase for variables and functions 18 + - PascalCase for classes and types 19 + - ALL_CAPS for constants 20 + - **Database**: Use Drizzle ORM with SQLite 21 + - **API**: Use Slack Edge API for bot interactions 22 + - Always do await context.respond and then return rather than returning the function 23 + - Always use Bun APIs when available as they're faster (e.g. `bun.randomUUID7()` instead of `crypto.uuid`) 24 + 25 + ## Architecture 26 + Slackbot that uses a SQLite database through Drizzle ORM to manage "takes" sessions. 27 + Keep feature implementations in the `src/features` directory and common utilities in `src/libs`.
+185
bun.lock
··· 4 4 "": { 5 5 "name": "takes", 6 6 "dependencies": { 7 + "@libsql/client": "^0.15.2", 7 8 "bottleneck": "^2.19.5", 8 9 "colors": "^1.4.0", 10 + "drizzle-kit": "^0.30.6", 11 + "drizzle-orm": "^0.41.0", 9 12 "slack-edge": "^1.3.7", 10 13 "yaml": "^2.7.1", 11 14 }, ··· 18 21 }, 19 22 }, 20 23 "packages": { 24 + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], 25 + 26 + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], 27 + 28 + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], 29 + 30 + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], 31 + 32 + "@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], 33 + 34 + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], 35 + 36 + "@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], 37 + 38 + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], 39 + 40 + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], 41 + 42 + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], 43 + 44 + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], 45 + 46 + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], 47 + 48 + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], 49 + 50 + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], 51 + 52 + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], 53 + 54 + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], 55 + 56 + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], 57 + 58 + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], 59 + 60 + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], 61 + 62 + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], 63 + 64 + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], 65 + 66 + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], 67 + 68 + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], 69 + 70 + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], 71 + 72 + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], 73 + 74 + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], 75 + 76 + "@libsql/client": ["@libsql/client@0.15.2", "", { "dependencies": { "@libsql/core": "^0.15.2", "@libsql/hrana-client": "^0.7.0", "js-base64": "^3.7.5", "libsql": "^0.5.4", "promise-limit": "^2.7.0" } }, "sha512-D0No4jqDj5I+buvEyFajBugohzJXCBt9aRHCEXGrJS/9obnAO2z18Os3xgyPsWX0Yw4NQfSYaayRdowqkssmXA=="], 77 + 78 + "@libsql/core": ["@libsql/core@0.15.2", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-+UIN0OlzWa54MqnHbtaJ3FEJj6k2VrwrjX1sSSxzYlM+dWuadjMwOVp7gHpSYJGKWw0RQWLGge4fbW4TCvIm3A=="], 79 + 80 + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4PnRdklaQg27vAZxtQgKl+xBHimCH2KRgKId+h63gkAtz5yFTMmX+Q4Ez804T1BgrZuB5ujIvueEEuust2ceSQ=="], 81 + 82 + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-r+Z3UXQWxluXKA5cPj5KciNsmSXVTnq9/tmDczngJrogyXwdbbSShYkzov5M+YBlUCKv2VCbNnfxxoIqQnV9Gg=="], 83 + 84 + "@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=="], 85 + 86 + "@libsql/isomorphic-fetch": ["@libsql/isomorphic-fetch@0.3.1", "", {}, "sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw=="], 87 + 88 + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], 89 + 90 + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-QmGXa3TGM6URe7vCOqdvr4Koay+4h5D6y4gdhnPCvXNYrRHgpq5OwEafP9GFalbO32Y1ppLY4enO2LwY0k63Qw=="], 91 + 92 + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-cx4/7/xUjgNbiRsghRHujSvIqaTNFQC7Oo1gkGXGsh8hBwkdXr1QdOpeitq745sl6OlbInRrW2C7B2juxX3hcQ=="], 93 + 94 + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-oPrE9Zyqd7fElS9uCGW2jn55cautD+gDIflfyF5+W/QYzll5OJ2vyMBZOBgdNopuZHrmHYihbespJn3t0WJDJg=="], 95 + 96 + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.4", "", { "os": "linux", "cpu": "x64" }, "sha512-XzyVdVe43MexkAaHzUvsi4tpPhfSDn3UndIYFrIu0lYkkiz4oKjTK7Iq96j2bcOeJv0pBGxiv+8Z9I6yp/aI2A=="], 97 + 98 + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.4", "", { "os": "win32", "cpu": "x64" }, "sha512-xWQyAQEsX+odBrMSXTpm3WOFeoJIX7QncCkaZcsaqdEFueOdNDIdcKAQKMoNlwtj1rCxE72RK4byw/Bflf6Jgg=="], 99 + 100 + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], 101 + 102 + "@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="], 103 + 21 104 "@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="], 22 105 23 106 "@types/node": ["@types/node@22.13.17", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g=="], ··· 25 108 "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], 26 109 27 110 "bottleneck": ["bottleneck@2.19.5", "", {}, "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw=="], 111 + 112 + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], 28 113 29 114 "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], 30 115 31 116 "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="], 32 117 118 + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], 119 + 120 + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 121 + 122 + "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], 123 + 124 + "drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="], 125 + 126 + "drizzle-orm": ["drizzle-orm@0.41.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q=="], 127 + 128 + "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], 129 + 130 + "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], 131 + 132 + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], 133 + 134 + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], 135 + 136 + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], 137 + 138 + "gel": ["gel@2.0.1", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-gfem3IGvqKqXwEq7XseBogyaRwGsQGuE7Cw/yQsjLGdgiyqX92G1xENPCE0ltunPGcsJIa6XBOTx/PK169mOqw=="], 139 + 140 + "get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="], 141 + 142 + "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], 143 + 144 + "js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="], 145 + 146 + "libsql": ["libsql@0.5.4", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.4", "@libsql/darwin-x64": "0.5.4", "@libsql/linux-arm64-gnu": "0.5.4", "@libsql/linux-arm64-musl": "0.5.4", "@libsql/linux-x64-gnu": "0.5.4", "@libsql/linux-x64-musl": "0.5.4", "@libsql/win32-x64-msvc": "0.5.4" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-GEFeWca4SDAQFxjHWJBE6GK52LEtSskiujbG3rqmmeTO9t4sfSBKIURNLLpKDDF7fb7jmTuuRkDAn9BZGITQNw=="], 147 + 148 + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], 149 + 150 + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], 151 + 152 + "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=="], 153 + 154 + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], 155 + 156 + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 157 + 158 + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], 159 + 160 + "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="], 161 + 33 162 "slack-edge": ["slack-edge@1.3.7", "", { "dependencies": { "slack-web-api-client": "^1.1.5" } }, "sha512-BI+V8WTlaMQmUkBmyJoJ8PDykf6GoJQiCeExkfJ1H6l8Za4Wuv0sM+oV4sOjLgS06+AvOKvya9FgBpcuAKGoAA=="], 34 163 35 164 "slack-web-api-client": ["slack-web-api-client@1.1.5", "", {}, "sha512-YmGGg3uU7tgW8djO2yn+xXgnkq5M1XeWYGODuDCwMbtr6OOJ5ys08Ju68XzadCSZNFqDKKSs31VSZKWJqb4KhA=="], 36 165 166 + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], 167 + 168 + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], 169 + 37 170 "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], 38 171 39 172 "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], 173 + 174 + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], 175 + 176 + "which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], 177 + 178 + "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=="], 40 179 41 180 "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], 181 + 182 + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], 183 + 184 + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], 185 + 186 + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], 187 + 188 + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], 189 + 190 + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], 191 + 192 + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], 193 + 194 + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], 195 + 196 + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], 197 + 198 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], 199 + 200 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], 201 + 202 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], 203 + 204 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], 205 + 206 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], 207 + 208 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], 209 + 210 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], 211 + 212 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], 213 + 214 + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], 215 + 216 + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], 217 + 218 + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], 219 + 220 + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], 221 + 222 + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], 223 + 224 + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], 225 + 226 + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], 42 227 } 43 228 }
+10
drizzle.config.ts
··· 1 + import type { Config } from "drizzle-kit"; 2 + 3 + export default { 4 + schema: "./src/libs/schema.ts", 5 + out: "./migrations", 6 + dialect: "sqlite", 7 + dbCredentials: { 8 + url: "./local.db", 9 + }, 10 + } satisfies Config;
local.db

This is a binary file and will not be displayed.

+18
migrations/0000_fearless_bloodscream.sql
··· 1 + CREATE TABLE `takes` ( 2 + `id` text PRIMARY KEY NOT NULL, 3 + `user_id` text NOT NULL, 4 + `channel_id` text NOT NULL, 5 + `status` text DEFAULT 'active' NOT NULL, 6 + `started_at` integer NOT NULL, 7 + `paused_at` integer, 8 + `completed_at` integer, 9 + `duration_minutes` integer DEFAULT 5 NOT NULL, 10 + `paused_time_ms` integer DEFAULT 0 NOT NULL, 11 + `notes` text 12 + ); 13 + --> statement-breakpoint 14 + CREATE TABLE `users` ( 15 + `id` text PRIMARY KEY NOT NULL, 16 + `name` text NOT NULL, 17 + `is_active` integer DEFAULT true NOT NULL 18 + );
+133
migrations/meta/0000_snapshot.json
··· 1 + { 2 + "version": "6", 3 + "dialect": "sqlite", 4 + "id": "a57561dd-0c99-4274-9ec1-8a2660da81da", 5 + "prevId": "00000000-0000-0000-0000-000000000000", 6 + "tables": { 7 + "takes": { 8 + "name": "takes", 9 + "columns": { 10 + "id": { 11 + "name": "id", 12 + "type": "text", 13 + "primaryKey": true, 14 + "notNull": true, 15 + "autoincrement": false 16 + }, 17 + "user_id": { 18 + "name": "user_id", 19 + "type": "text", 20 + "primaryKey": false, 21 + "notNull": true, 22 + "autoincrement": false 23 + }, 24 + "channel_id": { 25 + "name": "channel_id", 26 + "type": "text", 27 + "primaryKey": false, 28 + "notNull": true, 29 + "autoincrement": false 30 + }, 31 + "status": { 32 + "name": "status", 33 + "type": "text", 34 + "primaryKey": false, 35 + "notNull": true, 36 + "autoincrement": false, 37 + "default": "'active'" 38 + }, 39 + "started_at": { 40 + "name": "started_at", 41 + "type": "integer", 42 + "primaryKey": false, 43 + "notNull": true, 44 + "autoincrement": false 45 + }, 46 + "paused_at": { 47 + "name": "paused_at", 48 + "type": "integer", 49 + "primaryKey": false, 50 + "notNull": false, 51 + "autoincrement": false 52 + }, 53 + "completed_at": { 54 + "name": "completed_at", 55 + "type": "integer", 56 + "primaryKey": false, 57 + "notNull": false, 58 + "autoincrement": false 59 + }, 60 + "duration_minutes": { 61 + "name": "duration_minutes", 62 + "type": "integer", 63 + "primaryKey": false, 64 + "notNull": true, 65 + "autoincrement": false, 66 + "default": 5 67 + }, 68 + "paused_time_ms": { 69 + "name": "paused_time_ms", 70 + "type": "integer", 71 + "primaryKey": false, 72 + "notNull": true, 73 + "autoincrement": false, 74 + "default": 0 75 + }, 76 + "notes": { 77 + "name": "notes", 78 + "type": "text", 79 + "primaryKey": false, 80 + "notNull": false, 81 + "autoincrement": false 82 + } 83 + }, 84 + "indexes": {}, 85 + "foreignKeys": {}, 86 + "compositePrimaryKeys": {}, 87 + "uniqueConstraints": {}, 88 + "checkConstraints": {} 89 + }, 90 + "users": { 91 + "name": "users", 92 + "columns": { 93 + "id": { 94 + "name": "id", 95 + "type": "text", 96 + "primaryKey": true, 97 + "notNull": true, 98 + "autoincrement": false 99 + }, 100 + "name": { 101 + "name": "name", 102 + "type": "text", 103 + "primaryKey": false, 104 + "notNull": true, 105 + "autoincrement": false 106 + }, 107 + "is_active": { 108 + "name": "is_active", 109 + "type": "integer", 110 + "primaryKey": false, 111 + "notNull": true, 112 + "autoincrement": false, 113 + "default": true 114 + } 115 + }, 116 + "indexes": {}, 117 + "foreignKeys": {}, 118 + "compositePrimaryKeys": {}, 119 + "uniqueConstraints": {}, 120 + "checkConstraints": {} 121 + } 122 + }, 123 + "views": {}, 124 + "enums": {}, 125 + "_meta": { 126 + "schemas": {}, 127 + "tables": {}, 128 + "columns": {} 129 + }, 130 + "internal": { 131 + "indexes": {} 132 + } 133 + }
+13
migrations/meta/_journal.json
··· 1 + { 2 + "version": "7", 3 + "dialect": "sqlite", 4 + "entries": [ 5 + { 6 + "idx": 0, 7 + "version": "6", 8 + "when": 1743563155870, 9 + "tag": "0000_fearless_bloodscream", 10 + "breakpoints": true 11 + } 12 + ] 13 + }
+7 -1
package.json
··· 7 7 "private": true, 8 8 "scripts": { 9 9 "dev": "bun run --watch src/index.ts", 10 - "ngrok": "ngrok http 3000 --domain=casual-renewing-reptile.ngrok-free.app" 10 + "ngrok": "ngrok http 3000 --domain=casual-renewing-reptile.ngrok-free.app", 11 + "db:generate": "drizzle-kit generate", 12 + "db:migrate": "drizzle-kit migrate", 13 + "db:studio": "drizzle-kit studio --port 3001", 14 + "db:push": "drizzle-kit push" 11 15 }, 12 16 "devDependencies": { 13 17 "@types/bun": "latest" ··· 18 22 "dependencies": { 19 23 "bottleneck": "^2.19.5", 20 24 "colors": "^1.4.0", 25 + "drizzle-kit": "^0.30.6", 26 + "drizzle-orm": "^0.41.0", 21 27 "slack-edge": "^1.3.7", 22 28 "yaml": "^2.7.1" 23 29 }
+1
src/features/index.ts
··· 1 1 export { default as example } from "./example"; 2 + export { default as takes } from "./takes";
+629
src/features/takes.ts
··· 1 + import type { AnyMessageBlock } from "slack-edge"; 2 + import { slackApp } from "../index"; 3 + import { db } from "../libs/db"; 4 + import { takes as takesTable } from "../libs/schema"; 5 + import { eq, and, isNull } from "drizzle-orm"; 6 + 7 + type MessageResponse = { 8 + blocks?: AnyMessageBlock[]; 9 + text: string; 10 + response_type: "ephemeral" | "in_channel"; 11 + }; 12 + 13 + const takes = async () => { 14 + // Helper functions for command actions 15 + const getActiveTake = async (userId: string) => { 16 + return db 17 + .select() 18 + .from(takesTable) 19 + .where( 20 + and( 21 + eq(takesTable.userId, userId), 22 + eq(takesTable.status, "active"), 23 + ), 24 + ) 25 + .limit(1); 26 + }; 27 + 28 + const getPausedTake = async (userId: string) => { 29 + return db 30 + .select() 31 + .from(takesTable) 32 + .where( 33 + and( 34 + eq(takesTable.userId, userId), 35 + eq(takesTable.status, "paused"), 36 + ), 37 + ) 38 + .limit(1); 39 + }; 40 + 41 + const getCompletedTakes = async (userId: string) => { 42 + return db 43 + .select() 44 + .from(takesTable) 45 + .where( 46 + and( 47 + eq(takesTable.userId, userId), 48 + eq(takesTable.status, "completed"), 49 + ), 50 + ); 51 + }; 52 + 53 + // Command action handlers 54 + const handleStart = async ( 55 + userId: string, 56 + channelId: string, 57 + ): Promise<MessageResponse> => { 58 + const activeTake = await getActiveTake(userId); 59 + if (activeTake.length > 0) { 60 + return { 61 + text: `You already have an active takes session! Use \`/takes status\` to check it.`, 62 + response_type: "ephemeral", 63 + }; 64 + } 65 + 66 + // Create new takes session 67 + const newTake = { 68 + id: Bun.randomUUIDv7(), 69 + userId, 70 + channelId, 71 + status: "active", 72 + startedAt: new Date(), 73 + durationMinutes: 5, // 5 minutes for testing (should be 90) 74 + }; 75 + 76 + await db.insert(takesTable).values(newTake); 77 + 78 + // Calculate end time for message 79 + const endTime = new Date( 80 + newTake.startedAt.getTime() + newTake.durationMinutes * 60000, 81 + ); 82 + const endTimeStr = `<!date^${Math.floor(endTime.getTime() / 1000)}^{time}|${endTime.toLocaleTimeString()}>`; 83 + 84 + return { 85 + text: `🎬 Takes session started! You have ${newTake.durationMinutes} minutes until ${endTimeStr}.`, 86 + response_type: "in_channel", 87 + blocks: [ 88 + { 89 + type: "section", 90 + text: { 91 + type: "mrkdwn", 92 + text: `🎬 Takes session started! You have ${newTake.durationMinutes} minutes until ${endTimeStr}.`, 93 + }, 94 + }, 95 + { 96 + type: "actions", 97 + elements: [ 98 + { 99 + type: "button", 100 + text: { 101 + type: "plain_text", 102 + text: "⏸️ Pause", 103 + emoji: true, 104 + }, 105 + value: "pause", 106 + action_id: "takes_pause", 107 + }, 108 + { 109 + type: "button", 110 + text: { 111 + type: "plain_text", 112 + text: "⏹️ Stop", 113 + emoji: true, 114 + }, 115 + value: "stop", 116 + action_id: "takes_stop", 117 + style: "danger", 118 + }, 119 + ], 120 + }, 121 + ], 122 + }; 123 + }; 124 + 125 + const handlePause = async ( 126 + userId: string, 127 + ): Promise<MessageResponse | undefined> => { 128 + const activeTake = await getActiveTake(userId); 129 + if (activeTake.length === 0) { 130 + return { 131 + text: `You don't have an active takes session! Use \`/takes start\` to begin.`, 132 + response_type: "ephemeral", 133 + }; 134 + } 135 + 136 + const takeToUpdate = activeTake[0]; 137 + if (!takeToUpdate) { 138 + return; 139 + } 140 + 141 + // Update the takes entry to paused status 142 + await db 143 + .update(takesTable) 144 + .set({ 145 + status: "paused", 146 + pausedAt: new Date(), 147 + }) 148 + .where(eq(takesTable.id, takeToUpdate.id)); 149 + 150 + return { 151 + text: `⏸️ Takes session paused! Use \`/takes resume\` to continue.`, 152 + response_type: "in_channel", 153 + blocks: [ 154 + { 155 + type: "section", 156 + text: { 157 + type: "mrkdwn", 158 + text: `⏸️ Takes session paused!`, 159 + }, 160 + }, 161 + { 162 + type: "actions", 163 + elements: [ 164 + { 165 + type: "button", 166 + text: { 167 + type: "plain_text", 168 + text: "▶️ Resume", 169 + emoji: true, 170 + }, 171 + value: "resume", 172 + action_id: "takes_resume", 173 + }, 174 + { 175 + type: "button", 176 + text: { 177 + type: "plain_text", 178 + text: "⏹️ Stop", 179 + emoji: true, 180 + }, 181 + value: "stop", 182 + action_id: "takes_stop", 183 + style: "danger", 184 + }, 185 + ], 186 + }, 187 + ], 188 + }; 189 + }; 190 + 191 + const handleResume = async ( 192 + userId: string, 193 + ): Promise<MessageResponse | undefined> => { 194 + const pausedTake = await getPausedTake(userId); 195 + if (pausedTake.length === 0) { 196 + return { 197 + text: `You don't have a paused takes session!`, 198 + response_type: "ephemeral", 199 + }; 200 + } 201 + 202 + const pausedSession = pausedTake[0]; 203 + if (!pausedSession) { 204 + return; 205 + } 206 + 207 + const now = new Date(); 208 + 209 + // Calculate paused time 210 + if (pausedSession.pausedAt) { 211 + const pausedTimeMs = 212 + now.getTime() - pausedSession.pausedAt.getTime(); 213 + const totalPausedTime = 214 + (pausedSession.pausedTimeMs || 0) + pausedTimeMs; 215 + 216 + // Update the takes entry to active status 217 + await db 218 + .update(takesTable) 219 + .set({ 220 + status: "active", 221 + pausedAt: null, 222 + pausedTimeMs: totalPausedTime, 223 + }) 224 + .where(eq(takesTable.id, pausedSession.id)); 225 + } 226 + 227 + return { 228 + text: `▶️ Takes session resumed!`, 229 + response_type: "in_channel", 230 + blocks: [ 231 + { 232 + type: "section", 233 + text: { 234 + type: "mrkdwn", 235 + text: `▶️ Takes session resumed!`, 236 + }, 237 + }, 238 + { 239 + type: "actions", 240 + elements: [ 241 + { 242 + type: "button", 243 + text: { 244 + type: "plain_text", 245 + text: "⏸️ Pause", 246 + emoji: true, 247 + }, 248 + value: "pause", 249 + action_id: "takes_pause", 250 + }, 251 + { 252 + type: "button", 253 + text: { 254 + type: "plain_text", 255 + text: "⏹️ Stop", 256 + emoji: true, 257 + }, 258 + value: "stop", 259 + action_id: "takes_stop", 260 + style: "danger", 261 + }, 262 + ], 263 + }, 264 + ], 265 + }; 266 + }; 267 + 268 + const handleStop = async ( 269 + userId: string, 270 + ): Promise<MessageResponse | undefined> => { 271 + const activeTake = await getActiveTake(userId); 272 + 273 + if (activeTake.length === 0) { 274 + const pausedTake = await getPausedTake(userId); 275 + 276 + if (pausedTake.length === 0) { 277 + return { 278 + text: `You don't have an active or paused takes session!`, 279 + response_type: "ephemeral", 280 + }; 281 + } 282 + 283 + // Mark the paused session as completed 284 + const pausedTakeToStop = pausedTake[0]; 285 + if (!pausedTakeToStop) { 286 + return; 287 + } 288 + 289 + await db 290 + .update(takesTable) 291 + .set({ 292 + status: "completed", 293 + completedAt: new Date(), 294 + }) 295 + .where(eq(takesTable.id, pausedTakeToStop.id)); 296 + } else { 297 + // Mark the active session as completed 298 + const activeTakeToStop = activeTake[0]; 299 + if (!activeTakeToStop) { 300 + return; 301 + } 302 + 303 + await db 304 + .update(takesTable) 305 + .set({ 306 + status: "completed", 307 + completedAt: new Date(), 308 + }) 309 + .where(eq(takesTable.id, activeTakeToStop.id)); 310 + } 311 + 312 + return { 313 + text: `✅ Takes session completed! Thanks for your contribution.`, 314 + response_type: "in_channel", 315 + blocks: [ 316 + { 317 + type: "section", 318 + text: { 319 + type: "mrkdwn", 320 + text: `✅ Takes session completed! Thanks for your contribution.`, 321 + }, 322 + }, 323 + { 324 + type: "actions", 325 + elements: [ 326 + { 327 + type: "button", 328 + text: { 329 + type: "plain_text", 330 + text: "🎬 Start New Session", 331 + emoji: true, 332 + }, 333 + value: "start", 334 + action_id: "takes_start", 335 + }, 336 + ], 337 + }, 338 + ], 339 + }; 340 + }; 341 + 342 + const handleStatus = async ( 343 + userId: string, 344 + ): Promise<MessageResponse | undefined> => { 345 + const activeTake = await getActiveTake(userId); 346 + 347 + if (activeTake.length > 0) { 348 + const take = activeTake[0]; 349 + if (!take) { 350 + return; 351 + } 352 + 353 + const startTime = new Date(take.startedAt); 354 + const endTime = new Date( 355 + startTime.getTime() + take.durationMinutes * 60000, 356 + ); 357 + 358 + // Adjust for paused time 359 + if (take.pausedTimeMs) { 360 + endTime.setTime(endTime.getTime() + take.pausedTimeMs); 361 + } 362 + 363 + const now = new Date(); 364 + const remainingMs = endTime.getTime() - now.getTime(); 365 + let remaining: string; 366 + if (remainingMs < 120000) { 367 + // Less than 2 minutes 368 + remaining = `${Math.max(0, Math.floor(remainingMs / 1000))} seconds`; 369 + } else { 370 + remaining = `${Math.max(0, Math.floor(remainingMs / 60000))} minutes`; 371 + } 372 + 373 + return { 374 + text: `You have an active takes session with ${remaining} minutes remaining.`, 375 + response_type: "ephemeral", 376 + blocks: [ 377 + { 378 + type: "section", 379 + text: { 380 + type: "mrkdwn", 381 + text: `You have an active takes session with *${remaining}* remaining.`, 382 + }, 383 + }, 384 + { 385 + type: "actions", 386 + elements: [ 387 + { 388 + type: "button", 389 + text: { 390 + type: "plain_text", 391 + text: "⏸️ Pause", 392 + emoji: true, 393 + }, 394 + value: "pause", 395 + action_id: "takes_pause", 396 + }, 397 + { 398 + type: "button", 399 + text: { 400 + type: "plain_text", 401 + text: "⏹️ Stop", 402 + emoji: true, 403 + }, 404 + value: "stop", 405 + action_id: "takes_stop", 406 + style: "danger", 407 + }, 408 + ], 409 + }, 410 + ], 411 + }; 412 + } 413 + 414 + // Check for paused session 415 + const pausedTakeStatus = await getPausedTake(userId); 416 + 417 + if (pausedTakeStatus.length > 0) { 418 + return { 419 + text: `You have a paused takes session. Use \`/takes resume\` to continue.`, 420 + response_type: "ephemeral", 421 + blocks: [ 422 + { 423 + type: "section", 424 + text: { 425 + type: "mrkdwn", 426 + text: `You have a paused takes session.`, 427 + }, 428 + }, 429 + { 430 + type: "actions", 431 + elements: [ 432 + { 433 + type: "button", 434 + text: { 435 + type: "plain_text", 436 + text: "▶️ Resume", 437 + emoji: true, 438 + }, 439 + value: "resume", 440 + action_id: "takes_resume", 441 + }, 442 + { 443 + type: "button", 444 + text: { 445 + type: "plain_text", 446 + text: "⏹️ Stop", 447 + emoji: true, 448 + }, 449 + value: "stop", 450 + action_id: "takes_stop", 451 + style: "danger", 452 + }, 453 + ], 454 + }, 455 + ], 456 + }; 457 + } 458 + 459 + // Check history of completed sessions 460 + const completedSessions = await getCompletedTakes(userId); 461 + 462 + return { 463 + text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the past.`, 464 + response_type: "ephemeral", 465 + blocks: [ 466 + { 467 + type: "section", 468 + text: { 469 + type: "mrkdwn", 470 + text: `You have no active takes sessions. You've completed ${completedSessions.length} sessions in the past.`, 471 + }, 472 + }, 473 + { 474 + type: "actions", 475 + elements: [ 476 + { 477 + type: "button", 478 + text: { 479 + type: "plain_text", 480 + text: "🎬 Start New Session", 481 + emoji: true, 482 + }, 483 + value: "start", 484 + action_id: "takes_start", 485 + }, 486 + ], 487 + }, 488 + ], 489 + }; 490 + }; 491 + 492 + const handleHelp = async (): Promise<MessageResponse> => { 493 + return { 494 + text: `*Takes Commands*\n\n• \`/takes start\` - Start a new takes session\n• \`/takes pause\` - Pause your current session\n• \`/takes resume\` - Resume your paused session\n• \`/takes stop\` - End your current session\n• \`/takes status\` - Check the status of your session`, 495 + response_type: "ephemeral", 496 + blocks: [ 497 + { 498 + type: "section", 499 + text: { 500 + type: "mrkdwn", 501 + text: "*Takes Commands*", 502 + }, 503 + }, 504 + { 505 + type: "section", 506 + text: { 507 + type: "mrkdwn", 508 + text: "• `/takes start` - Start a new takes session\n• `/takes pause` - Pause your current session\n• `/takes resume` - Resume your paused session\n• `/takes stop` - End your current session\n• `/takes status` - Check the status of your session", 509 + }, 510 + }, 511 + { 512 + type: "actions", 513 + elements: [ 514 + { 515 + type: "button", 516 + text: { 517 + type: "plain_text", 518 + text: "🎬 Start New Session", 519 + emoji: true, 520 + }, 521 + value: "start", 522 + action_id: "takes_start", 523 + }, 524 + ], 525 + }, 526 + ], 527 + }; 528 + }; 529 + 530 + // Main command handler 531 + slackApp.command("/takes", async ({ payload, context }): Promise<void> => { 532 + const userId = payload.user_id; 533 + const channelId = payload.channel_id; 534 + const text = payload.text || ""; 535 + const args = text.trim().split(/\s+/); 536 + let subcommand = args[0]?.toLowerCase() || ""; 537 + 538 + // Check for active takes session 539 + const activeTake = await getActiveTake(userId); 540 + 541 + // Check for paused session if no active one 542 + const pausedTakeCheck = 543 + activeTake.length === 0 ? await getPausedTake(userId) : []; 544 + 545 + // Default to status if we have an active or paused session and no command specified 546 + if ( 547 + subcommand === "" && 548 + (activeTake.length > 0 || pausedTakeCheck.length > 0) 549 + ) { 550 + subcommand = "status"; 551 + } else if (subcommand === "") { 552 + subcommand = "help"; 553 + } 554 + 555 + let response: MessageResponse | undefined; 556 + 557 + // Route to the appropriate handler function 558 + switch (subcommand) { 559 + case "start": 560 + response = await handleStart(userId, channelId); 561 + break; 562 + case "pause": 563 + response = await handlePause(userId); 564 + break; 565 + case "resume": 566 + response = await handleResume(userId); 567 + break; 568 + case "stop": 569 + response = await handleStop(userId); 570 + break; 571 + case "status": 572 + response = await handleStatus(userId); 573 + break; 574 + default: 575 + case "help": 576 + response = await handleHelp(); 577 + break; 578 + } 579 + 580 + if (context.respond) 581 + await context.respond( 582 + response || { 583 + text: "An error occurred while processing your request.", 584 + response_type: "ephemeral", 585 + }, 586 + ); 587 + }); 588 + 589 + // Handle button actions 590 + slackApp.action(/^takes_(\w+)$/, async ({ body, context }) => { 591 + const userId = body.user.id; 592 + const channelId = body.channel?.id || ""; 593 + const actionId = body.actions[0].action_id; 594 + const command = actionId.replace("takes_", ""); 595 + 596 + let response: MessageResponse | undefined; 597 + 598 + // Route to the appropriate handler function 599 + switch (command) { 600 + case "start": 601 + response = await handleStart(userId, channelId); 602 + break; 603 + case "pause": 604 + response = await handlePause(userId); 605 + break; 606 + case "resume": 607 + response = await handleResume(userId); 608 + break; 609 + case "stop": 610 + response = await handleStop(userId); 611 + break; 612 + case "status": 613 + response = await handleStatus(userId); 614 + break; 615 + default: 616 + response = await handleHelp(); 617 + break; 618 + } 619 + 620 + if (context.respond) 621 + await context.respond( 622 + response || { 623 + text: "An error occurred while processing your request.", 624 + }, 625 + ); 626 + }); 627 + }; 628 + 629 + export default takes;
+15
src/libs/db.ts
··· 1 + import { drizzle } from "drizzle-orm/bun-sqlite"; 2 + import { Database } from "bun:sqlite"; 3 + import * as schema from "./schema"; 4 + 5 + // Use environment variable for the database path in production 6 + const dbPath = process.env.DATABASE_PATH || "./local.db"; 7 + 8 + // Create a SQLite database instance using Bun's built-in driver 9 + const sqlite = new Database(dbPath); 10 + 11 + // Create a Drizzle instance with the database and schema 12 + export const db = drizzle(sqlite, { schema }); 13 + 14 + // Export the sqlite instance and schema for use in other files 15 + export { sqlite, schema };
+22
src/libs/schema.ts
··· 1 + import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; 2 + 3 + // Define the takes table 4 + export const takes = sqliteTable("takes", { 5 + id: text("id").primaryKey(), 6 + userId: text("user_id").notNull(), 7 + channelId: text("channel_id").notNull(), 8 + status: text("status").notNull().default("active"), // active, paused, completed 9 + startedAt: integer("started_at", { mode: "timestamp" }).notNull(), 10 + pausedAt: integer("paused_at", { mode: "timestamp" }), 11 + completedAt: integer("completed_at", { mode: "timestamp" }), 12 + durationMinutes: integer("duration_minutes").notNull().default(5), // 5 minutes for testing (should be 90) 13 + pausedTimeMs: integer("paused_time_ms").notNull().default(0), // cumulative paused time 14 + notes: text("notes"), 15 + }); 16 + 17 + // Define the users table 18 + export const users = sqliteTable("users", { 19 + id: text("id").primaryKey(), 20 + name: text("name").notNull(), 21 + isActive: integer("is_active", { mode: "boolean" }).notNull().default(true), 22 + });