From f8fb2c03178126aa74c06aa1d8d6084063f6b18e Mon Sep 17 00:00:00 2001 From: qwerinope Date: Mon, 15 Sep 2025 01:46:39 +0200 Subject: [PATCH] rework aniv timeouts, fix ghost whisper message, invuln status tweaks --- README.md | 7 +++--- src/commands/getloot.ts | 3 +++ src/commands/useitem.ts | 3 +++ src/events/message.ts | 11 +++++---- src/events/whisper.ts | 3 ++- src/items/grenade.ts | 1 - src/items/tnt.ts | 1 - src/lib/handleAnivMessage.ts | 46 +++++++++++++++++++++++++++++------- src/lib/invuln.ts | 2 ++ 9 files changed, 59 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 655e18f..141ce8b 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,12 @@ Admins have the power to destroy the item economy. Be very careful with admin po ### Invulns Invulns, or invulnerable chatters cannot be shot with items and cannot get hit by explosives. -They can however use items. +When an invuln uses an item or a lootbox they lose their invuln status. The intended use for invulns is for when you need to talk to a chatter, or for using other bots. 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. -Admins can add and remove invulns. +Moderators can add and remove invulns. +On your first message in chat you will recieve 10 minutes of invuln status. ### Commands @@ -40,7 +41,7 @@ A full list of Commands can be found [here](#commands-1) ### Timeouts and 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 10 minutes. +You can only send one message every 5 minutes. Try to bargain for your release with the chatter that shot you, or just call them names. ### Leaderboards diff --git a/src/commands/getloot.ts b/src/commands/getloot.ts index ffeb178..5acffc0 100644 --- a/src/commands/getloot.ts +++ b/src/commands/getloot.ts @@ -4,6 +4,8 @@ import { getUserRecord, updateUserRecord } from "db/dbUser"; import items from "items"; import { buildTimeString } from "lib/dateManager"; import { timeout } from "lib/timeout"; +import { isInvuln, removeInvuln } from "lib/invuln"; +import { streamerUsers } from "main"; const COOLDOWN = 10 * 60 * 1000; // 10 mins (ms) @@ -13,6 +15,7 @@ export default new Command({ usertype: 'chatter', execution: async (msg, user) => { if (!await redis.exists('streamIsLive')) { await sendMessage(`No loot while stream is offline`, msg.messageId); return; }; + if (await isInvuln(msg.chatterId) && !streamerUsers.includes(msg.chatterId)) { await sendMessage(`You're no longer an invuln because used a lootbox.`, msg.messageId); await removeInvuln(msg.chatterId); }; if (await user.itemLock()) { await sendMessage(`Cannot get loot (itemlock)`, msg.messageId); return; }; const userData = await getUserRecord(user); const lastlootbox = Date.parse(userData.lastlootbox); diff --git a/src/commands/useitem.ts b/src/commands/useitem.ts index fedc0fd..9726f4c 100644 --- a/src/commands/useitem.ts +++ b/src/commands/useitem.ts @@ -1,6 +1,8 @@ import { redis } from "bun"; import { Command, sendMessage } from "commands"; import items from "items"; +import { isInvuln, removeInvuln } from "lib/invuln"; +import { streamerUsers } from "main"; export default new Command({ name: 'use', @@ -13,6 +15,7 @@ export default new Command({ const selection = items.get(messagequery[0].toLowerCase()); if (!selection) { await sendMessage(`'${messagequery[0]}' is not an item`, msg.messageId); return; }; if (await redis.sismember('disabledcommands', selection.name)) { await sendMessage(`The ${selection.prettyName} item is disabled`, msg.messageId); return; }; + if (await isInvuln(msg.chatterId) && !streamerUsers.includes(msg.chatterId)) { await sendMessage(`You're no longer an invuln because you used an item.`, msg.messageId); await removeInvuln(msg.chatterId); }; await selection.execute(msg, user); } }); diff --git a/src/events/message.ts b/src/events/message.ts index a78f457..24748c4 100644 --- a/src/events/message.ts +++ b/src/events/message.ts @@ -7,10 +7,11 @@ import { isAdmin } from "lib/admins"; import cheers from "cheers"; import logger from "lib/logger"; import { addMessageToChatWidget } from "web/chatWidget/message"; -import { isInvuln, setTemporaryInvuln } from "lib/invuln"; +import { isInvuln, removeInvuln, setTemporaryInvuln } from "lib/invuln"; import { getUserRecord } from "db/dbUser"; import { createCheerRecord } from "db/dbCheers"; import handleAnivMessage from "lib/handleAnivMessage"; +import { Item } from "items"; eventSub.onChannelChatMessage(streamerId, streamerId, parseChatMessage); @@ -50,8 +51,9 @@ async function handleChatMessage(msg: EventSubChannelChatMessageEvent, user: Use // Parse commands: const selected = selectCommand(msg.messageText); if (!selected) return; - const { cmd: selection, activation } = selected; + const { cmd: selection, activation, isitem } = selected; if (await redis.sismember('disabledcommands', selection.name)) return; + if (isitem && await isInvuln(msg.chatterId) && !streamerUsers.includes(msg.chatterId)) { await sendMessage(`You're no longer an invuln because you used an item.`, msg.messageId); await removeInvuln(msg.chatterId); }; switch (selection.usertype) { case "admin": @@ -80,15 +82,16 @@ async function handleChatMessage(msg: EventSubChannelChatMessageEvent, user: Use type selectedCommand = { cmd: Command; activation: string; + isitem: boolean; }; function selectCommand(message: string): selectedCommand | false { const specialcmdselector = message.trim().toLowerCase().split(' ')[0]!; const specialcmd = specialAliasCommands.get(specialcmdselector); - if (specialcmd) return { cmd: specialcmd, activation: specialcmdselector }; + if (specialcmd) return { cmd: specialcmd, activation: specialcmdselector, isitem: specialcmd instanceof Item }; const commandSelector = message.slice(commandPrefix.length).trim().toLowerCase().split(' ')[0]!; const normalcmd = commands.get(commandSelector); - if (normalcmd) return { cmd: normalcmd, activation: commandPrefix + commandSelector }; + if (normalcmd) return { cmd: normalcmd, activation: commandPrefix + commandSelector, isitem: normalcmd instanceof Item }; return false; }; diff --git a/src/events/whisper.ts b/src/events/whisper.ts index 5646d79..d393ab4 100644 --- a/src/events/whisper.ts +++ b/src/events/whisper.ts @@ -9,10 +9,11 @@ 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}`); - await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, "Message sent. You can send another ghost whisper in 10 minutes."); + 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.`); }; diff --git a/src/items/grenade.ts b/src/items/grenade.ts index 4cd8830..2ba0bc3 100644 --- a/src/items/grenade.ts +++ b/src/items/grenade.ts @@ -30,7 +30,6 @@ export default new Item({ await user.setLock(); await Promise.all([ timeout(target!, `You got hit by ${user.displayName}'s grenade!`, 60), - redis.del(selection), sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s grenade wybuh`), changeItemCount(user, userObj, ITEMNAME), createTimeoutRecord(user, target!, ITEMNAME), diff --git a/src/items/tnt.ts b/src/items/tnt.ts index 017a5f8..dd8ef66 100644 --- a/src/items/tnt.ts +++ b/src/items/tnt.ts @@ -31,7 +31,6 @@ export default new Item({ await getUserRecord(target!); // make sure the user record exist in the database await Promise.all([ timeout(target!, `You got hit by ${user.displayName}'s TNT!`, 60), - redis.del(`user:${targetid}:vulnerable`), sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s TNT wybuh`), createTimeoutRecord(user, target!, ITEMNAME), ]); diff --git a/src/lib/handleAnivMessage.ts b/src/lib/handleAnivMessage.ts index a538fac..dccddc8 100644 --- a/src/lib/handleAnivMessage.ts +++ b/src/lib/handleAnivMessage.ts @@ -5,15 +5,45 @@ import { timeout } from "lib/timeout"; import { sendMessage } from "commands"; import { createAnivTimeoutRecord } from "db/dbAnivTimeouts"; -const ANIVNAMES = ['a_n_e_e_v', 'a_n_i_v', 'a_c_a_c']; +const ANIVNAMES = ['a_n_e_e_v', 'a_n_i_v']; + +type anivMessageStore = { + [key: string]: string; +}; + +type IsAnivMessage = { + isAnivMessage: true; + message: string; + anivbot: string; +}; + +type isNotAnivMessage = { + isAnivMessage: false; +}; + +type anivMessageResult = IsAnivMessage | isNotAnivMessage; + +async function isAnivMessage(message: string): Promise { + const data: anivMessageStore = await redis.get('anivmessages').then(a => a === null ? {} : JSON.parse(a)); + for (const clanker of ANIVNAMES) { + const anivmessage = data[clanker]; + if (!anivmessage) continue; + if (anivmessage === message) return { isAnivMessage: true, message, anivbot: clanker }; + }; + return { isAnivMessage: false }; +}; export default async function handleMessage(msg: EventSubChannelChatMessageEvent, user: User) { if (ANIVNAMES.includes(user.displayName)) { - await redis.set('lastanivmessage', msg.messageText); - await redis.expire('lastanivmessage', 120); // store for 2 minutes - } else if (msg.messageText === await redis.get('lastanivmessage')) await Promise.all([ - timeout(user, 'copied an aniv message', 30), - sendMessage(`${user.displayName} got timed out for copying an a_n_e_e_v message`), - createAnivTimeoutRecord(msg.messageText, user, 30) - ]); + const data: anivMessageStore = await redis.get('anivmessages').then(a => a === null ? {} : JSON.parse(a)); + data[user.displayName] = msg.messageText; + await redis.set('anivmessages', JSON.stringify(data)); + } else { + const data = await isAnivMessage(msg.messageText); + if (data.isAnivMessage) await Promise.all([ + timeout(user, 'copied an aniv message', 30), + sendMessage(`${user.displayName} got timed out for copying an ${data.anivbot} message`), + createAnivTimeoutRecord(msg.messageText, user, 30) + ]); + }; }; diff --git a/src/lib/invuln.ts b/src/lib/invuln.ts index 29a2235..5fe18d9 100644 --- a/src/lib/invuln.ts +++ b/src/lib/invuln.ts @@ -1,4 +1,5 @@ import { redis } from "bun"; +import { streamerUsers } from "main"; export async function getInvulns() { const data = await redis.keys('user:*:invulnerable'); @@ -13,6 +14,7 @@ export async function addInvuln(userid: string) { return await redis.set(`user:${userid}:invulnerable`, '1'); }; export async function removeInvuln(userid: string) { + if (streamerUsers.includes(userid)) return; return await redis.del(`user:${userid}:invulnerable`); }; export async function setTemporaryInvuln(userid: string, duration = 600) {