Move Database from pocketbase to postgres

Move Database from pocketbase to postgres
This commit is contained in:
2025-09-19 02:08:25 +02:00
43 changed files with 694 additions and 474 deletions

View File

@@ -17,8 +17,11 @@ STREAMER_ID= # Twitch ID of the streaming user
CHATTER_ID= # Twitch ID of the chatting user CHATTER_ID= # Twitch ID of the chatting user
CHATTER_IS_STREAMER= # If the bot that activates on commands is on the same account as the streamer, set this to true. Make sure the STREAMER_ID and CHATTER_ID match in that case. CHATTER_IS_STREAMER= # If the bot that activates on commands is on the same account as the streamer, set this to true. Make sure the STREAMER_ID and CHATTER_ID match in that case.
# Pocketbase config # Postgres config
# POCKETBASE_URL= # Pocketbase URL. Defaults to http://localhost:8090 POSTGRES_HOST= # Hostname + port of the postgres database
POSTGRES_USER= # Username for logging in on the postgres database
POSTGRES_PASSWORD= # Password for logging in on the postgres database
POSTGRES_DB=twitchbot # Database name. Recommended value: twitchbot
# Redis/Valkey config # Redis/Valkey config
# REDIS_URL= # Redis URL. Defaults to redis://localhost:6379 # REDIS_URL= # Redis URL. Defaults to redis://localhost:6379

3
.gitignore vendored
View File

@@ -24,6 +24,9 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.env.production.local .env.production.local
.env.local .env.local
# Drizzle config files
*.config.ts
# caches # caches
.eslintcache .eslintcache
.cache .cache

View File

@@ -64,6 +64,8 @@ When using/giving an item or qbucks the itemlock will be set at the start of the
Admins can toggle the itemlock on chatters with the [`itemlock`](#administrative-commands) command. This will stop a chatter from giving, receiving and using items and qweribucks. Admins can toggle the itemlock on chatters with the [`itemlock`](#administrative-commands) command. This will stop a chatter from giving, receiving and using items and qweribucks.
It will NOT stop them from using items by cheering, but if that cheer item usage fails, they will not be given an equivalent item as compensation. It will NOT stop them from using items by cheering, but if that cheer item usage fails, they will not be given an equivalent item as compensation.
The only ways to get items is through the `getloot` command or by buying them with qbucks.
Items can be used with the alias as a command (example: `blast qwerinope`) or with the [`use` command](#qweribucksitem-commands). Items can be used with the alias as a command (example: `blast qwerinope`) or with the [`use` command](#qweribucksitem-commands).
When an Item is used it is removed from the inventory of the chatter. When an Item is used it is removed from the inventory of the chatter.
@@ -78,10 +80,10 @@ ITEM|RATE
`grenade`|`1/5` `grenade`|`1/5`
`blaster`|`1/5` `blaster`|`1/5`
`tnt`|`1/20` `tnt`|`1/20`
`silver bullet`|`1/250` `silver bullet`|`1/1000`
Each of these rates get pulled 5 times, then the result is added to your inventory. Each of these rates get pulled 3 times, then the result is added to your inventory.
It's theoretically possible to get 5 of each item. It's theoretically possible to get 3 of each item.
### Chatterbot/streamerbot ### Chatterbot/streamerbot
@@ -104,24 +106,25 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
`seiso`|Get a seiso rating|anyone|`seiso`|:white_check_mark: `seiso`|Get a seiso rating|anyone|`seiso`|:white_check_mark:
`backshot`|'Backshot' a random previous chatter|anyone|`backshot`|:white_check_mark: `backshot`|'Backshot' a random previous chatter|anyone|`backshot`|:white_check_mark:
`roulette`|Play russian roulette for a 5 minute timeout|anyone|`roulette`|:white_check_mark: `roulette`|Play russian roulette for a 5 minute timeout|anyone|`roulette`|:white_check_mark:
`timeout {target}`|Times targeted user out for 60 seconds (costs 100 qweribucks)|anyone|`timeout`|:white_check_mark:
`stats [target]`|Get timeout and some item stats for yourself or specified user this month|anyone|`stats` `monthlystats`|:white_check_mark: `stats [target]`|Get timeout and some item stats for yourself or specified user this month|anyone|`stats` `monthlystats`|:white_check_mark:
`alltime [target]`|Get timeout and some item stats for yourself or specified user of all time|anyone|`alltime` `alltimestats`|:white_check_mark: `alltime [target]`|Get timeout and some item stats for yourself or specified user of all time|anyone|`alltime` `alltimestats`|:white_check_mark:
`monthlyleaderboard`|Get the K/D leaderboard for this month [(info)](#leaderboards)|anyone|`monthlyleaderboard` `kdleaderboard` `leaderboard`|:white_check_mark:
`alltimeleaderboard`|Get the K/D leaderboard of all time [(info)](#leaderboards)|anyone|`alltimeleaderboard` `alltimekdleaderboard`|:white_check_mark:
`qbucksleaderboard`|Get the current qbucks leaderboard [(info)](#leaderboards)|anyone|`qbucksleaderboard` `moneyleaderboard` `baltop`|:white_check_mark:
### Qweribucks/Item commands ### Qweribucks/Item commands
COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
-|-|-|-|- -|-|-|-|-
`getloot`|Get a random assortment of items and qbucks every 10 minutes. [(drop rates)](#lootbox)|anyone|`getloot` `loot` `dig`|:white_check_mark: `getloot`|Get a random assortment of items and qbucks every 10 minutes. [(drop rates)](#lootbox)|anyone|`getloot` `loot` `dig`|:white_check_mark:
`getbalance [target]`|Get balance of target or self|anyone|`getbalance` `balance` `qbucks` `qweribucks` `wallet` `getwallet`|:white_check_mark:
`donate {target} {amount}`|Give the targeted user some or all of your qweribucks|anyone|`donate`|:white_check_mark:
`iteminfo {item}`|Get item function and aliases|anyone|`iteminfo` `itemhelp` `info`|:white_check_mark: `iteminfo {item}`|Get item function and aliases|anyone|`iteminfo` `itemhelp` `info`|:white_check_mark:
`inventory [target]`|Get inventory contents of target or self|anyone|`inventory` `inv` `pocket`|:white_check_mark: `inventory [target]`|Get inventory contents of target or self|anyone|`inventory` `inv` `pocket`|:white_check_mark:
`getprices`|Get the current price of items in the shop|anyone|`getprices` `prices` `shop`|:white_check_mark:
`buyitem {item} [amount]`|Buy one or more items for some qbucks. Prices are [here](#items)|anyone|`buyitem` `buy` `purchase`|:white_check_mark:
`getbalance [target]`|Get balance of target or self|anyone|`getbalance` `balance` `qbucks` `qweribucks` `wallet` `getwallet`|:white_check_mark:
`give {target} {item} {amount}`|Give targeted user amount of items|anyone|`give`|:white_check_mark: `give {target} {item} {amount}`|Give targeted user amount of items|anyone|`give`|:white_check_mark:
`donate {target} {amount}`|Give the targeted user some or all of your qweribucks|anyone|`donate`|:white_check_mark:
`use {item} ...`|Use item. More info at [The items section](#items)|anyone|`use`|:x: `use {item} ...`|Use item. More info at [The items section](#items)|anyone|`use`|:x:
`monthlyleaderboard`|Get the K/D leaderboard for this month [(info)](#leaderboards)|anyone|`monthlyleaderboard` `kdleaderboard` `leaderboard`|:white_check_mark:
`alltimeleaderboard`|Get the K/D leaderboard of all time [(info)](#leaderboards)|anyone|`alltimeleaderboard` `alltimekdleaderboard`|:white_check_mark:
`qbucksleaderboard`|Get the current qbucks leaderboard [(info)](#leaderboards)|anyone|`qbucksleaderboard` `moneyleaderboard` `baltop`|:white_check_mark:
`admindonate {target} {amount}`|Gives the targeted user amount of qweribucks|admins|`admindonate`|:white_check_mark: `admindonate {target} {amount}`|Gives the targeted user amount of qweribucks|admins|`admindonate`|:white_check_mark:
`admingive {target} {item} {amount}`|Give targeted user amount of new items|admins|`admingive`|:white_check_mark: `admingive {target} {item} {amount}`|Give targeted user amount of new items|admins|`admingive`|:white_check_mark:
@@ -149,12 +152,12 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
## Items ## Items
NAME|COMMAND|FUNCTION|ALIASES NAME|COMMAND|FUNCTION|ALIASES|COST
-|-|-|- -|-|-|-|-
Blaster|`blaster {target}`|Times targeted user out for 60 seconds|`blaster` `blast` Blaster|`blaster {target}`|Times targeted user out for 60 seconds|`blaster` `blast`|100
Silver Bullet|`silverbullet {target}`|Times targeted user out for 24 hours|`silverbullet` `execute` `{blastin}` Silver Bullet|`silverbullet {target}`|Times targeted user out for 24 hours|`silverbullet` `execute` `{blastin}`|6666
Grenade|`grenade`|Times a random vulnerable chatter out for 60 seconds|`grenade` Grenade|`grenade`|Times a random vulnerable chatter out for 60 seconds|`grenade`|99
TNT|`tnt`|Give 5-10 random chatters 60 second timeouts|`tnt` TNT|`tnt`|Give 5-10 random chatters 60 second timeouts|`tnt`|1000
## Cheers ## Cheers

158
bun.lock
View File

@@ -7,11 +7,13 @@
"@fontsource/jersey-15": "^5.2.6", "@fontsource/jersey-15": "^5.2.6",
"@twurple/auth": "^7.3.0", "@twurple/auth": "^7.3.0",
"@twurple/eventsub-ws": "^7.3.0", "@twurple/eventsub-ws": "^7.3.0",
"drizzle-orm": "^0.44.5",
"kleur": "^4.1.5", "kleur": "^4.1.5",
"pocketbase": "^0.26.1",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "latest", "@types/bun": "latest",
"drizzle-kit": "^0.31.4",
"pg": "^8.16.3",
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.8.3", "typescript": "^5.8.3",
@@ -39,6 +41,64 @@
"@d-fischer/typed-event-emitter": ["@d-fischer/typed-event-emitter@3.3.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-OvSEOa8icfdWDqcRtjSEZtgJTFOFNgTjje7zaL0+nAtu2/kZtRCSK5wUMrI/aXtCH8o0Qz2vA8UqkhWUTARFQQ=="], "@d-fischer/typed-event-emitter": ["@d-fischer/typed-event-emitter@3.3.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-OvSEOa8icfdWDqcRtjSEZtgJTFOFNgTjje7zaL0+nAtu2/kZtRCSK5wUMrI/aXtCH8o0Qz2vA8UqkhWUTARFQQ=="],
"@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.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="],
"@fontsource/jersey-15": ["@fontsource/jersey-15@5.2.6", "", {}, "sha512-3zkkEnu91esusWLqAK/AN1uc6jNtWT8idfO0UfYLqNlbMBKkbbiIVXtq6UbQsyegxnmRMppVV1J2t1zrJ36VgA=="], "@fontsource/jersey-15": ["@fontsource/jersey-15@5.2.6", "", {}, "sha512-3zkkEnu91esusWLqAK/AN1uc6jNtWT8idfO0UfYLqNlbMBKkbbiIVXtq6UbQsyegxnmRMppVV1J2t1zrJ36VgA=="],
"@twurple/api": ["@twurple/api@7.3.0", "", { "dependencies": { "@d-fischer/cache-decorators": "^4.0.0", "@d-fischer/cross-fetch": "^5.0.1", "@d-fischer/detect-node": "^3.0.1", "@d-fischer/logger": "^4.2.1", "@d-fischer/rate-limiter": "^1.1.0", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.1", "@twurple/api-call": "7.3.0", "@twurple/common": "7.3.0", "retry": "^0.13.1", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/auth": "7.3.0" } }, "sha512-QtaVgYi50E3AB/Nxivjou/u6w1cuQ6g4R8lzQawYDaQNtlP2Ue8vvYuSp2PfxSpe8vNiKhgV8hZAs+j4V29sxQ=="], "@twurple/api": ["@twurple/api@7.3.0", "", { "dependencies": { "@d-fischer/cache-decorators": "^4.0.0", "@d-fischer/cross-fetch": "^5.0.1", "@d-fischer/detect-node": "^3.0.1", "@d-fischer/logger": "^4.2.1", "@d-fischer/rate-limiter": "^1.1.0", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.1", "@twurple/api-call": "7.3.0", "@twurple/common": "7.3.0", "retry": "^0.13.1", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/auth": "7.3.0" } }, "sha512-QtaVgYi50E3AB/Nxivjou/u6w1cuQ6g4R8lzQawYDaQNtlP2Ue8vvYuSp2PfxSpe8vNiKhgV8hZAs+j4V29sxQ=="],
@@ -59,18 +119,64 @@
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"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.5", "", { "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-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ=="],
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], "klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"pocketbase": ["pocketbase@0.26.1", "", {}, "sha512-fjcPDpxyqTZCwqGUTPUV7vssIsNMqHxk9GxbhxYHPEf18RqX2d9cpSqbbHk7aas30jqkgptuKfG7aY/Mytjj3g=="], "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="],
"pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="],
"pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="],
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
"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=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"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=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -84,5 +190,53 @@
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], "ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"@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=="],
"@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=="],
} }
} }

View File

@@ -1,7 +1,7 @@
services: services:
valkey: valkey:
image: valkey/valkey:alpine image: valkey/valkey:alpine
container_name: valkey container_name: qweribot-valkey
ports: ports:
- 6379:6379 - 6379:6379
restart: no restart: no
@@ -9,12 +9,13 @@ services:
- ./db/redis:/data - ./db/redis:/data
environment: environment:
- VALKEY_EXTRA_FLAGS=--save 60 1 - VALKEY_EXTRA_FLAGS=--save 60 1
pocketbase: postgres:
container_name: qweribot-pocketbase container_name: qweribot-postgres
build: image: postgres:latest
context: ./pocketbase restart: unless-stopped
env_file:
- .env
ports: ports:
- 8090:8090 - "5432:5432"
restart: no
volumes: volumes:
- ./db/pocketbase:/pb/pb_data - ./db/postgresql:/var/lib/postgresql/data

View File

@@ -2,7 +2,17 @@
"name": "qweribot", "name": "qweribot",
"module": "src/index.ts", "module": "src/index.ts",
"devDependencies": { "devDependencies": {
"@types/bun": "latest" "@types/bun": "latest",
"drizzle-kit": "^0.31.4",
"pg": "^8.16.3"
},
"scripts": {
"start": "NODE_ENV=production bun src/index.ts",
"start-dev": "NODE_ENV=development bun src/index.ts",
"migrate": "drizzle-kit push --config=drizzle-prod.config.ts",
"migrate-dev": "drizzle-kit push --config=drizzle-dev.config.ts",
"studio": "drizzle-kit studio --config=drizzle-prod.config.ts",
"studio-dev": "drizzle-kit studio --config=drizzle-dev.config.ts"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^5.8.3" "typescript": "^5.8.3"
@@ -13,7 +23,7 @@
"@fontsource/jersey-15": "^5.2.6", "@fontsource/jersey-15": "^5.2.6",
"@twurple/auth": "^7.3.0", "@twurple/auth": "^7.3.0",
"@twurple/eventsub-ws": "^7.3.0", "@twurple/eventsub-ws": "^7.3.0",
"kleur": "^4.1.5", "drizzle-orm": "^0.44.5",
"pocketbase": "^0.26.1" "kleur": "^4.1.5"
} }
} }

