add stats command to readme, implement optional stacking timeouts, fully rework timeout management

This commit is contained in:
2025-07-29 01:36:28 +02:00
parent f9615b77e6
commit cde679e583
7 changed files with 69 additions and 11 deletions

View File

@@ -67,6 +67,8 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
`backshot`|'Backshot' a random previous chatter|anyone|`backshot`|:white_check_mark:
`roulette`|Play russian roulette for a 5 minute timeout|anyone|`roulette`|:white_check_mark:
`timeout {target}`|Times targeted user out for 60 seconds (costs 100 qweribucks)|anyone|`timeout`|:white_check_mark:
`stats [target]`|Get timeout and some item stats for yourself or specified user this month|anyone|`stats` `monthlystats`|:white_check_mark:
`alltime [target]`|Get timeout and some item stats for yourself or specified user of all time|anyone|`alltime` `alltimestats`|:white_check_mark:
### Qweribucks commands
@@ -93,6 +95,7 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE
`getcommands [enabled/disabled]`|Get a list of all, enabled or disabled commands|anyone|`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:
`vulnchatters`|Get amount of chatters vulnerable to explosives|anyone|`vulnchatters` `vulnc`|:white_check_mark:
`disablecommand {command/item}`|Disable a specific command/item|admins|`disablecommand`|:x:
`enablecommand {command/item}`|Re-enable a specific command/item|admins|`enablecommand`|:x:

View File

@@ -1,16 +1,16 @@
import { Command, sendMessage } from "commands";
import { streamerApi, streamerId } from "main";
import { buildTimeString } from "lib/dateManager";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
import { timeoutDuration } from "lib/timeout";
export default new Command('gettimeout', ['gett', 'gettimeout'], 'chatter', async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a target', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
const data = await streamerApi.moderation.getBannedUsers(streamerId, { userId: target.id }).then(a => a.data);
if (!data[0]) { await sendMessage(`Chatter ${target.displayName} isn't timed out`, msg.messageId); return; };
if (data[0].expiryDate) { await sendMessage(`${target.displayName} is still timed out for ${buildTimeString(data[0].expiryDate.getTime(), Date.now())}`, msg.messageId); return; };
const data = await timeoutDuration(target);
if (data === false) { await sendMessage(`Chatter ${target.displayName} isn't timed out`, msg.messageId); return; };
if (data) { await sendMessage(`${target.displayName} is still timed out for ${buildTimeString(data * 1000, Date.now())}`, msg.messageId); return; };
await sendMessage(`${target.displayName} is permanently banned`, msg.messageId);
});

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

@@ -0,0 +1,22 @@
import { redis } from "bun";
import { Command, sendMessage } from "commands";
import { isAdmin } from "lib/admins";
import parseCommandArgs from "lib/parseCommandArgs";
export default new Command('stacking', ['stacking'], 'chatter', async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0] || !await isAdmin(msg.chatterId)) { await sendMessage(`Timeout stacking is currently ${await redis.exists('timeoutStacking') ? "on" : "off"}`, msg.messageId); return; };
// Only admins can reach this part of code
switch (args[0]) {
case 'enable':
case 'on':
await redis.set('timeoutStacking', '1');
await sendMessage('Timeout stacking is now on')
break;
case 'disable':
case 'off':
await redis.del('timeoutStacking');
await sendMessage('Timeout stacking is now off')
break;
};
}, false);

View File

@@ -1,6 +1,9 @@
import { eventSub, streamerId } from "main";
import { deleteBannedUserMessagesFromChatWidget } from "web/chatWidget/message";
import { redis } from "bun";
eventSub.onChannelBan(streamerId, async msg => {
deleteBannedUserMessagesFromChatWidget(msg);
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));
});

6
src/events/unban.ts Normal file
View File

@@ -0,0 +1,6 @@
import { redis } from "bun";
import { eventSub, streamerId } from "main";
eventSub.onChannelUnban(streamerId, async msg => {
await redis.del(`user:${msg.userId}:timeout`);
});

View File

