mirror of
https://github.com/qwerinope/qweribot.git
synced 2025-12-17 02:31:39 +01:00
rework auth (i'm an idiot), add whisper commands, change whispercmds, back to webhook
This commit is contained in:
@@ -5,8 +5,11 @@ CLIENT_ID= # Client_id gotten from the twitch dev console
|
||||
CLIENT_SECRET= # Client_secret gotten from the twitch dev console
|
||||
# REDIRECT_URL= # Redirect URL that has been set in the twitch dev console. Defaults to: http://localhost:3456
|
||||
# REDIRECT_PORT= # Redirect port if the REDIRECT_URL has not been set. Defaults to 3456. This is also the port the bot will listen on to authenticate
|
||||
EVENTSUB_SECRET= # Should be a random string of characters
|
||||
EVENTSUB_HOSTNAME= # https secured URL that the bot is listening on (EVENTSUB_PORT)
|
||||
EVENTSUB_PORT=8081 # TCP port the bot will be listening on for eventsub events
|
||||
# COMMAND_PREFIX= # The prefix which will be used to activate commands. Defaults to '!'. When requiring a space between prefix and command, escape the space with a backslash
|
||||
WEB_PORT= # The port that the chat widget and sound alerts will be served on
|
||||
WEB_PORT=8080 # The port that the chat widget and sound alerts will be served on
|
||||
|
||||
# The Twitch IDs required below can be gotten from this website: https://www.streamweasels.com/tools/convert-twitch-username-to-user-id/
|
||||
|
||||
@@ -32,3 +35,6 @@ DISCORD_TOKEN= # Discord bot token
|
||||
DISCORD_CLIENT_ID= # Discord bot app client id
|
||||
DISCORD_GUILD_ID= # Discord guild (server) where the bot runs
|
||||
DISCORD_CHANNEL_ID= # Discord channel id, in guild, where bot should post announcements
|
||||
|
||||
# Development only
|
||||
# EVENTSUB_NGROK_TOKEN=
|
||||
|
||||
14
README.md
14
README.md
@@ -42,7 +42,13 @@ Commands can have special aliases, these don't require the prefix. Special alias
|
||||
|
||||
A full list of Commands can be found [here](#commands-1)
|
||||
|
||||
### Timeouts and ghost whispers
|
||||
### Whisper commands
|
||||
|
||||
Whisper commands use the same prefix as regular commands, but are whispered to the bot instead.
|
||||
Whisper commands can be found [here](#whisper-commands-1).
|
||||
Unlike regular commands, whisper commands cannot be disabled. This is fine as they don't bother anyone.
|
||||
|
||||
### Ghost whispers
|
||||
|
||||
If you've been timed out, you can ghost whisper a message to the chatterbot and it will relay your message to the chat.
|
||||
You can only send one message every 5 minutes.
|
||||
@@ -186,6 +192,12 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
|
||||
`addadmin {target}`|Adds an admin|streamer/chatterbot|`addadmin`|:x:
|
||||
`removeadmin {target}`|Removes an admin|streamer/chatterbot|`removeadmin`|:x:
|
||||
|
||||
## Whisper commands
|
||||
|
||||
COMMAND|FUNCTION|ALIASES
|
||||
-|-|-
|
||||
`ghostwhisper`|Sends a ghost whisper [(explanation)](#ghost-whispers)|`ghostwhisper` `ghost` `g`
|
||||
|
||||
## Items
|
||||
|
||||
NAME|COMMAND|FUNCTION|ALIASES|COST
|
||||
|
||||
74
bun.lock
74
bun.lock
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "qweribot",
|
||||
@@ -7,12 +8,13 @@
|
||||
"@fontsource/jersey-15": "^5.2.8",
|
||||
"@twurple/api": "7.4.0",
|
||||
"@twurple/auth": "^7.4.0",
|
||||
"@twurple/eventsub-ws": "^7.4.0",
|
||||
"@twurple/eventsub-http": "^7.4.0",
|
||||
"discord.js": "^14.24.0",
|
||||
"drizzle-orm": "^0.44.6",
|
||||
"kleur": "^4.1.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@twurple/eventsub-ngrok": "^7.4.0",
|
||||
"@types/bun": "latest",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"pg": "^8.16.3",
|
||||
@@ -25,20 +27,18 @@
|
||||
"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=="],
|
||||
|
||||
"@d-fischer/connection": ["@d-fischer/connection@9.0.0", "", { "dependencies": { "@d-fischer/isomorphic-ws": "^7.0.0", "@d-fischer/logger": "^4.2.1", "@d-fischer/shared-utils": "^3.5.0", "@d-fischer/typed-event-emitter": "^3.3.0", "@types/ws": "^8.5.4", "tslib": "^2.4.1", "ws": "^8.11.0" } }, "sha512-Mljp/EbaE+eYWfsFXUOk+RfpbHgrWGL/60JkAvjYixw6KREfi5r17XdUiXe54ByAQox6jwgdN2vebdmW1BT+nQ=="],
|
||||
|
||||
"@d-fischer/cross-fetch": ["@d-fischer/cross-fetch@5.0.5", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-symjDUPInTrkfIsZc2n2mo9hiAJLcTJsZkNICjZajEWnWpJ3s3zn50/FY8xpNUAf5w3eFuQii2wxztTGpvG1Xg=="],
|
||||
|
||||
"@d-fischer/detect-node": ["@d-fischer/detect-node@3.0.1", "", {}, "sha512-0Rf3XwTzuTh8+oPZW9SfxTIiL+26RRJ0BRPwj5oVjZFyFKmsj9RGfN2zuTRjOuA3FCK/jYm06HOhwNK+8Pfv8w=="],
|
||||
|
||||
"@d-fischer/isomorphic-ws": ["@d-fischer/isomorphic-ws@7.0.2", "", { "peerDependencies": { "ws": "^8.2.0" } }, "sha512-xK+qIJUF0ne3dsjq5Y3BviQ4M+gx9dzkN+dPP7abBMje4YRfow+X9jBgeEoTe5e+Q6+8hI9R0b37Okkk8Vf0hQ=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -115,6 +115,34 @@
|
||||
|
||||
"@fontsource/jersey-15": ["@fontsource/jersey-15@5.2.8", "", {}, "sha512-JwwDch3yuc2pm33mbmNwlsPRKMjD4jSDTCJk9ooW1+ryJFktQPmXvYBbP5wTAXZqNPaBWSYANibP4dd7CEFYZA=="],
|
||||
|
||||
"@ngrok/ngrok": ["@ngrok/ngrok@0.5.2", "", { "optionalDependencies": { "@ngrok/ngrok-android-arm-eabi": "0.5.2", "@ngrok/ngrok-android-arm64": "0.5.2", "@ngrok/ngrok-darwin-arm64": "0.5.2", "@ngrok/ngrok-darwin-universal": "0.5.2", "@ngrok/ngrok-darwin-x64": "0.5.2", "@ngrok/ngrok-freebsd-x64": "0.5.2", "@ngrok/ngrok-linux-arm-gnueabihf": "0.5.2", "@ngrok/ngrok-linux-arm64-gnu": "0.5.2", "@ngrok/ngrok-linux-arm64-musl": "0.5.2", "@ngrok/ngrok-linux-x64-gnu": "0.5.2", "@ngrok/ngrok-linux-x64-musl": "0.5.2", "@ngrok/ngrok-win32-ia32-msvc": "0.5.2", "@ngrok/ngrok-win32-x64-msvc": "0.5.2" } }, "sha512-IDTLnK93UZlpiN0Ftr5aIXvMADioMEHFcydrvmP27kypHGmW5ww1883TWiASGTPUwBEVtnVqfUtCzbu+NRhyPQ=="],
|
||||
|
||||
"@ngrok/ngrok-android-arm-eabi": ["@ngrok/ngrok-android-arm-eabi@0.5.2", "", { "os": "android", "cpu": "arm" }, "sha512-O8/qxTrtwvOLafnp2dRK2Jjbj7xf7bwTinXiAoEf8Y+/24p3EDCzMqcyFkJS3NuBQU/TiWloER5qCmOK/aX/UQ=="],
|
||||
|
||||
"@ngrok/ngrok-android-arm64": ["@ngrok/ngrok-android-arm64@0.5.2", "", { "os": "android", "cpu": "arm64" }, "sha512-SFFlxHKCHqcJPD/nKzJGibXAtQDWy+R5VuCakvzPmWKer47QQ1B/2kLy7ua4tFEmARGHYWOHGZHzP7mkq73oMA=="],
|
||||
|
||||
"@ngrok/ngrok-darwin-arm64": ["@ngrok/ngrok-darwin-arm64@0.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6OcddF5wioQIuawXh1ONxmIywP5GskVT7H4MeKp5BjN2s9sIr7zhy7JBkwfAXFvNJtqw1dasV6JbYeGWXYCBnQ=="],
|
||||
|
||||
"@ngrok/ngrok-darwin-universal": ["@ngrok/ngrok-darwin-universal@0.5.2", "", { "os": "darwin" }, "sha512-g3Q8qn5Z62m/z9zNxDGCMYVOgMBCXbZmNVpsfmhSBze5Rp1a1mUtloem8FvBeS9LyZYbXpbUbpeo2eJvZjF6Qg=="],
|
||||
|
||||
"@ngrok/ngrok-darwin-x64": ["@ngrok/ngrok-darwin-x64@0.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-/s+R2qGbkYUrzXkNBsrdpftGj80ZTnIzHVsvPsOhnSU0rOcWJ/+/MN/Be0f8AXw7uHBRr+i7smVYW9sSIfroKw=="],
|
||||
|
||||
"@ngrok/ngrok-freebsd-x64": ["@ngrok/ngrok-freebsd-x64@0.5.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-D2TVtW6ug8pFFR6Yrq/Q4XvYslIvQFbouSNyy7wvsZ/ZqSHwXR8XU3rG9D2+QnMG4qRS0DpUcf3qLQPWdWJAVw=="],
|
||||
|
||||
"@ngrok/ngrok-linux-arm-gnueabihf": ["@ngrok/ngrok-linux-arm-gnueabihf@0.5.2", "", { "os": "linux", "cpu": "arm" }, "sha512-3euA0sbSI6+AX8qrpjgpJSjW16IKnFjLtzN9DZo/QLajc237Vq1gc1/8GWD/zI1zT+zquIFR9clq0SRRwO83pQ=="],
|
||||
|
||||
"@ngrok/ngrok-linux-arm64-gnu": ["@ngrok/ngrok-linux-arm64-gnu@0.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-O5nEuB2wIG6IoX6bon7vAnhCqbC54bPfkul5wsNA9+A17GkmsFVwpKs0wI0a2OoskmyG2DRKo9DSXS+AwsJ6Xg=="],
|
||||
|
||||
"@ngrok/ngrok-linux-arm64-musl": ["@ngrok/ngrok-linux-arm64-musl@0.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-BryCXIGzaA3YPJ3OgSbOX3VHlDm7c7tj1dYI3PfQyI2Od7HC1uRUv4FRZgwl+OjY/AckXFm02oV1H6ho+iJqgA=="],
|
||||
|
||||
"@ngrok/ngrok-linux-x64-gnu": ["@ngrok/ngrok-linux-x64-gnu@0.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-6S4m4Tqbf4vxFAFY7b9U2NuhujxcHKWR3lR4Na9aLWR9VBg2E/3Qa0nTvfWk+SGBilU03QELKaT3yYhLz/Y2zw=="],
|
||||
|
||||
"@ngrok/ngrok-linux-x64-musl": ["@ngrok/ngrok-linux-x64-musl@0.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-33tt/nOTUm/QN1xx2jvqQpBSv5tOn2LrU0MXuvoTQFOOr0XwrlqaZguyFhdGoU5J5bPaecw5lfhZb2JH3zJliQ=="],
|
||||
|
||||
"@ngrok/ngrok-win32-ia32-msvc": ["@ngrok/ngrok-win32-ia32-msvc@0.5.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-rJ4+JP6TrWYmw6iaUwQEKoTIxX7vKGWJh9b2RL0ZcRbb8FMTZWY9KoyogkxSZDfx4BuTz2kSe4AMqe5RRhFt6Q=="],
|
||||
|
||||
"@ngrok/ngrok-win32-x64-msvc": ["@ngrok/ngrok-win32-x64-msvc@0.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-FMdljqqhbilwoY0FLb8iEW3179WkAHwb3i2e3U/XrqTlO1nvF8hbjoWLesrLzAICY9wSH5mgqC7i6qxHAA1Neg=="],
|
||||
|
||||
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
|
||||
|
||||
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
|
||||
@@ -131,14 +159,24 @@
|
||||
|
||||
"@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-ws": ["@twurple/eventsub-ws@7.4.0", "", { "dependencies": { "@d-fischer/connection": "^9.0.0", "@d-fischer/logger": "^4.2.1", "@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", "tslib": "^2.0.3" }, "peerDependencies": { "@twurple/api": "7.4.0" } }, "sha512-tA5zGmb2kK4w//V0mK5P9WXl+KwNjkXXRTv3YO8AK9sCObFag9rsqJdrBNNyx6G58NJvQzQIDtHI1U2M9q1h5w=="],
|
||||
"@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-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=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="],
|
||||
|
||||
"@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/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
|
||||
|
||||
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
|
||||
@@ -147,10 +185,14 @@
|
||||
|
||||
"bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="],
|
||||
|
||||
"bytes": ["bytes@3.1.0", "", {}, "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="],
|
||||
|
||||
"discord-api-types": ["discord-api-types@0.38.31", "", {}, "sha512-kC94ANsk8ackj8ENTuO8joTNEL0KtymVhHy9dyEC/s4QAZ7GCx40dYEzQaadyo8w+oP0X8QydE/nzAWRylTGtQ=="],
|
||||
|
||||
"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=="],
|
||||
@@ -167,6 +209,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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
@@ -209,12 +259,20 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.0", "", {}, "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
|
||||
@@ -227,6 +285,8 @@
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"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=="],
|
||||
@@ -241,6 +301,8 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "qweribot",
|
||||
"module": "src/index.ts",
|
||||
"devDependencies": {
|
||||
"@twurple/eventsub-ngrok": "^7.4.0",
|
||||
"@types/bun": "latest",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"pg": "^8.16.3"
|
||||
@@ -25,7 +26,7 @@
|
||||
"@fontsource/jersey-15": "^5.2.8",
|
||||
"@twurple/api": "7.4.0",
|
||||
"@twurple/auth": "^7.4.0",
|
||||
"@twurple/eventsub-ws": "^7.4.0",
|
||||
"@twurple/eventsub-http": "^7.4.0",
|
||||
"discord.js": "^14.24.0",
|
||||
"drizzle-orm": "^0.44.6",
|
||||
"kleur": "^4.1.5"
|
||||
|
||||
45
src/auth.ts
45
src/auth.ts
@@ -50,28 +50,23 @@ async function initAuth(userId: string, clientId: string, clientSecret: string,
|
||||
return tokenData;
|
||||
};
|
||||
|
||||
export async function createAuthProvider(user: string, intents: string[], streamer = false): Promise<RefreshingAuthProvider> {
|
||||
export type authProviderInstructions = {
|
||||
userId: string;
|
||||
intents: string[];
|
||||
streamer: boolean;
|
||||
};
|
||||
|
||||
export async function createAuthProvider(data: authProviderInstructions[]): Promise<RefreshingAuthProvider> {
|
||||
const clientId = process.env.CLIENT_ID;
|
||||
if (!clientId) { logger.enverr("CLIENT_ID"); process.exit(1); };
|
||||
|
||||
const clientSecret = process.env.CLIENT_SECRET;
|
||||
if (!clientSecret) { logger.enverr("CLIENT_SECRET"); process.exit(1); };
|
||||
|
||||
const authRecord = await getAuthRecord(user, intents);
|
||||
|
||||
const token = authRecord ? authRecord.accesstoken : await initAuth(user, clientId, clientSecret, intents, streamer);
|
||||
|
||||
const authData = new RefreshingAuthProvider({
|
||||
clientId,
|
||||
clientSecret
|
||||
});
|
||||
try {
|
||||
await authData.addUserForToken(token, intents);
|
||||
} catch (err) {
|
||||
logger.err(`Failed to setup user auth. Please restart the bot and re-authenticate.`);
|
||||
await deleteAuthRecord(user);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
authData.onRefresh(async (user, token) => {
|
||||
logger.ok(`Successfully refreshed auth for user ${user}`);
|
||||
@@ -82,12 +77,26 @@ export async function createAuthProvider(user: string, intents: string[], stream
|
||||
logger.err(`Failed to refresh auth for user ${user}: ${err.name} ${err.message}`);
|
||||
});
|
||||
|
||||
try {
|
||||
await authData.refreshAccessTokenForUser(user);
|
||||
} catch (err) {
|
||||
logger.err(`Failed to refresh user ${user}. 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);
|
||||
process.exit(1);
|
||||
for (const user of data) {
|
||||
const authRecord = await getAuthRecord(user.userId, user.intents);
|
||||
|
||||
const token = authRecord ? authRecord.accesstoken : await initAuth(user.userId, clientId, clientSecret, user.intents, user.streamer);
|
||||
|
||||
try {
|
||||
await authData.addUserForToken(token, user.intents);
|
||||
} catch (err) {
|
||||
logger.err(`Failed to setup user auth. Please restart the bot and re-authenticate.`);
|
||||
await deleteAuthRecord(user.userId);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
try {
|
||||
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.`);
|
||||
await deleteAuthRecord(user.userId);
|
||||
process.exit(1);
|
||||
};
|
||||
};
|
||||
|
||||
return authData;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { timeout } from "lib/timeout";
|
||||
|
||||
export default new Command({
|
||||
name: 'fakemodme',
|
||||
aliases: ['modme'],
|
||||
aliases: ['modme', 'mod'],
|
||||
usertype: 'chatter',
|
||||
execution: async (_msg, user) => {
|
||||
await Promise.all([
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { streamerId } from "main";
|
||||
import { deleteBannedUserMessagesFromChatWidget } from "web/chatWidget/message";
|
||||
import { eventSub, streamerApi } from "index";
|
||||
import { eventSub, api } from "index";
|
||||
import { redis } from "lib/redis";
|
||||
|
||||
eventSub.onChannelBan(streamerId, async msg => {
|
||||
deleteBannedUserMessagesFromChatWidget(msg);
|
||||
const welcomemessageid = await redis.get(`user:${msg.userId}:welcomemessageid`);
|
||||
if (welcomemessageid) { await streamerApi.moderation.deleteChatMessages(streamerId, welcomemessageid); await redis.del(`user:${msg.userId}:welcomemessageid`); };
|
||||
if (welcomemessageid) { await api.moderation.deleteChatMessages(streamerId, welcomemessageid); await redis.del(`user:${msg.userId}:welcomemessageid`); };
|
||||
await redis.set(`user:${msg.userId}:timeout`, '1');
|
||||
if (msg.endDate) await redis.expire(`user:${msg.userId}:timeout`, Math.floor((msg.endDate.getTime() - Date.now()) / 1000));
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { eventSub } from "index";
|
||||
import { streamerId } from "main";
|
||||
import { chatterId, streamerId } from "main";
|
||||
import { deleteMessageFromChatWidget } from "web/chatWidget/message";
|
||||
|
||||
eventSub.onChannelChatMessageDelete(streamerId, streamerId, async msg => {
|
||||
eventSub.onChannelChatMessageDelete(streamerId, chatterId, async msg => {
|
||||
deleteMessageFromChatWidget(msg);
|
||||
});
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import { streamerId, chatterId } from "main";
|
||||
import { HelixEventSubSubscription } from "@twurple/api";
|
||||
import kleur from "kleur";
|
||||
import logger from "lib/logger";
|
||||
import { chatterApi, chatterEventSub, eventSub, streamerApi } from "index";
|
||||
|
||||
// This file is such a fucking disaster lmaooooo
|
||||
|
||||
eventSub.onRevoke(event => {
|
||||
logger.ok(`Successfully revoked streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionCreateSuccess(event => {
|
||||
logger.ok(`Successfully created streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
deleteDuplicateStreamerSubscriptions.refresh();
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionCreateFailure(event => {
|
||||
logger.err(`Failed to create streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionDeleteSuccess(event => {
|
||||
logger.ok(`Successfully deleted streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionDeleteFailure(event => {
|
||||
logger.err(`Failed to delete streamer EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
chatterEventSub.onRevoke(event => {
|
||||
logger.ok(`Successfully revoked chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
chatterEventSub.onSubscriptionCreateSuccess(event => {
|
||||
logger.ok(`Successfully created chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
deleteDuplicateChatterSubscriptions.refresh();
|
||||
});
|
||||
|
||||
chatterEventSub.onSubscriptionCreateFailure(event => {
|
||||
logger.err(`Failed to create chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
chatterEventSub.onSubscriptionDeleteSuccess(event => {
|
||||
logger.ok(`Successfully deleted chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
chatterEventSub.onSubscriptionDeleteFailure(event => {
|
||||
logger.err(`Failed to delete chatter EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
const deleteDuplicateStreamerSubscriptions = setTimeout(async () => {
|
||||
logger.info('Deleting all double streamer subscriptions');
|
||||
await streamerApi.asUser(streamerId, async tempapi => {
|
||||
const subs = await tempapi.eventSub.getSubscriptionsForStatus("enabled");
|
||||
|
||||
const seen = new Map();
|
||||
const duplicates: HelixEventSubSubscription[] = [];
|
||||
|
||||
for (const sub of subs.data) {
|
||||
if (seen.has(sub.type)) {
|
||||
duplicates.push(sub);
|
||||
} else {
|
||||
seen.set(sub.type, sub);
|
||||
};
|
||||
};
|
||||
|
||||
for (const sub of duplicates) {
|
||||
await tempapi.eventSub.deleteSubscription(sub.id);
|
||||
logger.ok(`Deleted streamer sub: id: ${sub.id}, type: ${sub.type}`);
|
||||
};
|
||||
if (duplicates.length === 0) logger.ok('No duplicate streamer subscriptions found');
|
||||
else logger.ok('Deleted all duplicate streamer EventSub subscriptions');
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
const deleteDuplicateChatterSubscriptions = setTimeout(async () => {
|
||||
logger.info('Deleting all double chatter subscriptions');
|
||||
await chatterApi.asUser(chatterId, async tempapi => {
|
||||
const subs = await tempapi.eventSub.getSubscriptionsForStatus("enabled");
|
||||
|
||||
const seen = new Map();
|
||||
const duplicates: HelixEventSubSubscription[] = [];
|
||||
|
||||
for (const sub of subs.data) {
|
||||
if (seen.has(sub.type)) {
|
||||
duplicates.push(sub);
|
||||
} else {
|
||||
seen.set(sub.type, sub);
|
||||
};
|
||||
};
|
||||
|
||||
for (const sub of duplicates) {
|
||||
await tempapi.eventSub.deleteSubscription(sub.id);
|
||||
logger.ok(`Deleted chatter sub: id: ${sub.id}, type: ${sub.type}`);
|
||||
};
|
||||
if (duplicates.length === 0) logger.ok('No duplicate chatter subscriptions found');
|
||||
else logger.ok('Deleted all duplicate chatter EventSub subscriptions');
|
||||
});
|
||||
}, 10000);
|
||||
@@ -1,4 +1,28 @@
|
||||
import { eventSub, chatterEventSub } from "index";
|
||||
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)}`);
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionCreateSuccess(event => {
|
||||
logger.ok(`Successfully created EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionCreateFailure(event => {
|
||||
logger.err(`Failed to create EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionDeleteSuccess(event => {
|
||||
logger.ok(`Successfully deleted EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
eventSub.onSubscriptionDeleteFailure(event => {
|
||||
logger.err(`Failed to delete EventSub subscription: ${kleur.underline(event.id)}`);
|
||||
});
|
||||
|
||||
await api.eventSub.deleteAllSubscriptions();
|
||||
|
||||
import { readdir } from 'node:fs/promises';
|
||||
const files = await readdir(import.meta.dir);
|
||||
@@ -9,4 +33,3 @@ for (const file of files) {
|
||||
};
|
||||
|
||||
eventSub.start();
|
||||
chatterEventSub.start();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base"
|
||||
import { streamerId, commandPrefix, streamerUsers } from "main";
|
||||
import { streamerId, commandPrefix, streamerUsers, chatterId } from "main";
|
||||
import User from "user";
|
||||
import commands, { specialAliasCommands } from "commands";
|
||||
import { Command, sendMessage } from "lib/commandUtils";
|
||||
@@ -15,7 +15,7 @@ import { Item } from "items";
|
||||
import { eventSub } from "index";
|
||||
import { redis } from "lib/redis";
|
||||
|
||||
eventSub.onChannelChatMessage(streamerId, streamerId, parseChatMessage);
|
||||
eventSub.onChannelChatMessage(streamerId, chatterId, parseChatMessage);
|
||||
|
||||
async function parseChatMessage(msg: EventSubChannelChatMessageEvent) {
|
||||
addMessageToChatWidget(msg);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { sendMessage } from "lib/commandUtils";
|
||||
import { getUserRecord } from "db/dbUser";
|
||||
import { eventSub, streamerApi } from "index";
|
||||
import { eventSub, api } from "index";
|
||||
import { changeItemCount } from "items";
|
||||
import logger from "lib/logger";
|
||||
import { streamerId } from "main";
|
||||
@@ -13,10 +13,9 @@ eventSub.onChannelRaidTo(streamerId, async msg => {
|
||||
await redis.expire(`user:${msg.raidingBroadcasterId}:recentraid`, 60 * 30); // raid cooldown is 30 minutes
|
||||
await sendMessage(`Ty for raiding ${msg.raidingBroadcasterDisplayName}. You get 3 pieces of TNT. Enjoy!`);
|
||||
try {
|
||||
await streamerApi.chat.shoutoutUser(streamerId, msg.raidingBroadcasterId);
|
||||
await api.chat.shoutoutUser(streamerId, msg.raidingBroadcasterId);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to give automatic shoutout to ${msg.raidingBroadcasterDisplayName}`);
|
||||
logger.warn(e as string);
|
||||
};
|
||||
const raider = await User.initUsername(msg.raidingBroadcasterName);
|
||||
const result = await changeItemCount(raider!, await getUserRecord(raider!), 'tnt', 3);
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import { sendMessage } from "lib/commandUtils";
|
||||
import { chatterApi, chatterEventSub } from "index";
|
||||
import { api, eventSub } from "index";
|
||||
import { buildTimeString } from "lib/dateManager";
|
||||
import { chatterId } from "main";
|
||||
import { chatterId, commandPrefix } from "main";
|
||||
import { redis } from "lib/redis";
|
||||
|
||||
const WHISPERCOOLDOWN = 60 * 5; // 5 minutes
|
||||
|
||||
chatterEventSub.onUserWhisperMessage(chatterId, async msg => {
|
||||
if (await redis.ttl(`user:${msg.senderUserId}:timeout`) < 0) return;
|
||||
const cooldown = await redis.expiretime(`user:${msg.senderUserId}:whispercooldown`);
|
||||
if (cooldown < 0) {
|
||||
if (msg.messageText.length > 200) { await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Message too long. Please send a shorter one.`); return; };
|
||||
await redis.set(`user:${msg.senderUserId}:whispercooldown`, '1');
|
||||
await redis.expire(`user:${msg.senderUserId}:whispercooldown`, WHISPERCOOLDOWN);
|
||||
await sendMessage(`The ghost of ${msg.senderUserDisplayName} whispered: ${msg.messageText.replaceAll(/cheer[0-9]+/gi, '')}`);
|
||||
await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Message sent. You can send another ghost whisper in ${Math.floor(WHISPERCOOLDOWN / 60)} minutes.`);
|
||||
} else {
|
||||
await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another ghost whisper.`);
|
||||
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`); return; };
|
||||
const cmd = msg.messageText.slice(commandPrefix.length).trim().toLowerCase().split(' ')[0]!;
|
||||
|
||||
switch (cmd) {
|
||||
case 'help':
|
||||
case 'h':
|
||||
await whisper(msg.senderUserId, `All whisper commands can be found here: https://github.com/qwerinope/qweribot#whisper-commands-1`);
|
||||
break;
|
||||
case 'ghostwhisper':
|
||||
case 'ghost':
|
||||
case 'g':
|
||||
if (await redis.ttl(`user:${msg.senderUserId}:timeout`) < 0) { await whisper(msg.senderUserId, 'Cannot send ghost whisper while not timed out'); return; };
|
||||
const cooldown = await redis.expiretime(`user:${msg.senderUserId}:whispercooldown`);
|
||||
if (cooldown < 0) {
|
||||
if (msg.messageText.length > 200) { await whisper(msg.senderUserId, `Message too long. Please send a shorter one.`); return; };
|
||||
await redis.set(`user:${msg.senderUserId}:whispercooldown`, '1');
|
||||
await redis.expire(`user:${msg.senderUserId}:whispercooldown`, WHISPERCOOLDOWN);
|
||||
await sendMessage(`The ghost of ${msg.senderUserDisplayName} whispered: ${msg.messageText.split(' ').slice(1).join(' ').replaceAll(/cheer[0-9]+/gi, '')}`);
|
||||
await whisper(msg.senderUserId, `Message sent. You can send another ghost whisper in ${Math.floor(WHISPERCOOLDOWN / 60)} minutes.`);
|
||||
} else {
|
||||
await whisper(msg.senderUserId, `Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another ghost whisper.`);
|
||||
};
|
||||
break;
|
||||
};
|
||||
});
|
||||
|
||||
const whisper = async (target: string, message: string) => await api.whispers.sendWhisper(chatterId, target, message);
|
||||
|
||||
85
src/index.ts
85
src/index.ts
@@ -3,28 +3,10 @@ import { ApiClient } from "@twurple/api";
|
||||
import { connectionCheck } from "connectionCheck";
|
||||
import logger from "lib/logger";
|
||||
import { redis } from "lib/redis";
|
||||
import { createAuthProvider } from "auth";
|
||||
import { EventSubWsListener } from "@twurple/eventsub-ws";
|
||||
import { createAuthProvider, type authProviderInstructions } from "auth";
|
||||
import { user, password, database, host } from "db/connection";
|
||||
|
||||
await connectionCheck();
|
||||
|
||||
const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot", "user:manage:whispers"];
|
||||
const STREAMERINTENTS = ["channel:bot", "user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:chat_messages", "moderator:manage:banned_users", "bits:read", "channel:moderate", "moderator:manage:shoutouts", "channel:read:subscriptions", "channel:manage:redemptions"];
|
||||
|
||||
export const chatterAuthProvider = await createAuthProvider(chatterId, singleUserMode ? CHATTERINTENTS.concat(STREAMERINTENTS) : CHATTERINTENTS);
|
||||
export const streamerAuthProvider = singleUserMode ? undefined : await createAuthProvider(streamerId, STREAMERINTENTS, true);
|
||||
|
||||
/** chatterApi should be used for sending messages, retrieving user data, etc */
|
||||
export const chatterApi = new ApiClient({ authProvider: chatterAuthProvider });
|
||||
|
||||
/** streamerApi should be used for: adding/removing mods, managing timeouts, etc. */
|
||||
export const streamerApi = streamerAuthProvider ? new ApiClient({ authProvider: streamerAuthProvider }) : chatterApi; // if there is no streamer user, use the chatter user
|
||||
|
||||
/** As the streamerApi has either the streamer or the chatter if the chatter IS the streamer this has streamer permissions */
|
||||
export const eventSub = new EventSubWsListener({ apiClient: streamerApi });
|
||||
|
||||
export const chatterEventSub = singleUserMode ? eventSub : new EventSubWsListener({ apiClient: chatterApi });
|
||||
import { ConnectionAdapter, EventSubHttpListener, ReverseProxyAdapter } from "@twurple/eventsub-http";
|
||||
import { NgrokAdapter } from "@twurple/eventsub-ngrok";
|
||||
|
||||
if (chatterId === "") { logger.enverr('CHATTER_ID'); process.exit(1); };
|
||||
if (streamerId === "") { logger.enverr('STREAMER_ID'); process.exit(1); };
|
||||
@@ -33,6 +15,59 @@ if (!password) { logger.enverr("POSTGRES_USER"); process.exit(1); };
|
||||
if (!database) { logger.enverr("POSTGRES_DB"); process.exit(1); };
|
||||
if (!host) { logger.enverr("POSTGRES_HOST"); process.exit(1); };
|
||||
|
||||
const eventSubHostName = process.env.EVENTSUB_HOSTNAME ?? (() => {
|
||||
logger.enverr('EVENTSUB_HOSTNAME');
|
||||
process.exit(1);
|
||||
})();
|
||||
|
||||
const eventSubPort = process.env.EVENTSUB_PORT ?? (() => {
|
||||
logger.enverr('EVENTSUB_PORT');
|
||||
process.exit(1);
|
||||
})();
|
||||
|
||||
const eventSubSecret = process.env.EVENTSUB_SECRET ?? (() => {
|
||||
logger.enverr('EVENTSUB_SECRET');
|
||||
process.exit(1);
|
||||
})();
|
||||
|
||||
const eventSubPath = process.env.EVENTSUB_PATH;
|
||||
|
||||
await connectionCheck();
|
||||
|
||||
const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot", "user:manage:whispers"];
|
||||
const STREAMERINTENTS = ["channel:bot", "user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:chat_messages", "moderator:manage:banned_users", "bits:read", "channel:moderate", "moderator:manage:shoutouts", "channel:read:subscriptions", "channel:manage:redemptions"];
|
||||
|
||||
const users: authProviderInstructions[] = [
|
||||
{
|
||||
userId: streamerId,
|
||||
intents: singleUserMode ? CHATTERINTENTS.concat(STREAMERINTENTS) : STREAMERINTENTS,
|
||||
streamer: true
|
||||
}
|
||||
];
|
||||
|
||||
if (!singleUserMode) users.push({
|
||||
userId: chatterId,
|
||||
intents: CHATTERINTENTS,
|
||||
streamer: false
|
||||
});
|
||||
|
||||
const adapter: ConnectionAdapter = process.env.NODE_ENV === 'development' ? new NgrokAdapter({
|
||||
ngrokConfig: {
|
||||
authtoken: process.env.EVENTSUB_NGROK_TOKEN ?? (() => {
|
||||
logger.enverr('EVENTSUB_NGROK_TOKEN');
|
||||
process.exit(1);
|
||||
})()
|
||||
}
|
||||
}) : new ReverseProxyAdapter({
|
||||
pathPrefix: eventSubPath, hostName: eventSubHostName, port: parseInt(eventSubPort)
|
||||
});
|
||||
|
||||
const authProvider = await createAuthProvider(users);
|
||||
|
||||
export const api = new ApiClient({ authProvider });
|
||||
|
||||
export const eventSub = new EventSubHttpListener({ apiClient: api, secret: eventSubSecret, adapter });
|
||||
|
||||
if (!singleUserMode) await redis.set(`user:${chatterId}:bot`, '1');
|
||||
|
||||
import { addAdmin } from "lib/admins";
|
||||
@@ -42,7 +77,7 @@ import { remodMod, timeoutDuration } from "lib/timeout";
|
||||
|
||||
streamerUsers.forEach(async id => await Promise.all([addAdmin(id), addInvuln(id), redis.set(`user:${id}:mod`, '1')]));
|
||||
|
||||
const banned = await streamerApi.moderation.getBannedUsers(streamerId).then(a => a.data);
|
||||
const banned = await api.moderation.getBannedUsers(streamerId).then(a => a.data);
|
||||
for (const ban of banned) {
|
||||
await redis.set(`user:${ban.userId}:timeout`, '1');
|
||||
const banlength = ban.expiryDate;
|
||||
@@ -52,7 +87,7 @@ for (const ban of banned) {
|
||||
};
|
||||
};
|
||||
|
||||
const mods = await streamerApi.moderation.getModerators(streamerId).then(a => a.data);
|
||||
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.`);
|
||||
@@ -68,7 +103,7 @@ for (const remod of bannedmods) {
|
||||
logger.info(`Set the remod timer for ${target?.displayName} to ${duration} seconds.`);
|
||||
};
|
||||
|
||||
const subs = await streamerApi.subscriptions.getSubscriptions(streamerId).then(a => a.data);
|
||||
const subs = await api.subscriptions.getSubscriptions(streamerId).then(a => a.data);
|
||||
const redisSubs = await redis.keys('user:*:subbed').then(a => a.map(b => b.slice(5, -7)));
|
||||
for (const sub of subs) {
|
||||
if (redisSubs.includes(sub.userId)) {
|
||||
@@ -81,7 +116,7 @@ for (const sub of subs) {
|
||||
|
||||
redisSubs.map(async a => await redis.del(`user:${a}:subbed`));
|
||||
|
||||
const streamdata = await streamerApi.streams.getStreamByUserId(streamerId);
|
||||
const streamdata = await api.streams.getStreamByUserId(streamerId);
|
||||
if (streamdata) await redis.set('streamIsLive', '1');
|
||||
|
||||
await import("./events");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
|
||||
import User from "user";
|
||||
import { chatterId, streamerId } from "main";
|
||||
import { api } from "index";
|
||||
|
||||
export type userType = 'chatter' | 'admin' | 'streamer' | 'moderator';
|
||||
|
||||
@@ -37,10 +38,9 @@ export class Command {
|
||||
|
||||
/** Helper function to send a message to the stream */
|
||||
export const sendMessage = async (message: string, replyParentMessageId?: string) => {
|
||||
const chatterApi = await import("index").then(m => m.chatterApi);
|
||||
try {
|
||||
return await chatterApi.chat.sendChatMessageAsApp(chatterId, streamerId, message, { replyParentMessageId });
|
||||
return await api.chat.sendChatMessageAsApp(chatterId, streamerId, message, { replyParentMessageId });
|
||||
} catch (e) {
|
||||
return await chatterApi.chat.sendChatMessageAsApp(chatterId, streamerId, message);
|
||||
return await api.chat.sendChatMessageAsApp(chatterId, streamerId, message);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { streamerId } from "main";
|
||||
import logger from "lib/logger";
|
||||
import User from "user";
|
||||
import { isInvuln } from "lib/invuln";
|
||||
import { streamerApi } from "index";
|
||||
import { api } from "index";
|
||||
import { redis } from "lib/redis";
|
||||
|
||||
type SuccessfulTimeout = { status: true; };
|
||||
@@ -28,11 +28,11 @@ export const timeout = async (user: User, reason: string, duration?: number): Pr
|
||||
if (!duration) duration = 60; // make sure that mods don't get perma-banned
|
||||
await redis.set(`user:${user.id}:remod`, '1');
|
||||
remodMod(user, duration);
|
||||
await streamerApi.moderation.removeModerator(streamerId, user.id!);
|
||||
await api.moderation.removeModerator(streamerId, user.id!);
|
||||
};
|
||||
|
||||
try {
|
||||
await streamerApi.moderation.banUser(streamerId, { user: user.id, reason, duration });
|
||||
await api.moderation.banUser(streamerId, { user: user.id, reason, duration });
|
||||
} catch (err) {
|
||||
logger.err(err as string);
|
||||
return { status: false, reason: 'unknown' };
|
||||
@@ -54,7 +54,7 @@ export function remodMod(target: User, duration: number) {
|
||||
remodMod(target, timeoutleft); // Call the current function with new time (recursion)
|
||||
} else {
|
||||
try {
|
||||
await streamerApi.moderation.addModerator(streamerId, target.id);
|
||||
await api.moderation.addModerator(streamerId, target.id);
|
||||
await redis.del(`user:${target.id}:remod`);
|
||||
} catch (err) { }; // This triggers when the timeout got shortened. try/catch so no runtime error
|
||||
};
|
||||
|
||||
@@ -56,10 +56,10 @@ const idMap = new Map<string, string>;
|
||||
|
||||
import { streamerId } from "main";
|
||||
import logger from "lib/logger";
|
||||
import { streamerApi } from "index";
|
||||
import { api } from "index";
|
||||
|
||||
const currentRedeems = new Map<string, string>;
|
||||
await streamerApi.channelPoints.getCustomRewards(streamerId).then(a => a.map(b => currentRedeems.set(b.title, b.id)));
|
||||
await api.channelPoints.getCustomRewards(streamerId).then(a => a.map(b => currentRedeems.set(b.title, b.id)));
|
||||
for (const [_, redeem] of Array.from(namedRedeems)) {
|
||||
const selection = currentRedeems.get(redeem.title);
|
||||
if (selection) {
|
||||
@@ -68,7 +68,7 @@ for (const [_, redeem] of Array.from(namedRedeems)) {
|
||||
activeRedeems.set(selection, redeem);
|
||||
} else {
|
||||
if (process.env.NODE_ENV !== 'production') continue; // If created with dev-app we won't be able to change it with prod app
|
||||
const creation = await streamerApi.channelPoints.createCustomReward(streamerId, {
|
||||
const creation = await api.channelPoints.createCustomReward(streamerId, {
|
||||
title: redeem.title,
|
||||
prompt: redeem.prompt,
|
||||
cost: redeem.cost,
|
||||
@@ -82,14 +82,14 @@ for (const [_, redeem] of Array.from(namedRedeems)) {
|
||||
|
||||
Array.from(currentRedeems).map(async ([title, redeem]) => {
|
||||
if (process.env.NODE_ENV !== 'production') return;
|
||||
await streamerApi.channelPoints.deleteCustomReward(streamerId, redeem); logger.ok(`Deleted custom point redeem ${title}`);
|
||||
await api.channelPoints.deleteCustomReward(streamerId, redeem); logger.ok(`Deleted custom point redeem ${title}`);
|
||||
});
|
||||
|
||||
logger.ok("Successfully synced all custom point redeems");
|
||||
|
||||
export async function enableRedeem(redeem: PointRedeem, id: string) {
|
||||
if (process.env.NODE_ENV !== 'production') return;
|
||||
await streamerApi.channelPoints.updateCustomReward(streamerId, id, {
|
||||
await api.channelPoints.updateCustomReward(streamerId, id, {
|
||||
isEnabled: true
|
||||
});
|
||||
activeRedeems.set(id, redeem);
|
||||
@@ -98,7 +98,7 @@ export async function enableRedeem(redeem: PointRedeem, id: string) {
|
||||
|
||||
export async function disableRedeem(redeem: PointRedeem, id: string) {
|
||||
if (process.env.NODE_ENV !== 'production') return;
|
||||
await streamerApi.channelPoints.updateCustomReward(streamerId, id, {
|
||||
await api.channelPoints.updateCustomReward(streamerId, id, {
|
||||
isEnabled: false
|
||||
});
|
||||
activeRedeems.delete(id);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { redis } from "lib/redis";
|
||||
import { chatterApi } from "index";
|
||||
import { api } from "index";
|
||||
import { HelixUser } from "@twurple/api"
|
||||
import logger from "lib/logger";
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class User {
|
||||
userObj.username = username;
|
||||
const userid = await redis.get(`userlookup:${username}`);
|
||||
if (!userid) {
|
||||
const userdata = await chatterApi.users.getUserByName(username);
|
||||
const userdata = await api.users.getUserByName(username);
|
||||
if (!userdata) return null;
|
||||
userObj._setCache(userdata);
|
||||
userObj.id = userdata.id;
|
||||
@@ -50,7 +50,7 @@ export default class User {
|
||||
const userObj = new User();
|
||||
userObj.id = userId;
|
||||
if (!await redis.exists(`user:${userId}:displayName`)) {
|
||||
const userdata = await chatterApi.users.getUserById(userId);
|
||||
const userdata = await api.users.getUserById(userId);
|
||||
if (!userdata) return null;
|
||||
userObj._setCache(userdata);
|
||||
userObj.username = userdata.name;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { streamerId } from "main";
|
||||
import { chatterApi } from "index";
|
||||
import { api } from "index";
|
||||
import { redis } from "lib/redis";
|
||||
|
||||
type badgeObject = {
|
||||
@@ -16,8 +16,8 @@ export async function getBadges() {
|
||||
const redisdata = await redis.get('chatwidget:badges');
|
||||
if (redisdata) return new Response(redisdata);
|
||||
|
||||
const globalBadges = chatterApi.chat.getGlobalBadges();
|
||||
const channelBadges = chatterApi.chat.getChannelBadges(streamerId);
|
||||
const globalBadges = api.chat.getGlobalBadges();
|
||||
const channelBadges = api.chat.getChannelBadges(streamerId);
|
||||
const rawBadges = await Promise.all([globalBadges, channelBadges]);
|
||||
|
||||
const newObj: badgeObject = {};
|
||||
|
||||
@@ -27,10 +27,10 @@ export default Bun.serve({
|
||||
if (req.headers.get('Upgrade') === "websocket") { srv.upgrade(req); return; };
|
||||
const streamer = await User.initUserId(streamerId);
|
||||
return Response.redirect(`https://twitch.tv/${streamer?.username}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
websocket: {
|
||||
message(ws, omessage) {
|
||||
async message(ws, omessage) {
|
||||
try {
|
||||
const message = JSON.parse(omessage.toString()) as serverInstruction;
|
||||
if (!message.type) return;
|
||||
@@ -42,7 +42,7 @@ export default Bun.serve({
|
||||
ws.send(JSON.stringify({
|
||||
function: 'serverNotification',
|
||||
message: `Successfully subscribed to ${target} events`
|
||||
} as serverNotificationEvent)); // Both alerts and chatwidget eventsub subscriptions have the notification field
|
||||
} as serverNotificationEvent)); // Both alerts and chatwidget subscriptions have the notification field
|
||||
break;
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -56,7 +56,7 @@ export default Bun.serve({
|
||||
},
|
||||
development: process.env.NODE_ENV === "development",
|
||||
error(error) {
|
||||
logger.err(`Error at chatwidget server: ${error}`);
|
||||
logger.err(`Error at fullstack web server: ${error}`);
|
||||
return new Response("Internal Server Error", { status: 500 })
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user