diff --git a/README.md b/README.md index ac6392f..952d008 100644 --- a/README.md +++ b/README.md @@ -78,14 +78,18 @@ COMMAND|FUNCTION|USER|ALIASES|DISABLEABLE 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 commands|anyone|`getcheers` `getcheer`|:x: `gettimeout {target}`|Get the remaining timeout duration of targeted user|anyone|`gettimeout` `gett`|:white_check_mark: `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: +`disablecheer {cheer}`|Disable a specific cheer event|admins|`disablecheer`|:x: +`enablecheer {cheer}`|Re-enable a specific cheer event|admins|`enablecheer`|:x: `getadmins`|Get a list of every admin in the channel|anyone|`getadmins`|:x: `itemlock {target}`|Toggle the itemlock on the specified target|admins|`itemlock`|:x: -`addadmin {target}`|Adds an admin|streamer/botchatter|`addadmin`|:x: -`removeadmin {target}`|Removes an admin|streamer/botchatter|`removeadmin`|:x: +`testcheer {amount} [args]`|Create a fake cheering event|streamer/chatterbot|`testcheer`|:x: +`addadmin {target}`|Adds an admin|streamer/chatterbot|`addadmin`|:x: +`removeadmin {target}`|Removes an admin|streamer/chatterbot|`removeadmin`|:x: ## Items @@ -95,3 +99,9 @@ Blaster|`blaster {target}`|Times targeted user out for 60 seconds|`blaster` `bla Silver Bullet|`silverbullet {target}`|Times targeted user out for 24 hours|`silverbullet` `execute` Grenade|`grenade`|Times a random vulnerable chatter out for 60 seconds|`grenade` TNT|`tnt`|Give 5-10 random chatters 60 second timeouts|`tnt` + +## Cheers + +NAME|AMOUNT|USAGE|FUNCTION +-|-|-|- +`timeout`|100|`cheer100 {target}`|Times specified user out for 1 minute diff --git a/bot/cheers/index.ts b/bot/cheers/index.ts new file mode 100644 index 0000000..c4dfce9 --- /dev/null +++ b/bot/cheers/index.ts @@ -0,0 +1,29 @@ +import { User } from '../user'; +import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base"; + +export class Cheer { + public readonly name: string; + public readonly amount: number; + public readonly execute: (msg: EventSubChannelChatMessageEvent, sender: User, testmessage: boolean) => Promise; + constructor(name: string, amount: number, execution: (msg: EventSubChannelChatMessageEvent, sender: User, testmessage: boolean) => Promise) { + this.name = name.toLowerCase(); + this.amount = amount; + this.execute = execution; + }; +}; + +import { readdir } from 'node:fs/promises'; +const cheers = new Map; +const namedcheers = new Map; + +const files = await readdir(import.meta.dir); +for (const file of files) { + if (!file.endsWith('.ts')) continue; + if (file === import.meta.file) continue; + const cheer: Cheer = await import(import.meta.dir + '/' + file.slice(0, -3)).then(a => a.default); + cheers.set(cheer.amount, cheer); + namedcheers.set(cheer.name, cheer); +}; + +export default cheers; +export { namedcheers }; diff --git a/bot/cheers/timeout.ts b/bot/cheers/timeout.ts new file mode 100644 index 0000000..b1a4ee4 --- /dev/null +++ b/bot/cheers/timeout.ts @@ -0,0 +1,53 @@ +import { Cheer } from "."; +import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base" +import { changeItemCount } from "../items"; +import { sendMessage } from "../commands"; +import { getUserRecord } from "../db/dbUser"; +import { User } from "../user"; +import { timeout } from "../lib/timeout"; +import { createTimeoutRecord } from "../db/dbTimeouts"; +import logger from "../lib/logger"; + +export default new Cheer('timeout', 100, async (msg, user, testmessage) => { + const args = msg.messageText.split(' '); + if (testmessage) { args.shift(); args.shift(); }; //Discard the '!testcheer' and '100' arguments + if (!args[0]) { await handleNoBlasterTarget(msg, user, false); return; }; + const target = await User.initUsername(args[0].toLowerCase()); + if (!target) { await handleNoBlasterTarget(msg, user, false); return; }; + + const result = await timeout(target, `You got blasted by ${user.displayName}!`) + if (result.status) await Promise.all([ + sendMessage(`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`), + createTimeoutRecord(user, target, 'blaster'), + ]); + else { + await handleNoBlasterTarget(msg, user); + switch (result.reason) { + case "banned": + await sendMessage(`${target.displayName} is already timed out/banned`, msg.messageId); + break; + case "illegal": + await Promise.all([ + sendMessage(`${user.displayName} Nou Nou Nou`), + timeout(user, 'nah', 60) + ]); + break; + case "unknown": + await sendMessage('Something went wrong...', msg.messageId); + break; + }; + }; +}); + +async function handleNoBlasterTarget(msg: EventSubChannelChatMessageEvent, user: User, silent = true) { + if (await user.itemLock()) { + await sendMessage(`Cannot give ${user.displayName} a blaster`, msg.messageId); + logger.err(`Failed to give ${user.displayName} a blaster for their cheer`); + return; + }; + await user.setLock(); + const userRecord = await getUserRecord(user); + if (!silent) await sendMessage('No (valid) target specified. You got a blaster!', msg.messageId); + await changeItemCount(user, userRecord, 'blaster', 1); + await user.clearLock(); +}; diff --git a/bot/commands/disablecheer.ts b/bot/commands/disablecheer.ts new file mode 100644 index 0000000..1380f18 --- /dev/null +++ b/bot/commands/disablecheer.ts @@ -0,0 +1,14 @@ +import { redis } from "bun"; +import { Command, sendMessage } from "."; +import parseCommandArgs from "../lib/parseCommandArgs"; +import { namedcheers } from "../cheers"; + +export default new Command('disablecheer', ['disablecheer'], 'admin', async msg => { + const args = parseCommandArgs(msg.messageText); + if (!args[0]) { await sendMessage('Please specify a cheer to disable', msg.messageId); return; }; + const selection = namedcheers.get(args[0].toLowerCase()); + if (!selection) { await sendMessage(`There is no ${args[0]} cheer`, msg.messageId); return; }; + const result = await redis.sadd('disabledcheers', selection.name); + if (result === 0) { await sendMessage(`The ${selection.name} cheer is already disabled`, msg.messageId); return; }; + await sendMessage(`Successfully disabled the ${selection.name} cheer`, msg.messageId); +}, false); diff --git a/bot/commands/enablecheer.ts b/bot/commands/enablecheer.ts new file mode 100644 index 0000000..c222a76 --- /dev/null +++ b/bot/commands/enablecheer.ts @@ -0,0 +1,14 @@ +import { redis } from "bun"; +import { Command, sendMessage } from "."; +import parseCommandArgs from "../lib/parseCommandArgs"; +import { namedcheers } from "../cheers"; + +export default new Command('enablecheer', ['enablecheer'], 'admin', async msg => { + const args = parseCommandArgs(msg.messageText); + if (!args[0]) { await sendMessage('Please specify a cheer to enable', msg.messageId); return; }; + const selection = namedcheers.get(args[0].toLowerCase()); + if (!selection) { await sendMessage(`There is no ${args[0]} cheer`, msg.messageId); return; }; + const result = await redis.srem('disabledcheers', selection.name); + if (result === 0) { await sendMessage(`The ${selection.name} cheer isn't disabled`, msg.messageId); return; }; + await sendMessage(`Successfully enabled the ${selection.name} cheer`, msg.messageId); +}, false); diff --git a/bot/commands/getcheers.ts b/bot/commands/getcheers.ts new file mode 100644 index 0000000..6cbc4b9 --- /dev/null +++ b/bot/commands/getcheers.ts @@ -0,0 +1,33 @@ +import { redis } from "bun"; +import { Command, sendMessage } from "."; +import parseCommandArgs from "../lib/parseCommandArgs"; +import { namedcheers } from "../cheers"; + +export default new Command('getcheers', ['getcheers', 'getcheer'], 'chatter', async msg => { + const args = parseCommandArgs(msg.messageText); + if (!args[0]) { await sendMessage(`A full list of cheers can be found here: https://github.com/qwerinope/qweribot#cheers`, msg.messageId); return; }; + const disabledcheers = await redis.smembers('disabledcheers'); + const cheerstrings: string[] = []; + + if (args[0].toLowerCase() === "enabled") { + for (const [name, cheer] of Array.from(namedcheers.entries())) { + if (disabledcheers.includes(name)) continue; + cheerstrings.push(`${cheer.amount}: ${name}`); + }; + + const last = cheerstrings.pop(); + if (!last) { await sendMessage("No enabled cheers", msg.messageId); return; }; + await sendMessage(cheerstrings.length === 0 ? last : cheerstrings.join(', ') + " and " + last, msg.messageId); + + } else if (args[0].toLowerCase() === "disabled") { + for (const [name, cheer] of Array.from(namedcheers.entries())) { + if (!disabledcheers.includes(name)) continue; + cheerstrings.push(`${cheer.amount}: ${name}`); + }; + + const last = cheerstrings.pop(); + if (!last) { await sendMessage("No disabled cheers", msg.messageId); return; }; + await sendMessage(cheerstrings.length === 0 ? last : cheerstrings.join(', ') + " and " + last, msg.messageId); + + } else await sendMessage('Please specify if you want the enabled or disabled cheers', msg.messageId); +}, false); diff --git a/bot/commands/getcommands.ts b/bot/commands/getcommands.ts index 213fd42..ec37de5 100644 --- a/bot/commands/getcommands.ts +++ b/bot/commands/getcommands.ts @@ -4,7 +4,7 @@ import parseCommandArgs from "../lib/parseCommandArgs"; export default new Command('getcommands', ['getcommands', 'getc'], 'chatter', async msg => { const args = parseCommandArgs(msg.messageText); - if (!args[0]) { await sendMessage(`A full list of commands can be found here: https://github.com/qwerinope/qweribot#commands`, msg.messageId); return; }; + if (!args[0]) { await sendMessage(`A full list of commands can be found here: https://github.com/qwerinope/qweribot#commands-1`, msg.messageId); return; }; const disabledcommands = await redis.smembers('disabledcommands'); if (args[0].toLowerCase() === 'enabled') { const commandnames: string[] = []; diff --git a/bot/commands/testcheer.ts b/bot/commands/testcheer.ts new file mode 100644 index 0000000..d1451c9 --- /dev/null +++ b/bot/commands/testcheer.ts @@ -0,0 +1,11 @@ +import { Command, sendMessage } from "."; +import { handleCheer } from "../events/message"; +import parseCommandArgs from "../lib/parseCommandArgs"; + +export default new Command('testcheer', ['testcheer'], 'streamer', async (msg, user) => { + const args = parseCommandArgs(msg.messageText); + if (!args[0]) { await sendMessage('Please specify the amount of fake bits you want to send', msg.messageId); return; }; + if (isNaN(Number(args[0]))) { await sendMessage(`${args[0]} is not a valid amout of bits`); return; }; + const bits = Number(args.shift()); // we shift it so the amount of bits isn't part of the handleCheer message, we already know that args[0] can be parsed as a number so this is fine. + await handleCheer(msg, bits, true); +}, false); diff --git a/bot/events/message.ts b/bot/events/message.ts index 9996184..8aa4df2 100644 --- a/bot/events/message.ts +++ b/bot/events/message.ts @@ -1,13 +1,23 @@ +import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base" import { chatterId, streamerId, eventSub, commandPrefix, singleUserMode, streamerUsers } from ".."; import { User } from "../user"; import commands, { sendMessage } from "../commands"; import { redis } from "bun"; import { isAdmin } from "../lib/admins"; +import cheers from "../cheers"; import logger from "../lib/logger"; logger.info(`Loaded the following commands: ${commands.keys().toArray().join(', ')}`); -eventSub.onChannelChatMessage(streamerId, streamerId, async msg => { +eventSub.onChannelChatMessage(streamerId, streamerId, parseChatMessage); + +async function parseChatMessage(msg: EventSubChannelChatMessageEvent) { + if (!msg.isCheer && !msg.isRedemption) await handleChatMessage(msg) + else if (msg.isCheer && !msg.isRedemption) await handleCheer(msg, msg.bits) +}; + +async function handleChatMessage(msg: EventSubChannelChatMessageEvent) { + // return if double user mode is on and the chatter says something, we don't need them if (!singleUserMode && msg.chatterId === chatterId) return; @@ -49,4 +59,22 @@ eventSub.onChannelChatMessage(streamerId, streamerId, async msg => { await user?.clearLock(); }; }; -}); +}; + +export async function handleCheer(msg: EventSubChannelChatMessageEvent, bits: number, testmessage = false) { + const selection = cheers.get(bits); + if (!selection) return; + + const [user, disabledcheers] = await Promise.all([ + User.initUsername(msg.chatterName), + redis.smembers('disabledcheers') + ]); + + if (disabledcheers.includes(selection.name)) { await sendMessage(`The ${selection.name} cheer is disabled`); return; }; + + try { + selection.execute(msg, user!, testmessage); + } catch (err) { + logger.err(err as string); + }; +}; diff --git a/bot/index.ts b/bot/index.ts index 9148e0d..9970605 100644 --- a/bot/index.ts +++ b/bot/index.ts @@ -5,7 +5,7 @@ import { addAdmin } from "./lib/admins"; import logger from "./lib/logger"; const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot"]; -const STREAMERINTENTS = ["user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:banned_users"]; +const STREAMERINTENTS = ["user:read:chat", "moderation:read", "channel:manage:moderators", "moderator:manage:banned_users", "bits:read"]; export const singleUserMode = process.env.CHATTER_IS_STREAMER === 'true'; export const chatterId = process.env.CHATTER_ID ?? "";