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, 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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