@@ -4,6 +4,7 @@ import { EventSubWsListener } from "@twurple/eventsub-ws";
import { addAdmin } from "lib/admins";
import logger from "lib/logger";
import { addInvuln } from "lib/invuln";
import { redis } from "bun";
const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot"];
const STREAMERINTENTS = ["user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:banned_users", "bits:read", "channel:moderate"];
@@ -31,6 +32,13 @@ export const commandPrefix = process.env.COMMAND_PREFIX ?? "!";
export const streamerUsers = [chatterId, streamerId];
streamerUsers.forEach(async id => await Promise.all([addAdmin(id), addInvuln(id)]));
const banned = await streamerApi.moderation.getBannedUsers(streamerId).then(a => a.data);
banned.forEach(async ban => {
await redis.set(`user:${ban.userId}:timeout`, '1');
if (ban.expiryDate) redis.expire(`user:${ban.userId}:timeout`, Math.floor((ban.expiryDate.getTime() - Date.now()) / 1000));
logger.info(`Set the timeout/ban of ${ban.userDisplayName} in the Redis/Valkey database.`);
});
await import("./events");
await import("./web");

View File

@@ -2,6 +2,7 @@ import { streamerApi, streamerId } from "main";
import logger from "lib/logger";
import User from "user";
import { isInvuln } from "lib/invuln";
import { redis } from "bun";
type SuccessfulTimeout = { status: true; };
type UnSuccessfulTimeout = { status: false; reason: 'banned' | 'unknown' | 'illegal'; };
@@ -14,9 +15,13 @@ type TimeoutResult = SuccessfulTimeout | UnSuccessfulTimeout;
export const timeout = async (user: User, reason: string, duration?: number): Promise<TimeoutResult> => {
if (await isInvuln(user.id)) return { status: false, reason: 'illegal' }; // Don't timeout invulnerable chatters
// Check if user already has a timeout
const banStatus = await streamerApi.moderation.getBannedUsers(streamerId, { userId: user.id }).then(a => a.data);
if (banStatus[0]) return { status: false, reason: 'banned' };
// Check if user already has a timeout and handle stacking
const banStatus = await timeoutDuration(user);
if (banStatus) {
if (await redis.exists('timeoutStacking')) {
if (duration) duration += Math.floor((banStatus * 1000 - Date.now()) / 1000); // the target is timed out and stacking is on
} else return { status: false, reason: 'banned' }; // the target is timed out, but stacking is off
} else if (banStatus === null) return { status: false, reason: 'banned' }; // target is perma banned
if (await streamerApi.moderation.checkUserMod(streamerId, user.id!)) {
if (!duration) duration = 60; // make sure that mods don't get perma-banned
@@ -28,18 +33,21 @@ export const timeout = async (user: User, reason: string, duration?: number): Pr
await streamerApi.moderation.banUser(streamerId, { user: user.id, reason, duration });
} catch (err) {
logger.err(err as string);
return { status: false, reason: 'unknown' }
return { status: false, reason: 'unknown' };
};
await redis.set(`user:${user.id}:timeout`, '1');
if (duration) await redis.expire(`user:${user.id}:timeout`, duration);
return { status: true };
};
/** Give the target mod status back after timeout */
function remodMod(target: User, duration: number) {
setTimeout(async () => {
const bandata = await streamerApi.moderation.getBannedUsers(streamerId, { userId: target.id }).then(a => a.data);
if (bandata[0]) { // If the target is still timed out, try again when new timeout expires
const timeoutleft = Date.parse(bandata[0].expiryDate?.toString()!) - Date.now(); // date when timeout expires - current date
const bandata = await timeoutDuration(target);
if (bandata) { // If the target is still timed out, try again when new timeout expires
const timeoutleft = bandata * 1000 - Date.now(); // date when timeout expires - current date
remodMod(target, timeoutleft); // Call the current function with new time (recursion)
} else {
try {
@@ -48,3 +56,11 @@ function remodMod(target: User, duration: number) {
};
}, duration + 3000); // callback gets called after duration of timeout + 3 seconds
};
/** This returns number if there is a duration of time for the timeout, false if not banned and null if perma banned */
export async function timeoutDuration(user: User): Promise<number | null | false> {
const data = await redis.expiretime(`user:${user.id}:timeout`);
if (data === -1) return null; // Perma banned
else if (data === -2) return false; // Not banned
return data;
};