From 902d6cc6bcd591a56571ee33b5ebf7d79b04eaaa Mon Sep 17 00:00:00 2001 From: qwerinope Date: Fri, 12 Sep 2025 21:07:17 +0200 Subject: [PATCH] add whispering messages, add db connection check, add commands alias --- README.md | 10 +++- src/commands/getcommands.ts | 2 +- src/connectionCheck.ts | 24 ++++++++ src/events/handleSubscriptions.ts | 98 +++++++++++++++++++++++++++++++ src/events/index.ts | 53 +---------------- src/events/whisper.ts | 19 ++++++ src/index.ts | 8 ++- 7 files changed, 159 insertions(+), 55 deletions(-) create mode 100644 src/connectionCheck.ts create mode 100644 src/events/handleSubscriptions.ts create mode 100644 src/events/whisper.ts diff --git a/README.md b/README.md index 55041bb..520fb3d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,12 @@ Not all Commands can be disabled, the `DISABLEABLE` field below shows if they ca A full list of Commands can be found [here](#commands-1) +### Timeouts and whispering messages + +If you've been timed out, you can whisper a message to the chatterbot and it will relay your message to the chat. +You can only send one message every 10 minutes. +Try to bargain for your release with the chatter that shot you, or just call them names. + ### Items and Itemlock Items are commands that can only be used when the chatter has them in their inventory. @@ -73,6 +79,8 @@ The chatterbot is the user that types in chat. They have very minimal required s The streamerbot (not that streamerbot) is the broadcaster. This bot needs them to authenticate as well. This account will be used to perform moderation and watch the chat. +Using one account as both chatterbot and streamerbot hasn't been tested in a long time. There may be broken features. + ## Commands ### Fun commands @@ -105,7 +113,7 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE -|-|-|-|- -`getcommands [enabled/disabled]`|Get a list of all, enabled or disabled commands|anyone|`getcommands` `getc`|:x: +`getcommands [enabled/disabled]`|Get a list of all, enabled or disabled commands|anyone|`commands` `getcommands` `getc`|:x: `getcheers [enabled/disabled]`|Get a list of all, enabled or disabled cheers|anyone|`getcheers` `getcheer`|:x: `gettimeout {target}`|Get the remaining timeout duration of targeted user|anyone|`gettimeout` `gett`|:white_check_mark: `stacking [on/off]`|Check/set if timeouts are stacking. Only admins can set the stacking state|anyone/admins|`stacking`|:x: diff --git a/src/commands/getcommands.ts b/src/commands/getcommands.ts index 109c300..a7bdad8 100644 --- a/src/commands/getcommands.ts +++ b/src/commands/getcommands.ts @@ -4,7 +4,7 @@ import parseCommandArgs from "lib/parseCommandArgs"; export default new Command({ name: 'getcommands', - aliases: ['getcommands', 'getc'], + aliases: ['getcommands', 'getc', 'commands'], usertype: 'chatter', disableable: false, execution: async msg => { diff --git a/src/connectionCheck.ts b/src/connectionCheck.ts new file mode 100644 index 0000000..3308a24 --- /dev/null +++ b/src/connectionCheck.ts @@ -0,0 +1,24 @@ +import pocketbase from "db/connection"; +import { RedisClient } from "bun"; +import logger from "lib/logger"; + +export async function connectionCheck() { + let pbstatus = false; + try { + await pocketbase.health.check().then(a => a.code === 200); + pbstatus = true; + } catch { }; + const tempclient = new RedisClient(undefined, { + connectionTimeout: 100, + maxRetries: 1, + }); + let redisstatus = false; + try { + await tempclient.connect(); + redisstatus = true; + } catch { }; + logger.info(`Currently using the "${process.env.NODE_ENV ?? "production"}" database`); + pbstatus ? logger.ok(`Pocketbase status: good`) : logger.err(`Pocketbase status: bad`); + redisstatus ? logger.ok(`Redis/Valkey status: good`) : logger.err(`Redis/Valkey status: bad`); + if (!pbstatus || !redisstatus) process.exit(1); +}; diff --git a/src/events/handleSubscriptions.ts b/src/events/handleSubscriptions.ts new file mode 100644 index 0000000..c7ee454 --- /dev/null +++ b/src/events/handleSubscriptions.ts @@ -0,0 +1,98 @@ +import { eventSub, chatterEventSub, streamerApi, streamerId, chatterApi, chatterId } from "main"; +import { HelixEventSubSubscription } from "@twurple/api"; +import kleur from "kleur"; +import logger from "lib/logger"; + +// 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); diff --git a/src/events/index.ts b/src/events/index.ts index 38cf30f..8888d75 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -1,27 +1,4 @@ -import kleur from "kleur"; -import { eventSub, streamerApi, streamerId } from "main"; -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)}`); - deleteDuplicateSubscriptions.refresh(); -}); - -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)}`); -}); +import { eventSub, chatterEventSub } from "main"; import { readdir } from 'node:fs/promises'; const files = await readdir(import.meta.dir); @@ -32,30 +9,4 @@ for (const file of files) { }; eventSub.start(); - -import { HelixEventSubSubscription } from "@twurple/api"; - -const deleteDuplicateSubscriptions = setTimeout(async () => { - logger.info('Deleting all double 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 sub: id: ${sub.id}, type: ${sub.type}`); - }; - if (duplicates.length === 0) logger.ok('No duplicate subscriptions found'); - else logger.ok('Deleted all duplicate EventSub subscriptions'); - }); -}, 5000); +chatterEventSub.start(); diff --git a/src/events/whisper.ts b/src/events/whisper.ts new file mode 100644 index 0000000..aacd9ae --- /dev/null +++ b/src/events/whisper.ts @@ -0,0 +1,19 @@ +import { redis } from "bun"; +import { sendMessage } from "commands"; +import { buildTimeString } from "lib/dateManager"; +import { chatterEventSub, chatterApi, chatterId } from "main"; + +const WHISPERCOOLDOWN = 60 * 10; // 10 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) { + await redis.set(`user:${msg.senderUserId}:whispercooldown`, '1'); + await redis.expire(`user:${msg.senderUserId}:whispercooldown`, WHISPERCOOLDOWN); + await sendMessage(`${msg.senderUserDisplayName} whispered: ${msg.messageText}`); + await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, "Message sent. Please wait 10 minutes until you can send another message."); + } else { + await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another message.`); + }; +}); diff --git a/src/index.ts b/src/index.ts index 0024262..91892b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,9 +7,11 @@ import { addInvuln } from "lib/invuln"; import { redis } from "bun"; import { remodMod, timeoutDuration } from "lib/timeout"; import User from "user"; -import { buildTimeString } from "lib/dateManager"; +import { connectionCheck } from "connectionCheck"; -const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot"]; +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"]; export const singleUserMode = process.env.CHATTER_IS_STREAMER === 'true'; @@ -30,6 +32,8 @@ export const streamerApi = streamerAuthProvider ? new ApiClient({ authProvider: /** 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 }); + export const commandPrefix = process.env.COMMAND_PREFIX ?? "!"; export const streamerUsers = [chatterId, streamerId];