View File

@@ -1,20 +0,0 @@
FROM alpine:latest
ARG PB_VERSION=0.28.4
ARG PB_ZIPNAME=pocketbase_${PB_VERSION}_linux_amd64.zip
RUN apk add --no-cache \
unzip \
ca-certificates
ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/${PB_ZIPNAME} /tmp/${PB_ZIPNAME}
ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/checksums.txt /tmp/checksums.txt
WORKDIR /tmp
RUN grep ${PB_ZIPNAME} checksums.txt | sha256sum -c
RUN unzip /tmp/${PB_ZIPNAME} -d /pb/
COPY ./pb_migrations /pb/pb_migrations
COPY ./pb_hooks /pb/pb_hooks
EXPOSE 8090
CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8090"]

View File

@@ -45,5 +45,5 @@ export default new Cheer('execute', 6666, async (msg, user) => {
break; break;
}; };
}; };
}); }, true);

View File

@@ -30,4 +30,4 @@ export default new Cheer('grenade', 99, async (msg, user) => {
target: target?.displayName! target: target?.displayName!
}) })
]); ]);
}); }, true);

View File

@@ -5,10 +5,12 @@ export class Cheer {
public readonly name: string; public readonly name: string;
public readonly amount: number; public readonly amount: number;
public readonly execute: (msg: EventSubChannelChatMessageEvent, sender: User) => Promise<void>; public readonly execute: (msg: EventSubChannelChatMessageEvent, sender: User) => Promise<void>;
constructor(name: string, amount: number, execution: (msg: EventSubChannelChatMessageEvent, sender: User) => Promise<void>) { public readonly isItem: boolean;
constructor(name: string, amount: number, execution: (msg: EventSubChannelChatMessageEvent, sender: User) => Promise<void>, isItem = false) {
this.name = name.toLowerCase(); this.name = name.toLowerCase();
this.amount = amount; this.amount = amount;
this.execute = execution; this.execute = execution;
this.isItem = isItem;
}; };
}; };
@@ -29,14 +31,12 @@ export default cheers;
export { namedcheers }; export { namedcheers };
import { sendMessage } from 'commands'; import { sendMessage } from 'commands';
import logger from 'lib/logger';
import { getUserRecord } from 'db/dbUser'; import { getUserRecord } from 'db/dbUser';
import { changeItemCount } from 'items'; import { changeItemCount, type items } from 'items';
export async function handleNoTarget(msg: EventSubChannelChatMessageEvent, user: User, itemname: string, silent = true) { export async function handleNoTarget(msg: EventSubChannelChatMessageEvent, user: User, itemname: items, silent = true) {
if (await user.itemLock()) { if (await user.itemLock()) {
await sendMessage(`Cannot give ${user.displayName} a ${itemname}`, msg.messageId); await sendMessage(`Cannot give ${user.displayName} a ${itemname} (itemlock)`, msg.messageId);
logger.err(`Failed to give ${user.displayName} a ${itemname} for their cheer`);
return; return;
}; };
await user.setLock(); await user.setLock();

View File

@@ -46,4 +46,4 @@ export default new Cheer('timeout', 100, async (msg, user) => {
break; break;
}; };
}; };
}); }, true);

View File

@@ -23,16 +23,18 @@ export default new Cheer('tnt', 1000, async (msg, user) => {
timeout(target!, `You got hit by ${user.displayName}'s TNT!`, 60), timeout(target!, `You got hit by ${user.displayName}'s TNT!`, 60),
redis.del(`user:${targetid}:vulnerable`), redis.del(`user:${targetid}:vulnerable`),
sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s TNT wybuh`), sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s TNT wybuh`),
createTimeoutRecord(user, target!, ITEMNAME), createTimeoutRecord(user, target!, ITEMNAME)
createCheerEventRecord(user, ITEMNAME),
]); ]);
})); }));
await playAlert({
name: 'tntExplosion', await Promise.all([
user: user.displayName, createCheerEventRecord(user, ITEMNAME),
targets playAlert({
}) name: 'tntExplosion',
user: user.displayName,
targets
})
]);
await sendMessage(`RIPBOZO ${user.displayName} exploded ${targets.length} chatter${targets.length === 1 ? '' : 's'} with their TNT RIPBOZO`); await sendMessage(`RIPBOZO ${user.displayName} exploded ${targets.length} chatter${targets.length === 1 ? '' : 's'} with their TNT RIPBOZO`);
}); }, true);

View File

