From 9593b92843d380365bb056932ed157a635315342 Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Sun, 13 Jul 2025 19:26:15 +0100 Subject: [PATCH 01/14] chore(database): start move to drizzle --- .env.example | 16 +- .gitignore | 4 +- bun.lock | 124 ++++++++++++++ drizzle.config.ts | 14 ++ package.json | 3 + src/commands.ts | 10 +- src/config.ts | 30 +--- src/{utils => }/db/audit.ts | 0 src/{utils => }/db/botinfo.ts | 4 +- src/{utils => }/db/cron.ts | 2 +- src/{utils => }/db/discord.ts | 4 +- src/db/schema.ts | 101 +++++++++++ src/{utils => }/db/twitch.ts | 4 +- src/{utils => }/db/youtube.ts | 6 +- src/index.ts | 2 +- src/utils/cronJobs.ts | 2 +- src/utils/database.ts | 3 + src/utils/db/init.ts | 212 ------------------------ src/utils/youtube/fetchLatestUploads.ts | 2 +- 19 files changed, 272 insertions(+), 271 deletions(-) create mode 100644 drizzle.config.ts rename src/{utils => }/db/audit.ts (100%) rename src/{utils => }/db/botinfo.ts (97%) rename src/{utils => }/db/cron.ts (93%) rename src/{utils => }/db/discord.ts (97%) create mode 100644 src/db/schema.ts rename src/{utils => }/db/twitch.ts (95%) rename src/{utils => }/db/youtube.ts (95%) delete mode 100644 src/utils/db/init.ts diff --git a/.env.example b/.env.example index c3b2e22..b203af7 100644 --- a/.env.example +++ b/.env.example @@ -18,16 +18,6 @@ CONFIG_UPDATE_INTERVAL_YOUTUBE='10' CONFIG_UPDATE_INTERVAL_TWITCH='2' CONFIG_DISCORD_LOGS_CHANNEL='YOUR_DISCORD_LOGS_CHANNEL' -# Postgres -POSTGRES_HOST='YOUR_POSTGRES_HOST' -POSTGRES_PORT='YOUR_POSTGRES_PORT' -POSTGRES_USER='YOUR_POSTGRES_USER' -POSTGRES_PASSWORD='YOUR_POSTGRES_PASSWORD' -POSTGRES_DB='YOUR_POSTGRES_DB' - -# Postgres Dev -POSTGRES_DEV_HOST='YOUR_POSTGRES_DEV_HOST' -POSTGRES_DEV_PORT='YOUR_POSTGRES_DEV_PORT' -POSTGRES_DEV_USER='YOUR_POSTGRES_DEV_USER' -POSTGRES_DEV_PASSWORD='YOUR_POSTGRES_DEV_PASSWORD' -POSTGRES_DEV_DB='YOUR_POSTGRES_DEV_DB' +# Postgres URLs +POSTGRES_URL='postgresql://user:password@server:port/database' +POSTGRES_DEV_URL='postgresql://user:password@server:port/database' diff --git a/.gitignore b/.gitignore index 1d4ec2d..9810e77 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,10 @@ dist/ target/ # Database stuff -/backups *.db *.sqlite *.sqlite3* *.sql dbtests.ts -perftesting.ts \ No newline at end of file +perftesting.ts +drizzle/ \ No newline at end of file diff --git a/bun.lock b/bun.lock index f1b81df..972f9f1 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "cron": "^4.3.0", "discord.js": "^14.19.1", + "drizzle-orm": "^0.44.2", "hfksdjfskfhsjdfhkasfdhksf": "^1.0.5", "pg": "^8.15.6", }, @@ -15,6 +16,7 @@ "@typescript-eslint/eslint-plugin": "8.11.0", "@typescript-eslint/parser": "8.11.0", "concurrently": "^9.1.2", + "drizzle-kit": "^0.31.4", "eslint": "^8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.26.0", @@ -40,6 +42,64 @@ "@discordjs/ws": ["@discordjs/ws@1.2.2", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.0", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@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=="], + + "@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=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.6", "", { "os": "aix", "cpu": "ppc64" }, "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.6", "", { "os": "android", "cpu": "arm" }, "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.6", "", { "os": "android", "cpu": "arm64" }, "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.6", "", { "os": "android", "cpu": "x64" }, "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.6", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.6", "", { "os": "linux", "cpu": "arm" }, "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.6", "", { "os": "linux", "cpu": "ia32" }, "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.6", "", { "os": "linux", "cpu": "x64" }, "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.6", "", { "os": "none", "cpu": "x64" }, "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.6", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.6", "", { "os": "openbsd", "cpu": "x64" }, "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.6", "", { "os": "sunos", "cpu": "x64" }, "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], @@ -136,6 +196,8 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.2.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-b5ITZMnVdf3m1gMvJHG+gIfeJHiQPJak0f7925Hxu6ZN5VKA8AGy4GZ4lM+Xkn6jtWxg5S3ldWvfmXdvnkp3GQ=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], @@ -182,6 +244,10 @@ "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + "drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="], + + "drizzle-orm": ["drizzle-orm@0.44.2", "", { "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.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@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", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -200,6 +266,10 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "esbuild": ["esbuild@0.25.6", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.6", "@esbuild/android-arm": "0.25.6", "@esbuild/android-arm64": "0.25.6", "@esbuild/android-x64": "0.25.6", "@esbuild/darwin-arm64": "0.25.6", "@esbuild/darwin-x64": "0.25.6", "@esbuild/freebsd-arm64": "0.25.6", "@esbuild/freebsd-x64": "0.25.6", "@esbuild/linux-arm": "0.25.6", "@esbuild/linux-arm64": "0.25.6", "@esbuild/linux-ia32": "0.25.6", "@esbuild/linux-loong64": "0.25.6", "@esbuild/linux-mips64el": "0.25.6", "@esbuild/linux-ppc64": "0.25.6", "@esbuild/linux-riscv64": "0.25.6", "@esbuild/linux-s390x": "0.25.6", "@esbuild/linux-x64": "0.25.6", "@esbuild/netbsd-arm64": "0.25.6", "@esbuild/netbsd-x64": "0.25.6", "@esbuild/openbsd-arm64": "0.25.6", "@esbuild/openbsd-x64": "0.25.6", "@esbuild/openharmony-arm64": "0.25.6", "@esbuild/sunos-x64": "0.25.6", "@esbuild/win32-arm64": "0.25.6", "@esbuild/win32-ia32": "0.25.6", "@esbuild/win32-x64": "0.25.6" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -278,6 +348,8 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -502,6 +574,8 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], @@ -538,6 +612,10 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "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=="], + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -628,6 +706,8 @@ "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + "@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=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ=="], @@ -648,6 +728,50 @@ "pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..7e24a9c --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "drizzle-kit"; + +import { config } from "./src/config"; + +console.log("Using database URL:", config.databaseUrl); + +export default defineConfig({ + out: "./drizzle", + schema: "./src/db/schema.ts", + dialect: "postgresql", + dbCredentials: { + url: config.databaseUrl!, + }, +}); diff --git a/package.json b/package.json index 98b3aae..15c9f88 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@typescript-eslint/eslint-plugin": "8.11.0", "@typescript-eslint/parser": "8.11.0", "concurrently": "^9.1.2", + "drizzle-kit": "^0.31.4", "eslint": "^8.57.0", "eslint-config-prettier": "9.1.0", "eslint-plugin-import": "^2.26.0", @@ -20,6 +21,7 @@ "typescript": "^5.0.0" }, "scripts": { + "db:generate": "bun drizzle-kit generate", "dev": "concurrently --names \"WEB,API,BOT\" --prefix-colors \"blue,green,magenta\" \"cd web && bun dev\" \"cd api && cargo watch -x \\\"run -- --dev\\\"\" \"bun --watch . --dev\"", "dev:bot": "bun --watch . --dev", "test:jetstream": "bun --watch src/utils/bluesky/jetstream.ts", @@ -35,6 +37,7 @@ "dependencies": { "cron": "^4.3.0", "discord.js": "^14.19.1", + "drizzle-orm": "^0.44.2", "hfksdjfskfhsjdfhkasfdhksf": "^1.0.5", "pg": "^8.15.6" } diff --git a/src/commands.ts b/src/commands.ts index 22f87c8..a530e62 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -26,19 +26,19 @@ import { checkIfStreamerIsLive } from "./utils/twitch/checkIfStreamerIsLive"; import { checkIfChannelIsAlreadyTracked, addNewChannelToTrack, -} from "./utils/db/youtube"; +} from "./db/youtube"; import search from "./utils/youtube/search"; import { checkIfGuildIsTrackingUserAlready, discordAddGuildTrackingUser, -} from "./utils/db/discord"; +} from "./db/discord"; import { Platform, YouTubeContentType } from "./types/types.d"; import searchTwitch from "./utils/twitch/searchTwitch"; import { getStreamerName } from "./utils/twitch/getStreamerName"; import { addNewStreamerToTrack, checkIfStreamerIsAlreadyTracked, -} from "./utils/db/twitch"; +} from "./db/twitch"; import client from "."; @@ -681,9 +681,9 @@ const commands: Record = { const query = platform === "youtube" ? (interaction.options.get("channel_id") - ?.value as string) + ?.value as string) : (interaction.options.get("streamer_id") - ?.value as string); + ?.value as string); // If the query is empty or not a string, return an empty array if (!query || typeof query !== "string") { diff --git a/src/config.ts b/src/config.ts index 383bcdb..40dd781 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,6 +3,7 @@ export const runningInDevMode: boolean = process.argv.includes("--dev"); export interface Config { updateIntervalYouTube: number; updateIntervalTwitch: number; + databaseUrl: string | undefined; } export const config: Config = { @@ -12,6 +13,9 @@ export const config: Config = { updateIntervalTwitch: process.env?.CONFIG_UPDATE_INTERVAL_TWITCH ? parseInt(process.env?.CONFIG_UPDATE_INTERVAL_TWITCH) * 1000 : 60_000, + databaseUrl: runningInDevMode + ? process.env?.POSTGRES_DEV_URL + : process.env?.POSTGRES_URL, }; interface Env { @@ -29,29 +33,3 @@ export const env: Env = { twitchClientId: process.env?.TWITCH_CLIENT_ID, twitchClientSecret: process.env?.TWITCH_CLIENT_SECRET, }; - -interface DatabaseConfig { - host: string | undefined; - port: string | undefined; - user: string | undefined; - password: string | undefined; - database: string | undefined; -} - -export const dbCredentials: DatabaseConfig = { - host: runningInDevMode - ? process.env?.POSTGRES_DEV_HOST - : process.env?.POSTGRES_HOST, - port: runningInDevMode - ? process.env?.POSTGRES_DEV_PORT - : process.env?.POSTGRES_PORT, - user: runningInDevMode - ? process.env?.POSTGRES_DEV_USER - : process.env?.POSTGRES_USER, - password: runningInDevMode - ? process.env?.POSTGRES_DEV_PASSWORD - : process.env?.POSTGRES_PASSWORD, - database: runningInDevMode - ? process.env?.POSTGRES_DEV_DB - : process.env?.POSTGRES_DB, -}; diff --git a/src/utils/db/audit.ts b/src/db/audit.ts similarity index 100% rename from src/utils/db/audit.ts rename to src/db/audit.ts diff --git a/src/utils/db/botinfo.ts b/src/db/botinfo.ts similarity index 97% rename from src/utils/db/botinfo.ts rename to src/db/botinfo.ts index 9a36085..01f0327 100644 --- a/src/utils/db/botinfo.ts +++ b/src/db/botinfo.ts @@ -1,7 +1,7 @@ import type { PoolClient, QueryResult } from "pg"; -import type { dbBotInfo } from "../../types/database"; +import type { dbBotInfo } from "../types/database"; -import { pool } from "../database"; +import { pool } from "../utils/database"; export async function updateBotInfo( guilds_total: number = 0, diff --git a/src/utils/db/cron.ts b/src/db/cron.ts similarity index 93% rename from src/utils/db/cron.ts rename to src/db/cron.ts index 0d52dfc..f29ad54 100644 --- a/src/utils/db/cron.ts +++ b/src/db/cron.ts @@ -1,4 +1,4 @@ -import { pool } from "../database"; +import { pool } from "../utils/database"; export async function cronUpdateTopChannels(): Promise { const query = ` diff --git a/src/utils/db/discord.ts b/src/db/discord.ts similarity index 97% rename from src/utils/db/discord.ts rename to src/db/discord.ts index 26c4595..91a7784 100644 --- a/src/utils/db/discord.ts +++ b/src/db/discord.ts @@ -1,5 +1,5 @@ -import { Platform } from "../../types/types.d"; -import { pool } from "../database"; +import { Platform } from "../types/types"; +import { pool } from "../utils/database"; export async function checkIfGuildIsTrackingUserAlready( platform: Platform, diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..ecfe2cb --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,101 @@ +// To make it easier to work with the database, disable prettier for this file +/* eslint-disable prettier/prettier */ +import { pgTable, serial, text, boolean, integer, timestamp, date } from "drizzle-orm/pg-core"; + +export const dbDiscordTable = pgTable("discord", { + guildId: text("guild_id").primaryKey(), + allowedPublicSharing: boolean("allowed_public_sharing").notNull().default(false), + feedrUpdatesChannelId: text("feedr_updates_channel_id"), + +}); + +export const dbBlueskyTable = pgTable("bluesky", { + blueskyUserId: text("bluesky_user_id").primaryKey(), + latestPostId: text("latest_post_id"), + latestReplyId: text("latest_reply_id"), +}); + +export const dbYouTubeTable = pgTable("youtube", { + youtubeChannelId: text("youtube_channel_id").primaryKey(), + latestVideoId: text("latest_video_id"), + latestVideoIdUpdated: timestamp("latest_video_id_updated"), + latestShortId: text("latest_short_id"), + latestShortIdUpdated: timestamp("latest_short_id_updated"), + latestStreamId: text("latest_stream_id"), + latestStreamIdUpdated: timestamp("latest_stream_id_updated"), + youtubeChannelIsLive: boolean("youtube_channel_is_live"), +}); + +export const dbTwitchTable = pgTable("twitch", { + twitchChannelId: text("twitch_channel_id").primaryKey(), + twitchChannelIsLive: boolean("twitch_channel_is_live").notNull(), +}); + +export const dbGuildBlueskySubscriptionsTable = pgTable("guild_bluesky_subscriptions", { + id: serial("id").primaryKey(), + guildId: text("guild_id").notNull().references(() => dbDiscordTable.guildId), + blueskyUserId: text("bluesky_user_id").notNull().references(() => dbBlueskyTable.blueskyUserId), + notificationChannelId: text("notification_channel_id").notNull(), + notificationRoleId: text("notification_role_id"), + isDm: boolean("is_dm").notNull().default(false), + checkForReplies: boolean("check_for_replies").notNull().default(false), +}); + +export const dbGuildYouTubeSubscriptionsTable = pgTable("guild_youtube_subscriptions", { + id: serial("id").primaryKey(), + guildId: text("guild_id").notNull().references(() => dbDiscordTable.guildId), + youtubeChannelId: text("youtube_channel_id").notNull().references(() => dbYouTubeTable.youtubeChannelId), + notificationChannelId: text("notification_channel_id").notNull(), + notificationRoleId: text("notification_role_id"), + isDm: boolean("is_dm").notNull().default(false), + trackVideos: boolean("track_videos").notNull().default(false), + trackShorts: boolean("track_shorts").notNull().default(false), + trackStreams: boolean("track_streams").notNull().default(false), +}); + +export const dbGuildTwitchSubscriptionsTable = pgTable("guild_twitch_subscriptions", { + id: serial("id").primaryKey(), + guildId: text("guild_id").notNull().references(() => dbDiscordTable.guildId), + twitchChannelId: text("twitch_channel_id").notNull().references(() => dbTwitchTable.twitchChannelId), + notificationChannelId: text("notification_channel_id").notNull(), + notificationRoleId: text("notification_role_id"), + isDm: boolean("is_dm").notNull().default(false), +}); + +export const dbBotInfoTable = pgTable("bot_info", { + guildsTotal: integer("guilds_total").notNull().default(0), + channelsTracked: integer("channels_tracked").notNull().default(0), + totalMembers: integer("total_members").notNull().default(0), + time: timestamp("time").notNull().defaultNow(), +}); + +export const dbBotInfoNotificationsTable = pgTable("bot_info_notifications", { + date: date("date").notNull(), + totalYouTube: integer("total_youtube").notNull().default(0), + totalTwitch: integer("total_twitch").notNull().default(0), +}); + +export const dbBotInfoNotificationsTimingsTable = pgTable("bot_info_notifications_timings", { + time: timestamp("time").notNull(), + channelId: text("channel_id").notNull().references(() => dbYouTubeTable.youtubeChannelId), + timeMs: integer("time_ms").notNull().default(0), +}); + +export const dbBotInfoTopChannelsTable = pgTable("bot_info_top_channels", { + youtubeChannelId: text("youtube_channel_id").primaryKey().references(() => dbYouTubeTable.youtubeChannelId), + guildsTracking: integer("guilds_tracking").notNull().default(0), +}); + +export const dbBotInfoTopGuildsTable = pgTable("bot_info_top_guilds", { + guildId: text("guild_id").primaryKey().references(() => dbDiscordTable.guildId), + members: integer("members").notNull().default(0), +}); + +export const dbAuditLogsTable = pgTable("audit_logs", { + id: serial("id").primaryKey(), + eventType: text("event_type").notNull(), + guildId: text("guild_id").notNull().references(() => dbDiscordTable.guildId), + relatedId: text("related_id").notNull(), + note: text("note"), + occurredAt: timestamp("occurred_at").defaultNow(), +}); diff --git a/src/utils/db/twitch.ts b/src/db/twitch.ts similarity index 95% rename from src/utils/db/twitch.ts rename to src/db/twitch.ts index 3123380..03e915c 100644 --- a/src/utils/db/twitch.ts +++ b/src/db/twitch.ts @@ -1,6 +1,6 @@ -import type { dbTwitch } from "../../types/database"; +import type { dbTwitch } from "../types/database"; -import { pool } from "../database"; +import { pool } from "../utils/database"; export async function dbTwitchGetAllChannelsToTrack(): Promise<{ success: boolean; diff --git a/src/utils/db/youtube.ts b/src/db/youtube.ts similarity index 95% rename from src/utils/db/youtube.ts rename to src/db/youtube.ts index 3af88f3..d4021a1 100644 --- a/src/utils/db/youtube.ts +++ b/src/db/youtube.ts @@ -1,9 +1,9 @@ -import type { dbYouTube } from "../../types/database"; +import type { dbYouTube } from "../types/database"; -import { pool } from "../database"; +import { pool } from "../utils/database"; import getSinglePlaylistAndReturnVideoId, { PlaylistType, -} from "../youtube/getSinglePlaylistAndReturnVideoData"; +} from "../utils/youtube/getSinglePlaylistAndReturnVideoData"; export async function dbYouTubeGetAllChannelsToTrack(): Promise<{ success: boolean; diff --git a/src/index.ts b/src/index.ts index dc63822..c1a3053 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,7 @@ import { import { env } from "./config.ts"; import commandsMap from "./commands.ts"; -import initTables from "./utils/db/init.ts"; +import initTables from "./db/schema.ts"; import { getTwitchToken } from "./utils/twitch/auth.ts"; if (!env.discordToken || env.discordToken === "YOUR_DISCORD_TOKEN") { diff --git a/src/utils/cronJobs.ts b/src/utils/cronJobs.ts index 693d064..e38dce3 100644 --- a/src/utils/cronJobs.ts +++ b/src/utils/cronJobs.ts @@ -2,7 +2,7 @@ import { ActivityType, Guild, PresenceUpdateStatus } from "discord.js"; import client from ".."; -import { updateBotInfo } from "./db/botinfo"; +import { updateBotInfo } from "../db/botinfo"; export async function cronUpdateBotInfo() { if (!client?.user) return; diff --git a/src/utils/database.ts b/src/utils/database.ts index 2d67448..953e10d 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -18,6 +18,9 @@ if ( throw new Error("Database credentials are not set"); } +/** + * @deprecated This pool is deprecated and being removed in the future. + */ export const pool: Pool = new Pool({ host: dbCredentials.host, port: parseInt(dbCredentials.port), diff --git a/src/utils/db/init.ts b/src/utils/db/init.ts deleted file mode 100644 index ede13e6..0000000 --- a/src/utils/db/init.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { pool } from "../database"; - -export default async function initTables(): Promise { - const createDiscordTable = ` - CREATE TABLE IF NOT EXISTS discord ( - guild_id TEXT PRIMARY KEY, - is_dm BOOLEAN NOT NULL DEFAULT FALSE, - allowed_public_sharing BOOLEAN NOT NULL DEFAULT FALSE, - feedr_updates_channel_id TEXT - ); - `; - - const createBlueskyTable = ` - CREATE TABLE IF NOT EXISTS bluesky ( - bluesky_user_id TEXT PRIMARY KEY, - latest_post_id TEXT, - latest_reply_id TEXT - ); - `; - - const createYouTubeTable = ` - CREATE TABLE IF NOT EXISTS youtube ( - youtube_channel_id TEXT PRIMARY KEY, - latest_video_id TEXT, - latest_video_id_updated TIMESTAMP, - latest_short_id TEXT, - latest_short_id_updated TIMESTAMP, - latest_stream_id TEXT, - latest_stream_id_updated TIMESTAMP, - youtube_channel_is_live BOOLEAN - ); - `; - - const createTwitchTable = ` - CREATE TABLE IF NOT EXISTS twitch ( - twitch_channel_id TEXT PRIMARY KEY, - twitch_channel_is_live BOOLEAN NOT NULL - ); - `; - - const createGuildBlueskySubscriptionsTable = ` - CREATE TABLE IF NOT EXISTS guild_bluesky_subscriptions ( - id SERIAL PRIMARY KEY, - guild_id TEXT NOT NULL REFERENCES discord(guild_id), - bluesky_user_id TEXT NOT NULL REFERENCES bluesky(bluesky_user_id), - notification_channel_id TEXT NOT NULL, - notification_role_id TEXT, - is_dm BOOLEAN NOT NULL DEFAULT FALSE, - check_for_replies BOOLEAN NOT NULL DEFAULT FALSE - ); - `; - - const createGuildYouTubeSubscriptionsTable = ` - CREATE TABLE IF NOT EXISTS guild_youtube_subscriptions ( - id SERIAL PRIMARY KEY, - guild_id TEXT NOT NULL REFERENCES discord(guild_id), - youtube_channel_id TEXT NOT NULL REFERENCES youtube(youtube_channel_id), - notification_channel_id TEXT NOT NULL, - notification_role_id TEXT, - is_dm BOOLEAN NOT NULL DEFAULT FALSE, - track_videos BOOLEAN NOT NULL DEFAULT FALSE, - track_shorts BOOLEAN NOT NULL DEFAULT FALSE, - track_streams BOOLEAN NOT NULL DEFAULT FALSE - ); - `; - - const createGuildTwitchSubscriptionsTable = ` - CREATE TABLE IF NOT EXISTS guild_twitch_subscriptions ( - id SERIAL PRIMARY KEY, - guild_id TEXT NOT NULL REFERENCES discord(guild_id), - twitch_channel_id TEXT NOT NULL REFERENCES twitch(twitch_channel_id), - notification_channel_id TEXT NOT NULL, - notification_role_id TEXT, - is_dm BOOLEAN NOT NULL DEFAULT FALSE - ); - `; - - const createBotInfoTable = ` - CREATE TABLE IF NOT EXISTS bot_info ( - guilds_total INTEGER NOT NULL DEFAULT 0, - channels_tracked INTEGER NOT NULL DEFAULT 0, - total_members INTEGER NOT NULL DEFAULT 0, - time TIMESTAMP NOT NULL DEFAULT now() - ); - `; - - const createBotInfoNotificationsTable = ` - CREATE TABLE IF NOT EXISTS bot_info_notifications ( - date DATE NOT NULL, - total_youtube INTEGER NOT NULL DEFAULT 0, - total_twitch INTEGER NOT NULL DEFAULT 0 - ); - `; - - const createBotInfoNotificationsTimingsTable = ` - CREATE TABLE IF NOT EXISTS bot_info_notifications_timings ( - time TIMESTAMP NOT NULL, - channel_id TEXT NOT NULL REFERENCES youtube(youtube_channel_id), - time_ms INTEGER NOT NULL DEFAULT 0 - ); - `; - - const createBotInfoTopChannelsTable = ` - CREATE TABLE IF NOT EXISTS bot_info_top_channels ( - youtube_channel_id TEXT PRIMARY KEY REFERENCES youtube(youtube_channel_id), - guilds_tracking INTEGER NOT NULL DEFAULT 0 - ); - `; - - const createBotInfoTopGuildsTable = ` - CREATE TABLE IF NOT EXISTS bot_info_top_guilds ( - guild_id TEXT PRIMARY KEY REFERENCES discord(guild_id), - members INTEGER NOT NULL DEFAULT 0 - ); - `; - - const createAuditLogsTable = ` - CREATE TABLE IF NOT EXISTS audit_logs ( - id SERIAL PRIMARY KEY, - event_type TEXT NOT NULL, - guild_id TEXT NOT NULL REFERENCES discord(guild_id), - related_id TEXT NOT NULL, - note TEXT, - occurred_at TIMESTAMP DEFAULT now() - ); - `; - - const seedBotInfoTable = ` - INSERT INTO bot_info (time, guilds_total, channels_tracked, total_members) VALUES (now(), 0, 0, 0) - `; - - // TODO: Fix the guild table - const tempDropQuery = ` - DO $$ - BEGIN - IF EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE constraint_name = 'guild_youtube_subscriptions_guild_id_fkey' - AND table_name = 'guild_youtube_subscriptions' - ) THEN - ALTER TABLE guild_youtube_subscriptions - DROP CONSTRAINT guild_youtube_subscriptions_guild_id_fkey; - END IF; - - IF EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE constraint_name = 'guild_twitch_subscriptions_guild_id_fkey' - AND table_name = 'guild_twitch_subscriptions' - ) THEN - ALTER TABLE guild_twitch_subscriptions - DROP CONSTRAINT guild_twitch_subscriptions_guild_id_fkey; - END IF; - END$$; - `; - - try { - const client = await pool.connect(); - - await client.query(createDiscordTable); - console.log("Discord table created"); - - await client.query(createBlueskyTable); - console.log("Bluesky table created"); - - await client.query(createYouTubeTable); - console.log("YouTube table created"); - - await client.query(createTwitchTable); - console.log("Twitch table created"); - - await client.query(createGuildBlueskySubscriptionsTable); - console.log("Guild Bluesky Subscriptions table created"); - - await client.query(createGuildYouTubeSubscriptionsTable); - console.log("Guild YouTube Subscriptions table created"); - - await client.query(createGuildTwitchSubscriptionsTable); - console.log("Guild Twitch Subscriptions table created"); - - await client.query(createBotInfoTable); - console.log("Bot Info table created"); - - await client.query(createBotInfoNotificationsTable); - console.log("Bot Info Notifications table created"); - - await client.query(createBotInfoNotificationsTimingsTable); - console.log("Bot Info Notifications Timings table created"); - - await client.query(createBotInfoTopChannelsTable); - console.log("Bot Info Top Channels table created"); - - await client.query(createBotInfoTopGuildsTable); - console.log("Bot Info Top Guilds table created"); - - await client.query(createAuditLogsTable); - console.log("Audit Logs table created"); - - await client.query(seedBotInfoTable); - console.log("Bot Info table seeded"); - - await client.query(tempDropQuery); - console.log("Temporary drop query executed"); - - client.release(); - - return true; - } catch (err) { - console.error("Error creating tables:", err); - - return false; - } -} diff --git a/src/utils/youtube/fetchLatestUploads.ts b/src/utils/youtube/fetchLatestUploads.ts index a3fbed8..cd0422d 100644 --- a/src/utils/youtube/fetchLatestUploads.ts +++ b/src/utils/youtube/fetchLatestUploads.ts @@ -2,7 +2,7 @@ import type { dbDiscordTable, dbYouTube } from "../../types/database"; import { env } from "../../config"; import { getGuildsTrackingChannel, updateVideoId } from "../database"; -import { dbYouTubeGetAllChannelsToTrack } from "../db/youtube"; +import { dbYouTubeGetAllChannelsToTrack } from "../../db/youtube"; import getChannelDetails from "./getChannelDetails"; From 323d59587b290152bb15c922018ceb2b005d9128 Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Wed, 16 Jul 2025 12:25:26 +0100 Subject: [PATCH 02/14] chore(database): update db schema --- bun.lock | 3 ++ drizzle.config.ts | 9 +++-- new.dbml | 98 ----------------------------------------------- package.json | 3 ++ src/db/schema.ts | 93 +++++++++++++++++++++++++++++--------------- 5 files changed, 75 insertions(+), 131 deletions(-) delete mode 100644 new.dbml diff --git a/bun.lock b/bun.lock index 972f9f1..c9e7f16 100644 --- a/bun.lock +++ b/bun.lock @@ -16,6 +16,7 @@ "@typescript-eslint/eslint-plugin": "8.11.0", "@typescript-eslint/parser": "8.11.0", "concurrently": "^9.1.2", + "cross-env": "^7.0.3", "drizzle-kit": "^0.31.4", "eslint": "^8.57.0", "eslint-config-prettier": "9.1.0", @@ -222,6 +223,8 @@ "cron": ["cron@4.3.0", "", { "dependencies": { "@types/luxon": "~3.6.0", "luxon": "~3.6.0" } }, "sha512-ciiYNLfSlF9MrDqnbMdRWFiA6oizSF7kA1osPP9lRzNu0Uu+AWog1UKy7SkckiDY2irrNjeO6qLyKnXC8oxmrw=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], diff --git a/drizzle.config.ts b/drizzle.config.ts index 7e24a9c..1b19b0d 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,14 +1,17 @@ import { defineConfig } from "drizzle-kit"; -import { config } from "./src/config"; +const databaseUrl = + process.env.NODE_ENV === "development" + ? process.env.POSTGRES_DEV_URL + : process.env.POSTGRES_URL; -console.log("Using database URL:", config.databaseUrl); +console.log("Using database URL:", databaseUrl); export default defineConfig({ out: "./drizzle", schema: "./src/db/schema.ts", dialect: "postgresql", dbCredentials: { - url: config.databaseUrl!, + url: databaseUrl!, }, }); diff --git a/new.dbml b/new.dbml deleted file mode 100644 index d2a252c..0000000 --- a/new.dbml +++ /dev/null @@ -1,98 +0,0 @@ --- the database schema for feedr 2.0, just here temporarily during development - -Table discord { - guild_id string [pk, not null, note: "Discord guild ID"] - is_dm string [default: false, not null] - allowed_public_sharing boolean [default: false, not null, note: "False by default due to privacy stuff"] -} - -Table guild_bluesky_subscriptions { - id int [pk, not null, increment] - guild_id string [not null, ref: > discord.guild_id] - bluesky_user_id string [not null, ref: > bluesky.bluesky_user_id] - notification_channel_id string [not null] - notification_role_id string - is_dm boolean [not null, default: false] - check_for_replies boolean [not null, default: false] -} - -Table guild_youtube_subscriptions { - id int [pk, not null, increment] - guild_id string [not null, ref: > discord.guild_id] - youtube_channel_id string [not null, ref: > youtube.youtube_channel_id] - notification_channel_id string [not null] - notification_role_id string - is_dm boolean [not null, default: false] - track_videos boolean [default: false, not null] - track_shorts boolean [default: false, not null] - track_streams boolean [default: false, not null] -} - -Table guild_twitch_subscriptions { - id int [pk, not null, increment] - guild_id string [not null, ref: > discord.guild_id] - twitch_channel_id string [not null, ref: > twitch.twitch_channel_id] - notification_channel_id string [not null] - notification_role_id string - is_dm boolean [not null, default: false] -} - -Table bluesky { - bluesky_user_id string [pk, not null] - latest_post_id string - latest_reply_id string -} - -Table youtube { - youtube_channel_id string [pk, not null] - latest_video_id string - latest_video_id_updated datetime - latest_short_id string - latest_short_id_updated datetime - latest_stream_id string - latest_stream_id_updated datetime - youtube_channel_is_live boolean -} - -Table twitch { - twitch_channel_id string [pk, not null] - twitch_channel_is_live boolean [not null] -} - -Table bot_info { - time datetime [not null, default: `now()`] - guilds_total int [not null, default: 0] - channels_tracked int [not null, default: 0] - total_members int [not null, default: 0] -} - -Table bot_info_notifications { - date date [not null] - total_youtube int [not null, default: 0] - total_twitch int [not null, default: 0] -} - -Table bot_info_notifications_timings { - time datetime [not null] - channel_id string [not null, ref: > youtube.youtube_channel_id] - time_ms int [not null, default: 0] -} - -Table bot_info_top_channels { - youtube_channel_id string [not null, pk, ref: > youtube.youtube_channel_id] - guilds_tracking int [not null, default: 0] -} - -Table bot_info_top_guilds { - guild_id int [pk, not null, ref: > discord.guild_id] - members int [not null, default: 0] -} - -Table audit_logs { - id int [pk, not null, increment] - event_type string [not null, note: "e.g., subscribe_youtube, unsubscribe_twitch"] - guild_id string [not null, ref: > discord.guild_id] - related_id string [not null, note: "Related YouTube or Twitch channel ID"] - note string - occurred_at datetime [default: `now()`] -} diff --git a/package.json b/package.json index 15c9f88..095e3d7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@typescript-eslint/eslint-plugin": "8.11.0", "@typescript-eslint/parser": "8.11.0", "concurrently": "^9.1.2", + "cross-env": "^7.0.3", "drizzle-kit": "^0.31.4", "eslint": "^8.57.0", "eslint-config-prettier": "9.1.0", @@ -22,6 +23,8 @@ }, "scripts": { "db:generate": "bun drizzle-kit generate", + "db:push:dev": "cross-env NODE_ENV=development bun drizzle-kit push", + "db:push:prod": "bun drizzle-kit push", "dev": "concurrently --names \"WEB,API,BOT\" --prefix-colors \"blue,green,magenta\" \"cd web && bun dev\" \"cd api && cargo watch -x \\\"run -- --dev\\\"\" \"bun --watch . --dev\"", "dev:bot": "bun --watch . --dev", "test:jetstream": "bun --watch src/utils/bluesky/jetstream.ts", diff --git a/src/db/schema.ts b/src/db/schema.ts index ecfe2cb..0000fa4 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,19 +1,21 @@ // To make it easier to work with the database, disable prettier for this file /* eslint-disable prettier/prettier */ -import { pgTable, serial, text, boolean, integer, timestamp, date } from "drizzle-orm/pg-core"; +import { pgTable, serial, text, boolean, timestamp, decimal, unique, index, pgEnum, jsonb, integer } from "drizzle-orm/pg-core"; export const dbDiscordTable = pgTable("discord", { guildId: text("guild_id").primaryKey(), allowedPublicSharing: boolean("allowed_public_sharing").notNull().default(false), feedrUpdatesChannelId: text("feedr_updates_channel_id"), - + isInServer: boolean("is_in_server").notNull().default(true), }); export const dbBlueskyTable = pgTable("bluesky", { blueskyUserId: text("bluesky_user_id").primaryKey(), latestPostId: text("latest_post_id"), latestReplyId: text("latest_reply_id"), -}); +}, (table) => [ + index("idx_bluesky_user_id").on(table.blueskyUserId), +]); export const dbYouTubeTable = pgTable("youtube", { youtubeChannelId: text("youtube_channel_id").primaryKey(), @@ -23,13 +25,18 @@ export const dbYouTubeTable = pgTable("youtube", { latestShortIdUpdated: timestamp("latest_short_id_updated"), latestStreamId: text("latest_stream_id"), latestStreamIdUpdated: timestamp("latest_stream_id_updated"), - youtubeChannelIsLive: boolean("youtube_channel_is_live"), -}); + youtubeChannelIsLive: boolean("youtube_channel_is_live").notNull().default(false), + youtubeLiveIds: text("youtube_live_ids").array().notNull().default([]), +}, (table) => [ + index("idx_youtube_channel_id").on(table.youtubeChannelId), +]); export const dbTwitchTable = pgTable("twitch", { twitchChannelId: text("twitch_channel_id").primaryKey(), - twitchChannelIsLive: boolean("twitch_channel_is_live").notNull(), -}); + twitchChannelIsLive: boolean("twitch_channel_is_live").notNull().default(false), +}, (table) => [ + index("idx_twitch_channel_id").on(table.twitchChannelId), +]); export const dbGuildBlueskySubscriptionsTable = pgTable("guild_bluesky_subscriptions", { id: serial("id").primaryKey(), @@ -39,7 +46,9 @@ export const dbGuildBlueskySubscriptionsTable = pgTable("guild_bluesky_subscript notificationRoleId: text("notification_role_id"), isDm: boolean("is_dm").notNull().default(false), checkForReplies: boolean("check_for_replies").notNull().default(false), -}); +}, (table) => [ + unique("guild_bluesky_subscription").on(table.guildId, table.blueskyUserId), +]); export const dbGuildYouTubeSubscriptionsTable = pgTable("guild_youtube_subscriptions", { id: serial("id").primaryKey(), @@ -51,7 +60,9 @@ export const dbGuildYouTubeSubscriptionsTable = pgTable("guild_youtube_subscript trackVideos: boolean("track_videos").notNull().default(false), trackShorts: boolean("track_shorts").notNull().default(false), trackStreams: boolean("track_streams").notNull().default(false), -}); +}, (table) => [ + unique("guild_youtube_subscription").on(table.guildId, table.youtubeChannelId), +]); export const dbGuildTwitchSubscriptionsTable = pgTable("guild_twitch_subscriptions", { id: serial("id").primaryKey(), @@ -60,42 +71,64 @@ export const dbGuildTwitchSubscriptionsTable = pgTable("guild_twitch_subscriptio notificationChannelId: text("notification_channel_id").notNull(), notificationRoleId: text("notification_role_id"), isDm: boolean("is_dm").notNull().default(false), -}); + latestMessageId: text("latest_message_id"), +}, (table) => [ + unique("guild_twitch_subscription").on(table.guildId, table.twitchChannelId), +]); export const dbBotInfoTable = pgTable("bot_info", { + timestamp: timestamp("timestamp").notNull().defaultNow(), guildsTotal: integer("guilds_total").notNull().default(0), channelsTracked: integer("channels_tracked").notNull().default(0), totalMembers: integer("total_members").notNull().default(0), - time: timestamp("time").notNull().defaultNow(), + notificationsSent: integer("notifications_sent").notNull().default(0), }); export const dbBotInfoNotificationsTable = pgTable("bot_info_notifications", { - date: date("date").notNull(), - totalYouTube: integer("total_youtube").notNull().default(0), - totalTwitch: integer("total_twitch").notNull().default(0), + timestamp: timestamp("timestamp").notNull(), + service: text("service").notNull(), + delay: decimal("delay", { precision: 10, scale: 2 }).notNull().default("0.0"), }); -export const dbBotInfoNotificationsTimingsTable = pgTable("bot_info_notifications_timings", { - time: timestamp("time").notNull(), - channelId: text("channel_id").notNull().references(() => dbYouTubeTable.youtubeChannelId), - timeMs: integer("time_ms").notNull().default(0), -}); +// Deprecated, but kept for reference +// export const dbBotInfoNotificationsTimingsTable = pgTable("bot_info_notifications_timings", { +// time: timestamp("time").notNull(), +// channelId: text("channel_id").notNull().references(() => dbYouTubeTable.youtubeChannelId), +// timeMs: integer("time_ms").notNull().default(0), +// }); -export const dbBotInfoTopChannelsTable = pgTable("bot_info_top_channels", { - youtubeChannelId: text("youtube_channel_id").primaryKey().references(() => dbYouTubeTable.youtubeChannelId), - guildsTracking: integer("guilds_tracking").notNull().default(0), -}); +// Once again, can be aggregated in the API +// export const dbBotInfoTopChannelsTable = pgTable("bot_info_top_channels", { +// youtubeChannelId: text("youtube_channel_id").primaryKey().references(() => dbYouTubeTable.youtubeChannelId), +// guildsTracking: integer("guilds_tracking").notNull().default(0), +// }); -export const dbBotInfoTopGuildsTable = pgTable("bot_info_top_guilds", { - guildId: text("guild_id").primaryKey().references(() => dbDiscordTable.guildId), - members: integer("members").notNull().default(0), -}); +// Once again, can be aggregated in the API +// export const dbBotInfoTopGuildsTable = pgTable("bot_info_top_guilds", { +// guildId: text("guild_id").primaryKey().references(() => dbDiscordTable.guildId), +// members: integer("members").notNull().default(0), +// }); + +export const dbAuditLogsEventTypeEnum = pgEnum("event_type", [ + "subscription_created", + "subscription_deleted", + "notification_sent", + "guild_joined", + "guild_left", +]); + +export const dbAuditLogsSuccessTypeEnum = pgEnum("audit_log_success", [ + "success", + "failure", + "info", + "warning", +]); export const dbAuditLogsTable = pgTable("audit_logs", { id: serial("id").primaryKey(), - eventType: text("event_type").notNull(), guildId: text("guild_id").notNull().references(() => dbDiscordTable.guildId), - relatedId: text("related_id").notNull(), - note: text("note"), + eventType: dbAuditLogsEventTypeEnum().notNull(), + successType: dbAuditLogsSuccessTypeEnum().notNull(), + data: jsonb("data"), occurredAt: timestamp("occurred_at").defaultNow(), }); From 89d188efca7a2c24aeb46d7ce13cc8896815a4a6 Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Wed, 16 Jul 2025 21:04:34 +0100 Subject: [PATCH 03/14] chore(database): move current (dev) database schema to new one --- src/db/botinfo.ts | 125 +------------ src/db/cron.ts | 22 --- src/db/db.ts | 12 ++ src/db/discord.ts | 168 +++++++++--------- src/db/schema.ts | 16 +- src/db/twitch.ts | 65 +++---- src/db/youtube.ts | 92 +++++----- .../getSinglePlaylistAndReturnVideoData.ts | 2 + 8 files changed, 182 insertions(+), 320 deletions(-) delete mode 100644 src/db/cron.ts create mode 100644 src/db/db.ts diff --git a/src/db/botinfo.ts b/src/db/botinfo.ts index 01f0327..7f48e2a 100644 --- a/src/db/botinfo.ts +++ b/src/db/botinfo.ts @@ -1,124 +1 @@ -import type { PoolClient, QueryResult } from "pg"; -import type { dbBotInfo } from "../types/database"; - -import { pool } from "../utils/database"; - -export async function updateBotInfo( - guilds_total: number = 0, - channels_tracked: number = 0, - total_members: number = 0, -): Promise { - const query = ` - INSERT INTO bot_info (guilds_total, channels_tracked, total_members, time) - VALUES ($1, $2, $3, NOW()) - ON CONFLICT (time) DO UPDATE - SET guilds_total = EXCLUDED.guilds_total, - channels_tracked = EXCLUDED.channels_tracked, - total_members = EXCLUDED.total_members; - `; - - try { - const client = await pool.connect(); - - await client.query(query, [ - guilds_total, - channels_tracked, - total_members, - ]); - - client.release(); - - console.log("Bot info updated successfully"); - } catch (err) { - console.error("Error updating bot info:", err); - } -} - -export async function getBotInfo() { - const query = `SELECT * FROM bot_info`; - - try { - const client: PoolClient = await pool.connect(); - const result: QueryResult = await client.query(query); - - client.release(); - - return result.rows[0] as dbBotInfo; - } catch (err) { - console.error("Error getting bot info:", err); - - return null; - } -} - -export async function updateBotInfoNotifications( - platform: "youtube" | "bluesky" | "twitch", -) { - const query = ` - INSERT INTO bot_info_notifications (date, total_${platform}) - VALUES (CURRENT_DATE, 1) - ON CONFLICT (date) DO UPDATE - SET total_${platform} = bot_info_notifications.total_${platform} + 1; - `; - - try { - const client: PoolClient = await pool.connect(); - - await client.query(query, [platform]); - - client.release(); - - console.log(`Bot info notifications updated for ${platform}`); - } catch (err) { - console.error( - `Error updating bot info notifications for ${platform}:`, - err, - ); - } -} - -export async function getBotInfoNotifications() { - const query = `SELECT * FROM bot_info_notifications`; - - try { - const client: PoolClient = await pool.connect(); - const result: QueryResult = await client.query(query); - - client.release(); - - return result.rows; - } catch (err) { - console.error("Error getting bot info notifications:", err); - - return []; - } -} - -export async function updateBotInfoNotificationsTimings( - channel_id: string, - time_ms: number, -): Promise { - const query = ` - INSERT INTO bot_info_notifications_timings (time, channel_id, time_ms) - VALUES (NOW(), $1, $2) - ON CONFLICT (time, channel_id) DO UPDATE - SET time_ms = EXCLUDED.time_ms; - `; - - try { - const client: PoolClient = await pool.connect(); - - await client.query(query, [channel_id, time_ms]); - - client.release(); - - console.log( - `Bot info notifications timings updated for channel ${channel_id}`, - ); - } catch (err) { - console.error( - `Error updating bot info notifications timings for channel ${channel_id}:`, - err, - ); - } -} +// Rewriting \ No newline at end of file diff --git a/src/db/cron.ts b/src/db/cron.ts deleted file mode 100644 index f29ad54..0000000 --- a/src/db/cron.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { pool } from "../utils/database"; - -export async function cronUpdateTopChannels(): Promise { - const query = ` - TRUNCATE TABLE bot_info_top_channels; - - INSERT INTO bot_info_top_channels (youtube_channel_id, guilds_tracking) - SELECT youtube_channel_id, COUNT(*) AS guilds_tracking - FROM guild_youtube_subscriptions - GROUP BY youtube_channel_id - ORDER BY guilds_tracking DESC; - `; - - try { - const client = await pool.connect(); - - await client.query(query); - client.release(); - } catch (error) { - console.error("Error updating top channels:", error); - } -} diff --git a/src/db/db.ts b/src/db/db.ts new file mode 100644 index 0000000..4d3e459 --- /dev/null +++ b/src/db/db.ts @@ -0,0 +1,12 @@ +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; + +import { config } from "../config"; + +import * as schema from "./schema"; + +const pool = new Pool({ + connectionString: config.databaseUrl, +}); + +export const db = drizzle(pool, { schema }); diff --git a/src/db/discord.ts b/src/db/discord.ts index 91a7784..9f811a9 100644 --- a/src/db/discord.ts +++ b/src/db/discord.ts @@ -1,46 +1,70 @@ +import { eq, and } from "drizzle-orm"; + import { Platform } from "../types/types"; -import { pool } from "../utils/database"; + +import { db } from "./db"; +import { + dbGuildYouTubeSubscriptionsTable, + dbGuildTwitchSubscriptionsTable, +} from "./schema"; export async function checkIfGuildIsTrackingUserAlready( platform: Platform, userId: string, guildId: string, -): Promise<{ success: boolean; data: any[] | null }> { +): Promise< + | { + success: true; + data: + | (typeof dbGuildYouTubeSubscriptionsTable.$inferSelect)[] + | (typeof dbGuildTwitchSubscriptionsTable.$inferSelect)[] + | null; + } + | { success: false; data: null } +> { console.log( `Checking if guild ${guildId} is tracking user ${userId} on platform ${platform}`, ); - let query: string | null = null; - - if (platform === Platform.YouTube) { - query = ` - SELECT * FROM guild_youtube_subscriptions - WHERE youtube_channel_id = $1 AND guild_id = $2 - `; - } else if (platform === Platform.Twitch) { - query = ` - SELECT * FROM guild_twitch_subscriptions - WHERE twitch_user_id = $1 AND guild_id = $2 - `; - } - - if (!query) { - console.error("Invalid platform provided for tracking check."); - - return { success: false, data: null }; - } - try { - const client = await pool.connect(); - const result = await client.query(query, [userId, guildId]); - - client.release(); - - if (result.rows.length > 0) { - return { success: true, data: result.rows }; + let result: any[] = []; + + if (platform === Platform.YouTube) { + result = await db + .select() + .from(dbGuildYouTubeSubscriptionsTable) + .where( + and( + eq( + dbGuildYouTubeSubscriptionsTable.youtubeChannelId, + userId, + ), + eq(dbGuildYouTubeSubscriptionsTable.guildId, guildId), + ), + ); + } else if (platform === Platform.Twitch) { + result = await db + .select() + .from(dbGuildTwitchSubscriptionsTable) + .where( + and( + eq( + dbGuildTwitchSubscriptionsTable.twitchChannelId, + userId, + ), + eq(dbGuildTwitchSubscriptionsTable.guildId, guildId), + ), + ); } else { - return { success: true, data: null }; + console.error("Invalid platform provided for tracking check."); + + return { success: false, data: null }; } + + return { + success: true, + data: result.length > 0 ? result : null, + }; } catch (error) { console.error("Error checking if guild is tracking user:", error); @@ -65,60 +89,44 @@ export async function discordAddGuildTrackingUser( `Adding guild ${guildId} tracking for user ${platformUserId} on platform ${platform}`, ); - let query: string | null = null; - let params: any[] = []; - - if (platform === Platform.YouTube) { - if ( - youtubeTrackVideos === undefined || - youtubeTrackVideos === null || - youtubeTrackShorts === undefined || - youtubeTrackShorts === null || - youtubeTrackLive === undefined || - youtubeTrackLive === null - ) { - console.error( - "YouTube tracking options must be provided for YouTube subscriptions.", - ); + try { + if (platform === Platform.YouTube) { + if ( + youtubeTrackVideos == null || + youtubeTrackShorts == null || + youtubeTrackLive == null + ) { + console.error( + "YouTube tracking options must be provided for YouTube subscriptions.", + ); + + return { success: false, data: [] }; + } + + await db.insert(dbGuildYouTubeSubscriptionsTable).values({ + youtubeChannelId: platformUserId, + guildId, + notificationChannelId: guildChannelId, + notificationRoleId: roleId, + isDm, + trackVideos: youtubeTrackVideos, + trackShorts: youtubeTrackShorts, + trackStreams: youtubeTrackLive, + }); + } else if (platform === Platform.Twitch) { + await db.insert(dbGuildTwitchSubscriptionsTable).values({ + twitchChannelId: platformUserId, + guildId, + notificationChannelId: guildChannelId, + notificationRoleId: roleId, + isDm, + }); + } else { + console.error("Invalid platform provided."); return { success: false, data: [] }; } - query = ` - INSERT INTO guild_youtube_subscriptions ( - youtube_channel_id, guild_id, notification_channel_id, notification_role_id, is_dm, - track_videos, track_shorts, track_streams - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - `; - params = [ - platformUserId, - guildId, - guildChannelId, - roleId, - isDm, - youtubeTrackVideos ?? false, - youtubeTrackShorts ?? false, - youtubeTrackLive ?? false, - ]; - } else if (platform === Platform.Twitch) { - query = ` - INSERT INTO guild_twitch_subscriptions ( - twitch_channel_id, guild_id, notification_channel_id, notification_role_id, is_dm - ) VALUES ($1, $2, $3, $4, $5) - `; - params = [platformUserId, guildId, guildChannelId, roleId, isDm]; - } - - if (!query) { - return { success: false, data: [] }; - } - - try { - const client = await pool.connect(); - - await client.query(query, params); - client.release(); - return { success: true, data: [] }; } catch (error) { console.error("Error adding guild tracking user:", error); diff --git a/src/db/schema.ts b/src/db/schema.ts index 0000fa4..cef5888 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,4 +1,5 @@ -// To make it easier to work with the database, disable prettier for this file +// To make it easier to work with the database, disable prettier and some eslint rules for this file +/* eslint-disable no-inline-comments */ /* eslint-disable prettier/prettier */ import { pgTable, serial, text, boolean, timestamp, decimal, unique, index, pgEnum, jsonb, integer } from "drizzle-orm/pg-core"; @@ -19,6 +20,8 @@ export const dbBlueskyTable = pgTable("bluesky", { export const dbYouTubeTable = pgTable("youtube", { youtubeChannelId: text("youtube_channel_id").primaryKey(), + youtubeChannelName: text("youtube_channel_name").notNull().default(""), + latestAllId: text("latest_all_id"), // For verification and optimisation purposes latestVideoId: text("latest_video_id"), latestVideoIdUpdated: timestamp("latest_video_id_updated"), latestShortId: text("latest_short_id"), @@ -34,6 +37,7 @@ export const dbYouTubeTable = pgTable("youtube", { export const dbTwitchTable = pgTable("twitch", { twitchChannelId: text("twitch_channel_id").primaryKey(), twitchChannelIsLive: boolean("twitch_channel_is_live").notNull().default(false), + twitchChannelName: text("twitch_channel_name").notNull().default(""), }, (table) => [ index("idx_twitch_channel_id").on(table.twitchChannelId), ]); @@ -110,11 +114,11 @@ export const dbBotInfoNotificationsTable = pgTable("bot_info_notifications", { // }); export const dbAuditLogsEventTypeEnum = pgEnum("event_type", [ - "subscription_created", - "subscription_deleted", - "notification_sent", - "guild_joined", - "guild_left", + "subscription_created", + "subscription_deleted", + "notification_sent", + "guild_joined", + "guild_left", ]); export const dbAuditLogsSuccessTypeEnum = pgEnum("audit_log_success", [ diff --git a/src/db/twitch.ts b/src/db/twitch.ts index 03e915c..ab5e10a 100644 --- a/src/db/twitch.ts +++ b/src/db/twitch.ts @@ -1,22 +1,18 @@ -import type { dbTwitch } from "../types/database"; +import { eq } from "drizzle-orm"; -import { pool } from "../utils/database"; +import { db } from "./db"; +import { dbTwitchTable } from "./schema"; export async function dbTwitchGetAllChannelsToTrack(): Promise<{ success: boolean; - data: dbTwitch[] | []; + data: (typeof dbTwitchTable.$inferSelect)[]; }> { - const query = `SELECT * FROM twitch`; - try { - const client = await pool.connect(); - const result = await client.query(query); - - client.release(); + const result = await db.select().from(dbTwitchTable); return { success: true, - data: result.rows as dbTwitch[], + data: result, }; } catch (err) { console.error("Error getting all channels to track:", err); @@ -30,23 +26,14 @@ export async function dbTwitchGetAllChannelsToTrack(): Promise<{ export async function checkIfStreamerIsAlreadyTracked( streamerId: string, -): Promise<{ success: boolean; data: dbTwitch[] | [] }> { - const query = ` - SELECT * FROM twitch - WHERE twitch_channel_id = $1 - `; - +): Promise<{ success: boolean; data: (typeof dbTwitchTable.$inferSelect)[] }> { try { - const client = await pool.connect(); - const result = await client.query(query, [streamerId]); + const result = await db + .select() + .from(dbTwitchTable) + .where(eq(dbTwitchTable.twitchChannelId, streamerId)); - client.release(); - - if (result.rows.length > 0) { - return { success: true, data: result.rows }; - } else { - return { success: true, data: [] }; - } + return { success: true, data: result }; } catch (error) { console.error("Error checking if streamer is already tracked:", error); @@ -57,23 +44,25 @@ export async function checkIfStreamerIsAlreadyTracked( export async function addNewStreamerToTrack( streamerId: string, isLive: boolean, -): Promise<{ success: boolean; data?: dbTwitch }> { - const query = ` - INSERT INTO twitch (twitch_channel_id, twitch_channel_is_live) - VALUES ($1, $2) - RETURNING * - `; - + twitchChannelName: string, +): Promise<{ success: boolean; data?: typeof dbTwitchTable.$inferSelect }> { try { - const client = await pool.connect(); - const result = await client.query(query, [streamerId, isLive]); + const [inserted] = await db + .insert(dbTwitchTable) + .values({ + twitchChannelId: streamerId, + twitchChannelIsLive: isLive, + twitchChannelName: twitchChannelName || "", + }) + .returning(); - client.release(); - - return { success: true, data: result.rows[0] as dbTwitch }; + return { + success: true, + data: inserted, + }; } catch (error) { console.error("Error adding new streamer to track:", error); return { success: false }; } -} \ No newline at end of file +} diff --git a/src/db/youtube.ts b/src/db/youtube.ts index d4021a1..2209a61 100644 --- a/src/db/youtube.ts +++ b/src/db/youtube.ts @@ -1,25 +1,23 @@ -import type { dbYouTube } from "../types/database"; +import { eq } from "drizzle-orm"; -import { pool } from "../utils/database"; import getSinglePlaylistAndReturnVideoId, { PlaylistType, } from "../utils/youtube/getSinglePlaylistAndReturnVideoData"; +import getChannelDetails from "../utils/youtube/getChannelDetails"; + +import { dbYouTubeTable } from "./schema"; +import { db } from "./db"; export async function dbYouTubeGetAllChannelsToTrack(): Promise<{ success: boolean; - data: dbYouTube[] | []; + data: (typeof dbYouTubeTable.$inferSelect)[]; }> { - const query = `SELECT * FROM youtube`; - try { - const client = await pool.connect(); - const result = await client.query(query); - - client.release(); + const result = await db.select().from(dbYouTubeTable); return { success: true, - data: result.rows as dbYouTube[], + data: result, }; } catch (err) { console.error("Error getting all channels to track:", err); @@ -34,18 +32,16 @@ export async function dbYouTubeGetAllChannelsToTrack(): Promise<{ // These two functions are for checking/adding a new channel to the youtube table export async function checkIfChannelIsAlreadyTracked( channelId: string, -): Promise<{ success: boolean; data: dbYouTube[] | [] }> { - const query = `SELECT * FROM youtube WHERE youtube_channel_id = $1`; - +): Promise<{ success: boolean; data: (typeof dbYouTubeTable.$inferSelect)[] }> { try { - const client = await pool.connect(); - const result = await client.query(query, [channelId]); - - client.release(); + const result = await db + .select() + .from(dbYouTubeTable) + .where(eq(dbYouTubeTable.youtubeChannelId, channelId)); return { success: true, - data: result.rows as dbYouTube[], + data: result, }; } catch (err) { console.error("Error checking if channel is already tracked:", err); @@ -63,44 +59,40 @@ export async function addNewChannelToTrack( ): Promise<{ success: boolean; data: [] }> { console.log("Adding channel to track:", channelId); - const longId = await getSinglePlaylistAndReturnVideoId( - channelId, - PlaylistType.Video, - ); - const shortId = await getSinglePlaylistAndReturnVideoId( - channelId, - PlaylistType.Short, - ); - const liveId = await getSinglePlaylistAndReturnVideoId( - channelId, - PlaylistType.Stream, - ); - - const query = `INSERT INTO youtube (youtube_channel_id, latest_video_id, latest_video_id_updated, latest_short_id, latest_short_id_updated, latest_stream_id, latest_stream_id_updated) VALUES ($1, $2, $3, $4, $5, $6, $7)`; - try { - const client = await pool.connect(); + const channelDetails = await getChannelDetails(channelId); - console.log( + const latestId = await getSinglePlaylistAndReturnVideoId( + channelId, + PlaylistType.All, + ); + const longId = await getSinglePlaylistAndReturnVideoId( channelId, - longId?.videoId, - longId?.datePublished, - shortId?.videoId, - shortId?.datePublished, - liveId?.videoId, - liveId?.datePublished, + PlaylistType.Video, ); - await client.query(query, [ + const shortId = await getSinglePlaylistAndReturnVideoId( channelId, - longId?.videoId || null, - longId?.datePublished ? longId.datePublished : null, - shortId?.videoId || null, - shortId?.datePublished ? shortId.datePublished : null, - liveId?.videoId || null, - liveId?.datePublished ? liveId.datePublished : null, - ]); + PlaylistType.Short, + ); + const liveId = await getSinglePlaylistAndReturnVideoId( + channelId, + PlaylistType.Stream, + ); - client.release(); + await db.insert(dbYouTubeTable).values({ + youtubeChannelId: channelId, + youtubeChannelName: channelDetails?.channelName ?? "", + latestAllId: latestId?.videoId ?? null, + latestVideoId: longId?.videoId ?? null, + latestVideoIdUpdated: longId?.datePublished ?? null, + latestShortId: shortId?.videoId ?? null, + latestShortIdUpdated: shortId?.datePublished ?? null, + latestStreamId: liveId?.videoId ?? null, + latestStreamIdUpdated: liveId?.datePublished ?? null, + // TODO: Add better streaming capabilities in the future + youtubeChannelIsLive: false, + youtubeLiveIds: [], + }); console.log("Channel added to track successfully:", channelId); diff --git a/src/utils/youtube/getSinglePlaylistAndReturnVideoData.ts b/src/utils/youtube/getSinglePlaylistAndReturnVideoData.ts index 1ac29c2..aab23cb 100644 --- a/src/utils/youtube/getSinglePlaylistAndReturnVideoData.ts +++ b/src/utils/youtube/getSinglePlaylistAndReturnVideoData.ts @@ -6,12 +6,14 @@ import type { YouTubePlaylistResponse } from "../../types/youtube"; import { env } from "../../config"; export enum PlaylistType { + All = "all", Video = "video", Short = "short", Stream = "stream", } const playlistIdPrefixes: Record = { + [PlaylistType.All]: "UU", [PlaylistType.Video]: "UULF", [PlaylistType.Short]: "UUSH", [PlaylistType.Stream]: "UULV", From fc86ebee4dbcf294e773d5229b0b52f6c6b81db4 Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Thu, 17 Jul 2025 14:19:14 +0100 Subject: [PATCH 04/14] chore(database): remove old database.ts file --- package.json | 2 +- src/db/audit.ts | 39 +++-- src/db/discord.ts | 146 ++++++++++++++++++ src/db/twitch.ts | 20 +++ src/db/youtube.ts | 49 ++++++ src/types/database.d.ts | 37 ----- src/utils/database.ts | 188 ------------------------ src/utils/youtube/fetchLatestUploads.ts | 21 ++- tsconfig.json | 52 +++---- 9 files changed, 278 insertions(+), 276 deletions(-) delete mode 100644 src/types/database.d.ts delete mode 100644 src/utils/database.ts diff --git a/package.json b/package.json index 095e3d7..3668ee2 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dev": "concurrently --names \"WEB,API,BOT\" --prefix-colors \"blue,green,magenta\" \"cd web && bun dev\" \"cd api && cargo watch -x \\\"run -- --dev\\\"\" \"bun --watch . --dev\"", "dev:bot": "bun --watch . --dev", "test:jetstream": "bun --watch src/utils/bluesky/jetstream.ts", - "lint": "eslint . --ext .ts -c .eslintrc.json", + "lint": "eslint src/ --ext .ts -c .eslintrc.json --debug", "lint:fix": "eslint . --ext .ts -c .eslintrc.json --fix", "cargo:api:dev": "cd api && cargo watch -x \"run -- --dev\"", "web:dev": "concurrently --names \"WEB,API\" --prefix-colors \"blue,green\" \"cd web && bun dev\" \"cd api && cargo watch -x \\\"run -- --dev\\\"\"", diff --git a/src/db/audit.ts b/src/db/audit.ts index 117f84e..e18756e 100644 --- a/src/db/audit.ts +++ b/src/db/audit.ts @@ -1,18 +1,25 @@ -export enum AuditResolution { - AUDIT_RESOLUTION_FAIL = "fail", - AUDIT_RESOLUTION_SUCCESS = "success", - AUDIT_RESOLUTION_ERROR = "error", - AUDIT_RESOLUTION_NULL = "null", -} - -export enum AuditType { - GUILD_CHECKED_YOUTUBE_CHANNEL = "guild_checked_youtube_channel", -} +import { + dbAuditLogsEventTypeEnum, + dbAuditLogsSuccessTypeEnum, + dbAuditLogsTable, +} from "./schema"; +import { db } from "./db"; -export async function addAuditEntry( - type: AuditType, - resolution: AuditResolution, -) { - console.log("Adding audit entry:", type, resolution); - throw new Error("Not implemented"); +export async function dbAuditLogCreate( + guildId: string, + eventType: (typeof dbAuditLogsEventTypeEnum)["enumValues"][number], + successType: (typeof dbAuditLogsSuccessTypeEnum)["enumValues"][number], + data: Record | null = null, +): Promise { + try { + await db.insert(dbAuditLogsTable).values({ + guildId, + eventType, + successType, + data, + occurredAt: new Date(), + }); + } catch (error) { + console.error("Error creating audit log entry:", error); + } } diff --git a/src/db/discord.ts b/src/db/discord.ts index 9f811a9..f1a03d0 100644 --- a/src/db/discord.ts +++ b/src/db/discord.ts @@ -134,3 +134,149 @@ export async function discordAddGuildTrackingUser( return { success: false, data: [] }; } } + +// Get all the Discord guilds that are tracking either YouTube or Twitch channels +export async function discordGetAllGuildsTrackingChannel( + platform: Platform, + platformUserId: string, +): Promise< + | { + success: true; + data: (typeof dbGuildYouTubeSubscriptionsTable.$inferSelect)[]; + } + | { + success: true; + data: (typeof dbGuildTwitchSubscriptionsTable.$inferSelect)[]; + } + | { success: false; data: [] } +> { + try { + if (platform === Platform.YouTube) { + const result = await db + .select() + .from(dbGuildYouTubeSubscriptionsTable) + .where( + eq( + dbGuildYouTubeSubscriptionsTable.youtubeChannelId, + platformUserId, + ), + ); + + return { + success: true, + data: result, + }; + } else if (platform === Platform.Twitch) { + const result = await db + .select() + .from(dbGuildTwitchSubscriptionsTable) + .where( + eq( + dbGuildTwitchSubscriptionsTable.twitchChannelId, + platformUserId, + ), + ); + + return { + success: true, + data: result, + }; + } else { + console.error("Invalid platform provided for tracking guilds."); + + return { success: false, data: [] }; + } + } catch (error) { + console.error("Error getting all guilds tracking channels:", error); + + return { success: false, data: [] }; + } +} + +// Get all tracked in the guild +export async function discordGetAllTrackedInGuild(guildId: string): Promise< + | { + success: true; + data: { + youtubeSubscriptions: (typeof dbGuildYouTubeSubscriptionsTable.$inferSelect)[]; + twitchSubscriptions: (typeof dbGuildTwitchSubscriptionsTable.$inferSelect)[]; + }; + } + | { success: false; data: null } +> { + try { + const youtubeSubscriptions = await db + .select() + .from(dbGuildYouTubeSubscriptionsTable) + .where(eq(dbGuildYouTubeSubscriptionsTable.guildId, guildId)); + + const twitchSubscriptions = await db + .select() + .from(dbGuildTwitchSubscriptionsTable) + .where(eq(dbGuildTwitchSubscriptionsTable.guildId, guildId)); + + return { + success: true, + data: { + youtubeSubscriptions, + twitchSubscriptions, + }, + }; + } catch (error) { + console.error( + "Error getting all tracked subscriptions in guild:", + error, + ); + + return { success: false, data: null }; + } +} + +// Remove tracking for a specific channel in a guild +export async function discordRemoveGuildTrackingChannel( + guildId: string, + platform: Platform, + platformUserId: string, +): Promise<{ success: boolean; data: [] }> { + console.log( + `Removing guild ${guildId} tracking for user ${platformUserId} on platform ${platform}`, + ); + + try { + if (platform === Platform.YouTube) { + await db + .delete(dbGuildYouTubeSubscriptionsTable) + .where( + and( + eq(dbGuildYouTubeSubscriptionsTable.guildId, guildId), + eq( + dbGuildYouTubeSubscriptionsTable.youtubeChannelId, + platformUserId, + ), + ), + ); + } else if (platform === Platform.Twitch) { + await db + .delete(dbGuildTwitchSubscriptionsTable) + .where( + and( + eq(dbGuildTwitchSubscriptionsTable.guildId, guildId), + eq( + dbGuildTwitchSubscriptionsTable.twitchChannelId, + platformUserId, + ), + ), + ); + } else { + console.error("Invalid platform provided for removal."); + + return { success: false, data: [] }; + } + + return { success: true, data: [] }; + } catch (error) { + console.error("Error removing guild tracking channel:", error); + + return { success: false, data: [] }; + } +} diff --git a/src/db/twitch.ts b/src/db/twitch.ts index ab5e10a..4617cdf 100644 --- a/src/db/twitch.ts +++ b/src/db/twitch.ts @@ -66,3 +66,23 @@ export async function addNewStreamerToTrack( return { success: false }; } } + +// Update a channel to be live or not +export async function twitchUpdateIsLive( + channelId: string, + isLive: boolean, +): Promise<{ success: boolean; data?: typeof dbTwitchTable.$inferSelect }> { + try { + const [updated] = await db + .update(dbTwitchTable) + .set({ twitchChannelIsLive: isLive }) + .where(eq(dbTwitchTable.twitchChannelId, channelId)) + .returning(); + + return { success: true, data: updated }; + } catch (error) { + console.error("Error updating channel live status:", error); + + return { success: false }; + } +} \ No newline at end of file diff --git a/src/db/youtube.ts b/src/db/youtube.ts index 2209a61..71492d8 100644 --- a/src/db/youtube.ts +++ b/src/db/youtube.ts @@ -8,6 +8,7 @@ import getChannelDetails from "../utils/youtube/getChannelDetails"; import { dbYouTubeTable } from "./schema"; import { db } from "./db"; +// Get all the YouTube channels that are being tracked export async function dbYouTubeGetAllChannelsToTrack(): Promise<{ success: boolean; data: (typeof dbYouTubeTable.$inferSelect)[]; @@ -103,3 +104,51 @@ export async function addNewChannelToTrack( return { success: false, data: [] }; } } + +// Update the latest video ID for a channel +export async function youtubeUpdateVideoId( + channelId: string, + videoId: string, + contentType: PlaylistType, + updateTime: Date, +): Promise<{ success: boolean; data?: typeof dbYouTubeTable.$inferSelect }> { + try { + const updateData: Record = { + latestAllId: null, + latestVideoId: null, + latestShortId: null, + latestStreamId: null, + }; + + switch (contentType) { + case PlaylistType.Video: + updateData.latestVideoId = videoId; + updateData.latestVideoIdUpdated = updateTime; + break; + case PlaylistType.Short: + updateData.latestShortId = videoId; + updateData.latestShortIdUpdated = updateTime; + break; + case PlaylistType.Stream: + updateData.latestStreamId = videoId; + updateData.latestStreamIdUpdated = updateTime; + break; + } + + // Always update the "all" column regardless of the content type + updateData.latestAllId = videoId; + updateData.latestAllIdUpdated = updateTime; + + const [updated] = await db + .update(dbYouTubeTable) + .set(updateData) + .where(eq(dbYouTubeTable.youtubeChannelId, channelId)) + .returning(); + + return { success: true, data: updated }; + } catch (error) { + console.error("Error updating YouTube video ID:", error); + + return { success: false }; + } +} diff --git a/src/types/database.d.ts b/src/types/database.d.ts deleted file mode 100644 index 4375115..0000000 --- a/src/types/database.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -// This file contains TypeScript interfaces for the database schema used in the application. -// YouTube Table Interface -export interface dbYouTube { - youtube_channel_id: string; - latest_video_id: string | null; - latest_video_id_updated: Date | null; - latest_short_id: string | null; - latest_short_id_updated: Date | null; - latest_stream_id: string | null; - latest_stream_id_updated: Date | null; - youtube_channel_is_live: boolean; -} - -// Twitch Table Interface -export interface dbTwitch { - twitch_channel_id: string; - is_live: boolean; -} - -// Guild YouTube Subscriptions Table Interface -export interface dbDiscordTable { - guild_id: string; - guild_channel_id: string; - guild_platform: string; - platform_user_id: string; - guild_ping_role: null | string; -} - -// Bot Info Table Interface -export interface dbBotInfo { - locked_row: boolean; - guilds_total: number; - channels_tracked: number; - total_members: number; - updated_at: string; - extended_info_updated_at: string; -} diff --git a/src/utils/database.ts b/src/utils/database.ts deleted file mode 100644 index 953e10d..0000000 --- a/src/utils/database.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { dbDiscordTable } from "../types/database"; - -import { Pool } from "pg"; - -import { dbCredentials } from "../config"; - -// import path from "path"; -// import { Database } from "bun:sqlite"; -// const db = new Database(path.resolve(process.cwd(), "db.sqlite3")); - -if ( - !dbCredentials.host || - !dbCredentials.port || - !dbCredentials.user || - !dbCredentials.password || - !dbCredentials.database -) { - throw new Error("Database credentials are not set"); -} - -/** - * @deprecated This pool is deprecated and being removed in the future. - */ -export const pool: Pool = new Pool({ - host: dbCredentials.host, - port: parseInt(dbCredentials.port), - user: dbCredentials.user, - password: dbCredentials.password, - database: dbCredentials.database, -}); - -// #region YouTube -/** - * @deprecated This function is deprecated and being removed - */ -export async function getGuildsTrackingChannel(channelId: string) { - const query = `SELECT * FROM discord WHERE platform_user_id = ?`; - - try { - const statement = db.prepare(query); - const results = statement.all(channelId); - - return results; - } catch (err) { - console.error("Error getting guilds tracking channel:", err); - throw err; - } -} - -/** - * @deprecated This function is deprecated and being removed - */ -export async function updateVideoId(channelId: string, videoId: string) { - const query = `UPDATE youtube SET latest_video_id = ? WHERE youtube_channel_id = ?`; - - try { - const statement = db.prepare(query); - - statement.run(videoId, channelId); - - return true; - } catch (err) { - console.error("Error updating video ID:", err); - - return false; - } -} - -/** - * @deprecated This function is deprecated and being removed - */ -export async function stopGuildTrackingChannel( - guild_id: string, - channelId: string, -) { - const query = `DELETE FROM discord WHERE guild_id = ? AND platform_user_id = ?`; - - try { - const statement = db.prepare(query); - - statement.run(guild_id, channelId); - - return true; - } catch (err) { - console.error("Error stopping guild tracking channel:", err); - - return false; - } -} - -// #endregion -// #region Twitch -/** - * @deprecated This function is deprecated and being removed - */ -export async function twitchGetAllChannelsToTrack() { - const query = `SELECT * FROM twitch`; - - try { - const statement = db.prepare(query); - const results = statement.all(); - - return results; - } catch (err) { - console.error("Error getting all Twitch channels to track:", err); - throw err; - } -} - -/** - * @deprecated This function is deprecated and being removed - */ -export async function twitchGetGuildsTrackingChannel(channelId: string) { - const query = `SELECT * FROM discord WHERE platform_user_id = ?`; - - try { - const statement = db.prepare(query); - const results = statement.all(channelId); - - return results; - } catch (err) { - console.error("Error getting guilds tracking Twitch channel:", err); - throw err; - } -} - -/** - * @deprecated This function is deprecated and being removed - */ -export async function twitchUpdateIsLive(channelId: string, isLive: boolean) { - const query = `UPDATE twitch SET is_live = ? WHERE twitch_channel_id = ?`; - - try { - const statement = db.prepare(query); - - statement.run(isLive, channelId); - - return true; - } catch (err) { - console.error("Error updating is live:", err); - - return false; - } -} - -/** - * @deprecated This function is deprecated and being removed - */ -export async function twitchStopGuildTrackingChannel( - guild_id: string, - channelId: string, -) { - const query = `DELETE FROM discord WHERE guild_id = ? AND platform_user_id = ?`; - - try { - const statement = db.prepare(query); - - statement.run(guild_id, channelId); - - return true; - } catch (err) { - console.error("Error stopping guild tracking Twitch channel:", err); - - return false; - } -} -// #endregion - -// #region i have no idea what im doing here -/** - * @deprecated This function is deprecated and being removed - */ -export async function getAllTrackedInGuild( - guild_id: string, -): Promise { - const query = `SELECT * FROM discord WHERE guild_id = ?`; - - try { - const statement = db.prepare(query); - const results = statement.all(guild_id); - - return results as dbDiscordTable[]; - } catch (err) { - console.error("Error getting all tracked in guild:", err); - throw err; - } -} -// #endregion diff --git a/src/utils/youtube/fetchLatestUploads.ts b/src/utils/youtube/fetchLatestUploads.ts index cd0422d..722dcb4 100644 --- a/src/utils/youtube/fetchLatestUploads.ts +++ b/src/utils/youtube/fetchLatestUploads.ts @@ -1,8 +1,10 @@ -import type { dbDiscordTable, dbYouTube } from "../../types/database"; +import type { dbGuildYouTubeSubscriptionsTable } from "../../db/schema"; import { env } from "../../config"; -import { getGuildsTrackingChannel, updateVideoId } from "../database"; +import { updateVideoId } from "../database"; import { dbYouTubeGetAllChannelsToTrack } from "../../db/youtube"; +import { discordGetAllGuildsTrackingChannel } from "../../db/discord"; +import { Platform } from "../../types/types"; import getChannelDetails from "./getChannelDetails"; @@ -10,23 +12,23 @@ export const updates = new Map< string, { channelInfo: Awaited>; - discordGuildsToUpdate: dbDiscordTable[]; + discordGuildsToUpdate: (typeof dbGuildYouTubeSubscriptionsTable.$inferSelect)[]; } >(); export default async function fetchLatestUploads() { console.log("Fetching latest uploads..."); - const channels: dbYouTube[] | [] = await dbYouTubeGetAllChannelsToTrack(); + const channels = await dbYouTubeGetAllChannelsToTrack(); const channelDict: Record = {}; - if (!channels || channels.length === 0) { + if (!channels || !channels.success || channels.data.length === 0) { console.log("No channels to track."); return; } - channels.forEach((channel) => { + channels.data.forEach((channel) => { if (!channel.youtube_channel_id || !channel.latest_video_id) { console.error( "Channel ID or latest video ID is missing in fetchLatestUploads", @@ -92,7 +94,10 @@ export default async function fetchLatestUploads() { } const discordGuildsToUpdate = - await getGuildsTrackingChannel(channelId); + await discordGetAllGuildsTrackingChannel( + Platform.YouTube, + channelId, + ); if (!discordGuildsToUpdate) { console.error( @@ -148,4 +153,4 @@ export default async function fetchLatestUploads() { } } } -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index 238655f..8d4eb97 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} + "compilerOptions": { + // Enable latest features + "lib": [ + "ESNext", + "DOM" + ], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, +} \ No newline at end of file From afbeedcbd19780158ccf922d7f51045533ca7b0f Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Thu, 17 Jul 2025 21:24:31 +0100 Subject: [PATCH 05/14] chore(database): clean up errors --- src/commands.ts | 5 ----- src/db/discord.ts | 2 +- src/db/schema.ts | 15 +++++++++++++++ src/index.ts | 6 ------ src/utils/twitch/checkIfStreamerIsLive.ts | 6 ------ src/utils/youtube/fetchLatestUploads.ts | 14 ++++++++------ 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index a530e62..8a64561 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -16,11 +16,6 @@ import { PermissionFlagsBits } from "discord-api-types/v8"; import hfksdjfskfhsjdfhkasfdhksf from "hfksdjfskfhsjdfhkasfdhksf"; import checkIfChannelIdIsValid from "./utils/youtube/checkIfChannelIdIsValid"; -import { - getAllTrackedInGuild, - stopGuildTrackingChannel, - twitchStopGuildTrackingChannel, -} from "./utils/database"; import getChannelDetails from "./utils/youtube/getChannelDetails"; import { checkIfStreamerIsLive } from "./utils/twitch/checkIfStreamerIsLive"; import { diff --git a/src/db/discord.ts b/src/db/discord.ts index f1a03d0..f954df9 100644 --- a/src/db/discord.ts +++ b/src/db/discord.ts @@ -1,6 +1,6 @@ import { eq, and } from "drizzle-orm"; -import { Platform } from "../types/types"; +import { Platform } from "../types/types.d"; import { db } from "./db"; import { diff --git a/src/db/schema.ts b/src/db/schema.ts index cef5888..9cde7bf 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -136,3 +136,18 @@ export const dbAuditLogsTable = pgTable("audit_logs", { data: jsonb("data"), occurredAt: timestamp("occurred_at").defaultNow(), }); + +export default { + dbDiscordTable, + dbBlueskyTable, + dbYouTubeTable, + dbTwitchTable, + dbGuildBlueskySubscriptionsTable, + dbGuildYouTubeSubscriptionsTable, + dbGuildTwitchSubscriptionsTable, + dbBotInfoTable, + dbBotInfoNotificationsTable, + dbAuditLogsEventTypeEnum, + dbAuditLogsSuccessTypeEnum, + dbAuditLogsTable, +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index c1a3053..5c9d22e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,7 +13,6 @@ import { import { env } from "./config.ts"; import commandsMap from "./commands.ts"; -import initTables from "./db/schema.ts"; import { getTwitchToken } from "./utils/twitch/auth.ts"; if (!env.discordToken || env.discordToken === "YOUR_DISCORD_TOKEN") { @@ -57,11 +56,6 @@ const data = (await rest.put(Routes.applicationCommands(getAppId.id), { console.log(`Successfully reloaded ${data.length} application (/) commands.`); -// Check if Postgres is set up properly and its working -if (!(await initTables())) { - // throw new Error("Error initializing tables"); -} - // Get Twitch token if (!(await getTwitchToken())) { throw new Error("Error getting Twitch token"); diff --git a/src/utils/twitch/checkIfStreamerIsLive.ts b/src/utils/twitch/checkIfStreamerIsLive.ts index 3c47b45..8557dba 100644 --- a/src/utils/twitch/checkIfStreamerIsLive.ts +++ b/src/utils/twitch/checkIfStreamerIsLive.ts @@ -1,12 +1,6 @@ -import type { dbTwitch } from "../../types/database"; import type { TextChannel } from "discord.js"; import { env } from "../../config"; -import { - twitchGetAllChannelsToTrack, - twitchGetGuildsTrackingChannel, - twitchUpdateIsLive, -} from "../database"; import client from "../.."; import { twitchToken } from "./auth"; diff --git a/src/utils/youtube/fetchLatestUploads.ts b/src/utils/youtube/fetchLatestUploads.ts index 722dcb4..b684cea 100644 --- a/src/utils/youtube/fetchLatestUploads.ts +++ b/src/utils/youtube/fetchLatestUploads.ts @@ -1,10 +1,12 @@ -import type { dbGuildYouTubeSubscriptionsTable } from "../../db/schema"; +import type { Platform } from "../../types/types.d.ts"; +import { + dbGuildYouTubeSubscriptionsTable, + dbYouTubeTable, +} from "../../db/schema"; import { env } from "../../config"; -import { updateVideoId } from "../database"; import { dbYouTubeGetAllChannelsToTrack } from "../../db/youtube"; import { discordGetAllGuildsTrackingChannel } from "../../db/discord"; -import { Platform } from "../../types/types"; import getChannelDetails from "./getChannelDetails"; @@ -20,7 +22,7 @@ export default async function fetchLatestUploads() { console.log("Fetching latest uploads..."); const channels = await dbYouTubeGetAllChannelsToTrack(); - const channelDict: Record = {}; + const channelDict: Record = {}; if (!channels || !channels.success || channels.data.length === 0) { console.log("No channels to track."); @@ -29,14 +31,14 @@ export default async function fetchLatestUploads() { } channels.data.forEach((channel) => { - if (!channel.youtube_channel_id || !channel.latest_video_id) { + if (!channel.youtubeChannelId || !channel.latestAllId) { console.error( "Channel ID or latest video ID is missing in fetchLatestUploads", ); return; } - channelDict[channel.youtube_channel_id] = channel.latest_video_id; + channelDict[channel.youtubeChannelId] = channel; }); const chunkSize = 50; From 3cb988cad9afab4e64c104e09e93412d7b1ec64d Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Fri, 18 Jul 2025 14:57:11 +0100 Subject: [PATCH 06/14] feat(bot): add guild events --- src/db/discord.ts | 44 ++++++++++++ src/db/schema.ts | 1 + src/events/guildCreate.ts | 17 +++++ src/events/guildDelete.ts | 17 +++++ src/index.ts | 4 ++ src/utils/discord/updateGuildsOnStartup.ts | 78 ++++++++++++++++++++++ 6 files changed, 161 insertions(+) create mode 100644 src/events/guildCreate.ts create mode 100644 src/events/guildDelete.ts create mode 100644 src/utils/discord/updateGuildsOnStartup.ts diff --git a/src/db/discord.ts b/src/db/discord.ts index f954df9..f06d1b7 100644 --- a/src/db/discord.ts +++ b/src/db/discord.ts @@ -6,6 +6,7 @@ import { db } from "./db"; import { dbGuildYouTubeSubscriptionsTable, dbGuildTwitchSubscriptionsTable, + dbDiscordTable, } from "./schema"; export async function checkIfGuildIsTrackingUserAlready( @@ -280,3 +281,46 @@ export async function discordRemoveGuildTrackingChannel( return { success: false, data: [] }; } } + +// Add a new guild to track +export async function discordAddNewGuild( + guildId: string, +): Promise<{ success: boolean; data: [] }> { + console.log(`Adding new guild to track: ${guildId}`); + + try { + await db.insert(dbDiscordTable).values({ + guildId: guildId, + allowedPublicSharing: false, + isInServer: true, + memberCount: 0, + }); + + return { success: true, data: [] }; + } catch (error) { + console.error("Error adding new guild to track:", error); + + return { success: false, data: [] }; + } +} + +// "Remove" a guild from tracking +// Basically just set isInServer to false for archival purposes +export async function discordRemoveGuildFromTracking( + guildId: string, +): Promise<{ success: boolean; data: [] }> { + console.log(`Removing guild from tracking: ${guildId}`); + + try { + await db + .update(dbDiscordTable) + .set({ isInServer: false }) + .where(eq(dbDiscordTable.guildId, guildId)); + + return { success: true, data: [] }; + } catch (error) { + console.error("Error removing guild from tracking:", error); + + return { success: false, data: [] }; + } +} diff --git a/src/db/schema.ts b/src/db/schema.ts index 9cde7bf..1617f8a 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -8,6 +8,7 @@ export const dbDiscordTable = pgTable("discord", { allowedPublicSharing: boolean("allowed_public_sharing").notNull().default(false), feedrUpdatesChannelId: text("feedr_updates_channel_id"), isInServer: boolean("is_in_server").notNull().default(true), + memberCount: integer("member_count").notNull().default(0), }); export const dbBlueskyTable = pgTable("bluesky", { diff --git a/src/events/guildCreate.ts b/src/events/guildCreate.ts new file mode 100644 index 0000000..1a3697b --- /dev/null +++ b/src/events/guildCreate.ts @@ -0,0 +1,17 @@ +import { Events } from "discord.js"; + +import client from ".."; +import { discordAddNewGuild } from "../db/discord"; + +client.on(Events.GuildCreate, async (guild) => { + console.log(`Joined new guild: ${guild.name} (ID: ${guild.id})`); + + // Add the new guild to tracking + const result = await discordAddNewGuild(guild.id); + + if (result.success) { + console.log(`Successfully added guild ${guild.id} to tracking.`); + } else { + console.error(`Failed to add guild ${guild.id} to tracking.`); + } +}); diff --git a/src/events/guildDelete.ts b/src/events/guildDelete.ts new file mode 100644 index 0000000..02d878b --- /dev/null +++ b/src/events/guildDelete.ts @@ -0,0 +1,17 @@ +import { Events } from "discord.js"; + +import client from ".."; +import { discordRemoveGuildFromTracking } from "../db/discord"; + +client.on(Events.GuildDelete, async (guild) => { + console.log(`Left guild: ${guild.name} (ID: ${guild.id})`); + + // Remove the guild from tracking + const result = await discordRemoveGuildFromTracking(guild.id); + + if (result.success) { + console.log(`Successfully removed guild ${guild.id} from tracking.`); + } else { + console.error(`Failed to remove guild ${guild.id} from tracking.`); + } +}); diff --git a/src/index.ts b/src/index.ts index 5c9d22e..1f21bb1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { import { env } from "./config.ts"; import commandsMap from "./commands.ts"; import { getTwitchToken } from "./utils/twitch/auth.ts"; +import updateGuildsOnStartup from "./utils/discord/updateGuildsOnStartup.ts"; if (!env.discordToken || env.discordToken === "YOUR_DISCORD_TOKEN") { throw new Error("You MUST provide a discord token in .env!"); @@ -75,6 +76,9 @@ await Promise.all( }), ); +// Update the guilds on startup +await updateGuildsOnStartup(); + // Attempt the garbage collection every hour setInterval( () => { diff --git a/src/utils/discord/updateGuildsOnStartup.ts b/src/utils/discord/updateGuildsOnStartup.ts new file mode 100644 index 0000000..6c3b970 --- /dev/null +++ b/src/utils/discord/updateGuildsOnStartup.ts @@ -0,0 +1,78 @@ +// Checks for any guilds that may have been added/removed while the bot was offline +import { eq } from "drizzle-orm"; + +import { dbDiscordTable } from "../../db/schema"; +import client from "../.."; +import { db } from "../../db/db"; + +export default async function () { + console.log("Checking for guilds to update on startup..."); + + let currentGuilds: string[] = []; + + // Keep checking every 10 seconds until currentGuilds is not empty + while (currentGuilds.length === 0) { + console.log("Waiting for guilds to load..."); + currentGuilds = client.guilds.cache.map((guild) => guild.id); + if (currentGuilds.length === 0) { + await new Promise((resolve) => setTimeout(resolve, 10000)); + } + } + + // Get all the guilds from the database + const data = await db.select().from(dbDiscordTable); + + console.log( + `Currently in ${currentGuilds.length} guilds, checking against ${data.length} in the database.`, + ); + + // Find any guilds that are in the database but not in the current guilds + const missingGuilds = data.filter( + (guild) => !currentGuilds.includes(guild.guildId), + ); + + // Find any guilds that are in the current guilds but not in the database + const newGuilds = currentGuilds.filter( + (id) => !data.some((guild) => guild.guildId === id), + ); + + // Update the database for missing guilds + missingGuilds.forEach(async (guild) => { + console.log(`Removing guild from tracking: ${guild.guildId}`); + const result = await db + .update(dbDiscordTable) + .set({ isInServer: false }) + .where(eq(dbDiscordTable.guildId, guild.guildId)) + .returning(); + + if (result) { + console.log( + `Successfully removed guild ${guild.guildId} from tracking.`, + ); + } else { + console.error( + `Failed to remove guild ${guild.guildId} from tracking.`, + ); + } + }); + + newGuilds.forEach(async (guildId) => { + console.log(`Adding new guild to tracking: ${guildId}`); + const result = await db + .insert(dbDiscordTable) + .values({ + guildId, + allowedPublicSharing: false, + feedrUpdatesChannelId: null, + isInServer: true, + memberCount: 0, + }) + .returning(); + + if (result) { + console.log(`Successfully added guild ${guildId} to tracking.`); + } else { + console.error(`Failed to add guild ${guildId} to tracking.`); + } + }); +} From fb6819e88fd71c5f2c4c8f5f8094504339277ab1 Mon Sep 17 00:00:00 2001 From: Galvin Date: Fri, 18 Jul 2025 15:46:03 +0100 Subject: [PATCH 07/14] Update src/utils/discord/updateGuildsOnStartup.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/utils/discord/updateGuildsOnStartup.ts | 82 +++++++++++++--------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/src/utils/discord/updateGuildsOnStartup.ts b/src/utils/discord/updateGuildsOnStartup.ts index 6c3b970..b92ec1d 100644 --- a/src/utils/discord/updateGuildsOnStartup.ts +++ b/src/utils/discord/updateGuildsOnStartup.ts @@ -37,42 +37,54 @@ export default async function () { ); // Update the database for missing guilds - missingGuilds.forEach(async (guild) => { - console.log(`Removing guild from tracking: ${guild.guildId}`); - const result = await db - .update(dbDiscordTable) - .set({ isInServer: false }) - .where(eq(dbDiscordTable.guildId, guild.guildId)) - .returning(); + try { + await Promise.all( + missingGuilds.map(async (guild) => { + console.log(`Removing guild from tracking: ${guild.guildId}`); + const result = await db + .update(dbDiscordTable) + .set({ isInServer: false }) + .where(eq(dbDiscordTable.guildId, guild.guildId)) + .returning(); - if (result) { - console.log( - `Successfully removed guild ${guild.guildId} from tracking.`, - ); - } else { - console.error( - `Failed to remove guild ${guild.guildId} from tracking.`, - ); - } - }); + if (result) { + console.log( + `Successfully removed guild ${guild.guildId} from tracking.`, + ); + } else { + console.error( + `Failed to remove guild ${guild.guildId} from tracking.`, + ); + } + }), + ); + } catch (error) { + console.error("Error while removing missing guilds:", error); + } - newGuilds.forEach(async (guildId) => { - console.log(`Adding new guild to tracking: ${guildId}`); - const result = await db - .insert(dbDiscordTable) - .values({ - guildId, - allowedPublicSharing: false, - feedrUpdatesChannelId: null, - isInServer: true, - memberCount: 0, - }) - .returning(); + try { + await Promise.all( + newGuilds.map(async (guildId) => { + console.log(`Adding new guild to tracking: ${guildId}`); + const result = await db + .insert(dbDiscordTable) + .values({ + guildId, + allowedPublicSharing: false, + feedrUpdatesChannelId: null, + isInServer: true, + memberCount: 0, + }) + .returning(); - if (result) { - console.log(`Successfully added guild ${guildId} to tracking.`); - } else { - console.error(`Failed to add guild ${guildId} to tracking.`); - } - }); + if (result) { + console.log(`Successfully added guild ${guildId} to tracking.`); + } else { + console.error(`Failed to add guild ${guildId} to tracking.`); + } + }), + ); + } catch (error) { + console.error("Error while adding new guilds:", error); + } } From c40d5a732c4575e81f64924a6001012b6e4c5c5a Mon Sep 17 00:00:00 2001 From: Galvin Date: Fri, 18 Jul 2025 15:46:30 +0100 Subject: [PATCH 08/14] Update src/db/discord.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/db/discord.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/db/discord.ts b/src/db/discord.ts index f06d1b7..52be8e5 100644 --- a/src/db/discord.ts +++ b/src/db/discord.ts @@ -290,7 +290,7 @@ export async function discordAddNewGuild( try { await db.insert(dbDiscordTable).values({ - guildId: guildId, + guildId, allowedPublicSharing: false, isInServer: true, memberCount: 0, From 67d0c1866438f8b43fcaed9d698c8b70de5044dd Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Fri, 18 Jul 2025 16:24:23 +0100 Subject: [PATCH 09/14] style: update file based on suggestions --- .env.example | 1 + src/config.ts | 5 +++++ src/utils/discord/updateGuildsOnStartup.ts | 17 ++++++++++++----- src/utils/youtube/fetchLatestUploads.ts | 3 +-- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index b203af7..cd48a5d 100644 --- a/.env.example +++ b/.env.example @@ -17,6 +17,7 @@ TWITCH_CLIENT_SECRET='YOUR_TWITCH_CLIENT_SECRET' CONFIG_UPDATE_INTERVAL_YOUTUBE='10' CONFIG_UPDATE_INTERVAL_TWITCH='2' CONFIG_DISCORD_LOGS_CHANNEL='YOUR_DISCORD_LOGS_CHANNEL' +CONFIG_DISCORD_WAIT_FOR_GUILD_CACHE_TIME='YOUR_TIME_IN_SECONDS' # Postgres URLs POSTGRES_URL='postgresql://user:password@server:port/database' diff --git a/src/config.ts b/src/config.ts index 40dd781..4e827fd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,6 +4,7 @@ export interface Config { updateIntervalYouTube: number; updateIntervalTwitch: number; databaseUrl: string | undefined; + discordWaitForGuildCacheTime: number; } export const config: Config = { @@ -16,6 +17,10 @@ export const config: Config = { databaseUrl: runningInDevMode ? process.env?.POSTGRES_DEV_URL : process.env?.POSTGRES_URL, + discordWaitForGuildCacheTime: process.env + ?.CONFIG_DISCORD_WAIT_FOR_GUILD_CACHE_TIME + ? parseInt(process.env?.CONFIG_DISCORD_WAIT_FOR_GUILD_CACHE_TIME) * 1000 + : 10_000, }; interface Env { diff --git a/src/utils/discord/updateGuildsOnStartup.ts b/src/utils/discord/updateGuildsOnStartup.ts index b92ec1d..ba0c37a 100644 --- a/src/utils/discord/updateGuildsOnStartup.ts +++ b/src/utils/discord/updateGuildsOnStartup.ts @@ -4,6 +4,7 @@ import { eq } from "drizzle-orm"; import { dbDiscordTable } from "../../db/schema"; import client from "../.."; import { db } from "../../db/db"; +import { config } from "../../config"; export default async function () { console.log("Checking for guilds to update on startup..."); @@ -15,7 +16,9 @@ export default async function () { console.log("Waiting for guilds to load..."); currentGuilds = client.guilds.cache.map((guild) => guild.id); if (currentGuilds.length === 0) { - await new Promise((resolve) => setTimeout(resolve, 10000)); + await new Promise((resolve) => + setTimeout(resolve, config.discordWaitForGuildCacheTime), + ); } } @@ -47,7 +50,7 @@ export default async function () { .where(eq(dbDiscordTable.guildId, guild.guildId)) .returning(); - if (result) { + if (result.length > 0) { console.log( `Successfully removed guild ${guild.guildId} from tracking.`, ); @@ -77,10 +80,14 @@ export default async function () { }) .returning(); - if (result) { - console.log(`Successfully added guild ${guildId} to tracking.`); + if (result.length > 0) { + console.log( + `Successfully added guild ${guildId} to tracking.`, + ); } else { - console.error(`Failed to add guild ${guildId} to tracking.`); + console.error( + `Failed to add guild ${guildId} to tracking.`, + ); } }), ); diff --git a/src/utils/youtube/fetchLatestUploads.ts b/src/utils/youtube/fetchLatestUploads.ts index b684cea..d1f32e4 100644 --- a/src/utils/youtube/fetchLatestUploads.ts +++ b/src/utils/youtube/fetchLatestUploads.ts @@ -1,5 +1,4 @@ -import type { Platform } from "../../types/types.d.ts"; - +import { Platform } from "../../types/types.d"; import { dbGuildYouTubeSubscriptionsTable, dbYouTubeTable, From 2d9f693dc967581769d2c006cf277a4ba24b191d Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Sat, 19 Jul 2025 11:51:05 +0100 Subject: [PATCH 10/14] fix(bot): make youtube upload notifications work --- src/db/youtube.ts | 8 ++ src/utils/youtube/fetchLatestUploads.ts | 103 ++++++++++++++++++++++-- src/utils/youtube/sendLatestUploads.ts | 17 ++-- 3 files changed, 115 insertions(+), 13 deletions(-) diff --git a/src/db/youtube.ts b/src/db/youtube.ts index 71492d8..b7c8e27 100644 --- a/src/db/youtube.ts +++ b/src/db/youtube.ts @@ -133,6 +133,14 @@ export async function youtubeUpdateVideoId( updateData.latestStreamId = videoId; updateData.latestStreamIdUpdated = updateTime; break; + case PlaylistType.All: + console.error( + "All content type should not be used for updating video IDs", + ); + + return { success: false }; + default: + break; } // Always update the "all" column regardless of the content type diff --git a/src/utils/youtube/fetchLatestUploads.ts b/src/utils/youtube/fetchLatestUploads.ts index d1f32e4..6854f44 100644 --- a/src/utils/youtube/fetchLatestUploads.ts +++ b/src/utils/youtube/fetchLatestUploads.ts @@ -4,10 +4,16 @@ import { dbYouTubeTable, } from "../../db/schema"; import { env } from "../../config"; -import { dbYouTubeGetAllChannelsToTrack } from "../../db/youtube"; +import { + dbYouTubeGetAllChannelsToTrack, + youtubeUpdateVideoId, +} from "../../db/youtube"; import { discordGetAllGuildsTrackingChannel } from "../../db/discord"; import getChannelDetails from "./getChannelDetails"; +import getSinglePlaylistAndReturnVideoData, { + PlaylistType, +} from "./getSinglePlaylistAndReturnVideoData"; export const updates = new Map< string, @@ -30,10 +36,8 @@ export default async function fetchLatestUploads() { } channels.data.forEach((channel) => { - if (!channel.youtubeChannelId || !channel.latestAllId) { - console.error( - "Channel ID or latest video ID is missing in fetchLatestUploads", - ); + if (!channel.youtubeChannelId) { + console.error("Channel ID is missing in fetchLatestUploads"); return; } @@ -69,12 +73,14 @@ export default async function fetchLatestUploads() { const data = await res.json(); + // TODO: Upload time (https://github.com/GalvinPython/feedr/issues/136) for (const playlist of data.items) { const channelId = playlist.snippet.channelId; const videoId = playlist.snippet.thumbnails.default.url.split("/")[4]; - const requiresUpdate = channelDict[channelId] !== videoId; + const requiresUpdate = + channelDict[channelId].latestAllId !== videoId; console.log( "Channel ID:", @@ -86,7 +92,68 @@ export default async function fetchLatestUploads() { ); if (requiresUpdate) { - if (!(await updateVideoId(channelId, videoId))) { + const [longVideoId, shortVideoId, streamVideoId] = + await Promise.all([ + getSinglePlaylistAndReturnVideoData( + channelId, + PlaylistType.Video, + ), + getSinglePlaylistAndReturnVideoData( + channelId, + PlaylistType.Short, + ), + getSinglePlaylistAndReturnVideoData( + channelId, + PlaylistType.Stream, + ), + ]); + + if (!longVideoId && !shortVideoId && !streamVideoId) { + console.error( + "No video IDs found for channel in fetchLatestUploads", + ); + continue; + } + + let contentType: PlaylistType | null = null; + + const videoIdMap = { + [PlaylistType.Video]: longVideoId, + [PlaylistType.Short]: shortVideoId, + [PlaylistType.Stream]: streamVideoId, + }; + + contentType = Object.entries(videoIdMap).find( + ([, id]) => id, + )?.[0] as PlaylistType | null; + + if (contentType) { + console.log( + `Updating ${contentType} video ID for channel`, + channelId, + "to", + videoIdMap[contentType as keyof typeof videoIdMap], + ); + } else { + console.error( + "No valid video ID found for channel", + channelId, + "with video ID", + videoId, + ); + continue; + } + + const updateSuccess = await youtubeUpdateVideoId( + channelId, + videoId, + contentType, + + // Temporarily using current date for update time + new Date(), + ); + + if (!updateSuccess) { console.error( "Error updating video ID in fetchLatestUploads", ); @@ -110,9 +177,29 @@ export default async function fetchLatestUploads() { const channelInfo = await getChannelDetails(channelId); + console.log( + discordGuildsToUpdate.data.filter( + ( + guild, + ): guild is typeof dbGuildYouTubeSubscriptionsTable.$inferSelect => + "youtubeChannelId" in guild && + "trackVideos" in guild && + "trackShorts" in guild && + "trackStreams" in guild, + ), + ); + updates.set(videoId, { channelInfo, - discordGuildsToUpdate, + discordGuildsToUpdate: discordGuildsToUpdate.data.filter( + ( + guild, + ): guild is typeof dbGuildYouTubeSubscriptionsTable.$inferSelect => + "youtubeChannelId" in guild && + "trackVideos" in guild && + "trackShorts" in guild && + "trackStreams" in guild, + ), }); // console.log("Discord guilds to update:", discordGuildsToUpdate); diff --git a/src/utils/youtube/sendLatestUploads.ts b/src/utils/youtube/sendLatestUploads.ts index 9e7c94f..2933bb4 100644 --- a/src/utils/youtube/sendLatestUploads.ts +++ b/src/utils/youtube/sendLatestUploads.ts @@ -13,7 +13,7 @@ export default async function sendLatestUploads() { for (const guild of discordGuildsToUpdate) { try { const channelObj = await client.channels.fetch( - guild.guild_channel_id, + guild.notificationChannelId, ); if ( @@ -27,12 +27,19 @@ export default async function sendLatestUploads() { continue; } + console.log( + "Sending message to channel:", + channelObj.id, + "for video ID:", + videoId, + ); + await (channelObj as TextChannel).send({ content: - guild.guild_ping_role && channelInfo - ? `<@&${guild.guild_ping_role}> New video uploaded for ${channelInfo?.channelName}! https://www.youtube.com/watch?v=${videoId}` - : guild.guild_ping_role - ? `<@&${guild.guild_ping_role}> New video uploaded! https://www.youtube.com/watch?v=${videoId}` + guild.notificationRoleId && channelInfo + ? `<@&${guild.notificationRoleId}> New video uploaded for ${channelInfo?.channelName}! https://www.youtube.com/watch?v=${videoId}` + : guild.notificationRoleId + ? `<@&${guild.notificationRoleId}> New video uploaded! https://www.youtube.com/watch?v=${videoId}` : channelInfo ? `New video uploaded for ${channelInfo.channelName}! https://www.youtube.com/watch?v=${videoId}` : `New video uploaded! https://www.youtube.com/watch?v=${videoId}`, From b8834789216e3f316da6984fc53e95aa6963a6b5 Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Sat, 19 Jul 2025 15:18:04 +0100 Subject: [PATCH 11/14] fix(youtube): make content type selection work in fetchLatestUploads --- src/utils/youtube/fetchLatestUploads.ts | 46 ++++--------------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/src/utils/youtube/fetchLatestUploads.ts b/src/utils/youtube/fetchLatestUploads.ts index 6854f44..addfbe2 100644 --- a/src/utils/youtube/fetchLatestUploads.ts +++ b/src/utils/youtube/fetchLatestUploads.ts @@ -196,48 +196,14 @@ export default async function fetchLatestUploads() { guild, ): guild is typeof dbGuildYouTubeSubscriptionsTable.$inferSelect => "youtubeChannelId" in guild && - "trackVideos" in guild && - "trackShorts" in guild && - "trackStreams" in guild, + ((contentType === PlaylistType.Video && + guild.trackVideos) || + (contentType === PlaylistType.Short && + guild.trackShorts) || + (contentType === PlaylistType.Stream && + guild.trackStreams)), ), }); - - // console.log("Discord guilds to update:", discordGuildsToUpdate); - // for (const guild of discordGuildsToUpdate) { - // try { - // const channelObj = await client.channels.fetch( - // guild.guild_channel_id, - // ); - - // if ( - // !channelObj || - // (channelObj.type !== ChannelType.GuildText && - // channelObj.type !== - // ChannelType.GuildAnnouncement) - // ) { - // console.error( - // "Invalid channel or not a text channel in fetchLatestUploads", - // ); - // continue; - // } - - // await (channelObj as TextChannel).send({ - // content: - // guild.guild_ping_role && channelInfo - // ? `<@&${guild.guild_ping_role}> New video uploaded for ${channelInfo?.channelName}! https://www.youtube.com/watch?v=${videoId}` - // : guild.guild_ping_role - // ? `<@&${guild.guild_ping_role}> New video uploaded! https://www.youtube.com/watch?v=${videoId}` - // : channelInfo - // ? `New video uploaded for ${channelInfo.channelName}! https://www.youtube.com/watch?v=${videoId}` - // : `New video uploaded! https://www.youtube.com/watch?v=${videoId}`, - // }); - // } catch (error) { - // console.error( - // "Error fetching or sending message to channel in fetchLatestUploads:", - // error, - // ); - // } - // } } } } From ae9090b0d80b4bb3557d1b3ae564f7ff2d4c8c00 Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Sun, 20 Jul 2025 12:20:23 +0100 Subject: [PATCH 12/14] feat(twitch): wow twitch is back --- src/db/schema.ts | 15 +++++++- src/db/twitch.ts | 2 +- src/events/ready.ts | 12 +++--- src/utils/twitch/checkIfStreamerIsLive.ts | 45 +++++++++++++---------- 4 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/db/schema.ts b/src/db/schema.ts index 1617f8a..5607e0a 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,7 +1,8 @@ // To make it easier to work with the database, disable prettier and some eslint rules for this file /* eslint-disable no-inline-comments */ /* eslint-disable prettier/prettier */ -import { pgTable, serial, text, boolean, timestamp, decimal, unique, index, pgEnum, jsonb, integer } from "drizzle-orm/pg-core"; +import { sql } from "drizzle-orm"; +import { pgTable, serial, text, boolean, timestamp, decimal, unique, index, pgEnum, jsonb, integer, check } from "drizzle-orm/pg-core"; export const dbDiscordTable = pgTable("discord", { guildId: text("guild_id").primaryKey(), @@ -9,7 +10,17 @@ export const dbDiscordTable = pgTable("discord", { feedrUpdatesChannelId: text("feedr_updates_channel_id"), isInServer: boolean("is_in_server").notNull().default(true), memberCount: integer("member_count").notNull().default(0), -}); + isDm: boolean("is_dm").notNull().default(false), +}, (table) => [ + check("discord_is_dm_constraint", + sql`NOT ${table.isDm} OR ( + ${table.allowedPublicSharing} = false AND + ${table.feedrUpdatesChannelId} = ${table.guildId} AND + ${table.isInServer} = true AND + ${table.memberCount} = 1 + )` + ) +]); export const dbBlueskyTable = pgTable("bluesky", { blueskyUserId: text("bluesky_user_id").primaryKey(), diff --git a/src/db/twitch.ts b/src/db/twitch.ts index 4617cdf..2e69d51 100644 --- a/src/db/twitch.ts +++ b/src/db/twitch.ts @@ -85,4 +85,4 @@ export async function twitchUpdateIsLive( return { success: false }; } -} \ No newline at end of file +} diff --git a/src/events/ready.ts b/src/events/ready.ts index 0c1e383..3318013 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -1,12 +1,12 @@ import { Events } from "discord.js"; -import { CronJob } from "cron"; +// import { CronJob } from "cron"; import client from "../index"; import { config } from "../config"; -// import { checkIfStreamersAreLive } from "../utils/twitch/checkIfStreamerIsLive"; -import { cronUpdateBotInfo } from "../utils/cronJobs"; +// import { cronUpdateBotInfo } from "../utils/cronJobs"; import sendLatestUploads from "../utils/youtube/sendLatestUploads"; import fetchLatestUploads from "../utils/youtube/fetchLatestUploads"; +import { checkIfStreamersAreLive } from "../utils/twitch/checkIfStreamerIsLive"; // Log into the bot client.once(Events.ClientReady, async (bot) => { @@ -24,8 +24,6 @@ client.once(Events.ClientReady, async (bot) => { sendLatestUploads(); setInterval(sendLatestUploads, config.updateIntervalYouTube as number); - // TODO: Twitch integration is not ready yet - // One at a time - // checkIfStreamersAreLive(); - // setInterval(checkIfStreamersAreLive, config.updateIntervalTwitch as number); + checkIfStreamersAreLive(); + setInterval(checkIfStreamersAreLive, config.updateIntervalTwitch as number); }); \ No newline at end of file diff --git a/src/utils/twitch/checkIfStreamerIsLive.ts b/src/utils/twitch/checkIfStreamerIsLive.ts index 8557dba..a564a89 100644 --- a/src/utils/twitch/checkIfStreamerIsLive.ts +++ b/src/utils/twitch/checkIfStreamerIsLive.ts @@ -2,6 +2,12 @@ import type { TextChannel } from "discord.js"; import { env } from "../../config"; import client from "../.."; +import { + dbTwitchGetAllChannelsToTrack, + twitchUpdateIsLive, +} from "../../db/twitch"; +import { discordGetAllGuildsTrackingChannel } from "../../db/discord"; +import { Platform } from "../../types/types.d"; import { twitchToken } from "./auth"; import { getStreamerName } from "./getStreamerName"; @@ -48,22 +54,19 @@ export async function checkIfStreamersAreLive(): Promise { return; } - const allStreamerIds = await twitchGetAllChannelsToTrack(); + const allStreamerIds = await dbTwitchGetAllChannelsToTrack(); const chunkSize = 100; const chunks = []; - for (let i = 0; i < allStreamerIds.length; i += chunkSize) { - const chunk = allStreamerIds.slice(i, i + chunkSize); + for (let i = 0; i < allStreamerIds.data.length; i += chunkSize) { + const chunk = allStreamerIds.data.slice(i, i + chunkSize); chunks.push(chunk); } for (const chunk of chunks) { const urlQueries = chunk - .map( - (streamerId: dbTwitch) => - `user_id=${streamerId.twitch_channel_id}`, - ) + .map((streamerId) => `user_id=${streamerId.twitchChannelId}`) .join("&"); const res = await fetch( `https://api.twitch.tv/helix/streams?${urlQueries}`, @@ -89,15 +92,16 @@ export async function checkIfStreamersAreLive(): Promise { for (const streamerId of chunk) { const isLive = allLiveStreamers.includes( - streamerId.twitch_channel_id, + streamerId.twitchChannelId, ); - const needsUpdate = isLive !== Boolean(streamerId.is_live); + const needsUpdate = + isLive !== Boolean(streamerId.twitchChannelIsLive); console.log( - `[Twitch] ${streamerId.twitch_channel_id} is live:`, + `[Twitch] ${streamerId.twitchChannelId} is live:`, isLive, ". Was live:", - Boolean(streamerId.is_live), + Boolean(streamerId.twitchChannelIsLive), ". Needs update:", needsUpdate, ); @@ -105,35 +109,36 @@ export async function checkIfStreamersAreLive(): Promise { if (needsUpdate) { // Update the database console.log( - `Updating ${streamerId.twitch_channel_id} to be ${isLive ? "live" : "offline"}`, + `Updating ${streamerId.twitchChannelId} to be ${isLive ? "live" : "offline"}`, ); - await twitchUpdateIsLive(streamerId.twitch_channel_id, isLive); + await twitchUpdateIsLive(streamerId.twitchChannelId, isLive); if (isLive) { // Get the streamer's name const streamerName = await getStreamerName( - streamerId.twitch_channel_id, + streamerId.twitchChannelId, ); // Get all guilds that are tracking this streamer const guildsTrackingStreamer = - await twitchGetGuildsTrackingChannel( - streamerId.twitch_channel_id, + await discordGetAllGuildsTrackingChannel( + Platform.Twitch, + streamerId.twitchChannelId, ); - for (const guild of guildsTrackingStreamer) { + for (const guild of guildsTrackingStreamer.data) { // Send a message to the channel const channel = await client.channels.fetch( - guild.guild_channel_id, + guild.guildId, ); await (channel as TextChannel).send( - `${guild.guild_ping_role ? `<@&${guild.guild_ping_role}>` : ""} ${streamerName} is now live !`, + `${guild.notificationRoleId ? `<@&${guild.notificationRoleId}>` : ""} ${streamerName} is now live !`, ); } } else { console.log( - `[Twitch] ${streamerId.twitch_channel_id} is offline!`, + `[Twitch] ${streamerId.twitchChannelId} is offline!`, ); } } From 9970295d4fe59c8912e7071581934c45a1ab44a6 Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Sun, 20 Jul 2025 12:23:11 +0100 Subject: [PATCH 13/14] style: run lint --- src/commands.ts | 4 ++-- src/db/botinfo.ts | 2 +- src/events/ready.ts | 2 +- src/utils/cronJobs.ts | 1 - src/utils/quickEmbed.ts | 2 +- src/utils/youtube/search.ts | 24 +++++++++++++++++++----- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 8a64561..c80b5df 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -676,9 +676,9 @@ const commands: Record = { const query = platform === "youtube" ? (interaction.options.get("channel_id") - ?.value as string) + ?.value as string) : (interaction.options.get("streamer_id") - ?.value as string); + ?.value as string); // If the query is empty or not a string, return an empty array if (!query || typeof query !== "string") { diff --git a/src/db/botinfo.ts b/src/db/botinfo.ts index 7f48e2a..53f147e 100644 --- a/src/db/botinfo.ts +++ b/src/db/botinfo.ts @@ -1 +1 @@ -// Rewriting \ No newline at end of file +// Rewriting diff --git a/src/events/ready.ts b/src/events/ready.ts index 3318013..aed1b9a 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -26,4 +26,4 @@ client.once(Events.ClientReady, async (bot) => { checkIfStreamersAreLive(); setInterval(checkIfStreamersAreLive, config.updateIntervalTwitch as number); -}); \ No newline at end of file +}); diff --git a/src/utils/cronJobs.ts b/src/utils/cronJobs.ts index e38dce3..6db87cf 100644 --- a/src/utils/cronJobs.ts +++ b/src/utils/cronJobs.ts @@ -1,7 +1,6 @@ import { ActivityType, Guild, PresenceUpdateStatus } from "discord.js"; import client from ".."; - import { updateBotInfo } from "../db/botinfo"; export async function cronUpdateBotInfo() { diff --git a/src/utils/quickEmbed.ts b/src/utils/quickEmbed.ts index f561a84..18d2d43 100644 --- a/src/utils/quickEmbed.ts +++ b/src/utils/quickEmbed.ts @@ -7,4 +7,4 @@ export enum EmbedType { export default async function (query: string, type: EmbedType) { // Wow gonna build an embed no way! -} \ No newline at end of file +} diff --git a/src/utils/youtube/search.ts b/src/utils/youtube/search.ts index 4858294..8122d99 100644 --- a/src/utils/youtube/search.ts +++ b/src/utils/youtube/search.ts @@ -1,4 +1,5 @@ import type { InnertubeSearchRequest } from "../../types/youtube"; + import formatLargeNumber from "../formatLargeNumber"; export default async function (query: string) { @@ -33,6 +34,7 @@ export default async function (query: string) { if (!data || data.length === 0) { console.error("No search results found for query:", query); + return []; } @@ -42,21 +44,33 @@ export default async function (query: string) { subscribers: number | string; channel_id: string; }> = []; + for (const content of data ?? []) { - for (const channel of content?.itemSectionRenderer?.contents ?? []) { + for (const channel of content?.itemSectionRenderer?.contents ?? + []) { if (channel?.channelRenderer?.channelId) { channelsResponse.push({ - title: channel?.channelRenderer?.longBylineText?.runs?.[0]?.text || "N/A", - handle: channel?.channelRenderer?.subscriberCountText?.simpleText || "N/A", - subscribers: formatLargeNumber(channel?.channelRenderer?.videoCountText?.simpleText), - channel_id: channel?.channelRenderer?.channelId || "N/A", + title: + channel?.channelRenderer?.longBylineText?.runs?.[0] + ?.text || "N/A", + handle: + channel?.channelRenderer?.subscriberCountText + ?.simpleText || "N/A", + subscribers: formatLargeNumber( + channel?.channelRenderer?.videoCountText + ?.simpleText, + ), + channel_id: + channel?.channelRenderer?.channelId || "N/A", }); } } } + return channelsResponse; } catch (err) { console.error(err); + return []; } } From 6ffe40320c377089ba4d0feecac77afda4ed9d25 Mon Sep 17 00:00:00 2001 From: GalvinPython Date: Sun, 20 Jul 2025 13:39:45 +0100 Subject: [PATCH 14/14] chore(bot): apply suggestions --- src/utils/twitch/checkIfStreamerIsLive.ts | 2 +- src/utils/youtube/fetchLatestUploads.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/twitch/checkIfStreamerIsLive.ts b/src/utils/twitch/checkIfStreamerIsLive.ts index a564a89..207a922 100644 --- a/src/utils/twitch/checkIfStreamerIsLive.ts +++ b/src/utils/twitch/checkIfStreamerIsLive.ts @@ -129,7 +129,7 @@ export async function checkIfStreamersAreLive(): Promise { for (const guild of guildsTrackingStreamer.data) { // Send a message to the channel const channel = await client.channels.fetch( - guild.guildId, + guild.notificationChannelId, ); await (channel as TextChannel).send( diff --git a/src/utils/youtube/fetchLatestUploads.ts b/src/utils/youtube/fetchLatestUploads.ts index addfbe2..491d4ad 100644 --- a/src/utils/youtube/fetchLatestUploads.ts +++ b/src/utils/youtube/fetchLatestUploads.ts @@ -153,7 +153,7 @@ export default async function fetchLatestUploads() { new Date(), ); - if (!updateSuccess) { + if (!updateSuccess.success) { console.error( "Error updating video ID in fetchLatestUploads", ); @@ -177,8 +177,8 @@ export default async function fetchLatestUploads() { const channelInfo = await getChannelDetails(channelId); - console.log( - discordGuildsToUpdate.data.filter( + console.info(`Filtered guilds for channel ID ${channelId}:`, { + count: discordGuildsToUpdate.data.filter( ( guild, ): guild is typeof dbGuildYouTubeSubscriptionsTable.$inferSelect => @@ -186,8 +186,8 @@ export default async function fetchLatestUploads() { "trackVideos" in guild && "trackShorts" in guild && "trackStreams" in guild, - ), - ); + ).length, + }); updates.set(videoId, { channelInfo,