diff --git a/README.md b/README.md index 49e7c9d..b3ae0f2 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,13 @@ If you've been timed out, you can ghost whisper a message to the chatterbot and 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. +### Aniv timeouts + +When chatter `a_n_i_v` (a_normal_imyt_viewer) or `a_n_e_e_v` sends a message it's stored in the database. +If then someone copies them, they might get timed out. +These timeouts and dodges are stored. You can get your stats with the `anivtimeouts` command. +The current dodge rate is `1/2`, and the timeout duration range is between `30` and `60` seconds. + ### Leaderboards There are 3 types of leaderboards: monthlyKD, alltimeKD and qbucks. @@ -126,6 +133,7 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE `monthlyleaderboard`|Get the K/D leaderboard for this month [(info)](#leaderboards)|anyone|`monthlyleaderboard` `kdleaderboard` `leaderboard`|:white_check_mark: `alltimeleaderboard`|Get the K/D leaderboard of all time [(info)](#leaderboards)|anyone|`alltimeleaderboard` `alltimekdleaderboard`|:white_check_mark: `qbucksleaderboard`|Get the current qbucks leaderboard [(info)](#leaderboards)|anyone|`qbucksleaderboard` `moneyleaderboard` `baltop`|:white_check_mark: +`anivtimeouts`|Get the amount of timeouts, dodges and dodge percentage from aniv timeouts [(info)](#aniv-timeouts)|anyone|`anivtimeouts` `anivtimeout`|:white_check_mark: ### Qweribucks/Item commands @@ -174,8 +182,8 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE NAME|COMMAND|FUNCTION|ALIASES|COST -|-|-|-|- Blaster|`blaster {target}`|Times targeted user out for 60 seconds|`blaster` `blast`|100 -Silver Bullet|`silverbullet {target}`|Times targeted user out for 30 minutes|`silverbullet` `execute` `{blastin}`|666 Grenade|`grenade`|Times a random vulnerable chatter out for 60 seconds|`grenade`|99 +Silver Bullet|`silverbullet {target}`|Times targeted user out for 30 minutes|`silverbullet` `execute` `{blastin}`|666 TNT|`tnt`|Give 5-10 random chatters 60 second timeouts|`tnt`|1000 ## Cheers diff --git a/src/commands/anivtimeouts.ts b/src/commands/anivtimeouts.ts new file mode 100644 index 0000000..6163a91 --- /dev/null +++ b/src/commands/anivtimeouts.ts @@ -0,0 +1,19 @@ +import { Command, sendMessage } from "commands"; +import { getAnivTimeouts } from "db/dbAnivTimeouts"; +import parseCommandArgs from "lib/parseCommandArgs"; +import User from "user"; + +export default new Command({ + name: 'anivtimeouts', + aliases: ['anivtimeouts', 'anivtimeout'], + usertype: 'chatter', + execution: async (msg, user) => { + const args = parseCommandArgs(msg.messageText); + const target = args[0] ? await User.initUsername(args[0].toLowerCase()) : user; + if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist!`, msg.messageId); return; }; + const { dodge, dead } = await getAnivTimeouts(target); + const percentage = ((dodge / (dead + dodge)) * 100); + const message = `Aniv timeouts of ${target.displayName}: Dodge: ${dodge}, Timeout: ${dead}. Dodge percentage: ${isNaN(percentage) ? "0" : percentage.toFixed(1)}%`; + await sendMessage(message, msg.messageId); + } +}); diff --git a/src/db/dbAnivTimeouts.ts b/src/db/dbAnivTimeouts.ts index 579e269..f457115 100644 --- a/src/db/dbAnivTimeouts.ts +++ b/src/db/dbAnivTimeouts.ts @@ -2,12 +2,31 @@ import db from "db/connection"; import User from "user"; import { anivTimeouts } from "db/schema"; import { type anivBots } from "lib/handleAnivMessage"; +import { count, eq, and } from "drizzle-orm"; +/** To create a dodge record, set the duration to 0 */ export async function createAnivTimeoutRecord(message: string, anivBot: anivBots, user: User, duration: number) { await db.insert(anivTimeouts).values({ message, anivBot, user: parseInt(user.id), - duration + duration, + timeout: duration !== 0 }); }; + +export async function getAnivTimeouts(user: User) { + let [dodge, dead] = await Promise.all([ + db.select({ + dodge: count() + }).from(anivTimeouts).where(and(eq(anivTimeouts.user, parseInt(user.id)), eq(anivTimeouts.timeout, false))).then(a => a[0]?.dodge), + db.select({ + dead: count() + }).from(anivTimeouts).where(and(eq(anivTimeouts.user, parseInt(user.id)), eq(anivTimeouts.timeout, true))).then(a => a[0]?.dead) + ]); + + if (!dodge) dodge = 0; + if (!dead) dead = 0; + + return { dodge, dead }; +}; diff --git a/src/db/schema.ts b/src/db/schema.ts index c9d5b61..75be4b3 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,6 +1,6 @@ import type { AccessToken } from "@twurple/auth"; import type { inventory, items } from "items"; -import { integer, jsonb, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; +import { boolean, integer, jsonb, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; import type { anivBots } from "lib/handleAnivMessage"; import { relations } from "drizzle-orm"; @@ -95,8 +95,9 @@ export const anivTimeouts = pgTable('anivTimeouts', { user: integer().notNull().references(() => users.id), message: varchar().notNull(), anivBot: varchar().$type().notNull(), - duration: integer().notNull(), - created: timestamp().defaultNow().notNull() + duration: integer(), + created: timestamp().defaultNow().notNull(), + timeout: boolean().default(true) }); export const anivTimeoutsRelations = relations(anivTimeouts, ({ one }) => ({ diff --git a/src/events/message.ts b/src/events/message.ts index 8993a9a..9360b41 100644 --- a/src/events/message.ts +++ b/src/events/message.ts @@ -1,5 +1,5 @@ import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base" -import { streamerId, eventSub, commandPrefix, streamerUsers, chatterId } from "main"; +import { streamerId, eventSub, commandPrefix, streamerUsers } from "main"; import User from "user"; import commands, { Command, sendMessage, specialAliasCommands } from "commands"; import { redis } from "bun"; @@ -30,7 +30,7 @@ async function parseChatMessage(msg: EventSubChannelChatMessageEvent) { if (await redis.exists(`user:${user?.id}:bot`)) return; // Ignore all bot commands - if (!await redis.exists(`user:${user?.id}:haschatted`) && !msg.sourceMessageId) { + if (!await redis.exists(`user:${user?.id}:haschatted`) && !msg.sourceMessageId) { // The msg.sourceMessageId checks if the message is from shared chat. shared chat should be ignored const message = await sendMessage(`Welcome ${user?.displayName}. Please note: This chat has PvP, if you get timed out that's part of the qwerinope experience. You have 10 minutes of invincibility. A full list of commands and items can be found here: https://github.com/qwerinope/qweribot/#qweribot`); await redis.set(`user:${user?.id}:haschatted`, "1"); await redis.set(`user:${user?.id}:welcomemessageid`, message.id); @@ -69,8 +69,7 @@ async function handleChatMessage(msg: EventSubChannelChatMessageEvent, user: Use try { await selection.execute(msg, user, { activation }); - } - catch (err) { + } catch (err) { logger.err(err as string); await sendMessage('ERROR: Something went wrong', msg.messageId); await user.clearLock(); diff --git a/src/lib/handleAnivMessage.ts b/src/lib/handleAnivMessage.ts index 78aedb3..bfe6f60 100644 --- a/src/lib/handleAnivMessage.ts +++ b/src/lib/handleAnivMessage.ts @@ -42,10 +42,19 @@ export default async function handleMessage(msg: EventSubChannelChatMessageEvent 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, data.anivbot, user, 30) - ]); + if (data.isAnivMessage) { + if (Math.random() > 0.5) { // 1/2 chance to dodge aniv timeout + await createAnivTimeoutRecord(msg.messageText, data.anivbot, user, 0) + return; + }; + + const duration = Math.floor(Math.random() * 30) + 30 // minimum timeout of 30 sec, maximum of 60 sec + + 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, data.anivbot, user, duration) + ]); + }; }; }; diff --git a/src/lib/parseCommandArgs.ts b/src/lib/parseCommandArgs.ts index 9448625..90e6b2a 100644 --- a/src/lib/parseCommandArgs.ts +++ b/src/lib/parseCommandArgs.ts @@ -5,9 +5,9 @@ export default function parseCommandArgs(input: string, specialAlias?: string) { let nice = ''; let sliceLength = 0; if (specialAlias) { - nice = input.toLowerCase().slice(specialAlias.length).trim(); + nice = input.toLowerCase().slice(specialAlias.length).replace(/[^\x00-\x7F]/g, '').trim(); } else { - nice = input.toLowerCase().slice(commandPrefix.length).trim(); + nice = input.toLowerCase().slice(commandPrefix.length).replace(/[^\x00-\x7F]/g, '').trim(); sliceLength = nice.startsWith('use') ? 2 : 1; } return nice.split(' ').slice(sliceLength).map(a => a.replaceAll(/!/gi, '')); @@ -17,7 +17,7 @@ export function parseCheerArgs(input: string) { const nice = input.toLowerCase().trim(); // This is for the test command. Remove the command prefix, the command, the whitespace after and the amount of fake bits - if (nice.startsWith(commandPrefix + 'testcheer')) return nice.slice(commandPrefix.length + 'testcheer'.length + 1).split(' ').slice(1); + if (nice.startsWith(commandPrefix + 'testcheer')) return nice.slice(commandPrefix.length + 'testcheer'.length + 1).replaceAll(/\W/g, '').split(' ').slice(1); // This is for actual cheers. Remove all 'cheerx' parts of the message return nice.split(' ').filter(a => !/cheer[0-9]+/i.test(a));