add whispering messages, add db connection check, add commands alias

This commit is contained in:
2025-09-12 21:07:17 +02:00
parent aba024b49f
commit 902d6cc6bc
7 changed files with 159 additions and 55 deletions

View File

@@ -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:

View File

@@ -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 => {

24
src/connectionCheck.ts Normal file
View File

@@ -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);
};

View File

@@ -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);

View File

@@ -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();

19
src/events/whisper.ts Normal file
View File

@@ -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.`);
};
});

View File

@@ -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];