rework aniv timeouts, fix ghost whisper message, invuln status tweaks

This commit is contained in:
2025-09-15 01:46:39 +02:00
parent 2fb0670765
commit f8fb2c0317
9 changed files with 59 additions and 18 deletions

View File

@@ -15,11 +15,12 @@ Admins have the power to destroy the item economy. Be very careful with admin po
### Invulns ### Invulns
Invulns, or invulnerable chatters cannot be shot with items and cannot get hit by explosives. 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. 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. 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. 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 ### Commands
@@ -40,7 +41,7 @@ A full list of Commands can be found [here](#commands-1)
### Timeouts and ghost whispers ### 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. 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. Try to bargain for your release with the chatter that shot you, or just call them names.
### Leaderboards ### Leaderboards

View File

@@ -4,6 +4,8 @@ import { getUserRecord, updateUserRecord } from "db/dbUser";
import items from "items"; import items from "items";
import { buildTimeString } from "lib/dateManager"; import { buildTimeString } from "lib/dateManager";
import { timeout } from "lib/timeout"; import { timeout } from "lib/timeout";
import { isInvuln, removeInvuln } from "lib/invuln";
import { streamerUsers } from "main";
const COOLDOWN = 10 * 60 * 1000; // 10 mins (ms) const COOLDOWN = 10 * 60 * 1000; // 10 mins (ms)
@@ -13,6 +15,7 @@ export default new Command({
usertype: 'chatter', usertype: 'chatter',
execution: async (msg, user) => { execution: async (msg, user) => {
if (!await redis.exists('streamIsLive')) { await sendMessage(`No loot while stream is offline`, msg.messageId); return; }; 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; }; if (await user.itemLock()) { await sendMessage(`Cannot get loot (itemlock)`, msg.messageId); return; };
const userData = await getUserRecord(user); const userData = await getUserRecord(user);
const lastlootbox = Date.parse(userData.lastlootbox); const lastlootbox = Date.parse(userData.lastlootbox);

View File

@@ -1,6 +1,8 @@
import { redis } from "bun"; import { redis } from "bun";
import { Command, sendMessage } from "commands"; import { Command, sendMessage } from "commands";
import items from "items"; import items from "items";
import { isInvuln, removeInvuln } from "lib/invuln";
import { streamerUsers } from "main";
export default new Command({ export default new Command({
name: 'use', name: 'use',
@@ -13,6 +15,7 @@ export default new Command({
const selection = items.get(messagequery[0].toLowerCase()); const selection = items.get(messagequery[0].toLowerCase());
if (!selection) { await sendMessage(`'${messagequery[0]}' is not an item`, msg.messageId); return; }; 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 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); await selection.execute(msg, user);
} }
}); });

View File

@@ -7,10 +7,11 @@ import { isAdmin } from "lib/admins";
import cheers from "cheers"; import cheers from "cheers";
import logger from "lib/logger"; import logger from "lib/logger";
import { addMessageToChatWidget } from "web/chatWidget/message"; 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 { getUserRecord } from "db/dbUser";
import { createCheerRecord } from "db/dbCheers"; import { createCheerRecord } from "db/dbCheers";
import handleAnivMessage from "lib/handleAnivMessage"; import handleAnivMessage from "lib/handleAnivMessage";
import { Item } from "items";
eventSub.onChannelChatMessage(streamerId, streamerId, parseChatMessage); eventSub.onChannelChatMessage(streamerId, streamerId, parseChatMessage);
@@ -50,8 +51,9 @@ async function handleChatMessage(msg: EventSubChannelChatMessageEvent, user: Use
// Parse commands: // Parse commands:
const selected = selectCommand(msg.messageText); const selected = selectCommand(msg.messageText);
if (!selected) return; if (!selected) return;
const { cmd: selection, activation } = selected; const { cmd: selection, activation, isitem } = selected;
if (await redis.sismember('disabledcommands', selection.name)) return; 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) { switch (selection.usertype) {
case "admin": case "admin":
@@ -80,15 +82,16 @@ async function handleChatMessage(msg: EventSubChannelChatMessageEvent, user: Use
type selectedCommand = { type selectedCommand = {
cmd: Command; cmd: Command;
activation: string; activation: string;
isitem: boolean;
}; };
function selectCommand(message: string): selectedCommand | false { function selectCommand(message: string): selectedCommand | false {
const specialcmdselector = message.trim().toLowerCase().split(' ')[0]!; const specialcmdselector = message.trim().toLowerCase().split(' ')[0]!;
const specialcmd = specialAliasCommands.get(specialcmdselector); 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 commandSelector = message.slice(commandPrefix.length).trim().toLowerCase().split(' ')[0]!;
const normalcmd = commands.get(commandSelector); 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; return false;
}; };

View File

@@ -9,10 +9,11 @@ chatterEventSub.onUserWhisperMessage(chatterId, async msg => {
if (await redis.ttl(`user:${msg.senderUserId}:timeout`) < 0) return; if (await redis.ttl(`user:${msg.senderUserId}:timeout`) < 0) return;
const cooldown = await redis.expiretime(`user:${msg.senderUserId}:whispercooldown`); const cooldown = await redis.expiretime(`user:${msg.senderUserId}:whispercooldown`);
if (cooldown < 0) { 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.set(`user:${msg.senderUserId}:whispercooldown`, '1');
await redis.expire(`user:${msg.senderUserId}:whispercooldown`, WHISPERCOOLDOWN); await redis.expire(`user:${msg.senderUserId}:whispercooldown`, WHISPERCOOLDOWN);
await sendMessage(`The ghost of ${msg.senderUserDisplayName} whispered: ${msg.messageText}`); 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 { } else {
await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another ghost whisper.`); await chatterApi.whispers.sendWhisper(chatterId, msg.senderUserId, `Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another ghost whisper.`);
}; };

View File

@@ -30,7 +30,6 @@ export default new Item({
await user.setLock(); await user.setLock();
await Promise.all([ await Promise.all([
timeout(target!, `You got hit by ${user.displayName}'s grenade!`, 60), 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`), sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s grenade wybuh`),
changeItemCount(user, userObj, ITEMNAME), changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target!, ITEMNAME), createTimeoutRecord(user, target!, ITEMNAME),

View File

@@ -31,7 +31,6 @@ export default new Item({
await getUserRecord(target!); // make sure the user record exist in the database await getUserRecord(target!); // make sure the user record exist in the database
await Promise.all([ await Promise.all([
timeout(target!, `You got hit by ${user.displayName}'s TNT!`, 60), 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`), sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s TNT wybuh`),
createTimeoutRecord(user, target!, ITEMNAME), createTimeoutRecord(user, target!, ITEMNAME),
]); ]);

View File

@@ -5,15 +5,45 @@ import { timeout } from "lib/timeout";
import { sendMessage } from "commands"; import { sendMessage } from "commands";
import { createAnivTimeoutRecord } from "db/dbAnivTimeouts"; 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<anivMessageResult> {
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) { export default async function handleMessage(msg: EventSubChannelChatMessageEvent, user: User) {
if (ANIVNAMES.includes(user.displayName)) { if (ANIVNAMES.includes(user.displayName)) {
await redis.set('lastanivmessage', msg.messageText); const data: anivMessageStore = await redis.get('anivmessages').then(a => a === null ? {} : JSON.parse(a));
await redis.expire('lastanivmessage', 120); // store for 2 minutes data[user.displayName] = msg.messageText;
} else if (msg.messageText === await redis.get('lastanivmessage')) await Promise.all([ await redis.set('anivmessages', JSON.stringify(data));
timeout(user, 'copied an aniv message', 30), } else {
sendMessage(`${user.displayName} got timed out for copying an a_n_e_e_v message`), const data = await isAnivMessage(msg.messageText);
createAnivTimeoutRecord(msg.messageText, user, 30) 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)
]);
};
}; };

View File

@@ -1,4 +1,5 @@
import { redis } from "bun"; import { redis } from "bun";
import { streamerUsers } from "main";
export async function getInvulns() { export async function getInvulns() {
const data = await redis.keys('user:*:invulnerable'); 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'); return await redis.set(`user:${userid}:invulnerable`, '1');
}; };
export async function removeInvuln(userid: string) { export async function removeInvuln(userid: string) {
if (streamerUsers.includes(userid)) return;
return await redis.del(`user:${userid}:invulnerable`); return await redis.del(`user:${userid}:invulnerable`);
}; };
export async function setTemporaryInvuln(userid: string, duration = 600) { export async function setTemporaryInvuln(userid: string, duration = 600) {