Compare commits

...

24 Commits

Author SHA1 Message Date
b88a93a6cf add events to database, remove kleur dependency/slightly nicer logging, update twurple 2026-01-29 21:06:01 +01:00
f3c6f6a6b3 fix that lootboxes are disabled with use command as well 2026-01-22 00:37:10 +01:00
aa757a563d add economy command (so darkxoa for idea), streamer silverbullet doesn't get stored, minor blaster and silverbullet fixes 2025-12-27 00:56:35 +01:00
5a17e405fa add timeout cleaning at startup 2025-12-24 13:06:55 +01:00
e24e00701c not all redeems get fulfilled instantly 2025-12-22 17:39:14 +01:00
92fe7bb75d fix capitalization in readme 2025-12-21 21:12:46 +01:00
cb08cc0786 finish migrating to gitlab 2025-12-21 21:01:02 +01:00
45e09482be getloot now says if the lootbox was a scambox 2025-12-11 00:51:28 +01:00
64dd8e6dd5 disallow aniv bots from giving items 2025-12-08 18:41:19 +01:00
2bc1d2b953 add color to dectalk redeem 2025-12-08 18:32:17 +01:00
07c619f54d add tts documentation to readme 2025-12-08 18:30:51 +01:00
6a71806881 add dectalk tts for 25k channel points (let the suffering begin) 2025-12-08 18:14:25 +01:00
eb5cca7897 add personalized welcome messages (untested SMILERS) 2025-12-07 23:41:59 +01:00
3aca8a9210 !addinvuln now clears wipes the inventory and wallet of the target 2025-12-06 23:34:13 +01:00
fd0461a30f add @biomejs/biome to devdependencies to fix check script 2025-12-06 04:53:02 +01:00
afbf08f21a fix tsc configs, add realsilverbullets for 6666 bits 2025-12-06 04:51:34 +01:00
c37d2f0a8b add microsoft SAM tts, make aniv messages play tts 2025-12-05 16:04:12 +01:00
fd4afb1530 add fail sound alerts 2025-12-03 04:31:08 +01:00
87a99331a8 item class now extends command class, previously was implicit, now explicit 2025-12-03 02:20:01 +01:00
5ddbad1212 rename alerts/public to alerts/assets 2025-12-02 12:10:31 +01:00
a86ea710cb improve static alert object handling 2025-12-01 20:44:02 +01:00
d501277511 change explosion sounds to factorio sounds 2025-12-01 18:38:06 +01:00
461cefd5e8 add factorio destroy alert to readme 2025-11-27 20:50:42 +01:00
d59bbd3690 add factorio destruction sound alert, add command and cheer parser tests 2025-11-27 20:39:17 +01:00
91 changed files with 1074 additions and 297 deletions

View File