@@ -16,8 +16,8 @@ export default new Command({
const userRecord = await getUserRecord(target); const userRecord = await getUserRecord(target);
if (!args[1]) { await sendMessage('Please specify the amount qweribucks you want to give', msg.messageId); return; }; if (!args[1]) { await sendMessage('Please specify the amount qweribucks you want to give', msg.messageId); return; };
const amount = parseInt(args[1]); const amount = parseInt(args[1]);
if (isNaN(amount)) { await sendMessage(`${args[1]} is not a valid amount`); return; }; if (isNaN(amount)) { await sendMessage(`'${args[1]}' is not a valid amount`); return; };
if (await target.itemLock()) { await sendMessage('Cannot give qweribucks: item lock is set', msg.messageId); return; }; if (await target.itemLock()) { await sendMessage('Cannot give qweribucks (itemlock)', msg.messageId); return; };
await target.setLock(); await target.setLock();
const data = await changeBalance(target, userRecord, amount); const data = await changeBalance(target, userRecord, amount);
if (!data) { if (!data) {

View File

@@ -19,8 +19,8 @@ export default new Command({
if (!item) { await sendMessage(`Item ${args[1]} doesn't exist`, msg.messageId); return; }; if (!item) { await sendMessage(`Item ${args[1]} doesn't exist`, msg.messageId); return; };
if (!args[2]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; }; if (!args[2]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; };
const amount = parseInt(args[2]); const amount = parseInt(args[2]);
if (isNaN(amount)) { await sendMessage(`${args[2]} is not a valid amount`); return; }; if (isNaN(amount)) { await sendMessage(`'${args[2]}' is not a valid amount`); return; };
if (await target.itemLock()) { await sendMessage('Cannot give item: item lock is set', msg.messageId); return; }; if (await target.itemLock()) { await sendMessage('Cannot give item (itemlock)', msg.messageId); return; };
await target.setLock(); await target.setLock();
const data = await changeItemCount(target, userRecord, item.name, amount); const data = await changeItemCount(target, userRecord, item.name, amount);
if (data) { if (data) {

View File

@@ -1,6 +1,5 @@
import { Command, sendMessage } from "commands"; import { Command, sendMessage } from "commands";
import { getAllUserRecords } from "db/dbUser"; import { getKDLeaderboard } from "db/dbUser";
import { getTimeoutStats } from "lib/getStats";
import User from "user"; import User from "user";
type KD = { user: User; kd: number; }; type KD = { user: User; kd: number; };
@@ -10,27 +9,19 @@ export default new Command({
aliases: ['alltimeleaderboard', 'alltimekdleaderboard'], aliases: ['alltimeleaderboard', 'alltimekdleaderboard'],
usertype: 'chatter', usertype: 'chatter',
execution: async msg => { execution: async msg => {
const users = await getAllUserRecords(); const rawKD = await getKDLeaderboard();
if (!users) return; if (rawKD.length === 0) {
const userKDs: KD[] = [];
await Promise.all(users.map(async userRecord => {
const user = await User.initUserId(userRecord.id);
if (!user) return;
const data = await getTimeoutStats(user, false);
if (!data) return;
if (data.hit.blaster < 5) return;
let kd = data.shot.blaster / data.hit.blaster;
if (isNaN(kd)) kd = 0;
userKDs.push({ user, kd });
}));
if (userKDs.length === 0) {
await sendMessage(`No users on leaderboard yet!`, msg.messageId); await sendMessage(`No users on leaderboard yet!`, msg.messageId);
return; return;
}; };
const userKDs: KD[] = [];
await Promise.all(rawKD.map(async userRecord => {
const user = await User.initUserId(userRecord.userId.toString());
if (!user) return;
userKDs.push({ user, kd: userRecord.KD })
}));
userKDs.sort((a, b) => b.kd - a.kd); userKDs.sort((a, b) => b.kd - a.kd);
const txt: string[] = []; const txt: string[] = [];

37
src/commands/buyitem.ts Normal file
View File

@@ -0,0 +1,37 @@
import { Command, sendMessage } from "commands";
import parseCommandArgs from "lib/parseCommandArgs";
import items from "items";
import { getUserRecord, updateUserRecord } from "db/dbUser";
export default new Command({
name: 'buyitem',
aliases: ['buyitem', 'buy', 'purchase'],
usertype: 'chatter',
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage(`Specify the item you'd like to buy`, msg.messageId); return; };
const selecteditem = items.get(args[0].toLowerCase());
if (!selecteditem) { await sendMessage(`'${args[0]}' is not a valid item`, msg.messageId); return; };
const amount = args[1] ? parseInt(args[1]) : 1;
if (isNaN(amount) || amount < 1) { await sendMessage(`'${args[1]}' is not a valid amount to buy`, msg.messageId); return; };
const totalcost = amount * selecteditem.price;
if (await user.itemLock()) { await sendMessage('Cannot buy item (itemlock)', msg.messageId); return; };
await user.setLock();
const userRecord = await getUserRecord(user);
if (userRecord.balance < totalcost) { await sendMessage(`You don't have enough qbucks to buy ${amount} ${selecteditem.prettyName}${amount === 1 ? '' : selecteditem.plural}! You have ${userRecord.balance}, need ${totalcost}`); await user.clearLock(); return; };
if (userRecord.inventory[selecteditem.name]) userRecord.inventory[selecteditem.name]! += amount
else userRecord.inventory[selecteditem.name] = amount;
userRecord.balance -= totalcost;
await Promise.all([
updateUserRecord(user, userRecord),
sendMessage(`${user.displayName} bought ${amount} ${selecteditem.prettyName}${amount === 1 ? '' : selecteditem.plural} for ${totalcost} qbucks. They now have ${userRecord.inventory[selecteditem.name]} ${selecteditem.prettyName}${userRecord.inventory[selecteditem.name] === 1 ? '' : selecteditem.plural} and ${userRecord.balance} qbucks`, msg.messageId)
]);
await user.clearLock();
}
});

View File

@@ -1,5 +1,4 @@
import { Command, sendMessage } from "commands"; import { Command, sendMessage } from "commands";
import type { userRecord } from "db/connection";
import { getUserRecord } from "db/dbUser"; import { getUserRecord } from "db/dbUser";
import parseCommandArgs from "lib/parseCommandArgs"; import parseCommandArgs from "lib/parseCommandArgs";
import { changeBalance } from "lib/changeBalance"; import { changeBalance } from "lib/changeBalance";
@@ -19,12 +18,12 @@ export default new Command({
const targetRecord = await getUserRecord(target); const targetRecord = await getUserRecord(target);
if (!args[1]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; }; if (!args[1]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; };
const amount = parseInt(args[1]); const amount = parseInt(args[1]);
if (isNaN(amount) || amount < 1) { await sendMessage(`${args[1]} is not a valid amount`); return; }; if (isNaN(amount) || amount < 1) { await sendMessage(`'${args[1]}' is not a valid amount`); return; };
const userRecord = await getUserRecord(user); const userRecord = await getUserRecord(user);
if (userRecord.balance < amount) { await sendMessage(`You can't give qweribucks you don't have!`, msg.messageId); return; }; if (userRecord.balance < amount) { await sendMessage(`You can't give qweribucks you don't have!`, msg.messageId); return; };
if (await user.itemLock() || await target.itemLock()) { await sendMessage('Cannot give qweribucks', msg.messageId); return; }; if (await user.itemLock() || await target.itemLock()) { await sendMessage('Cannot give qweribucks (itemlock)', msg.messageId); return; };
await Promise.all([ await Promise.all([
user.setLock(), user.setLock(),
@@ -37,7 +36,7 @@ export default new Command({
]); ]);
if (!data.includes(false)) { if (!data.includes(false)) {
const { balance: newamount } = data[0] as userRecord; const { balance: newamount } = data[0];
await sendMessage(`${user.displayName} gave ${amount} qweribuck${amount === 1 ? '' : 's'} to ${target.displayName}. They now have ${newamount} qweribuck${newamount === 1 ? '' : 's'}`, msg.messageId); await sendMessage(`${user.displayName} gave ${amount} qweribuck${amount === 1 ? '' : 's'} to ${target.displayName}. They now have ${newamount} qweribuck${newamount === 1 ? '' : 's'}`, msg.messageId);
} else { } else {
// TODO: Rewrite this section // TODO: Rewrite this section

View File

@@ -8,7 +8,7 @@ export default new Command({
execution: async (_msg, user) => { execution: async (_msg, user) => {
await Promise.all([ await Promise.all([
timeout(user, "NO MODME", 60), timeout(user, "NO MODME", 60),
sendMessage(`NO MODME COMMAND!!! UltraMad`) sendMessage(`NO MODME COMMAND!!! UltraMad UltraMad UltraMad`)
]); ]);
} }
}); });

View File

@@ -1,7 +1,7 @@
import { redis } from "bun"; import { redis } from "bun";
import { Command, sendMessage } from "commands"; import { Command, sendMessage } from "commands";
import { getUserRecord, updateUserRecord } from "db/dbUser"; import { getUserRecord, updateUserRecord } from "db/dbUser";
import items from "items"; import itemMap, { type inventory, type items } from "items";
import { buildTimeString } from "lib/dateManager"; import { buildTimeString } from "lib/dateManager";
import { timeout } from "lib/timeout"; import { timeout } from "lib/timeout";
import { isInvuln, removeInvuln } from "lib/invuln"; import { isInvuln, removeInvuln } from "lib/invuln";
@@ -19,7 +19,7 @@ export default new Command({
if (await isInvuln(msg.chatterId) && !streamerUsers.includes(msg.chatterId)) { await sendMessage(`You're no longer an invuln because used a lootbox.`, msg.messageId); await removeInvuln(msg.chatterId); }; if (await isInvuln(msg.chatterId) && !streamerUsers.includes(msg.chatterId)) { await sendMessage(`You're no longer an invuln because used a lootbox.`, msg.messageId); await removeInvuln(msg.chatterId); };
if (await user.itemLock()) { await sendMessage(`Cannot get loot (itemlock)`, msg.messageId); return; }; if (await user.itemLock()) { await sendMessage(`Cannot get loot (itemlock)`, msg.messageId); return; };
const userData = await getUserRecord(user); const userData = await getUserRecord(user);
const lastlootbox = Date.parse(userData.lastlootbox); const lastlootbox = userData.lastlootbox.getTime();
const now = Date.now(); const now = Date.now();
if ((lastlootbox + COOLDOWN) > now) { if ((lastlootbox + COOLDOWN) > now) {
if (await user.greedy()) { if (await user.greedy()) {
@@ -40,26 +40,25 @@ export default new Command({
await user.clearGreed(); await user.clearGreed();
await user.setLock(); await user.setLock();
userData.lastlootbox = new Date(now).toISOString(); userData.lastlootbox = new Date(now);
const gainedqbucks = Math.floor(Math.random() * 100) + 50; // range from 50 to 150 const gainedqbucks = Math.floor(Math.random() * 100) + 50; // range from 50 to 150
userData.balance += gainedqbucks; userData.balance += gainedqbucks;
const itemDiff = { const itemDiff: inventory = {
grenade: 0, grenade: 0,
blaster: 0, blaster: 0,
tnt: 0, tnt: 0,
silverbullet: 0
}; };
for (let i = 0; i < 5; i++) { for (let i = 0; i < 3; i++) {
if (Math.floor(Math.random() * 5) === 0) itemDiff.grenade += 1; if (Math.floor(Math.random() * 5) === 0) itemDiff.grenade! += 1;
if (Math.floor(Math.random() * 5) === 0) itemDiff.blaster += 1; if (Math.floor(Math.random() * 5) === 0) itemDiff.blaster! += 1;
if (Math.floor(Math.random() * 25) === 0) itemDiff.tnt += 1; if (Math.floor(Math.random() * 25) === 0) itemDiff.tnt! += 1;
if (Math.floor(Math.random() * 250) === 0) itemDiff.silverbullet += 1; if (Math.floor(Math.random() * 1000) === 0) itemDiff.silverbullet! += 1;
}; };
for (const [item, amount] of Object.entries(itemDiff)) { for (const [item, amount] of Object.entries(itemDiff) as [items, number][]) {
if (userData.inventory[item]) userData.inventory[item] += amount; if (userData.inventory[item]) userData.inventory[item] += amount;
else userData.inventory[item] = amount; else userData.inventory[item] = amount;
}; };
@@ -68,7 +67,7 @@ export default new Command({
for (const [item, amount] of Object.entries(itemDiff)) { for (const [item, amount] of Object.entries(itemDiff)) {
if (amount === 0) continue; if (amount === 0) continue;
const selection = items.get(item); const selection = itemMap.get(item);
if (!selection) continue; if (!selection) continue;
itemstrings.push(`${amount} ${selection.prettyName + (amount === 1 ? '' : selection.plural)}`); itemstrings.push(`${amount} ${selection.prettyName + (amount === 1 ? '' : selection.plural)}`);
}; };

12
src/commands/getprices.ts Normal file
View File

@@ -0,0 +1,12 @@
import { Command, sendMessage } from "commands";
import { itemObjectArray } from "items";
export default new Command({
name: 'getprices',
aliases: ['getprices', 'prices', 'shop'],
usertype: 'chatter',
execution: async msg => {
const txt = itemObjectArray.toSorted((a, b) => a.price - b.price).map(item => `${item.prettyName}: ${item.price}`);
await sendMessage(`Prices: ${txt.join(' | ')}`, msg.messageId);
}
});

View File

@@ -1,5 +1,4 @@
import { Command, sendMessage } from "commands"; import { Command, sendMessage } from "commands";
import type { userRecord } from "db/connection";
import { getUserRecord } from "db/dbUser"; import { getUserRecord } from "db/dbUser";
import items, { changeItemCount } from "items"; import items, { changeItemCount } from "items";
import parseCommandArgs from "lib/parseCommandArgs"; import parseCommandArgs from "lib/parseCommandArgs";
@@ -22,11 +21,11 @@ export default new Command({
if (!item) { await sendMessage(`Item ${args[1]} doesn't exist`, msg.messageId); return; }; if (!item) { await sendMessage(`Item ${args[1]} doesn't exist`, msg.messageId); return; };
if (!args[2]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; }; if (!args[2]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; };
const amount = parseInt(args[2]); const amount = parseInt(args[2]);
if (isNaN(amount) || amount < 1) { await sendMessage(`${args[2]} is not a valid amount`); return; }; if (isNaN(amount) || amount < 1) { await sendMessage(`'${args[2]}' is not a valid amount`); return; };
const userRecord = await getUserRecord(user); const userRecord = await getUserRecord(user);
if (userRecord.inventory[item.name]! < amount) { await sendMessage(`You can't give items you don't have!`, msg.messageId); return; }; if (userRecord.inventory[item.name]! < amount) { await sendMessage(`You can't give items you don't have!`, msg.messageId); return; };
if (await user.itemLock() || await target.itemLock()) { await sendMessage('Cannot give item', msg.messageId); return; }; if (await user.itemLock() || await target.itemLock()) { await sendMessage('Cannot give item (itemlock)', msg.messageId); return; };
await Promise.all([ await Promise.all([
user.setLock(), user.setLock(),
@@ -38,14 +37,14 @@ export default new Command({
await changeItemCount(user, userRecord, item.name, -amount) await changeItemCount(user, userRecord, item.name, -amount)
]); ]);
if (!data.includes(false)) { if (data[0] !== false && data[1] !== false) {
const tempdata = data[0] as userRecord; const tempdata = data[0];
const newamount = tempdata.inventory[item.name]!; const newamount = tempdata.inventory[item.name]!;
await sendMessage(`${user.displayName} gave ${amount} ${item.prettyName + (amount === 1 ? '' : item.plural)} to ${target.displayName}. They now have ${newamount} ${item.prettyName + (newamount === 1 ? '' : item.plural)}`, msg.messageId); await sendMessage(`${user.displayName} gave ${amount} ${item.prettyName + (amount === 1 ? '' : item.plural)} to ${target.displayName}. They now have ${newamount} ${item.prettyName + (newamount === 1 ? '' : item.plural)}`, msg.messageId);
} else { } else {
// TODO: Rewrite this section // TODO: Rewrite this section
await sendMessage(`Failed to give ${target.displayName} ${amount} ${item.prettyName + (amount === 1 ? '' : item.plural)}`, msg.messageId); await sendMessage(`Failed to give ${target.displayName} ${amount} ${item.prettyName + (amount === 1 ? '' : item.plural)}`, msg.messageId);
logger.warn(`WARNING: Item donation failed: target success: ${data[0] !== false}, donator success: ${data[1] !== false}`); logger.warn(`WARNING: Item donation failed: target success: ${data[0] !== false ? "yes" : "no"}, donator success: ${data[1] !== false ? "yes" : "no"}`);
}; };
await user.clearLock(); await user.clearLock();
await target.clearLock(); await target.clearLock();

View File

@@ -1,6 +1,5 @@
import { Command, sendMessage } from "commands"; import { Command, sendMessage } from "commands";
import { getAllUserRecords } from "db/dbUser"; import { getKDLeaderboard } from "db/dbUser";
import { getTimeoutStats } from "lib/getStats";
import User from "user"; import User from "user";
type KD = { user: User; kd: number; }; type KD = { user: User; kd: number; };
@@ -10,27 +9,21 @@ export default new Command({
aliases: ['monthlyleaderboard', 'kdleaderboard', 'leaderboard'], aliases: ['monthlyleaderboard', 'kdleaderboard', 'leaderboard'],
usertype: 'chatter', usertype: 'chatter',
execution: async msg => { execution: async msg => {
const users = await getAllUserRecords(); const monthdata = new Date().toISOString().slice(0, 7);
if (!users) return;
const userKDs: KD[] = []; const rawKD = await getKDLeaderboard(monthdata);
await Promise.all(users.map(async userRecord => { if (rawKD.length === 0) {
const user = await User.initUserId(userRecord.id);
if (!user) return;
const data = await getTimeoutStats(user, true);
if (!data) return;
if (data.hit.blaster < 5) return;
let kd = data.shot.blaster / data.hit.blaster;
if (isNaN(kd)) kd = 0;
userKDs.push({ user, kd });
}));
if (userKDs.length === 0) {
await sendMessage(`No users on leaderboard yet!`, msg.messageId); await sendMessage(`No users on leaderboard yet!`, msg.messageId);
return; return;
}; };
const userKDs: KD[] = [];
await Promise.all(rawKD.map(async userRecord => {
const user = await User.initUserId(userRecord.userId.toString());
if (!user) return;
userKDs.push({ user, kd: userRecord.KD })
}));
userKDs.sort((a, b) => b.kd - a.kd); userKDs.sort((a, b) => b.kd - a.kd);
const txt: string[] = []; const txt: string[] = [];

View File

@@ -14,7 +14,7 @@ export default new Command({
const txt: string[] = []; const txt: string[] = [];
for (const userRecord of data) { for (const userRecord of data) {
if (userRecord.balance === 0) continue; if (userRecord.balance === 0) continue;
const user = await User.initUserId(userRecord.id); const user = await User.initUserId(userRecord.id.toString());
if (!user) continue; if (!user) continue;
txt.push(`${index}. ${user.displayName}: ${userRecord.balance}`); txt.push(`${index}. ${user.displayName}: ${userRecord.balance}`);
index++; index++;

View File

@@ -1,46 +0,0 @@
import { Command, sendMessage } from "commands";
import { getUserRecord } from "db/dbUser";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
import { timeout } from "lib/timeout";
import { changeBalance } from "lib/changeBalance";
import { createTimeoutRecord } from "db/dbTimeouts";
export default new Command({
name: 'timeout',
aliases: ['timeout'],
usertype: 'chatter',
execution: async (msg, user) => {
const userObj = await getUserRecord(user);
if (userObj.balance < 100) { await sendMessage(`You don't have enough qweribucks (need 100, have ${userObj.balance})`, msg.messageId); return; };
const messagequery = parseCommandArgs(msg.messageText);
if (!messagequery[0]) { await sendMessage('Please specify a target'); return; };
const target = await User.initUsername(messagequery[0].toLowerCase());
if (!target) { await sendMessage(`${messagequery[0]} doesn't exist`); return; };
await getUserRecord(target); // make sure the user record exist in the database
const result = await timeout(target, `You got BLASTED by ${user.displayName}`, 60);
if (result.status) {
await Promise.all([
sendMessage(`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`),
changeBalance(user, userObj, -100),
createTimeoutRecord(user, target, 'blaster')
]);
} else {
switch (result.reason) {
case "banned":
await sendMessage(`${target.displayName} is already timed out/banned`, msg.messageId);
break;
case "illegal":
await Promise.all([
sendMessage(`${user.displayName} Nou Nou Nou`),
timeout(user, 'nah', 60)
]);
break;
case "unknown":
await sendMessage('Something went wrong...', msg.messageId);
break;
};
};
}
});

View File

@@ -1,12 +1,13 @@
import pocketbase from "db/connection";
import { RedisClient } from "bun"; import { RedisClient } from "bun";
import db from "db/connection";
import { users } from "db/schema";
import logger from "lib/logger"; import logger from "lib/logger";
export async function connectionCheck() { export async function connectionCheck() {
let pbstatus = false; let pgstatus = false;
try { try {
await pocketbase.health.check().then(a => a.code === 200); await db.select().from(users); // The query doesn't matter, only that it fails. This also fails if the migration hasn't taken place
pbstatus = true; pgstatus = true;
} catch { }; } catch { };
const tempclient = new RedisClient(undefined, { const tempclient = new RedisClient(undefined, {
connectionTimeout: 100, connectionTimeout: 100,
@@ -18,7 +19,7 @@ export async function connectionCheck() {
redisstatus = true; redisstatus = true;
} catch { }; } catch { };
logger.info(`Currently using the "${process.env.NODE_ENV ?? "production"}" database`); logger.info(`Currently using the "${process.env.NODE_ENV ?? "production"}" database`);
pbstatus ? logger.ok(`Pocketbase status: good`) : logger.err(`Pocketbase status: bad`); pgstatus ? logger.ok(`Postgresql status: good`) : logger.err(`Postgresql status: bad`);
redisstatus ? logger.ok(`Redis/Valkey status: good`) : logger.err(`Redis/Valkey status: bad`); redisstatus ? logger.ok(`Redis/Valkey status: good`) : logger.err(`Redis/Valkey status: bad`);
if (!pbstatus || !redisstatus) process.exit(1); if (!pgstatus || !redisstatus) process.exit(1);
}; };

View File

@@ -1,75 +1,15 @@
import type { AccessToken } from "@twurple/auth"; import * as schema from "db/schema";
import PocketBase, { RecordService } from "pocketbase";
import type { inventory } from "items";
import logger from "lib/logger"; import logger from "lib/logger";
const pocketbaseurl = process.env.POCKETBASE_URL ?? "localhost:8090"; const host = process.env.POSTGRES_HOST ?? "";
if (pocketbaseurl === "") { logger.enverr("POCKETBASE_URL"); process.exit(1); }; if (!host) { logger.enverr("POSTGRES_HOST"); process.exit(1); };
const user = process.env.POSTGRES_USER ?? "";
if (!user) { logger.enverr("POSTGRES_USER"); process.exit(1); };
const password = process.env.POSTGRES_PASSWORD ?? "";
if (!password) { logger.enverr("POSTGRES_USER"); process.exit(1); };
const database = process.env.POSTGRES_DB ?? "";
if (!database) { logger.enverr("POSTGRES_DB"); process.exit(1); };
const url = `postgresql://${user}:${password}@${host}/${database}`;
export type authRecord = { import { drizzle } from 'drizzle-orm/bun-sql';
id: string; export default drizzle(url, { schema });
accesstoken: AccessToken;
};
export type userRecord = {
id: string;
username: string; // Don't use this, Use User.username or User.displayName. This is just to make the pocketbase data easier to read.
balance: number;
inventory: inventory;
lastlootbox: string;
};
export type usedItemRecord = {
id?: string;
user: string;
item: string;
created: string;
};
export type timeoutRecord = {
id?: string;
user: string;
target: string;
item: string;
created: string;
};
export type cheerEventRecord = {
id?: string;
user: string;
cheer: string;
created: string;
};
export type cheerRecord = {
id?: string;
user: string;
amount: number;
};
export type anivTimeoutRecord = {
id?: string;
message: string;
user: string;
duration: number;
};
export type getLootRecord = {
id?: string;
user: string;
qbucks: number;
items: inventory;
};
interface TypedPocketBase extends PocketBase {
collection(idOrName: 'auth'): RecordService<authRecord>;
collection(idOrName: 'users'): RecordService<userRecord>;
collection(idOrName: 'usedItems'): RecordService<usedItemRecord>;
collection(idOrName: 'timeouts'): RecordService<timeoutRecord>;
collection(idOrName: 'cheerEvents'): RecordService<cheerEventRecord>;
collection(idOrName: 'cheers'): RecordService<cheerRecord>;
collection(idOrName: 'anivTimeouts'): RecordService<anivTimeoutRecord>;
collection(idOrName: 'getLoots'): RecordService<getLootRecord>;
};
export default new PocketBase(pocketbaseurl).autoCancellation(false) as TypedPocketBase;

View File

@@ -1,14 +1,13 @@
import pocketbase from "db/connection"; import db from "db/connection";
import User from "user"; import User from "user";
import logger from "lib/logger"; import { anivTimeouts } from "db/schema";
import { type anivBots } from "lib/handleAnivMessage";
const pb = pocketbase.collection('anivTimeouts'); export async function createAnivTimeoutRecord(message: string, anivBot: anivBots, user: User, duration: number) {
await db.insert(anivTimeouts).values({
export async function createAnivTimeoutRecord(message: string, user: User, duration: number) { message,
try { anivBot,
await pb.create({ message, user: user.id, duration }); user: parseInt(user.id),
} catch (e) { duration
logger.err(`Failed to create anivTimeoutRecord: user: ${user.displayName} message: "${message}" duration: ${duration}`); });
logger.err(e as string);
};
}; };

View File

@@ -1,38 +1,28 @@
import type { AccessToken } from "@twurple/auth"; import type { AccessToken } from "@twurple/auth";
import pocketbase, { type authRecord } from "db/connection"; import db from "db/connection";
const pb = pocketbase.collection('auth'); import { auth } from "db/schema";
import { eq } from "drizzle-orm";
export async function createAuthRecord(token: AccessToken, userId: string) { export async function createAuthRecord(token: AccessToken, userId: string) {
try { await db.insert(auth).values({
const data: authRecord = { id: parseInt(userId),
accesstoken: token, accesstoken: token
id: userId });
};
await pb.create(data);
} catch (err) { };
}; };
export async function getAuthRecord(userId: string, requiredIntents: string[]) { export async function getAuthRecord(userId: string, requiredIntents: string[]) {
try { const data = await db.query.auth.findFirst({
const data = await pb.getOne(userId); where: eq(auth.id, parseInt(userId))
if (!requiredIntents.every(intent => data.accesstoken.scope.includes(intent))) return undefined; });
return { accesstoken: data.accesstoken }; if (!data) return undefined;
} catch (err) { if (!requiredIntents.every(intent => data.accesstoken.scope.includes(intent))) return undefined;
return undefined; return { accesstoken: data.accesstoken };
};
}; };
export async function updateAuthRecord(userId: string, newtoken: AccessToken) { export async function updateAuthRecord(userId: string, newtoken: AccessToken) {
try { await db.update(auth).set({ accesstoken: newtoken }).where(eq(auth.id, parseInt(userId)));
const newrecord = {
accesstoken: newtoken,
};
await pb.update(userId, newrecord);
} catch (err) { };
}; };
export async function deleteAuthRecord(userId: string): Promise<void> { export async function deleteAuthRecord(userId: string): Promise<void> {
try { await db.delete(auth).where(eq(auth.id, parseInt(userId)));
await pb.delete(userId);
} catch (err) { };
}; };

View File

@@ -1,26 +1,20 @@
import pocketbase from "db/connection"; import db from "db/connection";
import { cheerEvents } from "db/schema";
import { and, between, eq, SQL } from "drizzle-orm";
import type { items } from "items";
import User from "user"; import User from "user";
import logger from "lib/logger";
const pb = pocketbase.collection('cheerEvents');
export async function createCheerEventRecord(user: User, cheer: string): Promise<void> { export async function createCheerEventRecord(user: User, cheer: items): Promise<void> {
try { await db.insert(cheerEvents).values({ user: parseInt(user.id), event: cheer });
await pb.create({ user: user.id, cheer });
} catch (e) {
logger.err(`Failed to create cheerEvent record in database: user: ${user.id}, cheer: ${cheer}`);
logger.err(e as string);
};
}; };
export async function getCheerEvents(user: User, monthData?: string) { export async function getCheerEvents(user: User, monthData?: string) {
try { let condition: SQL<unknown> | undefined = eq(cheerEvents.user, parseInt(user.id));
const monthquery = monthData ? ` && created~"${monthData}"` : ''; if (monthData) {
const data = await pb.getFullList({ const begin = Date.parse(monthData);
filter: `user="${user.id}"${monthquery}` const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
}); condition = and(condition, between(cheerEvents.created, new Date(begin), new Date(end)));
return data;
} catch (e) {
logger.err(`Failed to get cheerEvents for user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
}; };
const data = await db.select().from(cheerEvents).where(condition);
return data;
}; };

View File

@@ -1,26 +1,19 @@
import pocketbase from "db/connection"; import db from "db/connection";
import { cheers } from "db/schema";
import User from "user"; import User from "user";
import logger from "lib/logger"; import { and, between, eq, SQL } from "drizzle-orm";
const pb = pocketbase.collection('cheers');
export async function createCheerRecord(user: User, amount: number): Promise<void> { export async function createCheerRecord(user: User, amount: number): Promise<void> {
try { await db.insert(cheers).values({ user: parseInt(user.id), amount });
await pb.create({ user: user.id, amount })
} catch (e) {
logger.err(`Failed to create cheer record in database: user: ${user.id}, amount: ${amount}`);
logger.err(e as string);
};
}; };
export async function getCheers(user: User, monthData?: string) { export async function getCheers(user: User, monthData?: string) {
try { let condition: SQL<unknown> | undefined = eq(cheers.user, parseInt(user.id));
const monthquery = monthData ? ` && created~"${monthData}"` : ''; if (monthData) {
const data = await pb.getFullList({ const begin = Date.parse(monthData);
filter: `user="${user.id}"${monthquery}` const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
}); condition = and(condition, between(cheers.created, new Date(begin), new Date(end)));
return data;
} catch (e) {
logger.err(`Failed to get cheers for user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
}; };
const data = await db.select().from(cheers).where(condition);
return data;
}; };

View File

@@ -1,19 +1,12 @@
import pocketbase from "db/connection"; import db from "db/connection";
import { getLoots } from "db/schema";
import type { inventory } from "items"; import type { inventory } from "items";
import logger from "lib/logger";
import type User from "user"; import type User from "user";
const pb = pocketbase.collection('getLoots');
export async function createGetLootRecord(user: User, qbucks: number, inventory: inventory) { export async function createGetLootRecord(user: User, qbucks: number, inventory: inventory) {
try { await db.insert(getLoots).values({
await pb.create({ user: parseInt(user.id),
user: user.id, qbucks: qbucks,
qbucks, items: inventory
items: inventory });
});
} catch (e) {
logger.err(`Failed to create getLoot record for ${user.displayName}: ${inventory}`);
logger.err(e as string);
};
}; };

View File

@@ -1,39 +1,35 @@
import pocketbase from "db/connection"; import db from "db/connection";
import { timeouts } from "db/schema";
import User from "user"; import User from "user";
import logger from "lib/logger"; import type { items } from "items";
const pb = pocketbase.collection('timeouts'); import { and, between, eq, type SQL } from "drizzle-orm";
export async function createTimeoutRecord(user: User, target: User, item: string): Promise<void> { export async function createTimeoutRecord(user: User, target: User, item: items): Promise<void> {
try { await db.insert(timeouts).values({
await pb.create({ user: user.id, target: target.id, item }); user: parseInt(user.id),
} catch (err) { target: parseInt(target.id),
logger.err(`Failed to create timeout record in database: user: ${user.id}, target: ${target.id}, item: ${item}`); item
logger.err(err as string); });
};
}; };
export async function getTimeoutsAsUser(user: User, monthData?: string) { export async function getTimeoutsAsUser(user: User, monthData?: string) {
try { let condition: SQL<unknown> | undefined = eq(timeouts.user, parseInt(user.id));
const monthquery = monthData ? ` && created~"${monthData}"` : ''; if (monthData) {
const data = await pb.getFullList({ const begin = Date.parse(monthData);
filter: `user="${user.id}"${monthquery}` const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
}); condition = and(condition, between(timeouts.created, new Date(begin), new Date(end)));
return data;
} catch (e) {
logger.err(`Failed to get timeouts as user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
}; };
const data = await db.select().from(timeouts).where(condition);
return data;
}; };
export async function getTimeoutsAsTarget(user: User, monthData?: string) { export async function getTimeoutsAsTarget(user: User, monthData?: string) {
try { let condition: SQL<unknown> | undefined = eq(timeouts.target, parseInt(user.id));
const monthquery = monthData ? ` && created~"${monthData}"` : ''; if (monthData) {
const data = await pb.getFullList({ const begin = Date.parse(monthData);
filter: `target="${user.id}"${monthquery}` const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
}); condition = and(condition, between(timeouts.created, new Date(begin), new Date(end)));
return data;
} catch (e) {
logger.err(`Failed to get timeouts as target: ${user.id}, month: ${monthData}`);
logger.err(e as string);
}; };
const data = await db.select().from(timeouts).where(condition);
return data;
}; };

View File

@@ -1,27 +1,20 @@
import pocketbase from "db/connection"; import db from "db/connection";
import { usedItems } from "db/schema";
import User from "user"; import User from "user";
import logger from "lib/logger"; import type { items } from "items";
const pb = pocketbase.collection('usedItems'); import { and, between, eq, type SQL } from "drizzle-orm";
export async function createUsedItemRecord(user: User, item: string): Promise<void> { export async function createUsedItemRecord(user: User, item: items): Promise<void> {
try { await db.insert(usedItems).values({ user: parseInt(user.id), item });
await pb.create({ user: user.id, item });
} catch (err) {
logger.err(`Failed to create usedItem record in database: user: ${user.id}, item: ${item}`);
logger.err(err as string);
};
}; };
export async function getItemsUsed(user: User, monthData?: string) { export async function getItemsUsed(user: User, monthData?: string) {
try { let condition: SQL<unknown> | undefined = eq(usedItems.user, parseInt(user.id));
const monthquery = monthData ? ` && created~"${monthData}"` : ''; if (monthData) {
const data = await pb.getFullList({ const begin = Date.parse(monthData);
filter: `user="${user.id}"${monthquery}` const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
}); condition = and(condition, between(usedItems.created, new Date(begin), new Date(end)));
return data;
} catch (e) {
logger.err(`Failed to get items used for user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
}; };
const data = await db.select().from(usedItems).where(condition);
return data;
}; };

View File

@@ -1,58 +1,92 @@
import pocketbase, { type userRecord } from "db/connection"; import db from "db/connection";
import { emptyInventory, itemarray } from "items"; import { timeouts, users } from "db/schema";
import { itemarray, type inventory } from "items";
import type User from "user"; import type User from "user";
import logger from "lib/logger"; import { count, desc, eq, inArray, sql, ne, between, and, SQL } from "drizzle-orm";
const pb = pocketbase.collection('users');
/** Use this function to both ensure existance and to retreive data */ /** Use this function to both ensure existance and to retreive data */
export async function getUserRecord(user: User): Promise<userRecord> { export async function getUserRecord(user: User) {
try { const data = await db.query.users.findFirst({ where: eq(users.id, parseInt(user.id)) });
const data = await pb.getOne(user.id); if (!data) return createUserRecord(user);
if (Object.keys(data.inventory).sort().toString() !== itemarray.sort().toString()) { // If the items in the user inventory are missing an item. if (Object.keys(data.inventory).sort().toString() !== itemarray.sort().toString()) { // If the items in the user inventory are missing an item.
itemarray.forEach(key => { itemarray.forEach(key => {
if (!(key in data.inventory)) data.inventory[key] = 0; if (!(key in data.inventory)) data.inventory[key] = 0;
}); });
};
return data;
} catch (err) {
// This gets triggered if the user doesn't exist in the database
return await createUserRecord(user);
}; };
};
export async function getAllUserRecords(): Promise<userRecord[]> {
return await pb.getFullList();
};
async function createUserRecord(user: User): Promise<userRecord> {
const data = await pb.create({
id: user.id,
username: user.username,
balance: 0,
inventory: emptyInventory,
lastlootbox: new Date(0).toISOString()
});
return data; return data;
}; };
export async function updateUserRecord(user: User, newData: userRecord): Promise<boolean> { export async function getAllUserRecords() {
try { return await db.select().from(users);
await pb.update(user.id, newData); };
return true;
} catch (err) { async function createUserRecord(user: User) {
logger.err(err as string); return await db.insert(users).values({
return false; id: parseInt(user.id),
}; username: user.username
}).returning().then(a => {
if (!a[0]) throw Error('Something went horribly wrong');
return a[0]
});
};
export type balanceUpdate = { balance: number; };
export type inventoryUpdate = { inventory: inventory; };
type updateUser = balanceUpdate | inventoryUpdate;
export async function updateUserRecord(user: User, newData: updateUser) {
await db.update(users).set(newData).where(eq(users.id, parseInt(user.id)));
return true;
}; };
export async function getBalanceLeaderboard() { export async function getBalanceLeaderboard() {
try { return await db.select().from(users).orderBy(desc(users.balance)).limit(10);
return await pb.getList(1, 10, { sort: '-balance,id' }).then(a => a.items); };
} catch (err) {
logger.err(err as string); export async function getKDLeaderboard(monthData?: string) {
}; let condition: SQL<unknown> | undefined = ne(timeouts.item, 'silverbullet');
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(timeouts.created, new Date(begin), new Date(end)));
};
const usersGotShot = await db.select({
userId: users.id,
amount: count(timeouts.target),
})
.from(users)
.innerJoin(timeouts, eq(users.id, timeouts.target))
.groupBy(users.id)
.having(sql`count(${timeouts.id}) > 5`)
.where(condition);
const usersThatShot = await db.select({
userId: users.id,
amount: count(timeouts.user)
})
.from(users)
.innerJoin(timeouts, eq(users.id, timeouts.user))
.groupBy(users.id)
.where(
and(
condition,
inArray(users.id, usersGotShot.map(a => a.userId))
)
);
const lookup = new Map(usersThatShot.map(a => [a.userId, a.amount]));
const result = usersGotShot.map(user => ({
userId: user.userId,
KD: lookup.get(user.userId)! / user.amount
}));
result.map(user => {
if (isNaN(user.KD)) user.KD = 0;
return user
});
return result;
}; };

122
src/db/schema.ts Normal file
View File

@@ -0,0 +1,122 @@
import type { AccessToken } from "@twurple/auth";
import type { inventory, items } from "items";
import { integer, jsonb, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
import type { anivBots } from "lib/handleAnivMessage";
import { relations } from "drizzle-orm";
export const auth = pgTable('auth', {
id: integer().primaryKey(),
accesstoken: jsonb().$type<AccessToken>().notNull()
});
export const users = pgTable('users', {
id: integer().primaryKey().notNull(),
username: varchar().notNull(),
balance: integer().default(0).notNull(),
inventory: jsonb().$type<inventory>().default({}).notNull(),
lastlootbox: timestamp().default(new Date(0)).notNull()
});
export const usersRelations = relations(users, ({ many }) => ({
timeouts_target: many(timeouts),
timeouts_shooter: many(timeouts),
usedItems: many(usedItems),
cheerEvents: many(cheerEvents),
cheers: many(cheers),
anivTimeouts: many(anivTimeouts),
getLoots: many(getLoots)
}));
export const timeouts = pgTable('timeouts', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
target: integer().notNull().references(() => users.id),
item: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull()
});
export const timeoutsRelations = relations(timeouts, ({ one }) => ({
user: one(users, {
fields: [timeouts.user],
references: [users.id],
relationName: 'shooter'
}),
target: one(users, {
fields: [timeouts.target],
references: [users.id],
relationName: 'target'
})
}))
export const usedItems = pgTable('usedItems', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
item: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull()
});
export const usedItemsRelations = relations(usedItems, ({ one }) => ({
user: one(users, {
fields: [usedItems.user],
references: [users.id]
})
}));
export const cheerEvents = pgTable('cheerEvents', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
event: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull()
});
export const cheerEventsRelations = relations(cheerEvents, ({ one }) => ({
user: one(users, {
fields: [cheerEvents.user],
references: [users.id]
})
}));
export const cheers = pgTable('cheers', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
amount: integer().notNull(),
created: timestamp().defaultNow().notNull()
});
export const cheersRelations = relations(cheers, ({ one }) => ({
user: one(users, {
fields: [cheers.user],
references: [users.id]
})
}));
export const anivTimeouts = pgTable('anivTimeouts', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
message: varchar().notNull(),
anivBot: varchar().$type<anivBots>().notNull(),
duration: integer().notNull(),
created: timestamp().defaultNow().notNull()
});
export const anivTimeoutsRelations = relations(anivTimeouts, ({ one }) => ({
user: one(users, {
fields: [anivTimeouts.user],
references: [users.id]
})
}));
export const getLoots = pgTable('getLoots', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
qbucks: integer().notNull(),
items: jsonb().$type<inventory>().notNull(),
created: timestamp().defaultNow().notNull()
});
export const getLootsRelations = relations(getLoots, ({ one }) => ({
user: one(users, {
fields: [getLoots.user],
references: [users.id]
})
}));

View File

@@ -28,7 +28,7 @@ async function parseChatMessage(msg: EventSubChannelChatMessageEvent) {
// and both are usable to target the same user (id is the same) // and both are usable to target the same user (id is the same)
// The only problem would be if a user changed their name and someone else took their name right after // The only problem would be if a user changed their name and someone else took their name right after
if (msg.chatterId === chatterId) return; if (msg.chatterId === chatterId && chatterId !== streamerId) return;
if (!await redis.exists(`user:${user?.id}:haschatted`) && !msg.sourceMessageId) { if (!await redis.exists(`user:${user?.id}:haschatted`) && !msg.sourceMessageId) {
const message = await sendMessage(`Welcome ${user?.displayName}. Please note: This chat has PvP, if you get timed out that's part of the qwerinope experience. You have 10 minutes of invincibility. A full list of commands and items can be found here: https://github.com/qwerinope/qweribot/#qweribot`); const message = await sendMessage(`Welcome ${user?.displayName}. Please note: This chat has PvP, if you get timed out that's part of the qwerinope experience. You have 10 minutes of invincibility. A full list of commands and items can be found here: https://github.com/qwerinope/qweribot/#qweribot`);
@@ -68,9 +68,7 @@ async function handleChatMessage(msg: EventSubChannelChatMessageEvent, user: Use
}; };
try { try {
await selection.execute(msg, user, { await selection.execute(msg, user, { activation });
activation
});
} }
catch (err) { catch (err) {
logger.err(err as string); logger.err(err as string);
@@ -105,9 +103,11 @@ export async function handleCheer(msg: EventSubChannelChatMessageEvent, bits: nu
if (!selection) return; if (!selection) return;
if (await redis.sismember('disabledcheers', selection.name)) { await sendMessage(`The ${selection.name} cheer is disabled! Sorry!`, msg.messageId); return; }; if (await redis.sismember('disabledcheers', selection.name)) { await sendMessage(`The ${selection.name} cheer is disabled! Sorry!`, msg.messageId); return; };
if (selection.isItem && await isInvuln(user.id) && !streamerUsers.includes(user.id)) { await sendMessage(`${user.displayName} Is no longer an invuln`); await removeInvuln(user.id); };
try { try {
selection.execute(msg, user); await selection.execute(msg, user);
} catch (err) { } catch (err) {
await sendMessage(`[ERROR]: Something went wrong with cheer execution`);
logger.err(err as string); logger.err(err as string);
}; };
}; };

View File

@@ -16,17 +16,20 @@ export default new Item({
plural: 's', plural: 's',
description: 'Times a specific person out for 60 seconds', description: 'Times a specific person out for 60 seconds',
aliases: ['blaster', 'blast'], aliases: ['blaster', 'blast'],
price: 100,
execution: async (msg, user) => { execution: async (msg, user) => {
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any blasters!`, msg.messageId); return; };
const messagequery = parseCommandArgs(msg.messageText); const messagequery = parseCommandArgs(msg.messageText);
if (!messagequery[0]) { await sendMessage('Please specify a target'); return; }; if (!messagequery[0]) { await sendMessage('Please specify a target'); return; };
const target = await User.initUsername(messagequery[0].toLowerCase()); const target = await User.initUsername(messagequery[0].toLowerCase());
if (!target) { await sendMessage(`${messagequery[0]} doesn't exist`); return; }; if (!target) { await sendMessage(`${messagequery[0]} doesn't exist`); return; };
await getUserRecord(target); // make sure the user record exist in the database await getUserRecord(target); // make sure the user record exist in the database
if (await user.itemLock()) { await sendMessage('Cannot use an item right now', msg.messageId); return; }; if (await user.itemLock()) { await sendMessage('Cannot use an item (itemlock)', msg.messageId); return; };
await user.setLock(); await user.setLock();
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any blasters!`, msg.messageId); await user.clearLock(); return; };
const result = await timeout(target, `You got blasted by ${user.displayName}!`, 60); const result = await timeout(target, `You got blasted by ${user.displayName}!`, 60);
if (result.status) await Promise.all([ if (result.status) await Promise.all([
sendMessage(`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`), sendMessage(`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`),

View File

@@ -16,9 +16,8 @@ export default new Item({
plural: 's', plural: 's',
description: 'Give a random chatter a 60s timeout', description: 'Give a random chatter a 60s timeout',
aliases: ['grenade'], aliases: ['grenade'],
price: 99,
execution: async (msg, user) => { execution: async (msg, user) => {
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any grenades!`, msg.messageId); return; };
const targets = await redis.keys(`user:*:vulnerable`); const targets = await redis.keys(`user:*:vulnerable`);
if (targets.length === 0) { await sendMessage('No vulnerable chatters to blow up', msg.messageId); return; }; if (targets.length === 0) { await sendMessage('No vulnerable chatters to blow up', msg.messageId); return; };
const selection = targets[Math.floor(Math.random() * targets.length)]!; const selection = targets[Math.floor(Math.random() * targets.length)]!;
@@ -26,8 +25,12 @@ export default new Item({
await getUserRecord(target!); // make sure the user record exist in the database await getUserRecord(target!); // make sure the user record exist in the database
if (await user.itemLock()) { await sendMessage('Cannot use an item right now', msg.messageId); return; }; if (await user.itemLock()) { await sendMessage('Cannot use an item (itemlock)', msg.messageId); return; };
await user.setLock(); await user.setLock();
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any grenades!`, msg.messageId); await user.clearLock(); return; };
await Promise.all([ await Promise.all([
timeout(target!, `You got hit by ${user.displayName}'s grenade!`, 60), timeout(target!, `You got hit by ${user.displayName}'s grenade!`, 60),
sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s grenade wybuh`), sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s grenade wybuh`),

View File

@@ -3,29 +3,31 @@ import User from "user";
import { type userType, type specialExecuteArgs } from "commands"; import { type userType, type specialExecuteArgs } from "commands";
type itemOptions = { type itemOptions = {
name: string; name: items;
aliases: string[]; aliases: string[];
prettyName: string; prettyName: string;
plural: string; plural: string;
description: string; description: string;
execution: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>; execution: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>;
specialaliases?: string[]; specialaliases?: string[];
price: number;
}; };
export class Item { export class Item {
public readonly name: string; public readonly name: items;
public readonly prettyName: string; public readonly prettyName: string;
public readonly plural: string; public readonly plural: string;
public readonly description: string; public readonly description: string;
public readonly aliases: string[]; public readonly aliases: string[];
public readonly specialaliases: string[]; public readonly specialaliases: string[];
public readonly usertype: userType; public readonly usertype: userType;
public readonly price: number;
public readonly execute: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>; public readonly execute: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>;
public readonly disableable: boolean; public readonly disableable: boolean;
/** Creates an item object */ /** Creates an item object */
constructor(options: itemOptions) { constructor(options: itemOptions) {
this.name = options.name.toLowerCase(); this.name = options.name;
this.prettyName = options.prettyName; this.prettyName = options.prettyName;
this.plural = options.plural; this.plural = options.plural;
this.description = options.description; this.description = options.description;
@@ -34,16 +36,17 @@ export class Item {
this.execute = options.execution; this.execute = options.execution;
this.disableable = true; this.disableable = true;
this.specialaliases = options.specialaliases ?? []; this.specialaliases = options.specialaliases ?? [];
this.price = options.price;
}; };
}; };
import { readdir } from 'node:fs/promises'; import { readdir } from 'node:fs/promises';
import type { userRecord } from "db/connection"; import { updateUserRecord, type inventoryUpdate } from "db/dbUser";
import { updateUserRecord } from "db/dbUser"; const itemAliasMap = new Map<string, Item>;
const items = new Map<string, Item>; const itemObjectArray: Item[] = []
const specialAliasItems = new Map<string, Item>; const specialAliasItems = new Map<string, Item>;
const emptyInventory: inventory = {}; const emptyInventory: inventory = {};
const itemarray: string[] = []; const itemarray: items[] = [];
const files = await readdir(import.meta.dir); const files = await readdir(import.meta.dir);
for (const file of files) { for (const file of files) {
@@ -52,21 +55,24 @@ for (const file of files) {
const item: Item = await import(import.meta.dir + '/' + file.slice(0, -3)).then(a => a.default); const item: Item = await import(import.meta.dir + '/' + file.slice(0, -3)).then(a => a.default);
emptyInventory[item.name] = 0; emptyInventory[item.name] = 0;
itemarray.push(item.name); itemarray.push(item.name);
itemObjectArray.push(item);
for (const alias of item.aliases) { for (const alias of item.aliases) {
items.set(alias, item); // Since it's not a primitive type the map is filled with references to the item, not the actual object itemAliasMap.set(alias, item); // Since it's not a primitive type the map is filled with references to the item, not the actual object
}; };
for (const alias of item.specialaliases) { for (const alias of item.specialaliases) {
specialAliasItems.set(alias, item); specialAliasItems.set(alias, item);
}; };
}; };
export default items; export default itemAliasMap;
export { emptyInventory, itemarray, specialAliasItems }; export { emptyInventory, itemarray, specialAliasItems, itemObjectArray };
export type items = "blaster" | "silverbullet" | "grenade" | "tnt";
export type inventory = { export type inventory = {
[key: string]: number; [key in items]?: number;
}; };
export async function changeItemCount(user: User, userRecord: userRecord, itemname: string, amount = -1): Promise<false | userRecord> { export async function changeItemCount(user: User, userRecord: inventoryUpdate, itemname: items, amount = -1): Promise<false | inventoryUpdate> {
userRecord.inventory[itemname] = userRecord.inventory[itemname]! += amount; userRecord.inventory[itemname] = userRecord.inventory[itemname]! += amount;
if (userRecord.inventory[itemname] < 0) return false; if (userRecord.inventory[itemname] < 0) return false;
await updateUserRecord(user, userRecord); await updateUserRecord(user, userRecord);

View File

@@ -17,17 +17,20 @@ export default new Item({
description: 'Times a specific person out for 24 hours', description: 'Times a specific person out for 24 hours',
aliases: ['execute', 'silverbullet'], aliases: ['execute', 'silverbullet'],
specialaliases: ['blastin'], specialaliases: ['blastin'],
price: 6666,
execution: async (msg, user, specialargs) => { execution: async (msg, user, specialargs) => {
const userObj = await getUserRecord(user); const messagequery = parseCommandArgs(msg.messageText, specialargs?.activation);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any silver bullets!`, msg.messageId); return; };
const messagequery = parseCommandArgs(msg.messageText);
if (!messagequery[0]) { await sendMessage('Please specify a target'); return; }; if (!messagequery[0]) { await sendMessage('Please specify a target'); return; };
const target = await User.initUsername(messagequery[0].toLowerCase()); const target = await User.initUsername(messagequery[0].toLowerCase());
if (!target) { await sendMessage(`${messagequery[0]} doesn't exist`); return; }; if (!target) { await sendMessage(`${messagequery[0]} doesn't exist`); return; };
await getUserRecord(target); // make sure the user record exist in the database await getUserRecord(target); // make sure the user record exist in the database
if (await user.itemLock()) { await sendMessage('Cannot use an item right now', msg.messageId); return; }; if (await user.itemLock()) { await sendMessage('Cannot use an item (itemlock)', msg.messageId); return; };
await user.setLock(); await user.setLock();
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any silver bullets!`, msg.messageId); await user.clearLock(); return; };
const result = await timeout(target, `You got blasted by ${user.displayName}!`, 60 * 60 * 24); const result = await timeout(target, `You got blasted by ${user.displayName}!`, 60 * 60 * 24);
if (result.status) await Promise.all([ if (result.status) await Promise.all([
sendMessage(`${target.displayName} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`), sendMessage(`${target.displayName} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`),

View File

@@ -16,16 +16,18 @@ export default new Item({
plural: 's', plural: 's',
description: 'Give 5-10 random chatters 60 second timeouts', description: 'Give 5-10 random chatters 60 second timeouts',
aliases: ['tnt'], aliases: ['tnt'],
price: 1000,
execution: async (msg, user) => { execution: async (msg, user) => {
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any TNTs!`, msg.messageId); return; };
const vulntargets = await redis.keys('user:*:vulnerable').then(a => a.map(b => b.slice(5, -11))); const vulntargets = await redis.keys('user:*:vulnerable').then(a => a.map(b => b.slice(5, -11)));
if (vulntargets.length === 0) { await sendMessage('No vulnerable chatters to blow up', msg.messageId); return; }; if (vulntargets.length === 0) { await sendMessage('No vulnerable chatters to blow up', msg.messageId); return; };
const targets = getTNTTargets(vulntargets); const targets = getTNTTargets(vulntargets);
if (await user.itemLock()) { await sendMessage('Cannot use an item right now', msg.messageId); return; }; if (await user.itemLock()) { await sendMessage('Cannot use an item (itemlock)', msg.messageId); return; };
await user.setLock(); await user.setLock();
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any TNTs!`, msg.messageId); await user.clearLock(); return; };
await Promise.all(targets.map(async targetid => { await Promise.all(targets.map(async targetid => {
const target = await User.initUserId(targetid); const target = await User.initUserId(targetid);
await getUserRecord(target!); // make sure the user record exist in the database await getUserRecord(target!); // make sure the user record exist in the database
@@ -45,6 +47,7 @@ export default new Item({
}), }),
changeItemCount(user, userObj, ITEMNAME) changeItemCount(user, userObj, ITEMNAME)
]); ]);
await user.clearLock(); await user.clearLock();
await sendMessage(`RIPBOZO ${user.displayName} exploded ${targets.length} chatter${targets.length === 1 ? '' : 's'} with their TNT RIPBOZO`); await sendMessage(`RIPBOZO ${user.displayName} exploded ${targets.length} chatter${targets.length === 1 ? '' : 's'} with their TNT RIPBOZO`);
} }

View File

@@ -40,7 +40,12 @@ export async function getItemStats(target: User, thismonth: boolean) {
]); ]);
if (!items || !cheers) return; if (!items || !cheers) return;
const returnObj: inventory = {}; const returnObj: inventory = {
blaster: 0,
silverbullet: 0,
grenade: 0,
tnt: 0,
};
for (const item of items) { for (const item of items) {
if (!returnObj[item.item]) returnObj[item.item] = 0; if (!returnObj[item.item]) returnObj[item.item] = 0;
@@ -48,8 +53,8 @@ export async function getItemStats(target: User, thismonth: boolean) {
}; };
for (const cheer of cheers) { for (const cheer of cheers) {
if (!returnObj[cheer.cheer]) returnObj[cheer.cheer] = 0; if (!returnObj[cheer.event]) returnObj[cheer.event] = 0;
returnObj[cheer.cheer]! += 1 returnObj[cheer.event]! += 1
}; };
return returnObj; return returnObj;

View File

@@ -5,7 +5,7 @@ import { timeout } from "lib/timeout";
import { sendMessage } from "commands"; import { sendMessage } from "commands";
import { createAnivTimeoutRecord } from "db/dbAnivTimeouts"; import { createAnivTimeoutRecord } from "db/dbAnivTimeouts";
const ANIVNAMES = ['a_n_e_e_v', 'a_n_i_v']; const ANIVNAMES: anivBots[] = ['a_n_e_e_v', 'a_n_i_v'];
type anivMessageStore = { type anivMessageStore = {
[key: string]: string; [key: string]: string;
@@ -14,13 +14,15 @@ type anivMessageStore = {
type IsAnivMessage = { type IsAnivMessage = {
isAnivMessage: true; isAnivMessage: true;
message: string; message: string;
anivbot: string; anivbot: anivBots;
}; };
type isNotAnivMessage = { type isNotAnivMessage = {
isAnivMessage: false; isAnivMessage: false;
}; };
export type anivBots = 'a_n_i_v' | 'a_n_e_e_v';
type anivMessageResult = IsAnivMessage | isNotAnivMessage; type anivMessageResult = IsAnivMessage | isNotAnivMessage;
async function isAnivMessage(message: string): Promise<anivMessageResult> { async function isAnivMessage(message: string): Promise<anivMessageResult> {
@@ -34,7 +36,7 @@ async function isAnivMessage(message: string): Promise<anivMessageResult> {
}; };
export default async function handleMessage(msg: EventSubChannelChatMessageEvent, user: User) { export default async function handleMessage(msg: EventSubChannelChatMessageEvent, user: User) {
if (ANIVNAMES.includes(user.displayName)) { if (ANIVNAMES.map(a => a.toLowerCase()).includes(user.username)) {
const data: anivMessageStore = await redis.get('anivmessages').then(a => a === null ? {} : JSON.parse(a)); const data: anivMessageStore = await redis.get('anivmessages').then(a => a === null ? {} : JSON.parse(a));
data[user.displayName] = msg.messageText; data[user.displayName] = msg.messageText;
await redis.set('anivmessages', JSON.stringify(data)); await redis.set('anivmessages', JSON.stringify(data));
@@ -43,7 +45,7 @@ export default async function handleMessage(msg: EventSubChannelChatMessageEvent
if (data.isAnivMessage) await Promise.all([ if (data.isAnivMessage) await Promise.all([
timeout(user, 'copied an aniv message', 30), timeout(user, 'copied an aniv message', 30),
sendMessage(`${user.displayName} got timed out for copying an ${data.anivbot} message`), sendMessage(`${user.displayName} got timed out for copying an ${data.anivbot} message`),
createAnivTimeoutRecord(msg.messageText, user, 30) createAnivTimeoutRecord(msg.messageText, data.anivbot, user, 30)
]); ]);
}; };
}; };