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_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
# POCKETBASE_URL= # Pocketbase URL. Defaults to http://localhost:8090
# Postgres config
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_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.local
# Drizzle config files
*.config.ts
# caches
.eslintcache
.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.
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).
When an Item is used it is removed from the inventory of the chatter.
@@ -78,10 +80,10 @@ ITEM|RATE
`grenade`|`1/5`
`blaster`|`1/5`
`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.
It's theoretically possible to get 5 of each item.
Each of these rates get pulled 3 times, then the result is added to your inventory.
It's theoretically possible to get 3 of each item.
### Chatterbot/streamerbot
@@ -104,24 +106,25 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
`seiso`|Get a seiso rating|anyone|`seiso`|: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:
`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:
`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
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:
`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:
`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:
`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:
`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:
`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
NAME|COMMAND|FUNCTION|ALIASES
-|-|-|-
Blaster|`blaster {target}`|Times targeted user out for 60 seconds|`blaster` `blast`
Silver Bullet|`silverbullet {target}`|Times targeted user out for 24 hours|`silverbullet` `execute` `{blastin}`
Grenade|`grenade`|Times a random vulnerable chatter out for 60 seconds|`grenade`
TNT|`tnt`|Give 5-10 random chatters 60 second timeouts|`tnt`
NAME|COMMAND|FUNCTION|ALIASES|COST
-|-|-|-|-
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}`|6666
Grenade|`grenade`|Times a random vulnerable chatter out for 60 seconds|`grenade`|99
TNT|`tnt`|Give 5-10 random chatters 60 second timeouts|`tnt`|1000
## Cheers

158
bun.lock
View File

@@ -7,11 +7,13 @@
"@fontsource/jersey-15": "^5.2.6",
"@twurple/auth": "^7.3.0",
"@twurple/eventsub-ws": "^7.3.0",
"drizzle-orm": "^0.44.5",
"kleur": "^4.1.5",
"pocketbase": "^0.26.1",
},
"devDependencies": {
"@types/bun": "latest",
"drizzle-kit": "^0.31.4",
"pg": "^8.16.3",
},
"peerDependencies": {
"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=="],
"@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=="],
"@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=="],
"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=="],
"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=="],
"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=="],
"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=="],
"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=="],
"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=="],
"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:
valkey:
image: valkey/valkey:alpine
container_name: valkey
container_name: qweribot-valkey
ports:
- 6379:6379
restart: no
@@ -9,12 +9,13 @@ services:
- ./db/redis:/data
environment:
- VALKEY_EXTRA_FLAGS=--save 60 1
pocketbase:
container_name: qweribot-pocketbase
build:
context: ./pocketbase
postgres:
container_name: qweribot-postgres
image: postgres:latest
restart: unless-stopped
env_file:
- .env
ports:
- 8090:8090
restart: no
- "5432:5432"
volumes:
- ./db/pocketbase:/pb/pb_data
- ./db/postgresql:/var/lib/postgresql/data

View File

@@ -2,7 +2,17 @@
"name": "qweribot",
"module": "src/index.ts",
"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": {
"typescript": "^5.8.3"
@@ -13,7 +23,7 @@
"@fontsource/jersey-15": "^5.2.6",
"@twurple/auth": "^7.3.0",
"@twurple/eventsub-ws": "^7.3.0",
"kleur": "^4.1.5",
"pocketbase": "^0.26.1"
"drizzle-orm": "^0.44.5",
"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;
};
};
});
}, true);

View File

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

View File

@@ -5,10 +5,12 @@ export class Cheer {
public readonly name: string;
public readonly amount: number;
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.amount = amount;
this.execute = execution;
this.isItem = isItem;
};
};
@@ -29,14 +31,12 @@ export default cheers;
export { namedcheers };
import { sendMessage } from 'commands';
import logger from 'lib/logger';
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()) {
await sendMessage(`Cannot give ${user.displayName} a ${itemname}`, msg.messageId);
logger.err(`Failed to give ${user.displayName} a ${itemname} for their cheer`);
await sendMessage(`Cannot give ${user.displayName} a ${itemname} (itemlock)`, msg.messageId);
return;
};
await user.setLock();

View File

@@ -46,4 +46,4 @@ export default new Cheer('timeout', 100, async (msg, user) => {
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),
redis.del(`user:${targetid}:vulnerable`),
sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s TNT wybuh`),
createTimeoutRecord(user, target!, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
createTimeoutRecord(user, target!, ITEMNAME)
]);
}));
await playAlert({
name: 'tntExplosion',
user: user.displayName,
targets
})
await Promise.all([
createCheerEventRecord(user, ITEMNAME),
playAlert({
name: 'tntExplosion',
user: user.displayName,
targets
})
]);
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);
if (!args[1]) { await sendMessage('Please specify the amount qweribucks you want to give', msg.messageId); return; };
const amount = parseInt(args[1]);
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 (isNaN(amount)) { await sendMessage(`'${args[1]}' is not a valid amount`); return; };
if (await target.itemLock()) { await sendMessage('Cannot give qweribucks (itemlock)', msg.messageId); return; };
await target.setLock();
const data = await changeBalance(target, userRecord, amount);
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 (!args[2]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; };
const amount = parseInt(args[2]);
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 (isNaN(amount)) { await sendMessage(`'${args[2]}' is not a valid amount`); return; };
if (await target.itemLock()) { await sendMessage('Cannot give item (itemlock)', msg.messageId); return; };
await target.setLock();
const data = await changeItemCount(target, userRecord, item.name, amount);
if (data) {

View File

@@ -1,6 +1,5 @@
import { Command, sendMessage } from "commands";
import { getAllUserRecords } from "db/dbUser";
import { getTimeoutStats } from "lib/getStats";
import { getKDLeaderboard } from "db/dbUser";
import User from "user";
type KD = { user: User; kd: number; };
@@ -10,27 +9,19 @@ export default new Command({
aliases: ['alltimeleaderboard', 'alltimekdleaderboard'],
usertype: 'chatter',
execution: async msg => {
const users = await getAllUserRecords();
if (!users) return;
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) {
const rawKD = await getKDLeaderboard();
if (rawKD.length === 0) {
await sendMessage(`No users on leaderboard yet!`, msg.messageId);
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);
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 type { userRecord } from "db/connection";
import { getUserRecord } from "db/dbUser";
import parseCommandArgs from "lib/parseCommandArgs";
import { changeBalance } from "lib/changeBalance";
@@ -19,12 +18,12 @@ export default new Command({
const targetRecord = await getUserRecord(target);
if (!args[1]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; };
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);
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([
user.setLock(),
@@ -37,7 +36,7 @@ export default new Command({
]);
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);
} else {
// TODO: Rewrite this section

View File

@@ -8,7 +8,7 @@ export default new Command({
execution: async (_msg, user) => {
await Promise.all([
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 { Command, sendMessage } from "commands";
import { getUserRecord, updateUserRecord } from "db/dbUser";
import items from "items";
import itemMap, { type inventory, type items } from "items";
import { buildTimeString } from "lib/dateManager";
import { timeout } from "lib/timeout";
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 user.itemLock()) { await sendMessage(`Cannot get loot (itemlock)`, msg.messageId); return; };
const userData = await getUserRecord(user);
const lastlootbox = Date.parse(userData.lastlootbox);
const lastlootbox = userData.lastlootbox.getTime();
const now = Date.now();
if ((lastlootbox + COOLDOWN) > now) {
if (await user.greedy()) {
@@ -40,26 +40,25 @@ export default new Command({
await user.clearGreed();
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
userData.balance += gainedqbucks;
const itemDiff = {
const itemDiff: inventory = {
grenade: 0,
blaster: 0,
tnt: 0,
silverbullet: 0
};
for (let i = 0; i < 5; i++) {
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() * 25) === 0) itemDiff.tnt += 1;
if (Math.floor(Math.random() * 250) === 0) itemDiff.silverbullet += 1;
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.blaster! += 1;
if (Math.floor(Math.random() * 25) === 0) itemDiff.tnt! += 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;
else userData.inventory[item] = amount;
};
@@ -68,7 +67,7 @@ export default new Command({
for (const [item, amount] of Object.entries(itemDiff)) {
if (amount === 0) continue;
const selection = items.get(item);
const selection = itemMap.get(item);
if (!selection) continue;
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 type { userRecord } from "db/connection";
import { getUserRecord } from "db/dbUser";
import items, { changeItemCount } from "items";
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 (!args[2]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; };
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);
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([
user.setLock(),
@@ -38,14 +37,14 @@ export default new Command({
await changeItemCount(user, userRecord, item.name, -amount)
]);
if (!data.includes(false)) {
const tempdata = data[0] as userRecord;
if (data[0] !== false && data[1] !== false) {
const tempdata = data[0];
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);
} else {
// TODO: Rewrite this section
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 target.clearLock();

View File

@@ -1,6 +1,5 @@
import { Command, sendMessage } from "commands";
import { getAllUserRecords } from "db/dbUser";
import { getTimeoutStats } from "lib/getStats";
import { getKDLeaderboard } from "db/dbUser";
import User from "user";
type KD = { user: User; kd: number; };
@@ -10,27 +9,21 @@ export default new Command({
aliases: ['monthlyleaderboard', 'kdleaderboard', 'leaderboard'],
usertype: 'chatter',
execution: async msg => {
const users = await getAllUserRecords();
if (!users) return;
const monthdata = new Date().toISOString().slice(0, 7);
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, 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) {
const rawKD = await getKDLeaderboard(monthdata);
if (rawKD.length === 0) {
await sendMessage(`No users on leaderboard yet!`, msg.messageId);
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);
const txt: string[] = [];

View File

@@ -14,7 +14,7 @@ export default new Command({
const txt: string[] = [];
for (const userRecord of data) {
if (userRecord.balance === 0) continue;
const user = await User.initUserId(userRecord.id);
const user = await User.initUserId(userRecord.id.toString());
if (!user) continue;
txt.push(`${index}. ${user.displayName}: ${userRecord.balance}`);
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 db from "db/connection";
import { users } from "db/schema";
import logger from "lib/logger";
export async function connectionCheck() {
let pbstatus = false;
let pgstatus = false;
try {
await pocketbase.health.check().then(a => a.code === 200);
pbstatus = true;
await db.select().from(users); // The query doesn't matter, only that it fails. This also fails if the migration hasn't taken place
pgstatus = true;
} catch { };
const tempclient = new RedisClient(undefined, {
connectionTimeout: 100,
@@ -18,7 +19,7 @@ export async function connectionCheck() {
redisstatus = true;
} catch { };
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`);
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 PocketBase, { RecordService } from "pocketbase";
import type { inventory } from "items";
import * as schema from "db/schema";
import logger from "lib/logger";
const pocketbaseurl = process.env.POCKETBASE_URL ?? "localhost:8090";
if (pocketbaseurl === "") { logger.enverr("POCKETBASE_URL"); process.exit(1); };
const host = process.env.POSTGRES_HOST ?? "";
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 = {
id: string;
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;
import { drizzle } from 'drizzle-orm/bun-sql';
export default drizzle(url, { schema });

View File

@@ -1,14 +1,13 @@
import pocketbase from "db/connection";
import db from "db/connection";
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, user: User, duration: number) {
try {
await pb.create({ message, user: user.id, duration });
} catch (e) {
logger.err(`Failed to create anivTimeoutRecord: user: ${user.displayName} message: "${message}" duration: ${duration}`);
logger.err(e as string);
};
export async function createAnivTimeoutRecord(message: string, anivBot: anivBots, user: User, duration: number) {
await db.insert(anivTimeouts).values({
message,
anivBot,
user: parseInt(user.id),
duration
});
};

View File

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

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 logger from "lib/logger";
const pb = pocketbase.collection('cheerEvents');
export async function createCheerEventRecord(user: User, cheer: string): Promise<void> {
try {
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 createCheerEventRecord(user: User, cheer: items): Promise<void> {
await db.insert(cheerEvents).values({ user: parseInt(user.id), event: cheer });
};
export async function getCheerEvents(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `user="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get cheerEvents for user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(cheerEvents.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(cheerEvents.created, new Date(begin), new Date(end)));
};
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 logger from "lib/logger";
const pb = pocketbase.collection('cheers');
import { and, between, eq, SQL } from "drizzle-orm";
export async function createCheerRecord(user: User, amount: number): Promise<void> {
try {
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);
};
await db.insert(cheers).values({ user: parseInt(user.id), amount });
};
export async function getCheers(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `user="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get cheers for user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(cheers.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(cheers.created, new Date(begin), new Date(end)));
};
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 logger from "lib/logger";
import type User from "user";
const pb = pocketbase.collection('getLoots');
export async function createGetLootRecord(user: User, qbucks: number, inventory: inventory) {
try {
await pb.create({
user: user.id,
qbucks,
items: inventory
});
} catch (e) {
logger.err(`Failed to create getLoot record for ${user.displayName}: ${inventory}`);
logger.err(e as string);
};
await db.insert(getLoots).values({
user: parseInt(user.id),
qbucks: qbucks,
items: inventory
});
};

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 logger from "lib/logger";
const pb = pocketbase.collection('timeouts');
import type { items } from "items";
import { and, between, eq, type SQL } from "drizzle-orm";
export async function createTimeoutRecord(user: User, target: User, item: string): Promise<void> {
try {
await pb.create({ user: user.id, target: target.id, item });
} catch (err) {
logger.err(`Failed to create timeout record in database: user: ${user.id}, target: ${target.id}, item: ${item}`);
logger.err(err as string);
};
export async function createTimeoutRecord(user: User, target: User, item: items): Promise<void> {
await db.insert(timeouts).values({
user: parseInt(user.id),
target: parseInt(target.id),
item
});
};
export async function getTimeoutsAsUser(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `user="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get timeouts as user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(timeouts.user, parseInt(user.id));
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 data = await db.select().from(timeouts).where(condition);
return data;
};
export async function getTimeoutsAsTarget(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `target="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get timeouts as target: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(timeouts.target, parseInt(user.id));
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 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 logger from "lib/logger";
const pb = pocketbase.collection('usedItems');
import type { items } from "items";
import { and, between, eq, type SQL } from "drizzle-orm";
export async function createUsedItemRecord(user: User, item: string): Promise<void> {
try {
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 createUsedItemRecord(user: User, item: items): Promise<void> {
await db.insert(usedItems).values({ user: parseInt(user.id), item });
};
export async function getItemsUsed(user: User, monthData?: string) {
try {
const monthquery = monthData ? ` && created~"${monthData}"` : '';
const data = await pb.getFullList({
filter: `user="${user.id}"${monthquery}`
});
return data;
} catch (e) {
logger.err(`Failed to get items used for user: ${user.id}, month: ${monthData}`);
logger.err(e as string);
let condition: SQL<unknown> | undefined = eq(usedItems.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(usedItems.created, new Date(begin), new Date(end)));
};
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 { emptyInventory, itemarray } from "items";
import db from "db/connection";
import { timeouts, users } from "db/schema";
import { itemarray, type inventory } from "items";
import type User from "user";
import logger from "lib/logger";
const pb = pocketbase.collection('users');
import { count, desc, eq, inArray, sql, ne, between, and, SQL } from "drizzle-orm";
/** Use this function to both ensure existance and to retreive data */
export async function getUserRecord(user: User): Promise<userRecord> {
try {
const data = await pb.getOne(user.id);
export async function getUserRecord(user: User) {
const data = await db.query.users.findFirst({ where: eq(users.id, parseInt(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.
itemarray.forEach(key => {
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);
if (Object.keys(data.inventory).sort().toString() !== itemarray.sort().toString()) { // If the items in the user inventory are missing an item.
itemarray.forEach(key => {
if (!(key in data.inventory)) data.inventory[key] = 0;
});
};
};
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;
};
export async function updateUserRecord(user: User, newData: userRecord): Promise<boolean> {
try {
await pb.update(user.id, newData);
return true;
} catch (err) {
logger.err(err as string);
return false;
};
export async function getAllUserRecords() {
return await db.select().from(users);
};
async function createUserRecord(user: User) {
return await db.insert(users).values({
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() {
try {
return await pb.getList(1, 10, { sort: '-balance,id' }).then(a => a.items);
} catch (err) {
logger.err(err as string);
};
return await db.select().from(users).orderBy(desc(users.balance)).limit(10);
};
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)
// 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) {
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 {
await selection.execute(msg, user, {
activation
});
await selection.execute(msg, user, { activation });
}
catch (err) {
logger.err(err as string);
@@ -105,9 +103,11 @@ export async function handleCheer(msg: EventSubChannelChatMessageEvent, bits: nu
if (!selection) 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 {
selection.execute(msg, user);
await selection.execute(msg, user);
} catch (err) {
await sendMessage(`[ERROR]: Something went wrong with cheer execution`);
logger.err(err as string);
};
};

View File

@@ -16,17 +16,20 @@ export default new Item({
plural: 's',
description: 'Times a specific person out for 60 seconds',
aliases: ['blaster', 'blast'],
price: 100,
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);
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
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();
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);
if (result.status) await Promise.all([
sendMessage(`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`),

View File

@@ -16,9 +16,8 @@ export default new Item({
plural: 's',
description: 'Give a random chatter a 60s timeout',
aliases: ['grenade'],
price: 99,
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`);
if (targets.length === 0) { await sendMessage('No vulnerable chatters to blow up', msg.messageId); return; };
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
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();
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([
timeout(target!, `You got hit by ${user.displayName}'s grenade!`, 60),
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";
type itemOptions = {
name: string;
name: items;
aliases: string[];
prettyName: string;
plural: string;
description: string;
execution: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>;
specialaliases?: string[];
price: number;
};
export class Item {
public readonly name: string;
public readonly name: items;
public readonly prettyName: string;
public readonly plural: string;
public readonly description: string;
public readonly aliases: string[];
public readonly specialaliases: string[];
public readonly usertype: userType;
public readonly price: number;
public readonly execute: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>;
public readonly disableable: boolean;
/** Creates an item object */
constructor(options: itemOptions) {
this.name = options.name.toLowerCase();
this.name = options.name;
this.prettyName = options.prettyName;
this.plural = options.plural;
this.description = options.description;
@@ -34,16 +36,17 @@ export class Item {
this.execute = options.execution;
this.disableable = true;
this.specialaliases = options.specialaliases ?? [];
this.price = options.price;
};
};
import { readdir } from 'node:fs/promises';
import type { userRecord } from "db/connection";
import { updateUserRecord } from "db/dbUser";
const items = new Map<string, Item>;
import { updateUserRecord, type inventoryUpdate } from "db/dbUser";
const itemAliasMap = new Map<string, Item>;
const itemObjectArray: Item[] = []
const specialAliasItems = new Map<string, Item>;
const emptyInventory: inventory = {};
const itemarray: string[] = [];
const itemarray: items[] = [];
const files = await readdir(import.meta.dir);
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);
emptyInventory[item.name] = 0;
itemarray.push(item.name);
itemObjectArray.push(item);
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) {
specialAliasItems.set(alias, item);
};
};
export default items;
export { emptyInventory, itemarray, specialAliasItems };
export default itemAliasMap;
export { emptyInventory, itemarray, specialAliasItems, itemObjectArray };
export type items = "blaster" | "silverbullet" | "grenade" | "tnt";
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;
if (userRecord.inventory[itemname] < 0) return false;
await updateUserRecord(user, userRecord);

View File

@@ -17,17 +17,20 @@ export default new Item({
description: 'Times a specific person out for 24 hours',
aliases: ['execute', 'silverbullet'],
specialaliases: ['blastin'],
price: 6666,
execution: async (msg, user, specialargs) => {
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any silver bullets!`, msg.messageId); return; };
const messagequery = parseCommandArgs(msg.messageText);
const messagequery = parseCommandArgs(msg.messageText, specialargs?.activation);
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
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();
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);
if (result.status) await Promise.all([
sendMessage(`${target.displayName} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`),

View File

@@ -16,16 +16,18 @@ export default new Item({
plural: 's',
description: 'Give 5-10 random chatters 60 second timeouts',
aliases: ['tnt'],
price: 1000,
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)));
if (vulntargets.length === 0) { await sendMessage('No vulnerable chatters to blow up', msg.messageId); return; };
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();
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 => {
const target = await User.initUserId(targetid);
await getUserRecord(target!); // make sure the user record exist in the database
@@ -45,6 +47,7 @@ export default new Item({
}),
changeItemCount(user, userObj, ITEMNAME)
]);
await user.clearLock();
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;
const returnObj: inventory = {};
const returnObj: inventory = {
blaster: 0,
silverbullet: 0,
grenade: 0,
tnt: 0,
};
for (const item of items) {
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) {
if (!returnObj[cheer.cheer]) returnObj[cheer.cheer] = 0;
returnObj[cheer.cheer]! += 1
if (!returnObj[cheer.event]) returnObj[cheer.event] = 0;
returnObj[cheer.event]! += 1
};
return returnObj;

View File

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