@@ -19,6 +19,7 @@ Invulns don't need moderator or vip status in the channel.
The chatterbot and streamer always are invuln and cannot be stripped of this status.
Moderators can add and remove invulns.
On your first message in chat you will recieve 10 minutes of invuln status.
When a moderator adds you as invuln you will lose all items and qbucks.
### Bots
@@ -121,6 +122,25 @@ The enable and disable redeem commands have a way to enable/disable all sound al
When running the development database and twitch api application, the redeems will not get created. This is because twitch only allows editing or deleting rewards when the same application created the reward. (fucking stupid)
### Welcome message
Welcome message in the context of this project means 2 things:
- The message the bot says when it's the first time you're chatting in the channel.
- The personalized message the bot says when you chat for the first time during a stream.
The personalized message can be set by using the channel point redemption. The message cannot be longer than 200 characters.
### TTS
There are 2 types of TTS supported:
- Microsoft Sam
- DecTalk
Microsoft s(c)am is currently only used by `a_n_i_v` and `a_n_e_e_v`. Whenever the aniv clankers say something MS Sam will pronounce their message.
Dectalk is available for 25k channel points. If your message is longer than 2 minutes your message will get scammed. I'm not sorry, 100% deserved.
### Chatterbot/streamerbot
This depends on if the `CHATTER_IS_STREAMER` environment variable is set.
@@ -150,6 +170,7 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
`anivtimeouts`|Get the amount of timeouts, dodges and dodge percentage from aniv timeouts [(info)](#aniv-timeouts)|anyone|`anivtimeouts` `anivtimeout`|:white_check_mark:
`racetime`|Get the racetime.gg room the streamer is currently in. Needs to have twitch linked to racetime account|anyone|`racetime` `raceroom`|:white_check_mark:
`randomchatter`|Get a random chatter for whatever reason|moderators|`randomchatter`|:white_check_mark:
`economy`|Get a count of all items in circulation|anyone|`economy` `eco`|:white_check_mark:
### Qweribucks/Item commands
@@ -186,7 +207,7 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
`getadmins`|Get a list of every admin in the channel|anyone|`getadmins`|:x:
`itemlock {target}`|Toggle the itemlock on the specified target|moderator|`itemlock`|:x:
`testcheer {amount} [args]`|Create a fake cheering event|streamer/chatterbot|`testcheer`|:x:
`addinvuln {target}`|Adds an invuln user|moderator|`addinvuln`|:x:
`addinvuln {target}`|Adds an invuln user and wipes the user's inventory and wallet|moderator|`addinvuln`|:x:
`removeinvuln {target}`|Removes an invuln user|moderator|`removeinvuln`|:x:
`addbot {target}`|Adds bot status to a specific chatter|streamer/chatterbot|`addbot`|:x:
`removebot {target}`|Removes bot status from a specific chatter|streamer/chatterbot|`removebot`|:x:
@@ -205,7 +226,7 @@ NAME|COMMAND|FUNCTION|ALIASES|COST
-|-|-|-|-
Blaster|`blaster {target}`|Times targeted user out for 60 seconds|`blaster` `blast`|100
Grenade|`grenade`|Times a random vulnerable chatter out for 60 seconds|`grenade`|99
Silver Bullet|`silverbullet [target]`|Times targeted or random vulnerable user out for 30 minutes|`silverbullet` `execute` `{blastin}`|666
Silver Bullet|`silverbullet [target]`|Times targeted or random vulnerable user out for 30 minutes|`silverbullet` `execute` `{blastin}` `{fuck}`|666
TNT|`tnt`|Give 5-10 random chatters 60 second timeouts|`tnt`|1000
## Cheers
@@ -217,12 +238,17 @@ NAME|AMOUNT|USAGE|FUNCTION
`superloot`|150|`cheer150`|Get superloot. Details and drop rates can be found [(here)](#lootbox).
`execute`|666|`cheer666 [target]`|Times specified or random vulnerable user out for 30 minutes. On failure gives cheerer a silver bullet
`tnt`|1000|`cheer1000`|Gives 5-10 random vulnerable chatters 60 second timeouts. On failure gives cheerer a TNT
`realsilverbullet`|6666|`cheer6666 [target]`|Times specified or random vulnerable chatter out for 24 hours. On failure gives the user nothing. Get scammed.
## Point Redeems
NAME|COST|DESCRIPTION|INTERNALNAME
-|-|-|-
`Dectalk TTS`|25000|Play a custom dectalk TTS. If the sound is too long you WILL get scammed.|`dectalk`
`Set welcome message`|15000|Set the message the bot will say when you first chat during a stream (character limit is 200)|`setwelcomemsg`
`FREE MONEY`|1000|Get 100 qbucks|`qbucksredeem`
`RIPBOZO`|500|Sound: Coffeezilla calls me a conman [(source)](https://www.youtube.com/watch?v=QRvEGn7i-wM)|`sfxripbozo`
`Welcome to the Madhouse`|100|Sound: mrockstar20 says: "Welcome to the Madhouse"|`sfxmrockmadhouse`
`Eddie Scream`|100|Sound: Eddie screams|`sfxeddiescream`
`Factorio Building Destroyed`|100|Sound: Factorio Building Destroyed alert|`sfxfactorioalert`
`Fail`|100|Sound: Either the sad trumpet or trombone meme sound|`sfxfail`

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
"$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
@@ -19,7 +19,8 @@
"suspicious": {
"noNonNullAssertedOptionalChain": "off",
"noExplicitAny": "off",
"noControlCharactersInRegex": "off"
"noControlCharactersInRegex": "off",
"noTsIgnore": "off"
},
"style": {
"noNonNullAssertion": "off"

130
bun.lock
View File

@@ -6,39 +6,51 @@
"name": "qweribot",
"dependencies": {
"@fontsource/jersey-15": "^5.2.8",
"@twurple/api": "7.4.0",
"@twurple/auth": "^7.4.0",
"@twurple/eventsub-http": "^7.4.0",
"discord.js": "^14.24.0",
"drizzle-orm": "^0.44.6",
"kleur": "^4.1.5",
"@twurple/api": "8.0.3",
"@twurple/auth": "^8.0.3",
"@twurple/eventsub-http": "^8.0.3",
"discord.js": "^14.25.1",
"drizzle-orm": "^0.45.1",
},
"devDependencies": {
"@twurple/eventsub-ngrok": "^7.4.0",
"@biomejs/biome": "^2.3.13",
"@twurple/eventsub-ngrok": "^8.0.3",
"@types/bun": "latest",
"drizzle-kit": "^0.31.5",
"pg": "^8.16.3",
"drizzle-kit": "^0.31.8",
"pg": "^8.17.2",
},
"peerDependencies": {
"typescript": "^5.8.3",
"typescript": "^5.9.3",
},
},
},
"packages": {
"@d-fischer/cache-decorators": ["@d-fischer/cache-decorators@4.0.1", "", { "dependencies": { "@d-fischer/shared-utils": "^3.6.3", "tslib": "^2.6.2" } }, "sha512-HNYLBLWs/t28GFZZeqdIBqq8f37mqDIFO6xNPof94VjpKvuP6ROqCZGafx88dk5zZUlBfViV9jD8iNNlXfc4CA=="],
"@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="],
"@d-fischer/cross-fetch": ["@d-fischer/cross-fetch@5.0.5", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-symjDUPInTrkfIsZc2n2mo9hiAJLcTJsZkNICjZajEWnWpJ3s3zn50/FY8xpNUAf5w3eFuQii2wxztTGpvG1Xg=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-AGr8OoemT/ejynbIu56qeil2+F2WLkIjn2d8jGK1JkchxnMUhYOfnqc9sVzcRxpG9Ycvw4weQ5sprRvtb7Yhcw=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-xvOiFkrDNu607MPMBUQ6huHmBG1PZLOrqhtK6pXJW3GjfVqJg0Z/qpTdhXfcqWdSZHcT+Nct2fOgewZvytESkw=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-TUdDCSY+Eo/EHjhJz7P2GnWwfqet+lFxBZzGHldrvULr59AgahamLs/N85SC4+bdF86EhqDuuw9rYLvLFWWlXA=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-s+YsZlgiXNq8XkgHs6xdvKDFOj/bwTEevqEY6rC2I3cBHbxXYU1LOZstH3Ffw9hE5tE1sqT7U23C00MzkXztMw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.13", "", { "os": "linux", "cpu": "x64" }, "sha512-0bdwFVSbbM//Sds6OjtnmQGp4eUjOTt6kHvR/1P0ieR9GcTUAlPNvPC3DiavTqq302W34Ae2T6u5VVNGuQtGlQ=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-QweDxY89fq0VvrxME+wS/BXKmqMrOTZlN9SqQ79kQSIc3FrEwvW/PvUegQF6XIVaekncDykB5dzPqjbwSKs9DA=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.13", "", { "os": "win32", "cpu": "x64" }, "sha512-trDw2ogdM2lyav9WFQsdsfdVy1dvZALymRpgmWsvSez0BJzBjulhOT/t+wyKeh3pZWvwP3VMs1SoOKwO3wecMQ=="],
"@d-fischer/cache-decorators": ["@d-fischer/cache-decorators@4.0.1", "", { "dependencies": { "@d-fischer/shared-utils": "^3.6.3", "tslib": "^2.6.2" } }, "sha512-HNYLBLWs/t28GFZZeqdIBqq8f37mqDIFO6xNPof94VjpKvuP6ROqCZGafx88dk5zZUlBfViV9jD8iNNlXfc4CA=="],
"@d-fischer/detect-node": ["@d-fischer/detect-node@3.0.1", "", {}, "sha512-0Rf3XwTzuTh8+oPZW9SfxTIiL+26RRJ0BRPwj5oVjZFyFKmsj9RGfN2zuTRjOuA3FCK/jYm06HOhwNK+8Pfv8w=="],
"@d-fischer/logger": ["@d-fischer/logger@4.2.3", "", { "dependencies": { "@d-fischer/detect-node": "^3.0.1", "@d-fischer/shared-utils": "^3.2.0", "tslib": "^2.0.3" } }, "sha512-mJUx9OgjrNVLQa4od/+bqnmD164VTCKnK5B4WOW8TX5y/3w2i58p+PMRE45gUuFjk2BVtOZUg55JQM3d619fdw=="],
"@d-fischer/qs": ["@d-fischer/qs@7.0.2", "", {}, "sha512-yAu3xDooiL+ef84Jo8nLjDjWBRk7RXk163Y6aTvRB7FauYd3spQD/dWvgT7R4CrN54Juhrrc3dMY7mc+jZGurQ=="],
"@d-fischer/rate-limiter": ["@d-fischer/rate-limiter@1.1.0", "", { "dependencies": { "@d-fischer/logger": "^4.2.3", "@d-fischer/shared-utils": "^3.6.3", "tslib": "^2.6.2" } }, "sha512-O5HgACwApyCZhp4JTEBEtbv/W3eAwEkrARFvgWnEsDmXgCMWjIHwohWoHre5BW6IYXFSHBGsuZB/EvNL3942kQ=="],
"@d-fischer/raw-body": ["@d-fischer/raw-body@2.4.3", "", { "dependencies": { "bytes": "3.1.0", "http-errors": "1.7.3", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-rtPTezQLROnTDdRij0Vo5OJ41aGvfKj9pQ7CkzFssQy+Jyc9BUVLV/DXLIGgvEGUaWt09Jq3im4WgvvPYqTomw=="],
"@d-fischer/shared-utils": ["@d-fischer/shared-utils@3.6.4", "", { "dependencies": { "tslib": "^2.4.1" } }, "sha512-BPkVLHfn2Lbyo/ENDBwtEB8JVQ+9OzkjJhUunLaxkw4k59YFlQxUUwlDBejVSFcpQT0t+D3CQlX+ySZnQj0wxw=="],
"@d-fischer/typed-event-emitter": ["@d-fischer/typed-event-emitter@3.3.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-OvSEOa8icfdWDqcRtjSEZtgJTFOFNgTjje7zaL0+nAtu2/kZtRCSK5wUMrI/aXtCH8o0Qz2vA8UqkhWUTARFQQ=="],
@@ -47,11 +59,11 @@
"@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
"@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="],
"@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="],
"@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="],
"@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],
"@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
@@ -149,23 +161,23 @@
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
"@twurple/api": ["@twurple/api@7.4.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.4.0", "@twurple/common": "7.4.0", "retry": "^0.13.1", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/auth": "7.4.0" } }, "sha512-RlXLs4ZvS8n0+iIk7YyVDwrjhlwpn+N+h7fX5Q61HoxlmzoCShmnnFo03abYw9i8Cc3deGpbQATOSVmigXM4qg=="],
"@twurple/api": ["@twurple/api@8.0.3", "", { "dependencies": { "@d-fischer/cache-decorators": "^4.0.0", "@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": "8.0.3", "@twurple/common": "8.0.3", "retry": "^0.13.1", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/auth": "8.0.3" } }, "sha512-vnqVi9YlNDbCqgpUUvTIq4sDitKCY0dkTw9zPluZvRNqUB1eCsuoaRNW96HQDhKtA9P4pRzwZ8xU7v/1KU2ytg=="],
"@twurple/api-call": ["@twurple/api-call@7.4.0", "", { "dependencies": { "@d-fischer/cross-fetch": "^5.0.1", "@d-fischer/qs": "^7.0.2", "@d-fischer/shared-utils": "^3.6.1", "@twurple/common": "7.4.0", "tslib": "^2.0.3" } }, "sha512-WNxvjp/hMqZREElbvE4rHMyUIrHdGY5cbG8xbqgSM9CESFvJ1wm5BubhyANOyKd1TxABacLddbfbO//Fz9YHgA=="],
"@twurple/api-call": ["@twurple/api-call@8.0.3", "", { "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "@twurple/common": "8.0.3", "tslib": "^2.0.3" } }, "sha512-/5DBTqFjpYB+qqOkkFzoTWE79a7+I8uLXmBIIIYjGoq/CIPxKcHnlemXlU8cQhTr87PVa3th8zJXGYiNkpRx8w=="],
"@twurple/auth": ["@twurple/auth@7.4.0", "", { "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.1", "@twurple/api-call": "7.4.0", "@twurple/common": "7.4.0", "tslib": "^2.0.3" } }, "sha512-WAQV6nJGkfY7r2BkRYhnzUpdfozLvjNsCxkyNVprl4dCWdJzccnTvqkKTdDRJc5ZJxDVaB9Drzwx9/fCp/gRDA=="],
"@twurple/auth": ["@twurple/auth@8.0.3", "", { "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.1", "@twurple/api-call": "8.0.3", "@twurple/common": "8.0.3", "tslib": "^2.0.3" } }, "sha512-Xlv+WNXmGQir4aBXYeRCqdno5XurA6jzYTIovSEHa7FZf3AMHMFqtzW7yqTCUn4iOahfUSA2TIIxmxFM0wis0g=="],
"@twurple/common": ["@twurple/common@7.4.0", "", { "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "klona": "^2.0.4", "tslib": "^2.0.3" } }, "sha512-lX5cVkYar6jGvni6iLmMYjhxH1oPSl2v7XVeZ4C7U1GbLz/Jwk0L0uldQNGUIf9gpRHPY+TXRlk0UIpz2yo8DA=="],
"@twurple/common": ["@twurple/common@8.0.3", "", { "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "klona": "^2.0.4", "tslib": "^2.0.3" } }, "sha512-JQ2lb5qSFT21Y9qMfIouAILb94ppedLHASq49Fe/AP8oq0k3IC9Q7tX2n6tiMzGWqn+n8MnONUpMSZ6FhulMXA=="],
"@twurple/eventsub-base": ["@twurple/eventsub-base@7.4.0", "", { "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", "@twurple/api": "7.4.0", "@twurple/auth": "7.4.0", "@twurple/common": "7.4.0", "tslib": "^2.0.3" } }, "sha512-Umx0kNZKxBUTF2/MHAlnnCuNPs8Tl1Aw8EzDJI2AW10tOiWvgeCR889fKCFBPlHXvcMYSEvsItkX+pXeZ8GkeQ=="],
"@twurple/eventsub-base": ["@twurple/eventsub-base@8.0.3", "", { "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", "@twurple/api": "8.0.3", "@twurple/auth": "8.0.3", "@twurple/common": "8.0.3", "tslib": "^2.0.3" } }, "sha512-59G5xJbHWLTSO6NAgwtkHPfIlmdjrABgiEumFnHhNusMbLM9qdA+kLcW5NB2NImNliytl6zZtqY92FInzUE6NA=="],
"@twurple/eventsub-http": ["@twurple/eventsub-http@7.4.0", "", { "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/raw-body": "^2.4.3", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", "@twurple/auth": "7.4.0", "@twurple/common": "7.4.0", "@twurple/eventsub-base": "7.4.0", "@types/express-serve-static-core": "^4.17.24", "httpanda": "^0.4.6", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/api": "7.4.0" } }, "sha512-Ua8cP4OPfgyUxlNG2BSJ2Ck02Axk3YXIBoQqoTURlSI0wix8+kTK0X4QuDvxicPxn9iRV1prNimOSvt4HXSkrQ=="],
"@twurple/eventsub-http": ["@twurple/eventsub-http@8.0.3", "", { "dependencies": { "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.6.1", "@d-fischer/typed-event-emitter": "^3.3.0", "@twurple/auth": "8.0.3", "@twurple/common": "8.0.3", "@twurple/eventsub-base": "8.0.3", "@types/express-serve-static-core": "^5.1.0", "httpanda": "^0.4.6", "raw-body": "^3.0.2", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/api": "8.0.3" } }, "sha512-ds8l01GfsIC0hhILepv/UUn/Ix8s0wLg9aGy10xWaG9/Hlfe82NPI8gAg0LYsmlCsOADPwJZSckMTGPJrpw1Iw=="],
"@twurple/eventsub-ngrok": ["@twurple/eventsub-ngrok@7.4.0", "", { "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "@ngrok/ngrok": "^0.5.1", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/api": "7.4.0", "@twurple/eventsub-http": "7.4.0" } }, "sha512-YPk4TtYmCFQwBuIUgEpd6D29wqhJJQq6fxjWG83E86lp3vfcaY6aq7kE39kSqTz8TDkx62xnSH9lsMmZImIQ0w=="],
"@twurple/eventsub-ngrok": ["@twurple/eventsub-ngrok@8.0.3", "", { "dependencies": { "@d-fischer/shared-utils": "^3.6.1", "@ngrok/ngrok": "^0.5.1", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/api": "8.0.3", "@twurple/eventsub-http": "8.0.3" } }, "sha512-wt4keLIivnEpv0EpQw1zgBD6tinaDmVf5VhvQqr8NABCpL4TuZNQAIveIUelHmY+phlISIX/42mvXqHNfmMTwg=="],
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
"@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.7", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
"@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
@@ -173,8 +185,6 @@
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
"@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
@@ -183,23 +193,21 @@
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
"bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="],
"bytes": ["bytes@3.1.0", "", {}, "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"discord-api-types": ["discord-api-types@0.38.31", "", {}, "sha512-kC94ANsk8ackj8ENTuO8joTNEL0KtymVhHy9dyEC/s4QAZ7GCx40dYEzQaadyo8w+oP0X8QydE/nzAWRylTGtQ=="],
"discord-api-types": ["discord-api-types@0.38.36", "", {}, "sha512-qrbUbjjwtyeBg5HsAlm1C859epfOyiLjPqAOzkdWlCNsZCWJrertnETF/NwM8H+waMFU58xGSc5eXUfXah+WTQ=="],
"discord.js": ["discord.js@14.24.0", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.31", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-KNq/ekT8bsmT3ZAfVre8cPbl+DfVYSdlLnDmGZPoz7Cw21LYeWHllRA9MivqNq5b1GPGAxGvyUN1vxbTb/PQWw=="],
"discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="],
"drizzle-kit": ["drizzle-kit@0.31.5", "", { "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-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="],
"drizzle-kit": ["drizzle-kit@0.31.8", "", { "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-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="],
"drizzle-orm": ["drizzle-orm@0.44.6", "", { "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-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ=="],
"drizzle-orm": ["drizzle-orm@0.45.1", "", { "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-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="],
"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=="],
@@ -209,16 +217,14 @@
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
"http-errors": ["http-errors@1.7.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.4", "setprototypeof": "1.1.1", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" } }, "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw=="],
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"httpanda": ["httpanda@0.4.7", "", { "dependencies": { "@types/node": "^14.11.2", "tslib": "^2.0.3" } }, "sha512-NieTiR7kfOheL9OeEi6+JKFmJ2JP9ZRqUQ4tiXZ9J+EMMKxApHUQlEM5l4gZ+l67lxE9Er6oigZnujmhlodNCg=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
@@ -229,19 +235,17 @@
"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=="],
"pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="],
"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.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
"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-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="],
"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-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="],
"pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="],
"pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="],
"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=="],
@@ -255,13 +259,15 @@
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"setprototypeof": ["setprototypeof@1.1.1", "", {}, "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -269,11 +275,9 @@
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"toidentifier": ["toidentifier@1.0.0", "", {}, "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
@@ -287,18 +291,28 @@
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"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=="],
"@discordjs/builders/@discordjs/formatters": ["@discordjs/formatters@0.6.1", "", { "dependencies": { "discord-api-types": "^0.38.1" } }, "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg=="],
"@discordjs/builders/@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],
"@discordjs/builders/discord-api-types": ["discord-api-types@0.38.31", "", {}, "sha512-kC94ANsk8ackj8ENTuO8joTNEL0KtymVhHy9dyEC/s4QAZ7GCx40dYEzQaadyo8w+oP0X8QydE/nzAWRylTGtQ=="],
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"@discordjs/rest/@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],
"@discordjs/rest/discord-api-types": ["discord-api-types@0.38.31", "", {}, "sha512-kC94ANsk8ackj8ENTuO8joTNEL0KtymVhHy9dyEC/s4QAZ7GCx40dYEzQaadyo8w+oP0X8QydE/nzAWRylTGtQ=="],
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"@discordjs/ws/@discordjs/util": ["@discordjs/util@1.1.1", "", {}, "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g=="],
"@discordjs/ws/discord-api-types": ["discord-api-types@0.38.31", "", {}, "sha512-kC94ANsk8ackj8ENTuO8joTNEL0KtymVhHy9dyEC/s4QAZ7GCx40dYEzQaadyo8w+oP0X8QydE/nzAWRylTGtQ=="],
"@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=="],
"httpanda/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="],

View File

@@ -8,7 +8,7 @@
in {
devShells."${system}" = {
default = pkgs.mkShell {
packages = with pkgs; [ bun nodejs deno ];
packages = with pkgs; [ bun biome ];
shellHook = ''
echo Loaded the qweribot dev shell
'';

View File

@@ -2,12 +2,14 @@
"name": "qweribot",
"module": "src/index.ts",
"devDependencies": {
"@twurple/eventsub-ngrok": "^7.4.0",
"@biomejs/biome": "^2.3.13",
"@twurple/eventsub-ngrok": "^8.0.3",
"@types/bun": "latest",
"drizzle-kit": "^0.31.5",
"pg": "^8.16.3"
"drizzle-kit": "^0.31.8",
"pg": "^8.17.2"
},
"scripts": {
"check": "biome check && tsc -b",
"start": "NODE_ENV=production bun src/index.ts",
"start-dev": "NODE_ENV=development bun src/index.ts",
"start-discord": "NODE_ENV=production bun src/discord/index.ts",
@@ -24,11 +26,10 @@
"type": "module",
"dependencies": {
"@fontsource/jersey-15": "^5.2.8",
"@twurple/api": "7.4.0",
"@twurple/auth": "^7.4.0",
"@twurple/eventsub-http": "^7.4.0",
"discord.js": "^14.24.0",
"drizzle-orm": "^0.44.6",
"kleur": "^4.1.5"
"@twurple/api": "8.0.3",
"@twurple/auth": "^8.0.3",
"@twurple/eventsub-http": "^8.0.3",
"discord.js": "^14.25.1",
"drizzle-orm": "^0.45.1"
}
}

View File

@@ -0,0 +1,46 @@
import { describe, expect, test } from "bun:test";
import parseCommandArgs, { parseCheerArgs } from "lib/parseCommandArgs";
describe("command argument parser", () => {
test("split command into chunks", () => {
expect(parseCommandArgs("!execute eponirewq")).toEqual(["eponirewq"]);
expect(parseCommandArgs("!getloot")).toEqual([]);
expect(parseCommandArgs("!inv qwerinope")).toEqual(["qwerinope"]);
expect(parseCommandArgs("!admingive qwerinope silverbullet 67")).toEqual([
"qwerinope",
"silverbullet",
"67",
]);
});
test("handle the !use command", () => {
expect(parseCommandArgs("!use silverbullet albeees")).toEqual(["albeees"]);
expect(parseCommandArgs("!use grenade")).toEqual([]);
});
test("handle special aliases", () => {
expect(parseCommandArgs("i blast mrockstar20", "i")).toEqual([
"mrockstar20",
]);
expect(parseCommandArgs("blastin sefi", "blastin")).toEqual(["sefi"]);
expect(parseCommandArgs("i grenade", "i")).toEqual([]);
});
});
describe("cheer argument parser", () => {
test("basic parsing", () => {
expect(parseCheerArgs("cheer99")).toEqual([]);
expect(parseCheerArgs("grenade out! cheer99")).toEqual(["grenade", "out!"]);
expect(parseCheerArgs("cheer666 albeees")).toEqual(["albeees"]);
expect(parseCheerArgs("albeees cheer666")).toEqual(["albeees"]);
});
test("Remove all cheers", () => {
expect(parseCheerArgs("cheer1 cheer1 cheer1")).toEqual([]);
expect(parseCheerArgs("TAKE CHEER1 THIS chEEr1 SPAM CheeR6969")).toEqual([
"take",
"this",
"spam",
]);
});
});

View File

@@ -9,7 +9,6 @@ import {
getAuthRecord,
updateAuthRecord,
} from "db/dbAuth";
import kleur from "kleur";
import logger from "lib/logger";
async function initAuth(
@@ -26,15 +25,10 @@ async function initAuth(
const state = Bun.randomUUIDv7().replace(/-/g, "").slice(0, 32).toUpperCase();
// Generate random state variable to prevent cross-site-scripting attacks
const instruction = `Visit this URL as ${kleur
.red()
.underline()
.italic(
streamer ? "the streamer" : "the chatter",
)} to authenticate the bot.`;
const instruction = `Visit this URL as \x1b[3;4;1;95m${streamer ? "the streamer" : "the chatter"}\x1b[0;97m to authenticate the bot.`;
logger.info(instruction);
logger.info(
`https://id.twitch.tv/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectURL}&response_type=code&scope=${requestedIntents.join("+")}&state=${state}`,
`\x1b[3;4;1;95mhttps://id.twitch.tv/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectURL}&response_type=code&scope=${requestedIntents.join("+")}&state=${state}\x1b[0;97m`,
);
const createCodePromise = () => {
@@ -61,7 +55,7 @@ async function initAuth(
return new Response("Successfully authenticated!");
} else {
return new Response(
`Authentication attempt unsuccessful, please make sure the redirect url in the twitch developer console is set to ${redirectURL} and that the bot is listening to that url & port.`,
`Authentication attempt unsuccessful, please make sure the redirect url in the twitch developer console is set to \x1b[3;4;1;95m${redirectURL}\x1b[0;97m and that the bot is listening to that url & port.`,
{ status: 400 },
);
}
@@ -112,13 +106,15 @@ export async function createAuthProvider(
});
authData.onRefresh(async (user, token) => {
logger.ok(`Successfully refreshed auth for user ${user}`);
logger.ok(
`Successfully refreshed auth for user \x1b[3;4;1;95m${user}\x1b[0;97m`,
);
await updateAuthRecord(user, token);
});
authData.onRefreshFailure((user, err) => {
logger.err(
`Failed to refresh auth for user ${user}: ${err.name} ${err.message}`,
`Failed to refresh auth for user \x1b[3;4;1;95m${user}\x1b[0;97m: ${err.name} ${err.message}`,
);
});
@@ -149,7 +145,7 @@ export async function createAuthProvider(
await authData.refreshAccessTokenForUser(user.userId);
} catch (_err) {
logger.err(
`Failed to refresh user ${user.userId}. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`,
`Failed to refresh user \x1b[3;4;1;95m${user.userId}\x1b[0;97m. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`,
);
await deleteAuthRecord(user.userId);
process.exit(1);

View File

@@ -1,6 +1,8 @@
import { Cheer, handleNoTarget } from "cheers";
import { createCheerEventRecord } from "db/dbCheerEvents";
import { createTimeoutRecord } from "db/dbTimeouts";
import {
createCompensatedItemCheer,
createTimeoutEventCheer,
} from "db/CheerEvents";
import { getUserRecord } from "db/dbUser";
import { sendMessage } from "lib/commandUtils";
import { parseCheerArgs } from "lib/parseCommandArgs";
@@ -30,7 +32,8 @@ export default new Cheer({
}
if (users.length === 0) {
await sendMessage("No vulnerable chatters");
await handleNoTarget(msg, user, ITEMNAME, true);
const compensated = await handleNoTarget(msg, user, ITEMNAME, true);
if (compensated) await createCompensatedItemCheer(user, ITEMNAME);
return;
}
target = users[Math.floor(Math.random() * users.length)]!;
@@ -45,7 +48,8 @@ export default new Cheer({
target = await User.initUsername(args[0].toLowerCase());
}
if (!target) {
await handleNoTarget(msg, user, ITEMNAME, false);
const compensated = await handleNoTarget(msg, user, ITEMNAME);
if (compensated) await createCompensatedItemCheer(user, ITEMNAME);
return;
}
await getUserRecord(target);
@@ -60,8 +64,7 @@ export default new Cheer({
sendMessage(
`KEKPOINT KEKPOINT KEKPOINT ${target.displayName.toUpperCase()} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`,
),
createTimeoutRecord(user, target, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
createTimeoutEventCheer(user, target, "execute"),
playAlert({
name: "userExecution",
user: user.displayName,
@@ -69,7 +72,8 @@ export default new Cheer({
}),
]);
else {
await handleNoTarget(msg, user, ITEMNAME);
const compensated = await handleNoTarget(msg, user, ITEMNAME);
if (compensated) await createCompensatedItemCheer(user, ITEMNAME);
switch (result.reason) {
case "banned":
await sendMessage(

View File

@@ -1,6 +1,8 @@
import { Cheer, handleNoTarget } from "cheers";
import { createCheerEventRecord } from "db/dbCheerEvents";
import { createTimeoutRecord } from "db/dbTimeouts";
import {
createCompensatedItemCheer,
createTimeoutEventCheer,
} from "db/CheerEvents";
import { getUserRecord } from "db/dbUser";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
@@ -18,7 +20,8 @@ export default new Cheer({
const targets = await redis.keys(`user:*:vulnerable`);
if (targets.length === 0) {
await sendMessage("No vulnerable chatters to blow up!", msg.messageId);
await handleNoTarget(msg, user, ITEMNAME);
const compensated = await handleNoTarget(msg, user, ITEMNAME, true);
if (compensated) await createCompensatedItemCheer(user, ITEMNAME);
return;
}
const selection = targets[Math.floor(Math.random() * targets.length)]!;
@@ -32,8 +35,7 @@ export default new Cheer({
sendMessage(
`wybuh ${target?.displayName} got hit by ${user.displayName}'s grenade wybuh`,
),
createTimeoutRecord(user, target!, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
createTimeoutEventCheer(user, target!, "grenade"),
playAlert({
name: "grenadeExplosion",
user: user.displayName,

View File

@@ -1,8 +1,16 @@
import type { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import type User from "user";
export type cheers =
| "execute"
| "grenade"
| "tnt"
| "timeout"
| "superloot"
| "realsilverbullet";
type cheerOptions = {
name: string;
name: cheers;
amount: number;
isItem: boolean;
execute: (
@@ -12,7 +20,7 @@ type cheerOptions = {
};
export class Cheer {
public readonly name: string;
public readonly name: cheers;
public readonly amount: number;
public readonly execute: (
msg: EventSubChannelChatMessageEvent,
@@ -20,7 +28,7 @@ export class Cheer {
) => Promise<void>;
public readonly isItem: boolean;
constructor(options: cheerOptions) {
this.name = options.name.toLowerCase();
this.name = options.name;
this.amount = options.amount;
this.execute = options.execute;
this.isItem = options.isItem;
@@ -55,13 +63,13 @@ export async function handleNoTarget(
user: User,
itemname: items,
silent = true,
) {
): Promise<boolean> {
if (await user.itemLock()) {
await sendMessage(
`Cannot give ${user.displayName} a ${itemname} (itemlock)`,
msg.messageId,
);
return;
return false;
}
await user.setLock();
const userRecord = await getUserRecord(user);
@@ -72,4 +80,5 @@ export async function handleNoTarget(
);
await changeItemCount(user, userRecord, itemname, 1);
await user.clearLock();
return true;
}

View File

@@ -0,0 +1,86 @@
import { Cheer } from "cheers";
import { createTimeoutEventCheer } from "db/CheerEvents";
import { getUserRecord } from "db/dbUser";
import { sendMessage } from "lib/commandUtils";
import { parseCheerArgs } from "lib/parseCommandArgs";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import User from "user";
import { playAlert } from "web/alerts/serverFunctions";
export default new Cheer({
name: "realsilverbullet",
amount: 6666,
isItem: false,
async execute(msg, user) {
const args = parseCheerArgs(msg.messageText);
let target: User | null;
if (!args[0]) {
const vulnsids = await redis.keys("user:*:vulnerable");
const baseusers = vulnsids.map((a) => User.initUserId(a.slice(5, -11)));
const users: User[] = [];
for (const user of baseusers) {
const a = await user;
if (!a) continue;
users.push(a);
}
if (users.length === 0) {
await sendMessage("No vulnerable chatters, -6666 KEKPOINT");
return;
}
target = users[Math.floor(Math.random() * users.length)]!;
await playAlert({
name: "blastinRoulette",
user: user.displayName,
targets: users.map((a) => a.displayName),
finaltarget: target.displayName,
});
await new Promise((res, _) => setTimeout(res, 4000));
} else {
target = await User.initUsername(args[0].toLowerCase());
}
if (!target) {
await sendMessage("dumbass wasted 6666 bits KEKPOINT", msg.messageId);
return;
}
await getUserRecord(target);
const result = await timeout(
target,
`You got fucking DELETED by ${user.displayName}!`,
60 * 60 * 24,
);
if (result.status)
await Promise.all([
sendMessage(
`KEKPOINT KEKPOINT KEKPOINT ${target.displayName.toUpperCase()} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`,
),
createTimeoutEventCheer(user, target, "realsilverbullet"),
playAlert({
name: "userExecution",
user: user.displayName,
target: target.displayName,
}),
]);
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 * 60 * 24),
]);
break;
case "unknown":
await sendMessage("Something went wrong... oops", msg.messageId);
break;
}
}
},
});

View File

@@ -1,5 +1,5 @@
import { Cheer } from "cheers";
import { createGetLootRecord } from "db/dbGetLoot";
import { createSuperLootEvent } from "db/CheerEvents";
import { getUserRecord, updateUserRecord } from "db/dbUser";
import itemMap, { type inventory, type items } from "items";
import { sendMessage } from "lib/commandUtils";
@@ -11,10 +11,10 @@ export default new Cheer({
amount: 150,
isItem: true,
async execute(msg, user) {
if (!(await redis.exists("streamIsLive"))) {
await sendMessage(`No loot while stream is offline`, msg.messageId);
return;
}
// if (!(await redis.exists("streamIsLive"))) {
// await sendMessage(`No loot while stream is offline`, msg.messageId);
// return;
// }
if (await user.itemLock()) {
await sendMessage(`Cannot get loot (itemlock)`, msg.messageId);
return;
@@ -80,7 +80,7 @@ export default new Cheer({
await Promise.all([
updateUserRecord(user, userData),
sendMessage(message, msg.messageId),
createGetLootRecord(user, gainedqbucks, itemDiff, "superloot"),
createSuperLootEvent(user, gainedqbucks, itemDiff),
user.clearLock(),
]);
},

View File

@@ -1,6 +1,8 @@
import { Cheer, handleNoTarget } from "cheers";
import { createCheerEventRecord } from "db/dbCheerEvents";
import { createTimeoutRecord } from "db/dbTimeouts";
import {
createTimeoutEventCheer,
createCompensatedItemCheer,
} from "db/CheerEvents";
import { getUserRecord } from "db/dbUser";
import { sendMessage } from "lib/commandUtils";
import { parseCheerArgs } from "lib/parseCommandArgs";
@@ -17,12 +19,14 @@ export default new Cheer({
async execute(msg, user) {
const args = parseCheerArgs(msg.messageText);
if (!args[0]) {
await handleNoTarget(msg, user, ITEMNAME, false);
const compensated = await handleNoTarget(msg, user, ITEMNAME, false);
if (compensated) await createCompensatedItemCheer(user, ITEMNAME);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await handleNoTarget(msg, user, ITEMNAME, false);
const compensated = await handleNoTarget(msg, user, ITEMNAME, false);
if (compensated) await createCompensatedItemCheer(user, ITEMNAME);
return;
}
await getUserRecord(target);
@@ -37,8 +41,7 @@ export default new Cheer({
sendMessage(
`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`,
),
createTimeoutRecord(user, target, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
createTimeoutEventCheer(user, target, "timeout"),
playAlert({
name: "userBlast",
user: user.displayName,
@@ -46,7 +49,8 @@ export default new Cheer({
}),
]);
else {
await handleNoTarget(msg, user, ITEMNAME);
const compensated = await handleNoTarget(msg, user, ITEMNAME);
if (compensated) await createCompensatedItemCheer(user, ITEMNAME);
switch (result.reason) {
case "banned":
await sendMessage(

View File

@@ -1,6 +1,8 @@
import { Cheer, handleNoTarget } from "cheers";
import { createCheerEventRecord } from "db/dbCheerEvents";
import { createTimeoutRecord } from "db/dbTimeouts";
import {
createTimeoutEventCheer,
createCompensatedItemCheer,
} from "db/CheerEvents";
import { getUserRecord } from "db/dbUser";
import { getTNTTargets } from "items/tnt";
import { sendMessage } from "lib/commandUtils";
@@ -21,7 +23,8 @@ export default new Cheer({
.then((a) => a.map((b) => b.slice(5, -11)));
if (vulntargets.length === 0) {
await sendMessage("No vulnerable chatters to blow up", msg.messageId);
await handleNoTarget(msg, user, ITEMNAME);
const compensated = await handleNoTarget(msg, user, ITEMNAME, true);
if (compensated) await createCompensatedItemCheer(user, ITEMNAME);
return;
}
const targets = getTNTTargets(vulntargets);
@@ -36,13 +39,12 @@ export default new Cheer({
sendMessage(
`wybuh ${target?.displayName} got hit by ${user.displayName}'s TNT wybuh`,
),
createTimeoutRecord(user, target!, ITEMNAME),
]);
}),
);
await Promise.all([
createCheerEventRecord(user, ITEMNAME),
createTimeoutEventCheer(user, targets, "tnt"),
playAlert({
name: "tntExplosion",
user: user.displayName,

View File

@@ -1,3 +1,5 @@
import { getUserRecord, updateUserRecord } from "db/dbUser";
import { emptyInventory } from "items";
import { Command, sendMessage } from "lib/commandUtils";
import { addInvuln } from "lib/invuln";
import parseCommandArgs from "lib/parseCommandArgs";
@@ -20,12 +22,16 @@ export default new Command({
return;
}
const data = await addInvuln(target.id);
if (data === "OK")
if (data === "OK") {
const userRecord = await getUserRecord(target);
userRecord.inventory = emptyInventory;
userRecord.balance = 0;
await updateUserRecord(target, userRecord);
await sendMessage(
`${target.displayName} is now an invuln`,
`${target.displayName} is now an invuln. Their inventory and wallet have been wiped`,
msg.messageId,
);
else
} else
await sendMessage(
`${target.displayName} is already an invuln`,
msg.messageId,

View File

@@ -40,7 +40,7 @@ export default new Command({
const selection = namedRedeems.get(args[0]);
if (!selection) {
await sendMessage(
`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://github.com/qwerinope/qweribot#point-redeems`,
`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://gitlab.com/qwerinope/qweribot#point-redeems`,
msg.messageId,
);
return;

22
src/commands/economy.ts Normal file
View File

@@ -0,0 +1,22 @@
import { getTotalItemCounts } from "db/dbUser";
import itemAliasMap from "items";
import { Command, sendMessage } from "lib/commandUtils";
export default new Command({
name: "economy",
aliases: ["economy", "eco"],
usertype: "chatter",
execution: async (msg) => {
const allitems = await getTotalItemCounts();
const itemList = Object.entries(allitems)
.sort(([, a], [, b]) => b - a)
.map(([item, count]) => {
const itemobj = itemAliasMap.get(item);
if (itemobj) return `${itemobj.prettyName}: ${count}`;
return `${item}: ${count}`; // Fallback if an item doesn't have their name as an alias
})
.join(" | ");
await sendMessage(`Total Items in circulation: ${itemList}`, msg.messageId);
},
});

View File

@@ -39,7 +39,7 @@ export default new Command({
const selection = namedRedeems.get(args[0]);
if (!selection) {
await sendMessage(
`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://github.com/qwerinope/qweribot#point-redeems`,
`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://gitlab.com/qwerinope/qweribot#point-redeems`,
msg.messageId,
);
return;

View File

@@ -12,7 +12,7 @@ export default new Command({
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage(
`A full list of cheers can be found here: https://github.com/qwerinope/qweribot#cheers`,
`A full list of cheers can be found here: https://gitlab.com/qwerinope/qweribot#cheers`,
msg.messageId,
);
return;

View File

@@ -12,7 +12,7 @@ export default new Command({
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage(
`A full list of commands can be found here: https://github.com/qwerinope/qweribot#commands-1`,
`A full list of commands can be found here: https://gitlab.com/qwerinope/qweribot#commands-1`,
msg.messageId,
);
return;

View File

@@ -1,5 +1,5 @@
import { createGetLootRecord } from "db/dbGetLoot";
import { getUserRecord, updateUserRecord } from "db/dbUser";
import { createGetLootEvent } from "db/LootEvents";
import itemMap, { type inventory, type items } from "items";
import { Command, sendMessage } from "lib/commandUtils";
import { buildTimeString } from "lib/dateManager";
@@ -132,8 +132,11 @@ export default new Command({
await Promise.all([
updateUserRecord(user, userData),
sendMessage(message, msg.messageId),
createGetLootRecord(user, gainedqbucks, itemDiff, "getloot"),
createGetLootEvent(user, gainedqbucks, itemDiff, "getloot"),
user.clearLock(),
]);
if (itemstrings.length === 0 && gainedqbucks < 100)
await sendMessage("YEOP THAT'S A SCAMBOX YEOP");
},
});

View File

@@ -1,8 +1,10 @@
import { getUserRecord } from "db/dbUser";
import items, { changeItemCount } from "items";
import { Command, sendMessage } from "lib/commandUtils";
import { ANIVNAMES } from "lib/handleAnivMessage";
import logger from "lib/logger";
import parseCommandArgs from "lib/parseCommandArgs";
import { timeout } from "lib/timeout";
import User from "user";
export default new Command({
@@ -10,6 +12,11 @@ export default new Command({
aliases: ["give"],
usertype: "chatter",
execution: async (msg, user) => {
if (Array.from<string>(ANIVNAMES).includes(msg.chatterName)) {
await sendMessage("CLANKERS CAN'T GIVE ITEMS UltraMad UltraMad UltraMad");
await timeout(user, "STUPID CLANKER", 30);
return;
}
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a user", msg.messageId);

View File

@@ -25,6 +25,10 @@ export default new Command({
}
const selection = items.get(messagequery[0].toLowerCase());
if (messagequery[0].toLowerCase() === "lootbox") {
if (await redis.sismember("disabledcommands", "getloot")) {
await sendMessage("Lootboxes are currently disabled", msg.messageId);
return;
}
await getloot.execute(msg, user);
return;
}

View File

@@ -19,7 +19,7 @@ export async function connectionCheck() {
redisstatus = true;
} catch {}
logger.info(
`Currently using the "${process.env.NODE_ENV ?? "production"}" database`,
`Currently using the \x1b[3;4;1;95m"${process.env.NODE_ENV ?? "production"}"\x1b[0;97m database`,
);
pgstatus
? logger.ok(`Postgresql status: good`)

151
src/db/CheerEvents.ts Normal file
View File

@@ -0,0 +1,151 @@
import type { cheers } from "cheers";
import db from "db/connection";
import { cheerEvents, events, timeouts } from "db/schema";
import type { inventory, items } from "items";
import type User from "user";
import { createGetLootEvent } from "./LootEvents";
import { eq } from "drizzle-orm";
/**
* Use this function to create a cheer event with timeouts
* This can only be used if the cheer succeeded
*
* The target can either be a single User object or an array of targetIDs
*/
export async function createTimeoutEventCheer(
user: User,
target: User | string[],
event: cheers,
) {
const userInt = parseInt(user.id, 10);
return await db.transaction(async (tx) => {
const cheerEventRecord = await tx
.insert(cheerEvents)
.values({
user: userInt,
event,
status: "success",
})
.returning();
if (Array.isArray(target))
target.map(
async (ripbozo) =>
await tx.insert(timeouts).values({
user: userInt,
target: parseInt(ripbozo, 10),
item: event,
cheer: cheerEventRecord[0]?.id,
}),
);
else
await tx.insert(timeouts).values({
user: userInt,
target: parseInt(target.id, 10),
item: event,
cheer: cheerEventRecord[0]?.id,
});
await tx.insert(events).values({
user: userInt,
cheer: cheerEventRecord[0]?.id,
});
if (!cheerEventRecord[0]) {
tx.rollback();
return false;
}
});
}
/**
* Use this function to create a cheer event without timeouts
* This can only be used if the cheer succeeded
*/
export async function createRegularEventCheer(
user: User,
event: cheers | items,
) {
const userInt = parseInt(user.id, 10);
return await db.transaction(async (tx) => {
const cheerEventRecord = await tx
.insert(cheerEvents)
.values({
user: userInt,
event,
status: "success",
})
.returning();
await tx.insert(events).values({
user: userInt,
cheer: cheerEventRecord[0]?.id,
});
if (!cheerEventRecord[0]) {
tx.rollback();
return false;
}
});
}
/**
* Use this function to create a cheer event where the user got an item after the cheer failed
*/
export async function createCompensatedItemCheer(user: User, item: items) {
const userInt = parseInt(user.id, 10);
return await db.transaction(async (tx) => {
const cheerEventRecord = await tx
.insert(cheerEvents)
.values({ user: userInt, event: item, status: "compensated" })
.returning();
await tx.insert(events).values({
user: userInt,
cheer: cheerEventRecord[0]?.id,
});
if (!cheerEventRecord[0]) {
tx.rollback();
return false;
}
});
}
/**
* Because superloot is a special case for cheers, as the event needs to link to the getLoot table and the cheerEvents table, we have this special function
*/
export async function createSuperLootEvent(
user: User,
qbucks: number,
inventory: inventory,
) {
const eventRecord = await createGetLootEvent(
user,
qbucks,
inventory,
"superloot",
);
if (eventRecord === false) return;
return await db.transaction(async (tx) => {
const cheerEventRecord = await tx
.insert(cheerEvents)
.values({
user: parseInt(user.id, 10),
event: "superloot",
})
.returning();
await tx
.update(events)
.set({ cheer: cheerEventRecord[0]?.id })
.where(eq(events.id, eventRecord.id));
if (!cheerEventRecord[0]) {
tx.rollback();
return false;
}
});
}

72
src/db/ItemEvents.ts Normal file
View File

@@ -0,0 +1,72 @@
import db from "db/connection";
import { events, timeouts, usedItems } from "db/schema";
import type { items } from "items";
import type User from "user";
/**
* Use this function for doing all item usages with timeouts
*/
export async function createTimeoutEventItem(
user: User,
target: User | string[],
item: items,
) {
const userInt = parseInt(user.id, 10);
return await db.transaction(async (tx) => {
const usedItemRecord = await tx
.insert(usedItems)
.values({ user: userInt, item })
.returning();
if (Array.isArray(target))
target.map(
async (ripbozo) =>
await tx.insert(timeouts).values({
user: userInt,
target: parseInt(ripbozo, 10),
item,
usedItem: usedItemRecord[0]?.id,
}),
);
else
await tx.insert(timeouts).values({
user: userInt,
target: parseInt(target.id, 10),
item,
usedItem: usedItemRecord[0]?.id,
});
await tx.insert(events).values({
user: userInt,
usedItem: usedItemRecord[0]?.id,
});
if (!usedItemRecord[0]) {
tx.rollback();
return false;
}
});
}
/**
* Use this function for doing all regular item usages (no timeouts)
*/
export async function createNormalEventItem(user: User, item: items) {
const userInt = parseInt(user.id, 10);
return await db.transaction(async (tx) => {
const usedItemRecord = await tx
.insert(usedItems)
.values({ user: userInt, item })
.returning();
await tx.insert(events).values({
user: userInt,
usedItem: usedItemRecord[0]?.id,
});
if (!usedItemRecord[0]) {
tx.rollback();
return false;
}
});
}

39
src/db/LootEvents.ts Normal file
View File

@@ -0,0 +1,39 @@
import db from "db/connection";
import type { lootTriggers } from "db/schema";
import { events, getLoots } from "db/schema";
import type { inventory } from "items";
import type User from "user";
export async function createGetLootEvent(
user: User,
qbucks: number,
inventory: inventory,
trigger: lootTriggers,
) {
return await db.transaction(async (tx) => {
const glRecord = await tx
.insert(getLoots)
.values({
user: parseInt(user.id, 10),
qbucks: qbucks,
items: inventory,
trigger,
})
.returning();
const eventRecord = await tx
.insert(events)
.values({
user: parseInt(user.id, 10),
getLoot: glRecord[0]?.id,
})
.returning();
if (!glRecord[0]) {
tx.rollback();
return false;
}
return eventRecord[0]!;
});
}

4
src/db/UndoEvent.ts Normal file
View File

@@ -0,0 +1,4 @@
import db from "db/connection";
import { events } from "db/schema";
import type User from "user";
import { desc, eq, and } from "drizzle-orm";

View File

@@ -1,18 +1,8 @@
import db from "db/connection";
import { cheerEvents } from "db/schema";
import { and, between, eq, type SQL } from "drizzle-orm";
import type { items } from "items";
import type User from "user";
export async function createCheerEventRecord(
user: User,
cheer: items,
): Promise<void> {
await db
.insert(cheerEvents)
.values({ user: parseInt(user.id, 10), event: cheer });
}
export async function getCheerEvents(user: User, monthData?: string) {
let condition: SQL<unknown> | undefined = eq(
cheerEvents.user,

View File

@@ -1,18 +0,0 @@
import db from "db/connection";
import { getLoots, type lootTriggers } from "db/schema";
import type { inventory } from "items";
import type User from "user";
export async function createGetLootRecord(
user: User,
qbucks: number,
inventory: inventory,
trigger: lootTriggers,
) {
await db.insert(getLoots).values({
user: parseInt(user.id, 10),
qbucks: qbucks,
items: inventory,
trigger,
});
}

View File

@@ -1,21 +1,8 @@
import db from "db/connection";
import { timeouts } from "db/schema";
import { and, between, eq, type SQL } from "drizzle-orm";
import type { items } from "items";
import type User from "user";
export async function createTimeoutRecord(
user: User,
target: User,
item: items,
): Promise<void> {
await db.insert(timeouts).values({
user: parseInt(user.id, 10),
target: parseInt(target.id, 10),
item,
});
}
export async function getTimeoutsAsUser(user: User, monthData?: string) {
let condition: SQL<unknown> | undefined = eq(
timeouts.user,

View File

@@ -1,16 +1,8 @@
import db from "db/connection";
import { usedItems } from "db/schema";
import { and, between, eq, type SQL } from "drizzle-orm";
import type { items } from "items";
import type User from "user";
export async function createUsedItemRecord(
user: User,
item: items,
): Promise<void> {
await db.insert(usedItems).values({ user: parseInt(user.id, 10), item });
}
export async function getItemsUsed(user: User, monthData?: string) {
let condition: SQL<unknown> | undefined = eq(
usedItems.user,

View File

@@ -12,7 +12,8 @@ import {
type SQL,
sql,
} from "drizzle-orm";
import { itemarray } from "items";
import { itemarray, type items } from "items";
import { ANIVNAMES } from "lib/handleAnivMessage";
import type User from "user";
/** Use this function to both ensure existance and to retreive data */
@@ -120,3 +121,25 @@ export async function getKDLeaderboard(monthData?: string) {
return result;
}
type ItemCounts = Record<items, number>;
export async function getTotalItemCounts(): Promise<ItemCounts> {
const allUsers = await db
.select({ username: users.username, inventory: users.inventory })
.from(users);
const filteredUsers = allUsers.filter(
(user) =>
!Array.from<string>(ANIVNAMES).includes(user.username.toLowerCase()),
);
const counts = itemarray.reduce((acc, item) => {
acc[item] = filteredUsers.reduce((sum, user) => {
return sum + (user.inventory[item] || 0);
}, 0);
return acc;
}, {} as ItemCounts);
return counts;
}

View File

@@ -1,4 +1,5 @@
import type { AccessToken } from "@twurple/auth";
import type { cheers as cheertypes } from "cheers";
import { relations } from "drizzle-orm";
import {
boolean,
@@ -32,6 +33,7 @@ export const usersRelations = relations(users, ({ many }) => ({
cheers: many(cheers),
anivTimeouts: many(anivTimeouts),
getLoots: many(getLoots),
events: many(events),
}));
export const timeouts = pgTable("timeouts", {
@@ -42,8 +44,10 @@ export const timeouts = pgTable("timeouts", {
target: integer()
.notNull()
.references(() => users.id),
item: varchar().$type<items>().notNull(),
item: varchar().$type<items | cheertypes>().notNull(),
created: timestamp().defaultNow().notNull(),
cheer: uuid().references(() => cheerEvents.id),
usedItem: uuid().references(() => usedItems.id),
});
export const timeoutsRelations = relations(timeouts, ({ one }) => ({
@@ -57,6 +61,14 @@ export const timeoutsRelations = relations(timeouts, ({ one }) => ({
references: [users.id],
relationName: "target",
}),
cheer: one(cheerEvents, {
fields: [timeouts.cheer],
references: [cheerEvents.id],
}),
usedItem: one(usedItems, {
fields: [timeouts.usedItem],
references: [usedItems.id],
}),
}));
export const usedItems = pgTable("usedItems", {
@@ -68,27 +80,36 @@ export const usedItems = pgTable("usedItems", {
created: timestamp().defaultNow().notNull(),
});
export const usedItemsRelations = relations(usedItems, ({ one }) => ({
export const usedItemsRelations = relations(usedItems, ({ one, many }) => ({
user: one(users, {
fields: [usedItems.user],
references: [users.id],
}),
timeouts: many(timeouts),
}));
/**
* "success" when everything works
* "compensated" when the user gets an item in their inventory for a cheer
*/
export type cheerEventStatus = "success" | "compensated";
export const cheerEvents = pgTable("cheerEvents", {
id: uuid().defaultRandom().primaryKey(),
user: integer()
.notNull()
.references(() => users.id),
event: varchar().$type<items>().notNull(),
event: varchar().$type<items | cheertypes>().notNull(),
status: varchar().$type<cheerEventStatus>().default("success").notNull(),
created: timestamp().defaultNow().notNull(),
});
export const cheerEventsRelations = relations(cheerEvents, ({ one }) => ({
export const cheerEventsRelations = relations(cheerEvents, ({ one, many }) => ({
user: one(users, {
fields: [cheerEvents.user],
references: [users.id],
}),
timeouts: many(timeouts),
}));
export const cheers = pgTable("cheers", {
@@ -145,3 +166,33 @@ export const getLootsRelations = relations(getLoots, ({ one }) => ({
references: [users.id],
}),
}));
export const events = pgTable("events", {
id: uuid().defaultRandom().primaryKey(),
user: integer()
.notNull()
.references(() => users.id),
created: timestamp().defaultNow().notNull(),
usedItem: uuid().references(() => usedItems.id),
cheer: uuid().references(() => cheerEvents.id),
getLoot: uuid().references(() => getLoots.id),
});
export const eventsRelations = relations(events, ({ one }) => ({
user: one(users, {
fields: [events.user],
references: [users.id],
}),
usedItem: one(usedItems, {
fields: [events.usedItem],
references: [usedItems.id],
}),
cheer: one(cheerEvents, {
fields: [events.cheer],
references: [cheerEvents.id],
}),
getLoot: one(getLoots, {
fields: [events.getLoot],
references: [getLoots.id],
}),
}));

View File

@@ -14,8 +14,6 @@ eventSub.onChannelRedemptionAdd(streamerId, async (msg) => {
const user = await User.initUsername(msg.userName);
try {
await selection.execute(msg, user!);
if (process.env.NODE_ENV === "production")
await msg.updateStatus("FULFILLED"); // only on prod
} catch (err) {
await sendMessage(
`[ERROR]: Something went wrong with ${user?.displayName}'s redeem!`,

View File

@@ -1,34 +1,33 @@
import { api, eventSub } from "index";
import kleur from "kleur";
import logger from "lib/logger";
eventSub.onRevoke((event) => {
logger.ok(
`Successfully revoked EventSub subscription: ${kleur.underline(event.id)}`,
`Successfully revoked EventSub subscription: \x1b[3;4;1;95m${event.id}`,
);
});
eventSub.onSubscriptionCreateSuccess((event) => {
logger.ok(
`Successfully created EventSub subscription: ${kleur.underline(event.id)}`,
`Successfully created EventSub subscription: \x1b[3;4;1;95m${event.id}`,
);
});
eventSub.onSubscriptionCreateFailure((event) => {
logger.err(
`Failed to create EventSub subscription: ${kleur.underline(event.id)}`,
`Failed to create EventSub subscription: \x1b[3;4;4;95m${event.id}`,
);
});
eventSub.onSubscriptionDeleteSuccess((event) => {
logger.ok(
`Successfully deleted EventSub subscription: ${kleur.underline(event.id)}`,
`Successfully deleted EventSub subscription: \x1b[3;4;1;95m${event.id}`,
);
});
eventSub.onSubscriptionDeleteFailure((event) => {
logger.err(
`Failed to delete EventSub subscription: ${kleur.underline(event.id)}`,
`Failed to delete EventSub subscription: \x1b[3;4;1;95m${event.id}`,
);
});

View File

@@ -38,7 +38,7 @@ async function parseChatMessage(msg: EventSubChannelChatMessageEvent) {
) {
// The msg.sourceMessageId checks if the message is from shared chat. shared chat should be ignored
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`,
`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://gitlab.com/qwerinope/qweribot/#qweribot`,
);
await redis.set(`user:${user?.id}:haschatted`, "1");
await redis.set(`user:${user?.id}:welcomemessageid`, message.id);
@@ -48,6 +48,18 @@ async function parseChatMessage(msg: EventSubChannelChatMessageEvent) {
if (!(await isInvuln(user?.id!))) user?.setVulnerable(); // Make the user vulnerable to explosions if not marked as invuln
// Custom welcome messages
const wcmessage = await redis.get(`user:${user?.id}:welcomemessagetext`);
if (
process.env.NODE_ENV === "production" && // when running prod DB
wcmessage && // when chatter has a welcome message set
(await redis.exists(`streamIsLive`)) && // when the stream is active
!(await redis.exists(`user:${user?.id}:haschattedthisstream`)) // when the user hasn't chatted this stream
)
await sendMessage(wcmessage);
await redis.set(`user:${user?.id}:haschattedthisstream`, "1");
if (!msg.isCheer && !msg.isRedemption) await handleChatMessage(msg, user!);
else if (msg.isCheer && !msg.isRedemption)
await handleCheer(msg, msg.bits, user!);

View File

@@ -5,16 +5,23 @@ import { streamerId } from "main";
import { sendDiscordMessage } from "web/discordConnection";
eventSub.onStreamOnline(streamerId, async (msg) => {
await redis.set("streamIsLive", "1");
await sendMessage(
`${msg.broadcasterDisplayName.toUpperCase()} IS LIVE! START DIGGING!`,
);
await sendDiscordMessage({ message: "live" });
await Promise.all([
redis.set("streamIsLive", "1"),
sendMessage(
`${msg.broadcasterDisplayName.toUpperCase()} IS LIVE! START DIGGING!`,
),
sendDiscordMessage({ message: "live" }),
redis
.keys("user:*:haschattedthisstream")
.then((a) => a.map(async (b) => await redis.del(b))),
]);
});
eventSub.onStreamOffline(streamerId, async (msg) => {
await redis.del("streamIsLive");
await sendMessage(
`${msg.broadcasterDisplayName.toUpperCase()} IS OFFLINE! NO MORE FREE LOOT!`,
);
await Promise.all([
redis.del("streamIsLive"),
sendMessage(
`${msg.broadcasterDisplayName.toUpperCase()} IS OFFLINE! NO MORE FREE LOOT!`,
),
]);
});

View File

@@ -48,7 +48,7 @@ eventSub.onChannelSubscription(streamerId, async (msg) => {
});
eventSub.onChannelSubscriptionGift(streamerId, async (msg) => {
if (msg.isAnonymous) {
if (msg.gifterName === null) {
switch (msg.tier) {
case "1000":
await sendMessage(

View File

@@ -10,7 +10,7 @@ eventSub.onUserWhisperMessage(chatterId, async (msg) => {
if (!msg.messageText.startsWith(commandPrefix)) {
await whisper(
msg.senderUserId,
`Whisper commands start with '${commandPrefix}'. All whisper commands can be found here: https://github.com/qwerinope/qweribot#whisper-commands-1`,
`Whisper commands start with '${commandPrefix}'. All whisper commands can be found here: https://gitlab.com/qwerinope/qweribot#whisper-commands-1`,
);
return;
}
@@ -25,7 +25,7 @@ eventSub.onUserWhisperMessage(chatterId, async (msg) => {
case "h":
await whisper(
msg.senderUserId,
`All whisper commands can be found here: https://github.com/qwerinope/qweribot#whisper-commands-1`,
`All whisper commands can be found here: https://gitlab.com/qwerinope/qweribot#whisper-commands-1`,
);
break;
case "ghostwhisper":

View File

@@ -144,6 +144,11 @@ streamerUsers.forEach(
]),
);
// Deleting all timeouts to prevent ghosts while bot was off
await redis
.keys("user:*:timeout")
.then(async (a) => a.map(async (b) => await redis.del(b)));
const banned = await api.moderation
.getBannedUsers(streamerId)
.then((a) => a.data);
@@ -156,7 +161,7 @@ for (const ban of banned) {
Math.floor((ban.expiryDate.getTime() - Date.now()) / 1000) + 1,
);
logger.info(
`Set the timeout of ${ban.userDisplayName} in the Redis/Valkey database.`,
`Set the timeout of \x1b[3;4;1;95m${ban.userDisplayName}\x1b[0;97m in the Redis/Valkey database.`,
);
}
}
@@ -165,7 +170,7 @@ const mods = await api.moderation.getModerators(streamerId).then((a) => a.data);
for (const mod of mods) {
await redis.set(`user:${mod.userId}:mod`, "1");
logger.info(
`Set the mod status of ${mod.userDisplayName} in the Redis/Valkey database.`,
`Set the mod status of \x1b[3;4;1;95m${mod.userDisplayName}\x1b[0;97m in the Redis/Valkey database.`,
);
}
@@ -180,7 +185,7 @@ for (const remod of bannedmods) {
duration = Math.floor((durationdata * 1000 - Date.now()) / 1000);
remodMod(target!, duration);
logger.info(
`Set the remod timer for ${target?.displayName} to ${duration} seconds.`,
`Set the remod timer for \x1b[3;4;1;95m${target?.displayName}\x1b[0;97m to \x1b[3;4;1;95m${duration}\x1b[0;97m seconds.`,
);
}

View File

@@ -1,6 +1,5 @@
import { createTimeoutRecord } from "db/dbTimeouts";
import { createUsedItemRecord } from "db/dbUsedItems";
import { getUserRecord } from "db/dbUser";
import { createTimeoutEventItem } from "db/ItemEvents";
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
@@ -28,7 +27,7 @@ export default new Item({
}
const target = await User.initUsername(messagequery[0].toLowerCase());
if (!target) {
await sendMessage(`${messagequery[0]} doesn't exist`);
await sendMessage(`${messagequery[0]} doesn't exist`, msg.messageId);
return;
}
await getUserRecord(target); // make sure the user record exist in the database
@@ -57,8 +56,7 @@ export default new Item({
`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`,
),
changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target, ITEMNAME),
createUsedItemRecord(user, ITEMNAME),
createTimeoutEventItem(user, target, ITEMNAME),
playAlert({
name: "userBlast",
user: user.displayName,

View File

@@ -1,6 +1,5 @@
import { createTimeoutRecord } from "db/dbTimeouts";
import { createUsedItemRecord } from "db/dbUsedItems";
import { getUserRecord } from "db/dbUser";
import { createTimeoutEventItem } from "db/ItemEvents";
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
@@ -47,8 +46,7 @@ export default new Item({
`wybuh ${target?.displayName} got hit by ${user.displayName}'s grenade wybuh`,
),
changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target!, ITEMNAME),
createUsedItemRecord(user, ITEMNAME),
createTimeoutEventItem(user, target!, ITEMNAME),
playAlert({
name: "grenadeExplosion",
user: user.displayName,

View File

@@ -1,49 +1,34 @@
import type { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import type { specialExecuteArgs, userType } from "lib/commandUtils";
import { Command, type commandOptions } from "lib/commandUtils";
import type User from "user";
type itemOptions = {
export type items = "blaster" | "silverbullet" | "grenade" | "tnt";
interface itemOptions extends Omit<commandOptions, "usertype"> {
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: items;
export class Item extends Command {
public readonly name: items = "blaster";
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) {
super({
...options,
usertype: "chatter", // Everyone can use items
disableable: true, // Items can always be disabled
});
this.name = options.name;
this.prettyName = options.prettyName;
this.plural = options.plural;
this.description = options.description;
this.aliases = options.aliases;
this.usertype = "chatter"; // Items are usable by everyone
this.execute = options.execution;
this.disableable = true;
this.specialaliases = options.specialaliases ?? [];
this.price = options.price;
}
}
@@ -78,7 +63,6 @@ for (const file of files) {
export default itemAliasMap;
export { emptyInventory, itemarray, specialAliasItems, itemObjectArray };
export type items = "blaster" | "silverbullet" | "grenade" | "tnt";
export type inventory = {
[key in items]?: number;
};

View File

@@ -1,6 +1,5 @@
import { createTimeoutRecord } from "db/dbTimeouts";
import { createUsedItemRecord } from "db/dbUsedItems";
import { getUserRecord } from "db/dbUser";
import { createTimeoutEventItem } from "db/ItemEvents";
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
@@ -18,7 +17,7 @@ export default new Item({
plural: "s",
description: "Times targeted or random vulnerable user out for 30 minutes",
aliases: ["execute", "silverbullet"],
specialaliases: ["blastin"],
specialaliases: ["blastin", "fuck"],
price: 666,
execution: async (msg, user, specialargs) => {
const messagequery = parseCommandArgs(
@@ -67,7 +66,7 @@ export default new Item({
}
if (!target) {
await user.clearLock();
await sendMessage(`${messagequery[0]} doesn't exist`);
await sendMessage(`${messagequery[0]} doesn't exist`, msg.messageId);
return;
}
@@ -78,21 +77,24 @@ export default new Item({
`You got blasted by ${user.displayName}!`,
60 * 30,
);
if (result.status)
if (result.status) {
await Promise.all([
sendMessage(
`KEKPOINT KEKPOINT KEKPOINT ${target.displayName.toUpperCase()} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`,
),
changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target, ITEMNAME),
createUsedItemRecord(user, ITEMNAME),
playAlert({
name: "userExecution",
user: user.displayName,
target: target.displayName,
}),
]);
else {
if (user.id !== streamerId || process.env.NODE_ENV === "development")
// streamer doesn't consume bullets and doesn't count for timeouts
await Promise.all([
changeItemCount(user, userObj, ITEMNAME),
createTimeoutEventItem(user, target, ITEMNAME),
]);
} else {
switch (result.reason) {
case "banned":
await sendMessage(

View File

@@ -1,6 +1,5 @@
import { createTimeoutRecord } from "db/dbTimeouts";
import { createUsedItemRecord } from "db/dbUsedItems";
import { getUserRecord } from "db/dbUser";
import { createTimeoutEventItem } from "db/ItemEvents";
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
@@ -49,13 +48,12 @@ export default new Item({
sendMessage(
`wybuh ${target?.displayName} got hit by ${user.displayName}'s TNT wybuh`,
),
createTimeoutRecord(user, target!, ITEMNAME),
]);
}),
);
await Promise.all([
createUsedItemRecord(user, ITEMNAME),
createTimeoutEventItem(user, targets, ITEMNAME),
playAlert({
name: "tntExplosion",
user: user.displayName,

View File

@@ -9,7 +9,7 @@ export type specialExecuteArgs = {
activation?: string;
};
export type commandOptions = {
export interface commandOptions {
name: string;
aliases: string[];
usertype: userType;
@@ -20,7 +20,7 @@ export type commandOptions = {
) => Promise<void>;
disableable?: boolean;
specialaliases?: string[];
};
}
/** The Command class represents a command */
export class Command {

View File

@@ -59,8 +59,11 @@ export async function getItemStats(target: User, thismonth: boolean) {
}
for (const cheer of cheers) {
if (!returnObj[cheer.event]) returnObj[cheer.event] = 0;
returnObj[cheer.event]! += 1;
if (cheer.event in returnObj) {
if (!returnObj[cheer.event as keyof inventory])
returnObj[cheer.event as keyof inventory] = 0;
returnObj[cheer.event as keyof inventory]! += 1;
}
}
return returnObj;

View File

@@ -4,8 +4,9 @@ import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import type User from "user";
import { playMSTTS } from "web/alerts/serverFunctions";
const ANIVNAMES: anivBots[] = ["a_n_e_e_v", "a_n_i_v"];
export const ANIVNAMES: anivBots[] = ["a_n_e_e_v", "a_n_i_v"];
type anivMessageStore = {
[key: string]: string;
@@ -43,6 +44,7 @@ export default async function handleMessage(
user: User,
) {
if (ANIVNAMES.map((a) => a.toLowerCase()).includes(user.username)) {
await playMSTTS({ text: msg.messageText });
const data: anivMessageStore = await redis
.get("anivmessages")
.then((a) => (a === null ? {} : JSON.parse(a)));

View File

@@ -1,18 +1,39 @@
import kleur from "kleur";
const logger = {
err: (arg: string) =>
console.error(
kleur.red().bold().italic("[ERROR] ") + kleur.red().bold(arg),
Bun.wrapAnsi(
`\x1b[1;91m[ERROR]: \x1b[0;97m${arg}\x1b[0m`,
process.stdout.columns,
),
),
warn: (arg: string) =>
console.warn(
kleur.yellow().bold().italic("[WARN] ") + kleur.yellow().bold(arg),
Bun.wrapAnsi(
`\x1b[1;93m[WARN]: \x1b[0;97m${arg}\x1b[0m`,
process.stdout.columns,
),
),
info: (arg: string) =>
console.info(kleur.white().bold().italic("[INFO] ") + kleur.white(arg)),
ok: (arg: string) => console.info(kleur.green().bold(arg)),
enverr: (arg: string) => logger.err(`Please provide a ${arg} in the .env`),
console.info(
Bun.wrapAnsi(
`\x1b[37;1m[INFO]: \x1b[0;97m${arg}\x1b[0m`,
process.stdout.columns,
),
),
ok: (arg: string) =>
console.info(
Bun.wrapAnsi(
`\x1b[1;92m[OK]: \x1b[0;97m${arg}\x1b[0m`,
process.stdout.columns,
),
),
enverr: (arg: string) =>
logger.err(
Bun.wrapAnsi(
`Please provide a \x1b[4;3;93m${arg}\x1b[0;97m in the .env`,
process.stdout.columns,
),
),
};
export default logger;

View File

@@ -0,0 +1,16 @@
import PointRedeem from "pointRedeems";
import { sendMessage } from "lib/commandUtils";
import { playDecTalk } from "web/alerts/serverFunctions";
export default new PointRedeem({
name: "dectalk",
cost: 25000,
title: "Dectalk TTS",
color: "#FF0000",
input: true,
prompt: "I HECKIN LOVE DECTALK TTS!!!",
async execution(msg) {
await playDecTalk(msg.input);
await sendMessage("LETSGO SUFFERING LETSGO");
},
});

View File

@@ -88,7 +88,9 @@ for (const [_, redeem] of Array.from(namedRedeems)) {
backgroundColor: redeem.color,
userInputRequired: redeem.input,
});
logger.ok(`Created custom point redeem ${redeem.title}`);
logger.ok(
`Created custom point redeem \x1b[3;4;1;95m${redeem.title}\x1b[0;97m`,
);
idMap.set(redeem.name, creation.id);
activeRedeems.set(creation.id, redeem);
}
@@ -97,7 +99,7 @@ for (const [_, redeem] of Array.from(namedRedeems)) {
Array.from(currentRedeems).map(async ([title, redeem]) => {
if (process.env.NODE_ENV !== "production") return;
await api.channelPoints.deleteCustomReward(streamerId, redeem);
logger.ok(`Deleted custom point redeem ${title}`);
logger.ok(`Deleted custom point redeem \x1b[3;4;1;95m${title}\x1b[0;97m`);
});
logger.ok("Successfully synced all custom point redeems");
@@ -108,7 +110,7 @@ export async function enableRedeem(redeem: PointRedeem, id: string) {
isEnabled: true,
});
activeRedeems.set(id, redeem);
logger.ok(`Enabled the ${redeem.name} point redeem`);
logger.ok(`Enabled the \x1b[3;4;1;95m${redeem.name}\x1b[0;97m point redeem`);
}
export async function disableRedeem(redeem: PointRedeem, id: string) {
@@ -117,7 +119,7 @@ export async function disableRedeem(redeem: PointRedeem, id: string) {
isEnabled: false,
});
activeRedeems.delete(id);
logger.ok(`Disabled the ${redeem.name} point redeem`);
logger.ok(`Disabled the \x1b[3;4;1;95m${redeem.name}\x1b[0;97m point redeem`);
}
export { activeRedeems, idMap };

View File

@@ -0,0 +1,17 @@
import PointRedeem from "pointRedeems";
import { playAlert } from "web/alerts/serverFunctions";
export default new PointRedeem({
name: "sfxFactorioAlert",
title: "Factorio Building Destroyed",
cost: 100,
color: "#A020F0",
prompt: 'Play the Factorio "Building Destroyed" sound effect',
sfxredeem: true,
execution: async (msg) =>
await playAlert({
name: "sound",
user: msg.userDisplayName,
sound: "factorioalert",
}),
});

View File

@@ -0,0 +1,17 @@
import PointRedeem from "pointRedeems";
import { playAlert } from "web/alerts/serverFunctions";
export default new PointRedeem({
name: "sfxFail",
title: "Fail",
cost: 100,
color: "#A020F0",
prompt: "failure horn or trombone meme sound",
sfxredeem: true,
execution: async (msg) =>
await playAlert({
name: "sound",
user: msg.userDisplayName,
sound: Math.random() > 0.5 ? "fail1" : "fail2",
}),
});

View File

@@ -0,0 +1,26 @@
import PointRedeem from "pointRedeems";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
export default new PointRedeem({
name: "setwelcomemsg",
cost: 15000,
title: "Set welcome message",
input: true,
color: "#0099FF",
prompt:
"Set your welcome message (echoed once per stream). Character limit is 200",
async execution(msg) {
if (msg.input.length > 200) {
await sendMessage(`Your desired welcome message is too long`);
if (process.env.NODE_ENV === "production")
await msg.updateStatus("CANCELED");
}
await Promise.all([
sendMessage(
`${msg.userDisplayName} successfully set their new welcome message`,
),
redis.set(`user:${msg.userId}:welcomemessagetext`, "1"),
]);
},
});

View File

@@ -0,0 +1,14 @@
export type MSTTS = {
type: "microsoft";
text: string;
voice: string;
pitch: string;
speed: string;
};
export type DecTalk = {
type: "dectalk";
text: string;
};
export type TTS = MSTTS | DecTalk;

View File

@@ -11,3 +11,39 @@ export async function playAlert(alert: alert) {
alert,
});
}
type MSTTSOptions = {
text: string;
voice?: string;
pitch?: number;
speed?: number;
};
export async function playMSTTS(options: MSTTSOptions) {
await sendAlertEvent({
function: "playTTS",
tts: {
type: "microsoft",
voice: options.voice ?? "Sam",
pitch: options.pitch ? options.pitch.toString() : "100",
speed: options.speed ? options.speed.toString() : "150",
text: options.text,
},
});
}
export async function playDecTalk(input: string) {
await sendAlertEvent({
function: "playTTS",
tts: {
type: "dectalk",
text: input,
},
});
}
export async function stopTTS() {
await sendAlertEvent({
function: "cancelTTS",
});
}

View File

@@ -1,4 +1,5 @@
import type { serverNotificationEvent } from "web/serverTypes";
import type { TTS } from "./TTSTypes";
type alertBase<name extends string> = {
name: name;
@@ -21,7 +22,13 @@ export type tntExplosionAlert = alertBase<"tntExplosion"> & {
targets: string[];
};
export type soundAlerts = "mrockmadhouse" | "eddiescream" | "ripbozo";
export type soundAlerts =
| "mrockmadhouse"
| "eddiescream"
| "ripbozo"
| "factorioalert"
| "fail1"
| "fail2";
export type soundAlert = alertBase<"sound"> & {
sound: soundAlerts;
@@ -45,4 +52,17 @@ type playAlertEvent = {
alert: alert;
};
export type alertEventData = playAlertEvent | serverNotificationEvent;
type playTTS = {
function: "playTTS";
tts: TTS;
};
type cancelTTS = {
function: "cancelTTS";
};
export type alertEventData =
| playAlertEvent
| playTTS
| cancelTTS
| serverNotificationEvent;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,55 @@
import type { TTS } from "web/alerts/TTSTypes";
class TTSManager {
private busy: boolean;
private ttsQueue: TTS[];
private audio: HTMLAudioElement;
constructor() {
this.busy = false;
this.ttsQueue = [];
this.audio = new Audio();
document.body.appendChild(this.audio);
}
private async playAudio(url: string) {
return new Promise((res) => {
this.audio.src = url;
this.audio.load();
this.audio.play();
this.audio.onended = res;
this.audio.onpause = res;
});
}
private async playTTS(tts: TTS) {
this.busy = true;
switch (tts.type) {
case "microsoft":
await this.playAudio(
`https://tetyys.com/SAPI4/SAPI4?text=${tts.text}&voice=${tts.voice}&pitch=${tts.pitch}&speed=${tts.speed}`,
);
break;
case "dectalk":
await this.playAudio(`http://tts.cyzon.us/tts?text=${tts.text}`);
break;
}
const newTTS = this.ttsQueue.shift();
if (!newTTS) {
this.busy = false;
return;
} else this.playTTS(newTTS);
}
public queueTTS(input: TTS) {
if (this.busy) this.ttsQueue.push(input);
else this.playTTS(input);
}
public endTTS() {
this.audio.pause();
}
}
export default new TTSManager();

View File

@@ -5,7 +5,7 @@ function easeOutQuad(t: number) {
}
export default async function execute(alert: blastinRoulette) {
const audio = new Audio("/alerts/public/mariokartbox.ogg");
const audio = new Audio("/alerts/assets/mariokartbox.ogg");
audio.play();
const div = document.createElement("div");
div.classList.add("blastin-roulette");

View File

@@ -2,18 +2,19 @@ import type { grenadeExplosionAlert } from "web/alerts/types";
import type { AlertRunner } from "./index";
const duration = 1000;
const volume = 0.1;
const volume = 1;
export default async function execute(
alert: grenadeExplosionAlert,
): Promise<AlertRunner> {
const audio = new Audio("/alerts/public/explosion2.ogg");
const audioPath = `/alerts/assets/explosions/factorioexplosion${Math.ceil(Math.random() * 5)}.ogg`;
const audio = new Audio(audioPath);
audio.volume = volume;
const parentDiv = document.createElement("div");
parentDiv.className = "grenadeExplosionAlert";
parentDiv.innerHTML = `
<img src="/alerts/public/getrekt.jpg">
<img src="/alerts/assets/getrekt.jpg">
<span class="thrower">
${alert.user}
</span>

View File

@@ -2,7 +2,7 @@ import type { soundAlert } from "web/alerts/types";
import type { AlertRunner } from "./index";
export default async function execute(alert: soundAlert): Promise<AlertRunner> {
const audio = new Audio(`/alerts/public/${alert.sound}.ogg`);
const audio = new Audio(`/alerts/assets/${alert.sound}.ogg`);
audio.play();
return {
blocking: false,

View File

@@ -24,7 +24,7 @@ export default async function execute(
const video = document.createElement("video");
video.volume = volume;
video.src = "/alerts/public/tnt.mp4";
video.src = "/alerts/assets/tnt.mp4";
video.autoplay = true;
video.height = 800;
video.width = 450;

View File

@@ -2,18 +2,19 @@ import type { userBlastAlert } from "web/alerts/types";
import type { AlertRunner } from "./index";
const duration = 1000;
const volume = 0.1;
const volume = 1;
export default async function execute(
alert: userBlastAlert,
): Promise<AlertRunner> {
const audio = new Audio("/alerts/public/explosion1.ogg");
const audioPath = `/alerts/assets/explosions/factorioexplosion${Math.ceil(Math.random() * 5)}.ogg`;
const audio = new Audio(audioPath);
audio.volume = volume;
const parentDiv = document.createElement("div");
parentDiv.className = "userBlastAlert";
parentDiv.innerHTML = `
<img src="/alerts/public/getrekt.jpg">
<img src="/alerts/assets/getrekt.jpg">
<span class="shooter">
${alert.user}
</span>

View File

@@ -2,7 +2,7 @@ import type { userExecutionAlert } from "web/alerts/types";
import type { AlertRunner } from "./index";
const duration = 3000;
const volume = 0.1;
const volume = 1;
export default async function execute(
alert: userExecutionAlert,
@@ -10,7 +10,7 @@ export default async function execute(
const parentDiv = document.createElement("div");
parentDiv.className = "userExecutionAlert";
parentDiv.innerHTML = `
<img src="/alerts/public/getrekt.jpg">
<img src="/alerts/assets/getrekt.jpg">
<span class="shooter">
${alert.user}
</span>
@@ -56,9 +56,13 @@ export default async function execute(
const randomX = Math.floor(Math.random() * (window.innerWidth - 800));
const randomY = Math.floor(Math.random() * (window.innerHeight - 800));
const audio1 = new Audio("/alerts/public/explosion1.ogg");
const audio2 = new Audio("/alerts/public/explosion2.ogg");
const audio3 = new Audio("/alerts/public/explosion3.ogg");
const audio1 = new Audio(
"/alerts/assets/explosions/factoriolargeexplosion1.ogg",
);
const audio2 = new Audio(
"/alerts/assets/explosions/factoriolargeexplosion1.ogg",
);
const audio3 = new Audio("/alerts/assets/explosions/factorionuke.ogg");
audio1.volume = volume;
audio2.volume = volume;

View File

@@ -1,7 +1,9 @@
import type { alertEventData } from "web/alerts/types";
import type { serverInstruction } from "web/serverTypes";
import alertManager from "./alertManager";
import AlertManager from "./AlertManager";
// @ts-ignore
import "@fontsource/jersey-15";
import TTSManager from "./TTSManager";
const wsAddress = `ws${location.protocol === "https:" ? "s" : ""}://${location.host}`;
@@ -19,7 +21,13 @@ socket.onmessage = (event) => {
const data: alertEventData = JSON.parse(event.data);
switch (data.function) {
case "playAlert":
alertManager.queueAlert(data.alert);
AlertManager.queueAlert(data.alert);
break;
case "playTTS":
TTSManager.queueTTS(data.tts);
break;
case "cancelTTS":
TTSManager.endTTS();
break;
case "serverNotification":
console.log(data.message);

View File

@@ -1,4 +1,6 @@
// @ts-ignore
import "./style.css";
// @ts-ignore
import "@fontsource/jersey-15";
import type { twitchEventData } from "web/chatWidget/websockettypes";

View File

@@ -26,11 +26,14 @@ export default Bun.serve({
"/chat/getEmotes": getExternalEmotes,
"/alerts": alerts,
"/alerts/public/:filename": async (req) => {
const target = req.params.filename;
const file = Bun.file(`${import.meta.dir}/alerts/www/public/${target}`);
"/alerts/assets/*": async (req) => {
const url = new URL(req.url);
const path = url.pathname.slice("/alerts/assets/".length);
const file = Bun.file(`${import.meta.dir}/alerts/www/assets/${path}`);
if (!(await file.exists()))
return new Response(`${target} not found`, { status: 404 });
return new Response(`404: ${url.pathname} not found`, {
status: 404,
});
return new Response(file);
},
"/": async (req, srv) => {

View File

@@ -1,6 +1,7 @@
{
"files": [],
"compilerOptions": {
"noEmit": true,
"baseUrl": "./src",
"paths": {
"lib/*": ["./lib/*"],

View File

@@ -3,7 +3,10 @@
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"lib": ["DOM", "ES2020"]
"moduleResolution": "bundler",
"lib": ["DOM", "ES2022"],
"skipLibCheck": true
},
"include": ["src/web/chatWidget/www/**/*", "src/web/alerts/www/**/*"]
"include": ["src/web/chatWidget/www/**/*", "src/web/alerts/www/**/*"],
"exclude": ["node_modules"]
}