proper formatting and linting YAY, change cheer constructor to take object

This commit is contained in:
2025-11-24 17:05:18 +01:00
parent 253775a66e
commit af946e59b8
123 changed files with 4890 additions and 3383 deletions

43
biome.json Normal file
View File

@@ -0,0 +1,43 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.7/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noNonNullAssertedOptionalChain": "off",
"noExplicitAny": "off",
"noControlCharactersInRegex": "off"
},
"style": {
"noNonNullAssertion": "off"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}

View File

@@ -1,34 +1,34 @@
{
"name": "qweribot",
"module": "src/index.ts",
"devDependencies": {
"@twurple/eventsub-ngrok": "^7.4.0",
"@types/bun": "latest",
"drizzle-kit": "^0.31.5",
"pg": "^8.16.3"
},
"scripts": {
"start": "NODE_ENV=production bun src/index.ts",
"start-dev": "NODE_ENV=development bun src/index.ts",
"start-discord": "NODE_ENV=production bun src/discord/index.ts",
"start-dev-discord": "NODE_ENV=development bun src/discord/index.ts",
"migrate": "drizzle-kit push --config=drizzle-prod.config.ts",
"migrate-dev": "drizzle-kit push --config=drizzle-dev.config.ts",
"studio": "drizzle-kit studio --config=drizzle-prod.config.ts",
"studio-dev": "drizzle-kit studio --config=drizzle-dev.config.ts"
},
"peerDependencies": {
"typescript": "^5.8.3"
},
"private": true,
"type": "module",
"dependencies": {
"@fontsource/jersey-15": "^5.2.8",
"@twurple/api": "7.4.0",
"@twurple/auth": "^7.4.0",
"@twurple/eventsub-http": "^7.4.0",
"discord.js": "^14.24.0",
"drizzle-orm": "^0.44.6",
"kleur": "^4.1.5"
}
"name": "qweribot",
"module": "src/index.ts",
"devDependencies": {
"@twurple/eventsub-ngrok": "^7.4.0",
"@types/bun": "latest",
"drizzle-kit": "^0.31.5",
"pg": "^8.16.3"
},
"scripts": {
"start": "NODE_ENV=production bun src/index.ts",
"start-dev": "NODE_ENV=development bun src/index.ts",
"start-discord": "NODE_ENV=production bun src/discord/index.ts",
"start-dev-discord": "NODE_ENV=development bun src/discord/index.ts",
"migrate": "drizzle-kit push --config=drizzle-prod.config.ts",
"migrate-dev": "drizzle-kit push --config=drizzle-dev.config.ts",
"studio": "drizzle-kit studio --config=drizzle-prod.config.ts",
"studio-dev": "drizzle-kit studio --config=drizzle-dev.config.ts"
},
"peerDependencies": {
"typescript": "^5.8.3"
},
"private": true,
"type": "module",
"dependencies": {
"@fontsource/jersey-15": "^5.2.8",
"@twurple/api": "7.4.0",
"@twurple/auth": "^7.4.0",
"@twurple/eventsub-http": "^7.4.0",
"discord.js": "^14.24.0",
"drizzle-orm": "^0.44.6",
"kleur": "^4.1.5"
}
}

View File

@@ -1,103 +1,160 @@
import { RefreshingAuthProvider, exchangeCode, type AccessToken } from "@twurple/auth";
import { createAuthRecord, deleteAuthRecord, getAuthRecord, updateAuthRecord } from "db/dbAuth";
import logger from "lib/logger";
import {
type AccessToken,
exchangeCode,
RefreshingAuthProvider,
} from "@twurple/auth";
import {
createAuthRecord,
deleteAuthRecord,
getAuthRecord,
updateAuthRecord,
} from "db/dbAuth";
import kleur from "kleur";
import logger from "lib/logger";
async function initAuth(userId: string, clientId: string, clientSecret: string, requestedIntents: string[], streamer: boolean): Promise<AccessToken> {
const port = process.env.REDIRECT_PORT ?? 3456
const redirectURL = process.env.REDIRECT_URL ?? `http://localhost:${port}`;
// Set the default url and port to http://localhost:3456
async function initAuth(
userId: string,
clientId: string,
clientSecret: string,
requestedIntents: string[],
streamer: boolean,
): Promise<AccessToken> {
const port = process.env.REDIRECT_PORT ?? 3456;
const redirectURL = process.env.REDIRECT_URL ?? `http://localhost:${port}`;
// Set the default url and port to http://localhost:3456
const state = Bun.randomUUIDv7().replace(/-/g, "").slice(0, 32).toUpperCase();
// Generate random state variable to prevent cross-site-scripting attacks
const state = Bun.randomUUIDv7().replace(/-/g, "").slice(0, 32).toUpperCase();
// Generate random state variable to prevent cross-site-scripting attacks
const instruction = `Visit this URL as ${kleur.red().underline().italic(streamer ? 'the streamer' : 'the chatter')} to authenticate the bot.`
logger.info(instruction);
logger.info(`https://id.twitch.tv/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectURL}&response_type=code&scope=${requestedIntents.join('+')}&state=${state}`);
const instruction = `Visit this URL as ${kleur
.red()
.underline()
.italic(
streamer ? "the streamer" : "the chatter",
)} to authenticate the bot.`;
logger.info(instruction);
logger.info(
`https://id.twitch.tv/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectURL}&response_type=code&scope=${requestedIntents.join("+")}&state=${state}`,
);
const createCodePromise = () => {
let resolver: (code: string) => void;
const promise = new Promise<string>((resolve) => { resolver = resolve; });
return { promise, resolver: resolver! };
}
const createCodePromise = () => {
let resolver: (code: string) => void;
const promise = new Promise<string>((resolve) => {
resolver = resolve;
});
return { promise, resolver: resolver! };
};
const { promise: codepromise, resolver } = createCodePromise();
const { promise: codepromise, resolver } = createCodePromise();
const server = Bun.serve({
port,
fetch(request) {
const { searchParams } = new URL(request.url);
if (searchParams.has('code') && searchParams.has('state') && searchParams.get('state') === state) {
// Check if the required fields exist on the params and validate the state
resolver(searchParams.get('code')!);
return new Response("Successfully authenticated!");
} else {
return new Response(`Authentication attempt unsuccessful, please make sure the redirect url in the twitch developer console is set to ${redirectURL} and that the bot is listening to that url & port.`, { status: 400 });
}
}
});
const server = Bun.serve({
port,
fetch(request) {
const { searchParams } = new URL(request.url);
if (
searchParams.has("code") &&
searchParams.has("state") &&
searchParams.get("state") === state
) {
// Check if the required fields exist on the params and validate the state
resolver(searchParams.get("code")!);
return new Response("Successfully authenticated!");
} else {
return new Response(
`Authentication attempt unsuccessful, please make sure the redirect url in the twitch developer console is set to ${redirectURL} and that the bot is listening to that url & port.`,
{ status: 400 },
);
}
},
});
await deleteAuthRecord(userId);
await deleteAuthRecord(userId);
const code = await codepromise;
server.stop(false);
logger.info(`Authentication code received.`);
const tokenData = await exchangeCode(clientId, clientSecret, code, redirectURL);
logger.info(`Successfully authenticated code.`);
await createAuthRecord(tokenData, userId);
logger.ok(`Successfully saved auth data in the database.`);
return tokenData;
};
const code = await codepromise;
server.stop(false);
logger.info(`Authentication code received.`);
const tokenData = await exchangeCode(
clientId,
clientSecret,
code,
redirectURL,
);
logger.info(`Successfully authenticated code.`);
await createAuthRecord(tokenData, userId);
logger.ok(`Successfully saved auth data in the database.`);
return tokenData;
}
export type authProviderInstructions = {
userId: string;
intents: string[];
streamer: boolean;
userId: string;
intents: string[];
streamer: boolean;
};
export async function createAuthProvider(data: authProviderInstructions[]): Promise<RefreshingAuthProvider> {
const clientId = process.env.CLIENT_ID;
if (!clientId) { logger.enverr("CLIENT_ID"); process.exit(1); };
export async function createAuthProvider(
data: authProviderInstructions[],
): Promise<RefreshingAuthProvider> {
const clientId = process.env.CLIENT_ID;
if (!clientId) {
logger.enverr("CLIENT_ID");
process.exit(1);
}
const clientSecret = process.env.CLIENT_SECRET;
if (!clientSecret) { logger.enverr("CLIENT_SECRET"); process.exit(1); };
const clientSecret = process.env.CLIENT_SECRET;
if (!clientSecret) {
logger.enverr("CLIENT_SECRET");
process.exit(1);
}
const authData = new RefreshingAuthProvider({
clientId,
clientSecret
});
const authData = new RefreshingAuthProvider({
clientId,
clientSecret,
});
authData.onRefresh(async (user, token) => {
logger.ok(`Successfully refreshed auth for user ${user}`);
await updateAuthRecord(user, token);
});
authData.onRefresh(async (user, token) => {
logger.ok(`Successfully refreshed auth for user ${user}`);
await updateAuthRecord(user, token);
});
authData.onRefreshFailure((user, err) => {
logger.err(`Failed to refresh auth for user ${user}: ${err.name} ${err.message}`);
});
authData.onRefreshFailure((user, err) => {
logger.err(
`Failed to refresh auth for user ${user}: ${err.name} ${err.message}`,
);
});
for (const user of data) {
const authRecord = await getAuthRecord(user.userId, user.intents);
for (const user of data) {
const authRecord = await getAuthRecord(user.userId, user.intents);
const token = authRecord ? authRecord.accesstoken : await initAuth(user.userId, clientId, clientSecret, user.intents, user.streamer);
const token = authRecord
? authRecord.accesstoken
: await initAuth(
user.userId,
clientId,
clientSecret,
user.intents,
user.streamer,
);
try {
await authData.addUserForToken(token, user.intents);
} catch (err) {
logger.err(`Failed to setup user auth. Please restart the bot and re-authenticate.`);
await deleteAuthRecord(user.userId);
process.exit(1);
};
try {
await authData.addUserForToken(token, user.intents);
} catch (_err) {
logger.err(
`Failed to setup user auth. Please restart the bot and re-authenticate.`,
);
await deleteAuthRecord(user.userId);
process.exit(1);
}
try {
await authData.refreshAccessTokenForUser(user.userId);
} catch (err) {
logger.err(`Failed to refresh user ${user.userId}. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`);
await deleteAuthRecord(user.userId);
process.exit(1);
};
};
try {
await authData.refreshAccessTokenForUser(user.userId);
} catch (_err) {
logger.err(
`Failed to refresh user ${user.userId}. Please restart the bot and re-authenticate it. Make sure the user that auths the bot and the user that's defined in .env are the same.`,
);
await deleteAuthRecord(user.userId);
process.exit(1);
}
}
return authData;
};
return authData;
}

View File

@@ -1,71 +1,92 @@
import { Cheer, handleNoTarget } from "cheers";
import { sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import User from "user";
import { timeout } from "lib/timeout";
import { createTimeoutRecord } from "db/dbTimeouts";
import { parseCheerArgs } from "lib/parseCommandArgs";
import { createCheerEventRecord } from "db/dbCheerEvents";
import { playAlert } from "web/alerts/serverFunctions";
import { createTimeoutRecord } from "db/dbTimeouts";
import { getUserRecord } from "db/dbUser";
import { sendMessage } from "lib/commandUtils";
import { parseCheerArgs } from "lib/parseCommandArgs";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import User from "user";
import { playAlert } from "web/alerts/serverFunctions";
const ITEMNAME = 'silverbullet';
const ITEMNAME = "silverbullet";
export default new Cheer('execute', 666, async (msg, user) => {
const args = parseCheerArgs(msg.messageText);
export default new Cheer({
name: "execute",
amount: 666,
isItem: true,
async execute(msg, user) {
const args = parseCheerArgs(msg.messageText);
let target: User | null;
if (!args[0]) {
const vulnsids = await redis.keys('user:*:vulnerable');
const baseusers = vulnsids.map(a => User.initUserId(a.slice(5, -11)));
const users: User[] = [];
for (const user of baseusers) {
const a = await user;
if (!a) continue;
users.push(a);
};
if (users.length === 0) { await sendMessage('No vulnerable chatters'); await handleNoTarget(msg, user, ITEMNAME, true); return; };
target = users[Math.floor(Math.random() * users.length)]!;
await playAlert({
name: 'blastinRoulette',
user: user.displayName,
targets: users.map(a => a.displayName),
finaltarget: target.displayName
});
await new Promise((res, _) => setTimeout(res, 4000));
} else {
target = await User.initUsername(args[0].toLowerCase());
};
if (!target) { await handleNoTarget(msg, user, ITEMNAME, false); return; };
await getUserRecord(target);
const result = await timeout(target, `You got executed by ${user.displayName}!`, 60 * 30);
if (result.status) await Promise.all([
sendMessage(`KEKPOINT KEKPOINT KEKPOINT ${target.displayName.toUpperCase()} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`),
createTimeoutRecord(user, target, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
playAlert({
name: 'userExecution',
user: user.displayName,
target: target.displayName
})
]);
else {
await handleNoTarget(msg, user, ITEMNAME);
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;
};
};
}, true);
let target: User | null;
if (!args[0]) {
const vulnsids = await redis.keys("user:*:vulnerable");
const baseusers = vulnsids.map((a) => User.initUserId(a.slice(5, -11)));
const users: User[] = [];
for (const user of baseusers) {
const a = await user;
if (!a) continue;
users.push(a);
}
if (users.length === 0) {
await sendMessage("No vulnerable chatters");
await handleNoTarget(msg, user, ITEMNAME, true);
return;
}
target = users[Math.floor(Math.random() * users.length)]!;
await playAlert({
name: "blastinRoulette",
user: user.displayName,
targets: users.map((a) => a.displayName),
finaltarget: target.displayName,
});
await new Promise((res, _) => setTimeout(res, 4000));
} else {
target = await User.initUsername(args[0].toLowerCase());
}
if (!target) {
await handleNoTarget(msg, user, ITEMNAME, false);
return;
}
await getUserRecord(target);
const result = await timeout(
target,
`You got executed by ${user.displayName}!`,
60 * 30,
);
if (result.status)
await Promise.all([
sendMessage(
`KEKPOINT KEKPOINT KEKPOINT ${target.displayName.toUpperCase()} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`,
),
createTimeoutRecord(user, target, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
playAlert({
name: "userExecution",
user: user.displayName,
target: target.displayName,
}),
]);
else {
await handleNoTarget(msg, user, ITEMNAME);
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;
}
}
},
});

View File

@@ -1,33 +1,44 @@
import { redis } from "lib/redis";
import { Cheer, handleNoTarget } from "cheers";
import { createCheerEventRecord } from "db/dbCheerEvents";
import { createTimeoutRecord } from "db/dbTimeouts";
import { getUserRecord } from "db/dbUser";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import User from "user";
import { getUserRecord } from "db/dbUser";
import { createTimeoutRecord } from "db/dbTimeouts";
import { createCheerEventRecord } from "db/dbCheerEvents";
import { Cheer, handleNoTarget } from "cheers";
import { playAlert } from "web/alerts/serverFunctions";
const ITEMNAME = 'grenade';
const ITEMNAME = "grenade";
export default new Cheer('grenade', 99, async (msg, user) => {
const targets = await redis.keys(`user:*:vulnerable`);
if (targets.length === 0) { await sendMessage('No vulnerable chatters to blow up!', msg.messageId); await handleNoTarget(msg, user, ITEMNAME); return; };
const selection = targets[Math.floor(Math.random() * targets.length)]!;
const target = await User.initUserId(selection.slice(5, -11));
export default new Cheer({
name: "grenade",
amount: 99,
isItem: true,
async execute(msg, user) {
const targets = await redis.keys(`user:*:vulnerable`);
if (targets.length === 0) {
await sendMessage("No vulnerable chatters to blow up!", msg.messageId);
await handleNoTarget(msg, user, ITEMNAME);
return;
}
const selection = targets[Math.floor(Math.random() * targets.length)]!;
const target = await User.initUserId(selection.slice(5, -11));
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([
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`),
createTimeoutRecord(user, target!, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
playAlert({
name: 'grenadeExplosion',
user: user.displayName,
target: target?.displayName!
})
]);
}, true);
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`,
),
createTimeoutRecord(user, target!, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
playAlert({
name: "grenadeExplosion",
user: user.displayName,
target: target?.displayName!,
}),
]);
},
});

View File

@@ -1,47 +1,75 @@
import User from 'user';
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import type { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import type User from "user";
export class Cheer {
public readonly name: string;
public readonly amount: number;
public readonly execute: (msg: EventSubChannelChatMessageEvent, sender: User) => Promise<void>;
public readonly isItem: boolean;
constructor(name: string, amount: number, execution: (msg: EventSubChannelChatMessageEvent, sender: User) => Promise<void>, isItem = false) {
this.name = name.toLowerCase();
this.amount = amount;
this.execute = execution;
this.isItem = isItem;
};
type cheerOptions = {
name: string;
amount: number;
isItem: boolean;
execute: (
msg: EventSubChannelChatMessageEvent,
sender: User,
) => Promise<void>;
};
import { readdir } from 'node:fs/promises';
const cheers = new Map<number, Cheer>;
const namedcheers = new Map<string, Cheer>;
export class Cheer {
public readonly name: string;
public readonly amount: number;
public readonly execute: (
msg: EventSubChannelChatMessageEvent,
sender: User,
) => Promise<void>;
public readonly isItem: boolean;
constructor(options: cheerOptions) {
this.name = options.name.toLowerCase();
this.amount = options.amount;
this.execute = options.execute;
this.isItem = options.isItem;
}
}
import { readdir } from "node:fs/promises";
const cheers = new Map<number, Cheer>();
const namedcheers = new Map<string, Cheer>();
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);
};
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 };
import { sendMessage } from 'lib/commandUtils';
import { getUserRecord } from 'db/dbUser';
import { changeItemCount, type items } from 'items';
import { getUserRecord } from "db/dbUser";
import { changeItemCount, type items } from "items";
import { sendMessage } from "lib/commandUtils";
export async function handleNoTarget(msg: EventSubChannelChatMessageEvent, user: User, itemname: items, silent = true) {
if (await user.itemLock()) {
await sendMessage(`Cannot give ${user.displayName} a ${itemname} (itemlock)`, msg.messageId);
return;
};
await user.setLock();
const userRecord = await getUserRecord(user);
if (!silent) await sendMessage(`No (valid) target specified. You got a ${itemname}!`, msg.messageId);
await changeItemCount(user, userRecord, itemname, 1);
await user.clearLock();
export async function handleNoTarget(
msg: EventSubChannelChatMessageEvent,
user: User,
itemname: items,
silent = true,
) {
if (await user.itemLock()) {
await sendMessage(
`Cannot give ${user.displayName} a ${itemname} (itemlock)`,
msg.messageId,
);
return;
}
await user.setLock();
const userRecord = await getUserRecord(user);
if (!silent)
await sendMessage(
`No (valid) target specified. You got a ${itemname}!`,
msg.messageId,
);
await changeItemCount(user, userRecord, itemname, 1);
await user.clearLock();
}

View File

@@ -1,68 +1,87 @@
import { Cheer } from "cheers";
import { sendMessage } from "lib/commandUtils";
import { createGetLootRecord } from "db/dbGetLoot";
import { getUserRecord, updateUserRecord } from "db/dbUser";
import itemMap, { type inventory, type items } from "items";
import { createGetLootRecord } from "db/dbGetLoot";
import { timeout } from "lib/timeout";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
export default new Cheer('superloot', 150, async (msg, user) => {
if (!await redis.exists('streamIsLive')) { await sendMessage(`No loot while stream is offline`, msg.messageId); return; };
if (await user.itemLock()) { await sendMessage(`Cannot get loot (itemlock)`, msg.messageId); return; };
await user.setLock();
const userData = await getUserRecord(user);
export default new Cheer({
name: "superloot",
amount: 150,
isItem: true,
async execute(msg, user) {
if (!(await redis.exists("streamIsLive"))) {
await sendMessage(`No loot while stream is offline`, msg.messageId);
return;
}
if (await user.itemLock()) {
await sendMessage(`Cannot get loot (itemlock)`, msg.messageId);
return;
}
await user.setLock();
const userData = await getUserRecord(user);
await sendMessage("HOLD");
await new Promise(res => setTimeout(res, 1000 * 5));
await sendMessage("HOLD");
await new Promise((res) => setTimeout(res, 1000 * 5));
if (Math.random() > 0.5) {
await Promise.all([
sendMessage(`SUPERLOOT FAILED!!! KEKPOINT KEKPOINT KEKPOINT ${msg.chatterDisplayName.toUpperCase()} SEE YOU IN 5 MINUTES!!!`),
timeout(user, `RIP BOZO! NO SUPERLOOT FOR YOU`, 60 * 5),
user.clearLock()
]);
return;
};
if (Math.random() > 0.5) {
await Promise.all([
sendMessage(
`SUPERLOOT FAILED!!! KEKPOINT KEKPOINT KEKPOINT ${msg.chatterDisplayName.toUpperCase()} SEE YOU IN 5 MINUTES!!!`,
),
timeout(user, `RIP BOZO! NO SUPERLOOT FOR YOU`, 60 * 5),
user.clearLock(),
]);
return;
}
const gainedqbucks = Math.floor(Math.random() * 250) + 150; // range from 150 to 400
userData.balance += gainedqbucks;
const gainedqbucks = Math.floor(Math.random() * 250) + 150; // range from 150 to 400
userData.balance += gainedqbucks;
const itemDiff: inventory = {
grenade: 0,
blaster: 0,
tnt: 0,
silverbullet: 0
};
const itemDiff: inventory = {
grenade: 0,
blaster: 0,
tnt: 0,
silverbullet: 0,
};
for (let i = 0; i < 15; i++) {
if (Math.floor(Math.random() * 5) === 0) itemDiff.grenade! += 1; // 1 in 5
if (Math.floor(Math.random() * 5) === 0) itemDiff.blaster! += 1; // 1 in 5
if (Math.floor(Math.random() * 50) === 0) itemDiff.tnt! += 1; // 1 in 50
if (Math.floor(Math.random() * 50) === 0) itemDiff.silverbullet! += 1; // 1 in 50
};
for (let i = 0; i < 15; i++) {
if (Math.floor(Math.random() * 5) === 0) itemDiff.grenade! += 1; // 1 in 5
if (Math.floor(Math.random() * 5) === 0) itemDiff.blaster! += 1; // 1 in 5
if (Math.floor(Math.random() * 50) === 0) itemDiff.tnt! += 1; // 1 in 50
if (Math.floor(Math.random() * 50) === 0) itemDiff.silverbullet! += 1; // 1 in 50
}
for (const [item, amount] of Object.entries(itemDiff) as [items, number][]) {
if (userData.inventory[item]) userData.inventory[item] += amount;
else userData.inventory[item] = amount;
};
for (const [item, amount] of Object.entries(itemDiff) as [
items,
number,
][]) {
if (userData.inventory[item]) userData.inventory[item] += amount;
else userData.inventory[item] = amount;
}
const itemstrings: string[] = [`${gainedqbucks} qbucks`];
const itemstrings: string[] = [`${gainedqbucks} qbucks`];
for (const [item, amount] of Object.entries(itemDiff)) {
if (amount === 0) continue;
const selection = itemMap.get(item);
if (!selection) continue;
itemstrings.push(`${amount} ${selection.prettyName + (amount === 1 ? '' : selection.plural)}`);
};
for (const [item, amount] of Object.entries(itemDiff)) {
if (amount === 0) continue;
const selection = itemMap.get(item);
if (!selection) continue;
itemstrings.push(
`${amount} ${selection.prettyName + (amount === 1 ? "" : selection.plural)}`,
);
}
const last = itemstrings.pop();
const itemstring = itemstrings.length === 0 ? last : itemstrings.join(', ') + " and " + last;
const message = `You got ${itemstring}`;
const last = itemstrings.pop();
const itemstring =
itemstrings.length === 0 ? last : `${itemstrings.join(", ")} and ${last}`;
const message = `You got ${itemstring}`;
await Promise.all([
updateUserRecord(user, userData),
sendMessage(message, msg.messageId),
createGetLootRecord(user, gainedqbucks, itemDiff, 'superloot'),
user.clearLock()
]);
}, true);
await Promise.all([
updateUserRecord(user, userData),
sendMessage(message, msg.messageId),
createGetLootRecord(user, gainedqbucks, itemDiff, "superloot"),
user.clearLock(),
]);
},
});

View File

@@ -1,49 +1,69 @@
import { Cheer, handleNoTarget } from "cheers";
import { sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import User from "user";
import { timeout } from "lib/timeout";
import { createTimeoutRecord } from "db/dbTimeouts";
import { createCheerEventRecord } from "db/dbCheerEvents";
import { createTimeoutRecord } from "db/dbTimeouts";
import { getUserRecord } from "db/dbUser";
import { sendMessage } from "lib/commandUtils";
import { parseCheerArgs } from "lib/parseCommandArgs";
import { timeout } from "lib/timeout";
import User from "user";
import { playAlert } from "web/alerts/serverFunctions";
const ITEMNAME = 'blaster';
const ITEMNAME = "blaster";
export default new Cheer('timeout', 100, async (msg, user) => {
const args = parseCheerArgs(msg.messageText);
if (!args[0]) { await handleNoTarget(msg, user, ITEMNAME, false); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await handleNoTarget(msg, user, ITEMNAME, false); return; };
await getUserRecord(target);
export default new Cheer({
name: "timeout",
amount: 100,
isItem: true,
async execute(msg, user) {
const args = parseCheerArgs(msg.messageText);
if (!args[0]) {
await handleNoTarget(msg, user, ITEMNAME, false);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await handleNoTarget(msg, user, ITEMNAME, false);
return;
}
await getUserRecord(target);
const result = await timeout(target, `You got blasted by ${user.displayName}!`, 60);
if (result.status) await Promise.all([
sendMessage(`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`),
createTimeoutRecord(user, target, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
playAlert({
name: 'userBlast',
user: user.displayName,
target: target.displayName
})
]);
else {
await handleNoTarget(msg, user, ITEMNAME);
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;
};
};
}, true);
const result = await timeout(
target,
`You got blasted by ${user.displayName}!`,
60,
);
if (result.status)
await Promise.all([
sendMessage(
`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`,
),
createTimeoutRecord(user, target, ITEMNAME),
createCheerEventRecord(user, ITEMNAME),
playAlert({
name: "userBlast",
user: user.displayName,
target: target.displayName,
}),
]);
else {
await handleNoTarget(msg, user, ITEMNAME);
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;
}
}
},
});

View File

@@ -1,40 +1,57 @@
import { Cheer, handleNoTarget } from "cheers";
import { sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import User from "user";
import { timeout } from "lib/timeout";
import { createTimeoutRecord } from "db/dbTimeouts";
import { createCheerEventRecord } from "db/dbCheerEvents";
import { createTimeoutRecord } from "db/dbTimeouts";
import { getUserRecord } from "db/dbUser";
import { getTNTTargets } from "items/tnt";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import User from "user";
import { playAlert } from "web/alerts/serverFunctions";
const ITEMNAME = 'tnt';
const ITEMNAME = "tnt";
export default new Cheer('tnt', 1000, async (msg, user) => {
const vulntargets = await redis.keys('user:*:vulnerable').then(a => a.map(b => b.slice(5, -11)));
if (vulntargets.length === 0) { await sendMessage('No vulnerable chatters to blow up', msg.messageId); await handleNoTarget(msg, user, ITEMNAME); return; };
const targets = getTNTTargets(vulntargets);
export default new Cheer({
name: "tnt",
amount: 1000,
isItem: true,
async execute(msg, user) {
const vulntargets = await redis
.keys("user:*:vulnerable")
.then((a) => a.map((b) => b.slice(5, -11)));
if (vulntargets.length === 0) {
await sendMessage("No vulnerable chatters to blow up", msg.messageId);
await handleNoTarget(msg, user, ITEMNAME);
return;
}
const targets = getTNTTargets(vulntargets);
await Promise.all(targets.map(async targetid => {
const target = await User.initUserId(targetid);
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)
]);
}));
await Promise.all(
targets.map(async (targetid) => {
const target = await User.initUserId(targetid);
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),
]);
}),
);
await Promise.all([
createCheerEventRecord(user, ITEMNAME),
playAlert({
name: 'tntExplosion',
user: user.displayName,
targets
})
]);
await Promise.all([
createCheerEventRecord(user, ITEMNAME),
playAlert({
name: "tntExplosion",
user: user.displayName,
targets,
}),
]);
await sendMessage(`RIPBOZO ${user.displayName} exploded ${targets.length} chatter${targets.length === 1 ? '' : 's'} with their TNT RIPBOZO`);
}, true);
await sendMessage(
`RIPBOZO ${user.displayName} exploded ${targets.length} chatter${targets.length === 1 ? "" : "s"} with their TNT RIPBOZO`,
);
},
});

View File

@@ -1,20 +1,31 @@
import { Command, sendMessage } from "lib/commandUtils";
import { addAdmin } from "lib/admins";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
export default new Command({
name: 'addadmin',
aliases: ['addadmin'],
usertype: 'streamer',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a target', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
const data = await addAdmin(target.id);
if (data === "OK") await sendMessage(`${target.displayName} is now an admin`, msg.messageId);
else await sendMessage(`${target.displayName} is already an admin`, msg.messageId);
}
name: "addadmin",
aliases: ["addadmin"],
usertype: "streamer",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a target", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
const data = await addAdmin(target.id);
if (data === "OK")
await sendMessage(`${target.displayName} is now an admin`, msg.messageId);
else
await sendMessage(
`${target.displayName} is already an admin`,
msg.messageId,
);
},
});

View File

@@ -1,23 +1,40 @@
import { redis } from "lib/redis";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { redis } from "lib/redis";
import { streamerUsers } from "main";
import User from "user";
export default new Command({
name: 'addbot',
aliases: ['addbot'],
usertype: 'streamer',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a target', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
if (streamerUsers.includes(target.id)) { await sendMessage(`Cannot change bot status of qweribot managed user`, msg.messageId); return; };
const data = await redis.set(`user:${target.id}:bot`, '1');
await target.clearVulnerable();
if (data === "OK") await sendMessage(`${target.displayName} is now a bot`, msg.messageId);
else await sendMessage(`${target.displayName} is already a bot`, msg.messageId);
}
name: "addbot",
aliases: ["addbot"],
usertype: "streamer",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a target", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
if (streamerUsers.includes(target.id)) {
await sendMessage(
`Cannot change bot status of qweribot managed user`,
msg.messageId,
);
return;
}
const data = await redis.set(`user:${target.id}:bot`, "1");
await target.clearVulnerable();
if (data === "OK")
await sendMessage(`${target.displayName} is now a bot`, msg.messageId);
else
await sendMessage(
`${target.displayName} is already a bot`,
msg.messageId,
);
},
});

View File

@@ -1,20 +1,34 @@
import { addInvuln } from "lib/invuln";
import { Command, sendMessage } from "lib/commandUtils";
import { addInvuln } from "lib/invuln";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
export default new Command({
name: 'addinvuln',
aliases: ['addinvuln'],
usertype: 'moderator',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a target', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
const data = await addInvuln(target.id);
if (data === "OK") await sendMessage(`${target.displayName} is now an invuln`, msg.messageId);
else await sendMessage(`${target.displayName} is already an invuln`, msg.messageId);
}
name: "addinvuln",
aliases: ["addinvuln"],
usertype: "moderator",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a target", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
const data = await addInvuln(target.id);
if (data === "OK")
await sendMessage(
`${target.displayName} is now an invuln`,
msg.messageId,
);
else
await sendMessage(
`${target.displayName} is already an invuln`,
msg.messageId,
);
},
});

View File

@@ -1,30 +1,54 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import { changeBalance } from "lib/changeBalance";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
export default new Command({
name: 'admindonate',
aliases: ['admindonate'],
usertype: 'admin',
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a user', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
const userRecord = await getUserRecord(target);
if (!args[1]) { await sendMessage('Please specify the amount qweribucks you want to give', msg.messageId); return; };
const amount = parseInt(args[1]);
if (isNaN(amount)) { await sendMessage(`'${args[1]}' is not a valid amount`); return; };
if (await target.itemLock()) { await sendMessage('Cannot give qweribucks (itemlock)', msg.messageId); return; };
await target.setLock();
const data = await changeBalance(target, userRecord, amount);
if (!data) {
await sendMessage(`Failed to give ${target.displayName} ${amount} qweribuck${amount === 1 ? '' : 's'}`, msg.messageId);
} else {
await sendMessage(`${target.displayName} now has ${data.balance} qweribuck${data.balance === 1 ? '' : 's'}`, msg.messageId);
};
await target.clearLock();
}
name: "admindonate",
aliases: ["admindonate"],
usertype: "admin",
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a user", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
const userRecord = await getUserRecord(target);
if (!args[1]) {
await sendMessage(
"Please specify the amount qweribucks you want to give",
msg.messageId,
);
return;
}
const amount = parseInt(args[1], 10);
if (Number.isNaN(amount)) {
await sendMessage(`'${args[1]}' is not a valid amount`);
return;
}
if (await target.itemLock()) {
await sendMessage("Cannot give qweribucks (itemlock)", msg.messageId);
return;
}
await target.setLock();
const data = await changeBalance(target, userRecord, amount);
if (!data) {
await sendMessage(
`Failed to give ${target.displayName} ${amount} qweribuck${amount === 1 ? "" : "s"}`,
msg.messageId,
);
} else {
await sendMessage(
`${target.displayName} now has ${data.balance} qweribuck${data.balance === 1 ? "" : "s"}`,
msg.messageId,
);
}
await target.clearLock();
},
});

View File

@@ -1,34 +1,64 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import items, { changeItemCount } from "items";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
export default new Command({
name: 'admingive',
aliases: ['admingive'],
usertype: 'admin',
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a user', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
const userRecord = await getUserRecord(target);
if (!args[1]) { await sendMessage('Please specify an item to give', msg.messageId); return; };
const item = items.get(args[1].toLowerCase());
if (!item) { await sendMessage(`Item ${args[1]} doesn't exist`, msg.messageId); return; };
if (!args[2]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; };
const amount = parseInt(args[2]);
if (isNaN(amount)) { await sendMessage(`'${args[2]}' is not a valid amount`); return; };
if (await target.itemLock()) { await sendMessage('Cannot give item (itemlock)', msg.messageId); return; };
await target.setLock();
const data = await changeItemCount(target, userRecord, item.name, amount);
if (data) {
const newamount = data.inventory[item.name]!;
await sendMessage(`${target.displayName} now has ${newamount} ${item.prettyName + (newamount === 1 ? '' : item.plural)}`, msg.messageId);
} else {
await sendMessage(`Failed to give ${target.displayName} ${amount} ${item.prettyName + (amount === 1 ? '' : item.plural)}`, msg.messageId);
};
await target.clearLock();
}
name: "admingive",
aliases: ["admingive"],
usertype: "admin",
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a user", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
const userRecord = await getUserRecord(target);
if (!args[1]) {
await sendMessage("Please specify an item to give", msg.messageId);
return;
}
const item = items.get(args[1].toLowerCase());
if (!item) {
await sendMessage(`Item ${args[1]} doesn't exist`, msg.messageId);
return;
}
if (!args[2]) {
await sendMessage(
"Please specify the amount of the item you want to give",
msg.messageId,
);
return;
}
const amount = parseInt(args[2], 10);
if (Number.isNaN(amount)) {
await sendMessage(`'${args[2]}' is not a valid amount`);
return;
}
if (await target.itemLock()) {
await sendMessage("Cannot give item (itemlock)", msg.messageId);
return;
}
await target.setLock();
const data = await changeItemCount(target, userRecord, item.name, amount);
if (data) {
const newamount = data.inventory[item.name]!;
await sendMessage(
`${target.displayName} now has ${newamount} ${item.prettyName + (newamount === 1 ? "" : item.plural)}`,
msg.messageId,
);
} else {
await sendMessage(
`Failed to give ${target.displayName} ${amount} ${item.prettyName + (amount === 1 ? "" : item.plural)}`,
msg.messageId,
);
}
await target.clearLock();
},
});

View File

@@ -1,34 +1,38 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getKDLeaderboard } from "db/dbUser";
import { Command, sendMessage } from "lib/commandUtils";
import User from "user";
type KD = { user: User; kd: number; };
type KD = { user: User; kd: number };
export default new Command({
name: 'alltimekdleaderboard',
aliases: ['alltimeleaderboard', 'alltimekdleaderboard'],
usertype: 'chatter',
execution: async msg => {
const rawKD = await getKDLeaderboard();
if (rawKD.length === 0) {
await sendMessage(`No users on leaderboard yet!`, msg.messageId);
return;
};
name: "alltimekdleaderboard",
aliases: ["alltimeleaderboard", "alltimekdleaderboard"],
usertype: "chatter",
execution: async (msg) => {
const rawKD = await getKDLeaderboard();
if (rawKD.length === 0) {
await sendMessage(`No users on leaderboard yet!`, msg.messageId);
return;
}
const userKDs: KD[] = [];
await Promise.all(rawKD.map(async userRecord => {
const user = await User.initUserId(userRecord.userId.toString());
if (!user) return;
userKDs.push({ user, kd: userRecord.KD })
}));
const userKDs: KD[] = [];
await Promise.all(
rawKD.map(async (userRecord) => {
const user = await User.initUserId(userRecord.userId.toString());
if (!user) return;
userKDs.push({ user, kd: userRecord.KD });
}),
);
userKDs.sort((a, b) => b.kd - a.kd);
userKDs.sort((a, b) => b.kd - a.kd);
const txt: string[] = [];
for (let i = 0; i < (userKDs.length < 10 ? userKDs.length : 10); i++) {
txt.push(`${i + 1}. ${userKDs[i]?.user.displayName}: ${userKDs[i]?.kd.toFixed(2)}`);
};
const txt: string[] = [];
for (let i = 0; i < (userKDs.length < 10 ? userKDs.length : 10); i++) {
txt.push(
`${i + 1}. ${userKDs[i]?.user.displayName}: ${userKDs[i]?.kd.toFixed(2)}`,
);
}
await sendMessage(`Alltime leaderboard: ${txt.join(' | ')}`, msg.messageId);
}
await sendMessage(`Alltime leaderboard: ${txt.join(" | ")}`, msg.messageId);
},
});

View File

@@ -1,33 +1,45 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getTimeoutStats, getItemStats } from "lib/getStats";
import { getItemStats, getTimeoutStats } from "lib/getStats";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
export default new Command({
name: 'alltimestats',
aliases: ['alltime', 'alltimestats'],
usertype: 'chatter',
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
let target: User | null = user;
if (args[0]) {
target = await User.initUsername(args[0]);
if (!target) { await sendMessage(`User ${args[0]} doesn't exist!`, msg.messageId); return; };
};
name: "alltimestats",
aliases: ["alltime", "alltimestats"],
usertype: "chatter",
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
let target: User | null = user;
if (args[0]) {
target = await User.initUsername(args[0]);
if (!target) {
await sendMessage(`User ${args[0]} doesn't exist!`, msg.messageId);
return;
}
}
const [timeout, item] = await Promise.all([getTimeoutStats(target, false), getItemStats(target, false)]);
if (!timeout || !item) { await sendMessage(`ERROR: Something went wrong!`, msg.messageId); return; };
const [timeout, item] = await Promise.all([
getTimeoutStats(target, false),
getItemStats(target, false),
]);
if (!timeout || !item) {
await sendMessage(`ERROR: Something went wrong!`, msg.messageId);
return;
}
const KD = timeout.shot.blaster / timeout.hit.blaster;
const KD = timeout.shot.blaster / timeout.hit.blaster;
await sendMessage(`
await sendMessage(
`
Alltime: stats of ${target.displayName}:
Users blasted: ${timeout.shot.blaster},
Blasted by others: ${timeout.hit.blaster} (${isNaN(KD) ? 0 : KD.toFixed(2)} K/D).
Blasted by others: ${timeout.hit.blaster} (${Number.isNaN(KD) ? 0 : KD.toFixed(2)} K/D).
Grenades lobbed: ${item.grenade},
TNT exploded: ${item.tnt}.
Silver bullets fired: ${timeout.shot.silverbullet},
Silver bullets taken: ${timeout.hit.silverbullet}.
`, msg.messageId);
}
`,
msg.messageId,
);
},
});

View File

@@ -1,19 +1,24 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getAnivTimeouts } from "db/dbAnivTimeouts";
import { Command, sendMessage } from "lib/commandUtils";
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);
}
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: ${Number.isNaN(percentage) ? "0" : percentage.toFixed(1)}%`;
await sendMessage(message, msg.messageId);
},
});

View File

@@ -1,15 +1,18 @@
import { Command, sendMessage } from "lib/commandUtils";
import User from "user";
import { redis } from "lib/redis";
import User from "user";
export default new Command({
name: 'backshot',
aliases: ['backshot'],
usertype: 'chatter',
execution: async (msg, user) => {
const targets = await redis.keys(`user:*:haschatted`);
const selection = targets[Math.floor(Math.random() * targets.length)]!;
const target = await User.initUserId(selection.slice(5, -11));
await sendMessage(`${user.displayName} backshotted ${target?.displayName}`, msg.messageId);
}
name: "backshot",
aliases: ["backshot"],
usertype: "chatter",
execution: async (msg, user) => {
const targets = await redis.keys(`user:*:haschatted`);
const selection = targets[Math.floor(Math.random() * targets.length)]!;
const target = await User.initUserId(selection.slice(5, -11));
await sendMessage(
`${user.displayName} backshotted ${target?.displayName}`,
msg.messageId,
);
},
});

View File

@@ -1,37 +1,62 @@
import { getUserRecord, updateUserRecord } from "db/dbUser";
import items from "items";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import items from "items";
import { getUserRecord, updateUserRecord } from "db/dbUser";
export default new Command({
name: 'buyitem',
aliases: ['buyitem', 'buy', 'purchase'],
usertype: 'chatter',
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage(`Specify the item you'd like to buy`, msg.messageId); return; };
const selecteditem = items.get(args[0].toLowerCase());
if (!selecteditem) { await sendMessage(`'${args[0]}' is not a valid item`, msg.messageId); return; };
const amount = args[1] ? parseInt(args[1]) : 1;
if (isNaN(amount) || amount < 1) { await sendMessage(`'${args[1]}' is not a valid amount to buy`, msg.messageId); return; };
const totalcost = amount * selecteditem.price;
name: "buyitem",
aliases: ["buyitem", "buy", "purchase"],
usertype: "chatter",
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage(`Specify the item you'd like to buy`, msg.messageId);
return;
}
const selecteditem = items.get(args[0].toLowerCase());
if (!selecteditem) {
await sendMessage(`'${args[0]}' is not a valid item`, msg.messageId);
return;
}
const amount = args[1] ? parseInt(args[1], 10) : 1;
if (Number.isNaN(amount) || amount < 1) {
await sendMessage(
`'${args[1]}' is not a valid amount to buy`,
msg.messageId,
);
return;
}
const totalcost = amount * selecteditem.price;
if (await user.itemLock()) { await sendMessage('Cannot buy item (itemlock)', msg.messageId); return; };
await user.setLock();
if (await user.itemLock()) {
await sendMessage("Cannot buy item (itemlock)", msg.messageId);
return;
}
await user.setLock();
const userRecord = await getUserRecord(user);
if (userRecord.balance < totalcost) { await sendMessage(`You don't have enough qbucks to buy ${amount} ${selecteditem.prettyName}${amount === 1 ? '' : selecteditem.plural}! You have ${userRecord.balance}, need ${totalcost}`); await user.clearLock(); return; };
const userRecord = await getUserRecord(user);
if (userRecord.balance < totalcost) {
await sendMessage(
`You don't have enough qbucks to buy ${amount} ${selecteditem.prettyName}${amount === 1 ? "" : selecteditem.plural}! You have ${userRecord.balance}, need ${totalcost}`,
);
await user.clearLock();
return;
}
if (userRecord.inventory[selecteditem.name]) userRecord.inventory[selecteditem.name]! += amount
else userRecord.inventory[selecteditem.name] = amount;
if (userRecord.inventory[selecteditem.name])
userRecord.inventory[selecteditem.name]! += amount;
else userRecord.inventory[selecteditem.name] = amount;
userRecord.balance -= totalcost;
userRecord.balance -= totalcost;
await Promise.all([
updateUserRecord(user, userRecord),
sendMessage(`${user.displayName} bought ${amount} ${selecteditem.prettyName}${amount === 1 ? '' : selecteditem.plural} for ${totalcost} qbucks. They now have ${userRecord.inventory[selecteditem.name]} ${selecteditem.prettyName}${userRecord.inventory[selecteditem.name] === 1 ? '' : selecteditem.plural} and ${userRecord.balance} qbucks`, msg.messageId)
]);
await Promise.all([
updateUserRecord(user, userRecord),
sendMessage(
`${user.displayName} bought ${amount} ${selecteditem.prettyName}${amount === 1 ? "" : selecteditem.plural} for ${totalcost} qbucks. They now have ${userRecord.inventory[selecteditem.name]} ${selecteditem.prettyName}${userRecord.inventory[selecteditem.name] === 1 ? "" : selecteditem.plural} and ${userRecord.balance} qbucks`,
msg.messageId,
),
]);
await user.clearLock();
}
await user.clearLock();
},
});

View File

@@ -1,20 +1,35 @@
import { redis } from "lib/redis";
import { namedcheers } from "cheers";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { namedcheers } from "cheers";
import { redis } from "lib/redis";
export default new Command({
name: 'disablecheer',
aliases: ['disablecheer'],
usertype: 'moderator',
disableable: false,
execution: 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);
}
name: "disablecheer",
aliases: ["disablecheer"],
usertype: "moderator",
disableable: false,
execution: 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,
);
},
});

View File

@@ -1,21 +1,42 @@
import { redis } from "lib/redis";
import commands from "commands";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { redis } from "lib/redis";
export default new Command({
name: 'disablecommand',
aliases: ['disablecommand'],
usertype: 'moderator',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a command to disable', msg.messageId); return; };
const selection = commands.get(args[0].toLowerCase());
if (!selection) { await sendMessage(`There is no ${args[0]} command`, msg.messageId); return; };
if (!selection.disableable) { await sendMessage(`Cannot disable ${selection.name} as the command is not disableable`, msg.messageId); return; };
const result = await redis.sadd('disabledcommands', selection.name);
if (result === 0) { await sendMessage(`The ${selection.name} command is already disabled`, msg.messageId); return; };
await sendMessage(`Successfully disabled the ${selection.name} command`, msg.messageId);
}
name: "disablecommand",
aliases: ["disablecommand"],
usertype: "moderator",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a command to disable", msg.messageId);
return;
}
const selection = commands.get(args[0].toLowerCase());
if (!selection) {
await sendMessage(`There is no ${args[0]} command`, msg.messageId);
return;
}
if (!selection.disableable) {
await sendMessage(
`Cannot disable ${selection.name} as the command is not disableable`,
msg.messageId,
);
return;
}
const result = await redis.sadd("disabledcommands", selection.name);
if (result === 0) {
await sendMessage(
`The ${selection.name} command is already disabled`,
msg.messageId,
);
return;
}
await sendMessage(
`Successfully disabled the ${selection.name} command`,
msg.messageId,
);
},
});

View File

@@ -1,30 +1,55 @@
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { disableRedeem, idMap, namedRedeems, sfxRedeems } from "pointRedeems";
import { Command, sendMessage } from "lib/commandUtils";
import logger from "lib/logger";
import parseCommandArgs from "lib/parseCommandArgs";
export default new Command({
name: 'disableRedeem',
aliases: ['disableredeem'],
usertype: 'moderator',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage("Please specify a point redemption to disable", msg.messageId); return; };
if (args[0] === 'sfx' || args[0] === 'sound') {
sfxRedeems.forEach(async redeem => {
const id = idMap.get(redeem.name);
if (!id) { await sendMessage(`Failed to find the ID for redeem ${redeem.name}`, msg.messageId); logger.err(`Failed to find the ID for ${redeem.name} while enabling`); return; };
await disableRedeem(redeem, id);
});
await sendMessage(`Disabled all sound (sfx) channel point redemptions`, msg.messageId);
return;
};
name: "disableRedeem",
aliases: ["disableredeem"],
usertype: "moderator",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage(
"Please specify a point redemption to disable",
msg.messageId,
);
return;
}
if (args[0] === "sfx" || args[0] === "sound") {
sfxRedeems.forEach(async (redeem) => {
const id = idMap.get(redeem.name);
if (!id) {
await sendMessage(
`Failed to find the ID for redeem ${redeem.name}`,
msg.messageId,
);
logger.err(`Failed to find the ID for ${redeem.name} while enabling`);
return;
}
await disableRedeem(redeem, id);
});
await sendMessage(
`Disabled all sound (sfx) channel point redemptions`,
msg.messageId,
);
return;
}
const selection = namedRedeems.get(args[0]);
if (!selection) { await sendMessage(`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://github.com/qwerinope/qweribot#point-redeems`, msg.messageId); return; };
const id = idMap.get(selection.name);
await disableRedeem(selection, id!);
await sendMessage(`The ${selection.name} point redeem is now disabled`, msg.messageId);
}
const selection = namedRedeems.get(args[0]);
if (!selection) {
await sendMessage(
`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://github.com/qwerinope/qweribot#point-redeems`,
msg.messageId,
);
return;
}
const id = idMap.get(selection.name);
await disableRedeem(selection, id!);
await sendMessage(
`The ${selection.name} point redeem is now disabled`,
msg.messageId,
);
},
});

View File

@@ -1,52 +1,81 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import parseCommandArgs from "lib/parseCommandArgs";
import { changeBalance } from "lib/changeBalance";
import User from "user";
import { Command, sendMessage } from "lib/commandUtils";
import logger from "lib/logger";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
export default new Command({
name: 'donate',
aliases: ['donate'],
usertype: 'chatter',
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a user', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
if (target.username === user.username) { await sendMessage("You can't give yourself qweribucks", msg.messageId); return; };
const targetRecord = await getUserRecord(target);
if (!args[1]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; };
const amount = parseInt(args[1]);
if (isNaN(amount) || amount < 1) { await sendMessage(`'${args[1]}' is not a valid amount`); return; };
name: "donate",
aliases: ["donate"],
usertype: "chatter",
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a user", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
if (target.username === user.username) {
await sendMessage("You can't give yourself qweribucks", msg.messageId);
return;
}
const targetRecord = await getUserRecord(target);
if (!args[1]) {
await sendMessage(
"Please specify the amount of the item you want to give",
msg.messageId,
);
return;
}
const amount = parseInt(args[1], 10);
if (Number.isNaN(amount) || amount < 1) {
await sendMessage(`'${args[1]}' is not a valid amount`);
return;
}
const userRecord = await getUserRecord(user);
if (userRecord.balance < amount) { await sendMessage(`You can't give qweribucks you don't have!`, msg.messageId); return; };
const userRecord = await getUserRecord(user);
if (userRecord.balance < amount) {
await sendMessage(
`You can't give qweribucks you don't have!`,
msg.messageId,
);
return;
}
if (await user.itemLock() || await target.itemLock()) { await sendMessage('Cannot give qweribucks (itemlock)', msg.messageId); return; };
if ((await user.itemLock()) || (await target.itemLock())) {
await sendMessage("Cannot give qweribucks (itemlock)", msg.messageId);
return;
}
await Promise.all([
user.setLock(),
target.setLock()
]);
await Promise.all([user.setLock(), target.setLock()]);
const data = await Promise.all([
await changeBalance(target, targetRecord, amount),
await changeBalance(user, userRecord, -amount)
]);
const data = await Promise.all([
await changeBalance(target, targetRecord, amount),
await changeBalance(user, userRecord, -amount),
]);
if (data[0] !== false && data[1] !== false) {
const { balance: newamount } = data[0];
await sendMessage(`${user.displayName} gave ${amount} qweribuck${amount === 1 ? '' : 's'} to ${target.displayName}. They now have ${newamount} qweribuck${newamount === 1 ? '' : 's'}`, msg.messageId);
} else {
// TODO: Rewrite this section
await sendMessage(`Failed to give ${target.displayName} ${amount} qbuck${(amount === 1 ? '' : 's')}`, msg.messageId);
logger.err(`WARNING: Qweribucks donation failed: target success: ${data[0] !== false}, donator success: ${data[1] !== false}`);
};
if (data[0] !== false && data[1] !== false) {
const { balance: newamount } = data[0];
await sendMessage(
`${user.displayName} gave ${amount} qweribuck${amount === 1 ? "" : "s"} to ${target.displayName}. They now have ${newamount} qweribuck${newamount === 1 ? "" : "s"}`,
msg.messageId,
);
} else {
// TODO: Rewrite this section
await sendMessage(
`Failed to give ${target.displayName} ${amount} qbuck${amount === 1 ? "" : "s"}`,
msg.messageId,
);
logger.err(
`WARNING: Qweribucks donation failed: target success: ${data[0] !== false}, donator success: ${data[1] !== false}`,
);
}
await Promise.all([
user.clearLock(),
target.clearLock()
]);
}
await Promise.all([user.clearLock(), target.clearLock()]);
},
});

View File

@@ -1,20 +1,35 @@
import { redis } from "lib/redis";
import { namedcheers } from "cheers";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { namedcheers } from "cheers";
import { redis } from "lib/redis";
export default new Command({
name: 'enablecheer',
aliases: ['enablecheer'],
usertype: 'moderator',
disableable: false,
execution: 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);
}
name: "enablecheer",
aliases: ["enablecheer"],
usertype: "moderator",
disableable: false,
execution: 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,
);
},
});

View File

@@ -1,20 +1,35 @@
import { redis } from "lib/redis";
import { Command, sendMessage } from "lib/commandUtils";
import commands from "commands";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { redis } from "lib/redis";
export default new Command({
name: 'enablecommand',
aliases: ['enablecommand'],
usertype: 'moderator',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a command to enable', msg.messageId); return; };
const selection = commands.get(args[0].toLowerCase());
if (!selection) { await sendMessage(`There is no ${args[0]} command`, msg.messageId); return; };
const result = await redis.srem('disabledcommands', selection.name);
if (result === 0) { await sendMessage(`The ${selection.name} command isn't disabled`, msg.messageId); return; };
await sendMessage(`Successfully enabled the ${selection.name} command`, msg.messageId);
}
name: "enablecommand",
aliases: ["enablecommand"],
usertype: "moderator",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a command to enable", msg.messageId);
return;
}
const selection = commands.get(args[0].toLowerCase());
if (!selection) {
await sendMessage(`There is no ${args[0]} command`, msg.messageId);
return;
}
const result = await redis.srem("disabledcommands", selection.name);
if (result === 0) {
await sendMessage(
`The ${selection.name} command isn't disabled`,
msg.messageId,
);
return;
}
await sendMessage(
`Successfully enabled the ${selection.name} command`,
msg.messageId,
);
},
});

View File

@@ -1,29 +1,54 @@
import { enableRedeem, idMap, namedRedeems, sfxRedeems } from "pointRedeems";
import { Command, sendMessage } from "lib/commandUtils";
import logger from "lib/logger";
import parseCommandArgs from "lib/parseCommandArgs";
import { enableRedeem, idMap, namedRedeems, sfxRedeems } from "pointRedeems";
export default new Command({
name: 'enableRedeem',
aliases: ['enableredeem'],
usertype: 'moderator',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage("Please specify a point redemption to enable", msg.messageId); return; };
if (args[0] === 'sfx' || args[0] === 'sound') {
sfxRedeems.forEach(async redeem => {
const id = idMap.get(redeem.name);
if (!id) { await sendMessage(`Failed to find the ID for redeem ${redeem.name}`, msg.messageId); logger.err(`Failed to find the ID for ${redeem.name} while enabling`); return; };
await enableRedeem(redeem, id);
});
await sendMessage(`Enabled all sound (sfx) channel point redemptions`, msg.messageId);
return;
};
const selection = namedRedeems.get(args[0]);
if (!selection) { await sendMessage(`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://github.com/qwerinope/qweribot#point-redeems`, msg.messageId); return; };
const id = idMap.get(selection.name);
await enableRedeem(selection, id!);
await sendMessage(`The ${selection.name} point redeem is now enabled`, msg.messageId);
}
name: "enableRedeem",
aliases: ["enableredeem"],
usertype: "moderator",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage(
"Please specify a point redemption to enable",
msg.messageId,
);
return;
}
if (args[0] === "sfx" || args[0] === "sound") {
sfxRedeems.forEach(async (redeem) => {
const id = idMap.get(redeem.name);
if (!id) {
await sendMessage(
`Failed to find the ID for redeem ${redeem.name}`,
msg.messageId,
);
logger.err(`Failed to find the ID for ${redeem.name} while enabling`);
return;
}
await enableRedeem(redeem, id);
});
await sendMessage(
`Enabled all sound (sfx) channel point redemptions`,
msg.messageId,
);
return;
}
const selection = namedRedeems.get(args[0]);
if (!selection) {
await sendMessage(
`Redeem ${args[0]} doesn't exist. The internal names for redeems are here: https://github.com/qwerinope/qweribot#point-redeems`,
msg.messageId,
);
return;
}
const id = idMap.get(selection.name);
await enableRedeem(selection, id!);
await sendMessage(
`The ${selection.name} point redeem is now enabled`,
msg.messageId,
);
},
});

View File

@@ -2,13 +2,13 @@ import { Command, sendMessage } from "lib/commandUtils";
import { timeout } from "lib/timeout";
export default new Command({
name: 'fakemodme',
aliases: ['modme', 'mod'],
usertype: 'chatter',
execution: async (_msg, user) => {
await Promise.all([
timeout(user, "NO MODME", 60),
sendMessage(`NO MODME COMMAND!!! UltraMad UltraMad UltraMad`)
]);
}
name: "fakemodme",
aliases: ["modme", "mod"],
usertype: "chatter",
execution: async (_msg, user) => {
await Promise.all([
timeout(user, "NO MODME", 60),
sendMessage(`NO MODME COMMAND!!! UltraMad UltraMad UltraMad`),
]);
},
});

View File

@@ -1,19 +1,22 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getAdmins } from "lib/admins";
import { Command, sendMessage } from "lib/commandUtils";
import User from "user";
export default new Command({
name: 'getadmins',
aliases: ['getadmins'],
usertype: 'chatter',
disableable: false,
execution: async msg => {
const admins = await getAdmins()
const adminnames: string[] = [];
for (const id of admins) {
const admin = await User.initUserId(id);
adminnames.push(admin?.displayName!);
};
await sendMessage(`Current admins: ${adminnames.join(', ')}`, msg.messageId);
}
name: "getadmins",
aliases: ["getadmins"],
usertype: "chatter",
disableable: false,
execution: async (msg) => {
const admins = await getAdmins();
const adminnames: string[] = [];
for (const id of admins) {
const admin = await User.initUserId(id);
adminnames.push(admin?.displayName!);
}
await sendMessage(
`Current admins: ${adminnames.join(", ")}`,
msg.messageId,
);
},
});

View File

@@ -1,17 +1,32 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
export default new Command({
name: 'getbalance',
aliases: ['getbalance', 'balance', 'qbucks', 'qweribucks', 'wallet', 'getwallet'],
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 data = await getUserRecord(target);
await sendMessage(`${target.displayName} has ${data.balance} qbuck${data.balance === 1 ? '' : 's'}`, msg.messageId);
}
name: "getbalance",
aliases: [
"getbalance",
"balance",
"qbucks",
"qweribucks",
"wallet",
"getwallet",
],
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 data = await getUserRecord(target);
await sendMessage(
`${target.displayName} has ${data.balance} qbuck${data.balance === 1 ? "" : "s"}`,
msg.messageId,
);
},
});

View File

@@ -1,39 +1,63 @@
import { redis } from "lib/redis";
import { namedcheers } from "cheers";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { namedcheers } from "cheers";
import { redis } from "lib/redis";
export default new Command({
name: 'getcheers',
aliases: ['getcheers', 'getcheer'],
usertype: 'chatter',
disableable: false,
execution: 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[] = [];
name: "getcheers",
aliases: ["getcheers", "getcheer"],
usertype: "chatter",
disableable: false,
execution: 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}`);
};
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);
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}`);
}
} 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);
}
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,
);
},
});

View File

@@ -1,30 +1,51 @@
import { redis } from "lib/redis";
import { Command, sendMessage } from "lib/commandUtils";
import { basecommands } from "commands";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { redis } from "lib/redis";
export default new Command({
name: 'getcommands',
aliases: ['getcommands', 'getc', 'commands'],
usertype: 'chatter',
disableable: false,
execution: 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-1`, msg.messageId); return; };
const disabledcommands = await redis.smembers('disabledcommands');
if (args[0].toLowerCase() === 'enabled') {
const commandnames: string[] = [];
for (const [name, command] of Array.from(basecommands.entries())) {
if (command.usertype !== 'chatter') continue; // Admin only commands should be somewhat hidden
if (disabledcommands.includes(name)) continue;
commandnames.push(name);
};
if (commandnames.length === 0) await sendMessage('No commands besides non-disableable commands are enabled', msg.messageId);
else await sendMessage(`Currently enabled commands: ${commandnames.join(', ')}`, msg.messageId);
} else if (args[0].toLowerCase() === 'disabled') {
if (disabledcommands.length === 0) await sendMessage('No commands are disabled', msg.messageId);
else await sendMessage(`Currently disabled commands: ${disabledcommands.join(', ')}`);
}
else await sendMessage('Please specify if you want the enabled or disabled commands', msg.messageId);
}
name: "getcommands",
aliases: ["getcommands", "getc", "commands"],
usertype: "chatter",
disableable: false,
execution: 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-1`,
msg.messageId,
);
return;
}
const disabledcommands = await redis.smembers("disabledcommands");
if (args[0].toLowerCase() === "enabled") {
const commandnames: string[] = [];
for (const [name, command] of Array.from(basecommands.entries())) {
if (command.usertype !== "chatter") continue; // Admin only commands should be somewhat hidden
if (disabledcommands.includes(name)) continue;
commandnames.push(name);
}
if (commandnames.length === 0)
await sendMessage(
"No commands besides non-disableable commands are enabled",
msg.messageId,
);
else
await sendMessage(
`Currently enabled commands: ${commandnames.join(", ")}`,
msg.messageId,
);
} else if (args[0].toLowerCase() === "disabled") {
if (disabledcommands.length === 0)
await sendMessage("No commands are disabled", msg.messageId);
else
await sendMessage(
`Currently disabled commands: ${disabledcommands.join(", ")}`,
);
} else
await sendMessage(
"Please specify if you want the enabled or disabled commands",
msg.messageId,
);
},
});

View File

@@ -1,31 +1,42 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import items from "items";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
import items from "items";
export default new Command({
name: 'inventory',
aliases: ['inv', 'inventory', 'pocket'],
usertype: 'chatter',
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
let target: User = user;
if (args[0]) {
const obj = await User.initUsername(args[0].toLowerCase());
if (!obj) { await sendMessage(`User ${args[0]} doesn't exist`, msg.messageId); return; };
target = obj;
};
name: "inventory",
aliases: ["inv", "inventory", "pocket"],
usertype: "chatter",
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
let target: User = user;
if (args[0]) {
const obj = await User.initUsername(args[0].toLowerCase());
if (!obj) {
await sendMessage(`User ${args[0]} doesn't exist`, msg.messageId);
return;
}
target = obj;
}
const data = await getUserRecord(target);
const messagedata: string[] = [];
for (const [key, amount] of Object.entries(data.inventory)) {
if (amount === 0) continue;
const itemselection = items.get(key);
messagedata.push(`${itemselection?.prettyName}${amount === 1 ? '' : itemselection?.plural}: ${amount}`);
};
const data = await getUserRecord(target);
const messagedata: string[] = [];
for (const [key, amount] of Object.entries(data.inventory)) {
if (amount === 0) continue;
const itemselection = items.get(key);
messagedata.push(
`${itemselection?.prettyName}${amount === 1 ? "" : itemselection?.plural}: ${amount}`,
);
}
if (messagedata.length === 0) { await sendMessage(`${target.displayName} has no items`, msg.messageId); return; };
await sendMessage(`Inventory of ${target.displayName}: ${messagedata.join(', ')}`, msg.messageId);
}
if (messagedata.length === 0) {
await sendMessage(`${target.displayName} has no items`, msg.messageId);
return;
}
await sendMessage(
`Inventory of ${target.displayName}: ${messagedata.join(", ")}`,
msg.messageId,
);
},
});

View File

@@ -3,17 +3,20 @@ import { getInvulns } from "lib/invuln";
import User from "user";
export default new Command({
name: 'getinvulns',
aliases: ['getinvulns'],
usertype: 'chatter',
disableable: false,
execution: async msg => {
const invulns = await getInvulns()
const invulnnames: string[] = [];
for (const id of invulns) {
const invuln = await User.initUserId(id);
invulnnames.push(invuln?.displayName!);
};
await sendMessage(`Current invulnerable chatters: ${invulnnames.join(', ')}`, msg.messageId);
}
name: "getinvulns",
aliases: ["getinvulns"],
usertype: "chatter",
disableable: false,
execution: async (msg) => {
const invulns = await getInvulns();
const invulnnames: string[] = [];
for (const id of invulns) {
const invuln = await User.initUserId(id);
invulnnames.push(invuln?.displayName!);
}
await sendMessage(
`Current invulnerable chatters: ${invulnnames.join(", ")}`,
msg.messageId,
);
},
});

View File

@@ -1,103 +1,139 @@
import { redis } from "lib/redis";
import { Command, sendMessage } from "lib/commandUtils";
import { createGetLootRecord } from "db/dbGetLoot";
import { getUserRecord, updateUserRecord } from "db/dbUser";
import itemMap, { type inventory, type items } from "items";
import { Command, sendMessage } from "lib/commandUtils";
import { buildTimeString } from "lib/dateManager";
import { timeout } from "lib/timeout";
import { isInvuln, removeInvuln } from "lib/invuln";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import { streamerUsers } from "main";
import { createGetLootRecord } from "db/dbGetLoot";
import { playAlert } from "web/alerts/serverFunctions";
const COOLDOWN = 10 * 60; // 10 mins (s)
export default new Command({
name: 'getloot',
aliases: ['getloot', 'dig', 'loot', 'mine'],
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; };
await user.setLock();
const userData = await getUserRecord(user);
const timeData = await redis.expiretime(`user:${user.id}:lootboxcooldown`) * 1000;
if ((timeData) > Date.now()) {
await user.clearLock();
if (await user.greedy()) {
await Promise.all([
sendMessage(`${user.displayName} STOP BEING GREEDY!!! UltraMad UltraMad UltraMad`),
timeout(user, `Wait ${buildTimeString(timeData, Date.now())} for another lootbox`, 60)
]);
return;
} else {
await Promise.all([
user.setGreed(),
sendMessage(`Wait ${buildTimeString(timeData, Date.now())} for another lootbox.`, msg.messageId)
]);
return;
};
};
name: "getloot",
aliases: ["getloot", "dig", "loot", "mine"],
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;
}
await user.setLock();
const userData = await getUserRecord(user);
const timeData =
(await redis.expiretime(`user:${user.id}:lootboxcooldown`)) * 1000;
if (timeData > Date.now()) {
await user.clearLock();
if (await user.greedy()) {
await Promise.all([
sendMessage(
`${user.displayName} STOP BEING GREEDY!!! UltraMad UltraMad UltraMad`,
),
timeout(
user,
`Wait ${buildTimeString(timeData, Date.now())} for another lootbox`,
60,
),
]);
return;
} else {
await Promise.all([
user.setGreed(),
sendMessage(
`Wait ${buildTimeString(timeData, Date.now())} for another lootbox.`,
msg.messageId,
),
]);
return;
}
}
await user.clearGreed();
await user.clearGreed();
await redis.set(`user:${user.id}:lootboxcooldown`, '1');
await redis.expire(`user:${user.id}:lootboxcooldown`, COOLDOWN);
await redis.set(`user:${user.id}:lootboxcooldown`, "1");
await redis.expire(`user:${user.id}:lootboxcooldown`, COOLDOWN);
if (!await redis.exists(`user:${user.id}:subbed`) && Math.random() < 0.1) {
await Promise.all([
user.clearLock(),
updateUserRecord(user, userData),
timeout(user, "THE LOOTBOX WAS TRAPPED!!!", 60),
sendMessage(`wybuh wybuh ${user.displayName.toUpperCase()} FOUND A TRAPPED LOOTBOX!!! wybuh wybuh`),
playAlert({
name: 'grenadeExplosion',
user: 'trapped lootbox',
target: user.displayName
})
]);
return;
};
const gainedqbucks = Math.floor(Math.random() * 100) + 50; // range from 50 to 150
userData.balance += gainedqbucks;
if (
!(await redis.exists(`user:${user.id}:subbed`)) &&
Math.random() < 0.1
) {
await Promise.all([
user.clearLock(),
updateUserRecord(user, userData),
timeout(user, "THE LOOTBOX WAS TRAPPED!!!", 60),
sendMessage(
`wybuh wybuh ${user.displayName.toUpperCase()} FOUND A TRAPPED LOOTBOX!!! wybuh wybuh`,
),
playAlert({
name: "grenadeExplosion",
user: "trapped lootbox",
target: user.displayName,
}),
]);
return;
}
const gainedqbucks = Math.floor(Math.random() * 100) + 50; // range from 50 to 150
userData.balance += gainedqbucks;
const itemDiff: inventory = {
grenade: 0,
blaster: 0,
tnt: 0,
silverbullet: 0
};
const itemDiff: inventory = {
grenade: 0,
blaster: 0,
tnt: 0,
silverbullet: 0,
};
for (let i = 0; i < 3; i++) {
if (Math.floor(Math.random() * 5) === 0) itemDiff.grenade! += 1; // 1 in 5
if (Math.floor(Math.random() * 5) === 0) itemDiff.blaster! += 1; // 1 in 5
if (Math.floor(Math.random() * 50) === 0) itemDiff.tnt! += 1; // 1 in 50
if (Math.floor(Math.random() * 50) === 0) itemDiff.silverbullet! += 1; // 1 in 50
};
for (let i = 0; i < 3; i++) {
if (Math.floor(Math.random() * 5) === 0) itemDiff.grenade! += 1; // 1 in 5
if (Math.floor(Math.random() * 5) === 0) itemDiff.blaster! += 1; // 1 in 5
if (Math.floor(Math.random() * 50) === 0) itemDiff.tnt! += 1; // 1 in 50
if (Math.floor(Math.random() * 50) === 0) itemDiff.silverbullet! += 1; // 1 in 50
}
for (const [item, amount] of Object.entries(itemDiff) as [items, number][]) {
if (userData.inventory[item]) userData.inventory[item] += amount;
else userData.inventory[item] = amount;
};
for (const [item, amount] of Object.entries(itemDiff) as [
items,
number,
][]) {
if (userData.inventory[item]) userData.inventory[item] += amount;
else userData.inventory[item] = amount;
}
const itemstrings: string[] = [`${gainedqbucks} qbucks`];
const itemstrings: string[] = [`${gainedqbucks} qbucks`];
for (const [item, amount] of Object.entries(itemDiff)) {
if (amount === 0) continue;
const selection = itemMap.get(item);
if (!selection) continue;
itemstrings.push(`${amount} ${selection.prettyName + (amount === 1 ? '' : selection.plural)}`);
};
for (const [item, amount] of Object.entries(itemDiff)) {
if (amount === 0) continue;
const selection = itemMap.get(item);
if (!selection) continue;
itemstrings.push(
`${amount} ${selection.prettyName + (amount === 1 ? "" : selection.plural)}`,
);
}
const last = itemstrings.pop();
const itemstring = itemstrings.length === 0 ? last : itemstrings.join(', ') + " and " + last;
const message = `You got ${itemstring}`;
const last = itemstrings.pop();
const itemstring =
itemstrings.length === 0 ? last : `${itemstrings.join(", ")} and ${last}`;
const message = `You got ${itemstring}`;
await Promise.all([
updateUserRecord(user, userData),
sendMessage(message, msg.messageId),
createGetLootRecord(user, gainedqbucks, itemDiff, 'getloot'),
user.clearLock()
]);
}
await Promise.all([
updateUserRecord(user, userData),
sendMessage(message, msg.messageId),
createGetLootRecord(user, gainedqbucks, itemDiff, "getloot"),
user.clearLock(),
]);
},
});

View File

@@ -1,12 +1,14 @@
import { Command, sendMessage } from "lib/commandUtils";
import { itemObjectArray } from "items";
import { Command, sendMessage } from "lib/commandUtils";
export default new Command({
name: 'getprices',
aliases: ['getprices', 'prices', 'shop'],
usertype: 'chatter',
execution: async msg => {
const txt = itemObjectArray.toSorted((a, b) => a.price - b.price).map(item => `${item.prettyName}: ${item.price}`);
await sendMessage(`Prices: ${txt.join(' | ')}`, msg.messageId);
}
name: "getprices",
aliases: ["getprices", "prices", "shop"],
usertype: "chatter",
execution: async (msg) => {
const txt = itemObjectArray
.toSorted((a, b) => a.price - b.price)
.map((item) => `${item.prettyName}: ${item.price}`);
await sendMessage(`Prices: ${txt.join(" | ")}`, msg.messageId);
},
});

View File

@@ -1,21 +1,42 @@
import { Command, sendMessage } from "lib/commandUtils";
import { buildTimeString } from "lib/dateManager";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
import { timeoutDuration } from "lib/timeout";
import User from "user";
export default new Command({
name: 'gettimeout',
aliases: ['gett', 'gettimeout', 'releasetime'],
usertype: 'chatter',
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a target', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
const data = await timeoutDuration(target);
if (data === false) { await sendMessage(`Chatter ${target.displayName} isn't timed out`, msg.messageId); return; };
if (data) { await sendMessage(`${target.displayName} is still timed out for ${buildTimeString(data * 1000, Date.now())}`, msg.messageId); return; };
await sendMessage(`${target.displayName} is permanently banned`, msg.messageId);
}
name: "gettimeout",
aliases: ["gett", "gettimeout", "releasetime"],
usertype: "chatter",
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a target", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
const data = await timeoutDuration(target);
if (data === false) {
await sendMessage(
`Chatter ${target.displayName} isn't timed out`,
msg.messageId,
);
return;
}
if (data) {
await sendMessage(
`${target.displayName} is still timed out for ${buildTimeString(data * 1000, Date.now())}`,
msg.messageId,
);
return;
}
await sendMessage(
`${target.displayName} is permanently banned`,
msg.messageId,
);
},
});

View File

@@ -1,52 +1,87 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import items, { changeItemCount } from "items";
import { Command, sendMessage } from "lib/commandUtils";
import logger from "lib/logger";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
import logger from "lib/logger";
export default new Command({
name: 'give',
aliases: ['give'],
usertype: 'chatter',
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a user', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
if (target.username === user.username) { await sendMessage("You can't give yourself items", msg.messageId); return; };
const targetRecord = await getUserRecord(target);
if (!args[1]) { await sendMessage('Please specify an item to give', msg.messageId); return; };
const item = items.get(args[1].toLowerCase());
if (!item) { await sendMessage(`Item ${args[1]} doesn't exist`, msg.messageId); return; };
if (!args[2]) { await sendMessage('Please specify the amount of the item you want to give', msg.messageId); return; };
const amount = parseInt(args[2]);
if (isNaN(amount) || amount < 1) { await sendMessage(`'${args[2]}' is not a valid amount`); return; };
const userRecord = await getUserRecord(user);
if (userRecord.inventory[item.name]! < amount) { await sendMessage(`You can't give items you don't have!`, msg.messageId); return; };
name: "give",
aliases: ["give"],
usertype: "chatter",
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a user", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
if (target.username === user.username) {
await sendMessage("You can't give yourself items", msg.messageId);
return;
}
const targetRecord = await getUserRecord(target);
if (!args[1]) {
await sendMessage("Please specify an item to give", msg.messageId);
return;
}
const item = items.get(args[1].toLowerCase());
if (!item) {
await sendMessage(`Item ${args[1]} doesn't exist`, msg.messageId);
return;
}
if (!args[2]) {
await sendMessage(
"Please specify the amount of the item you want to give",
msg.messageId,
);
return;
}
const amount = parseInt(args[2], 10);
if (Number.isNaN(amount) || amount < 1) {
await sendMessage(`'${args[2]}' is not a valid amount`);
return;
}
const userRecord = await getUserRecord(user);
if (userRecord.inventory[item.name]! < amount) {
await sendMessage(`You can't give items you don't have!`, msg.messageId);
return;
}
if (await user.itemLock() || await target.itemLock()) { await sendMessage('Cannot give item (itemlock)', msg.messageId); return; };
if ((await user.itemLock()) || (await target.itemLock())) {
await sendMessage("Cannot give item (itemlock)", msg.messageId);
return;
}
await Promise.all([
user.setLock(),
target.setLock()
]);
await Promise.all([user.setLock(), target.setLock()]);
const data = await Promise.all([
await changeItemCount(target, targetRecord, item.name, amount),
await changeItemCount(user, userRecord, item.name, -amount)
]);
const data = await Promise.all([
await changeItemCount(target, targetRecord, item.name, amount),
await changeItemCount(user, userRecord, item.name, -amount),
]);
if (data[0] !== false && data[1] !== false) {
const tempdata = data[0];
const newamount = tempdata.inventory[item.name]!;
await sendMessage(`${user.displayName} gave ${amount} ${item.prettyName + (amount === 1 ? '' : item.plural)} to ${target.displayName}. They now have ${newamount} ${item.prettyName + (newamount === 1 ? '' : item.plural)}`, msg.messageId);
} else {
// TODO: Rewrite this section
await sendMessage(`Failed to give ${target.displayName} ${amount} ${item.prettyName + (amount === 1 ? '' : item.plural)}`, msg.messageId);
logger.warn(`WARNING: Item donation failed: target success: ${data[0] !== false ? "yes" : "no"}, donator success: ${data[1] !== false ? "yes" : "no"}`);
};
await user.clearLock();
await target.clearLock();
}
if (data[0] !== false && data[1] !== false) {
const tempdata = data[0];
const newamount = tempdata.inventory[item.name]!;
await sendMessage(
`${user.displayName} gave ${amount} ${item.prettyName + (amount === 1 ? "" : item.plural)} to ${target.displayName}. They now have ${newamount} ${item.prettyName + (newamount === 1 ? "" : item.plural)}`,
msg.messageId,
);
} else {
// TODO: Rewrite this section
await sendMessage(
`Failed to give ${target.displayName} ${amount} ${item.prettyName + (amount === 1 ? "" : item.plural)}`,
msg.messageId,
);
logger.warn(
`WARNING: Item donation failed: target success: ${data[0] !== false ? "yes" : "no"}, donator success: ${data[1] !== false ? "yes" : "no"}`,
);
}
await user.clearLock();
await target.clearLock();
},
});

View File

@@ -1,30 +1,34 @@
import { Command } from 'lib/commandUtils';
import { readdir } from 'node:fs/promises';
const commands = new Map<string, Command>; // This map has all command/item aliases mapped to commands/items (many-to-one)
const specialAliasCommands = new Map<string, Command>; // This map has all special command/item aliases mapped to commands/items (just like commands map)
const basecommands = new Map<string, Command>; // This map has all command names mapped to commands (one-to-one) (no items)
import { readdir } from "node:fs/promises";
import type { Command } from "lib/commandUtils";
const commands = new Map<string, Command>(); // This map has all command/item aliases mapped to commands/items (many-to-one)
const specialAliasCommands = new Map<string, Command>(); // This map has all special command/item aliases mapped to commands/items (just like commands map)
const basecommands = new Map<string, Command>(); // This map has all command names mapped to commands (one-to-one) (no items)
const files = await readdir(import.meta.dir);
for (const file of files) {
if (!file.endsWith('.ts')) continue;
if (file === import.meta.file) continue;
const command: Command = await import(import.meta.dir + '/' + file.slice(0, -3)).then(a => a.default);
basecommands.set(command.name, command);
for (const alias of command.aliases) {
commands.set(alias, command); // Since it's not a primitive type the map is filled with references to the command, not the actual object
};
for (const alias of command.specialaliases) {
specialAliasCommands.set(alias, command);
};
};
if (!file.endsWith(".ts")) continue;
if (file === import.meta.file) continue;
const command: Command = await import(
`${import.meta.dir}/${file.slice(0, -3)}`
).then((a) => a.default);
basecommands.set(command.name, command);
for (const alias of command.aliases) {
commands.set(alias, command); // Since it's not a primitive type the map is filled with references to the command, not the actual object
}
for (const alias of command.specialaliases) {
specialAliasCommands.set(alias, command);
}
}
import items, { specialAliasItems } from "items";
for (const [name, item] of Array.from(items)) {
commands.set(name, item); // As Item is basically just Command but with more parameters, this should work fine
};
commands.set(name, item); // As Item is basically just Command but with more parameters, this should work fine
}
for (const [alias, item] of Array.from(specialAliasItems)) {
specialAliasCommands.set(alias, item);
};
specialAliasCommands.set(alias, item);
}
export default commands;
export { specialAliasCommands, basecommands };

View File

@@ -1,16 +1,28 @@
import { Command, sendMessage } from "lib/commandUtils";
import items from "items";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
export default new Command({
name: 'iteminfo',
aliases: ['iteminfo', 'itemhelp', 'info'],
usertype: 'chatter',
execution: async msg => {
const messagequery = parseCommandArgs(msg.messageText).join(' ');
if (!messagequery) { await sendMessage('Please specify an item you would like to get info about', msg.messageId); return; };
const selection = items.get(messagequery.toLowerCase());
if (!selection) { await sendMessage(`'${messagequery}' is not an item`, msg.messageId); return; };
await sendMessage(`Name: ${selection.prettyName}, Description: ${selection.description}, Aliases: ${selection.aliases.join(', ')}`, msg.messageId);
}
name: "iteminfo",
aliases: ["iteminfo", "itemhelp", "info"],
usertype: "chatter",
execution: async (msg) => {
const messagequery = parseCommandArgs(msg.messageText).join(" ");
if (!messagequery) {
await sendMessage(
"Please specify an item you would like to get info about",
msg.messageId,
);
return;
}
const selection = items.get(messagequery.toLowerCase());
if (!selection) {
await sendMessage(`'${messagequery}' is not an item`, msg.messageId);
return;
}
await sendMessage(
`Name: ${selection.prettyName}, Description: ${selection.description}, Aliases: ${selection.aliases.join(", ")}`,
msg.messageId,
);
},
});

View File

@@ -3,17 +3,29 @@ import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
export default new Command({
name: 'itemlock',
aliases: ['itemlock'],
usertype: 'moderator',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a chatter to toggle the lock for', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage('Targeted user does not exist', msg.messageId); return; };
const status = await target.itemLock();
status ? await target.clearLock() : await target.setLock();
await sendMessage(`Successfully ${status ? 'cleared' : 'set'} the item lock on ${target.displayName}`, msg.messageId);
}
name: "itemlock",
aliases: ["itemlock"],
usertype: "moderator",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage(
"Please specify a chatter to toggle the lock for",
msg.messageId,
);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage("Targeted user does not exist", msg.messageId);
return;
}
const status = await target.itemLock();
status ? await target.clearLock() : await target.setLock();
await sendMessage(
`Successfully ${status ? "cleared" : "set"} the item lock on ${target.displayName}`,
msg.messageId,
);
},
});

View File

@@ -1,36 +1,40 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getKDLeaderboard } from "db/dbUser";
import { Command, sendMessage } from "lib/commandUtils";
import User from "user";
type KD = { user: User; kd: number; };
type KD = { user: User; kd: number };
export default new Command({
name: 'monthlykdleaderboard',
aliases: ['monthlyleaderboard', 'kdleaderboard', 'leaderboard'],
usertype: 'chatter',
execution: async msg => {
const monthdata = new Date().toISOString().slice(0, 7);
name: "monthlykdleaderboard",
aliases: ["monthlyleaderboard", "kdleaderboard", "leaderboard"],
usertype: "chatter",
execution: async (msg) => {
const monthdata = new Date().toISOString().slice(0, 7);
const rawKD = await getKDLeaderboard(monthdata);
if (rawKD.length === 0) {
await sendMessage(`No users on leaderboard yet!`, msg.messageId);
return;
};
const rawKD = await getKDLeaderboard(monthdata);
if (rawKD.length === 0) {
await sendMessage(`No users on leaderboard yet!`, msg.messageId);
return;
}
const userKDs: KD[] = [];
await Promise.all(rawKD.map(async userRecord => {
const user = await User.initUserId(userRecord.userId.toString());
if (!user) return;
userKDs.push({ user, kd: userRecord.KD })
}));
const userKDs: KD[] = [];
await Promise.all(
rawKD.map(async (userRecord) => {
const user = await User.initUserId(userRecord.userId.toString());
if (!user) return;
userKDs.push({ user, kd: userRecord.KD });
}),
);
userKDs.sort((a, b) => b.kd - a.kd);
userKDs.sort((a, b) => b.kd - a.kd);
const txt: string[] = [];
for (let i = 0; i < (userKDs.length < 10 ? userKDs.length : 10); i++) {
txt.push(`${i + 1}. ${userKDs[i]?.user.displayName}: ${userKDs[i]?.kd.toFixed(2)}`);
};
const txt: string[] = [];
for (let i = 0; i < (userKDs.length < 10 ? userKDs.length : 10); i++) {
txt.push(
`${i + 1}. ${userKDs[i]?.user.displayName}: ${userKDs[i]?.kd.toFixed(2)}`,
);
}
await sendMessage(`Monthly leaderboard: ${txt.join(' | ')}`, msg.messageId);
}
await sendMessage(`Monthly leaderboard: ${txt.join(" | ")}`, msg.messageId);
},
});

View File

@@ -1,33 +1,45 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getTimeoutStats, getItemStats } from "lib/getStats";
import { getItemStats, getTimeoutStats } from "lib/getStats";
import parseCommandArgs from "lib/parseCommandArgs";
import User from "user";
export default new Command({
name: 'monthlystats',
aliases: ['stats', 'monthlystats'],
usertype: 'chatter',
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
let target: User | null = user;
if (args[0]) {
target = await User.initUsername(args[0]);
if (!target) { await sendMessage(`User ${args[0]} doesn't exist!`, msg.messageId); return; };
};
name: "monthlystats",
aliases: ["stats", "monthlystats"],
usertype: "chatter",
execution: async (msg, user) => {
const args = parseCommandArgs(msg.messageText);
let target: User | null = user;
if (args[0]) {
target = await User.initUsername(args[0]);
if (!target) {
await sendMessage(`User ${args[0]} doesn't exist!`, msg.messageId);
return;
}
}
const [timeout, item] = await Promise.all([getTimeoutStats(target, true), getItemStats(target, true)]);
if (!timeout || !item) { await sendMessage(`ERROR: Something went wrong!`, msg.messageId); return; };
const [timeout, item] = await Promise.all([
getTimeoutStats(target, true),
getItemStats(target, true),
]);
if (!timeout || !item) {
await sendMessage(`ERROR: Something went wrong!`, msg.messageId);
return;
}
const KD = timeout.shot.blaster / timeout.hit.blaster;
const KD = timeout.shot.blaster / timeout.hit.blaster;
await sendMessage(`
await sendMessage(
`
This month: stats of ${target.displayName}:
Users blasted: ${timeout.shot.blaster},
Blasted by others: ${timeout.hit.blaster} (${isNaN(KD) ? 0 : KD.toFixed(2)} K/D).
Blasted by others: ${timeout.hit.blaster} (${Number.isNaN(KD) ? 0 : KD.toFixed(2)} K/D).
Grenades lobbed: ${item.grenade},
TNT exploded: ${item.tnt}.
Silver bullets fired: ${timeout.shot.silverbullet},
Silver bullets taken: ${timeout.hit.silverbullet}.
`, msg.messageId);
}
`,
msg.messageId,
);
},
});

View File

@@ -1,25 +1,25 @@
import { Command, sendMessage } from "lib/commandUtils";
import { getBalanceLeaderboard } from "db/dbUser";
import { Command, sendMessage } from "lib/commandUtils";
import User from "user";
export default new Command({
name: 'qbucksleaderboard',
aliases: ['qbucksleaderboard', 'baltop', 'moneyleaderboard'],
usertype: 'chatter',
execution: async msg => {
const data = await getBalanceLeaderboard();
if (!data) return;
name: "qbucksleaderboard",
aliases: ["qbucksleaderboard", "baltop", "moneyleaderboard"],
usertype: "chatter",
execution: async (msg) => {
const data = await getBalanceLeaderboard();
if (!data) return;
let index = 1;
const txt: string[] = [];
for (const userRecord of data) {
if (userRecord.balance === 0) continue;
const user = await User.initUserId(userRecord.id.toString());
if (!user) continue;
txt.push(`${index}. ${user.displayName}: ${userRecord.balance}`);
index++;
};
let index = 1;
const txt: string[] = [];
for (const userRecord of data) {
if (userRecord.balance === 0) continue;
const user = await User.initUserId(userRecord.id.toString());
if (!user) continue;
txt.push(`${index}. ${user.displayName}: ${userRecord.balance}`);
index++;
}
await sendMessage(`Balance leaderboard: ${txt.join(' | ')}`, msg.messageId);
}
await sendMessage(`Balance leaderboard: ${txt.join(" | ")}`, msg.messageId);
},
});

View File

@@ -4,29 +4,40 @@ import { streamerId } from "main";
import User from "user";
export default new Command({
name: 'racetime',
aliases: ['racetime', 'raceroom'],
usertype: 'chatter',
execution: async msg => {
try { // this might be some of the worst http code ever
const streamer = await User.initUserId(streamerId);
name: "racetime",
aliases: ["racetime", "raceroom"],
usertype: "chatter",
execution: async (msg) => {
try {
// this might be some of the worst http code ever
const streamer = await User.initUserId(streamerId);
const races = await fetch(`https://racetime.gg/smr/data`).then(a => a.json() as any);
if (races.current_races.length < 1) { await sendMessage(`No Super Metroid Randomizer races active`, msg.messageId); return; };
const races = await fetch(`https://racetime.gg/smr/data`).then(
(a) => a.json() as any,
);
if (races.current_races.length < 1) {
await sendMessage(
`No Super Metroid Randomizer races active`,
msg.messageId,
);
return;
}
for (const race of races.current_races) {
const data = await fetch(`https://racetime.gg${race.data_url}`).then(a => a.json() as any);
for (const racer of data.entrants) {
if (racer.user.twitch_name === streamer?.username) {
await sendMessage(`https://racetime.gg${data.url}`, msg.messageId);
return;
};
};
};
await sendMessage('Streamer is not in a racetime race.', msg.messageId);
} catch (err) {
await sendMessage("Failed to get racetime status", msg.messageId);
logger.err(err as string);
};
}
for (const race of races.current_races) {
const data = await fetch(`https://racetime.gg${race.data_url}`).then(
(a) => a.json() as any,
);
for (const racer of data.entrants) {
if (racer.user.twitch_name === streamer?.username) {
await sendMessage(`https://racetime.gg${data.url}`, msg.messageId);
return;
}
}
}
await sendMessage("Streamer is not in a racetime race.", msg.messageId);
} catch (err) {
await sendMessage("Failed to get racetime status", msg.messageId);
logger.err(err as string);
}
},
});

View File

@@ -4,19 +4,19 @@ import { streamerId } from "main";
import { playAlert } from "web/alerts/serverFunctions";
export default new Command({
name: 'randomchatter',
aliases: ['randomchatter'],
usertype: 'moderator',
execution: async (msg) => {
const data = await api.chat.getChatters(streamerId).then(a => a.data);
const target = data[Math.floor(Math.random() * data.length)];
await playAlert({
name: 'blastinRoulette',
user: msg.chatterName,
targets: data.map(a => a.userDisplayName),
finaltarget: target?.userDisplayName
});
await new Promise((res, _) => setTimeout(res, 4000));
await sendMessage(`${target?.userDisplayName}`, msg.messageId);
}
name: "randomchatter",
aliases: ["randomchatter"],
usertype: "moderator",
execution: async (msg) => {
const data = await api.chat.getChatters(streamerId).then((a) => a.data);
const target = data[Math.floor(Math.random() * data.length)];
await playAlert({
name: "blastinRoulette",
user: msg.chatterName,
targets: data.map((a) => a.userDisplayName),
finaltarget: target?.userDisplayName,
});
await new Promise((res, _) => setTimeout(res, 4000));
await sendMessage(`${target?.userDisplayName}`, msg.messageId);
},
});

View File

@@ -1,22 +1,39 @@
import { Command, sendMessage } from "lib/commandUtils";
import { streamerUsers } from "main";
import { removeAdmin } from "lib/admins";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { streamerUsers } from "main";
import User from "user";
export default new Command({
name: 'removeadmin',
aliases: ['removeadmin'],
usertype: 'streamer',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a target', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
if (streamerUsers.includes(target.id)) { await sendMessage(`Can't remove admin ${target.displayName} as they are managed by the bot program`, msg.messageId); return; };
const data = await removeAdmin(target.id);
if (data === 1) await sendMessage(`${target.displayName} is no longer an admin`, msg.messageId);
else await sendMessage(`${target.displayName} isn't an admin`, msg.messageId);
}
name: "removeadmin",
aliases: ["removeadmin"],
usertype: "streamer",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a target", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
if (streamerUsers.includes(target.id)) {
await sendMessage(
`Can't remove admin ${target.displayName} as they are managed by the bot program`,
msg.messageId,
);
return;
}
const data = await removeAdmin(target.id);
if (data === 1)
await sendMessage(
`${target.displayName} is no longer an admin`,
msg.messageId,
);
else
await sendMessage(`${target.displayName} isn't an admin`, msg.messageId);
},
});

View File

@@ -1,22 +1,38 @@
import { Command, sendMessage } from "lib/commandUtils";
import { streamerUsers } from "main";
import { redis } from "lib/redis";
import parseCommandArgs from "lib/parseCommandArgs";
import { redis } from "lib/redis";
import { streamerUsers } from "main";
import User from "user";
export default new Command({
name: 'removebot',
aliases: ['removebot'],
usertype: 'streamer',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a target', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
if (streamerUsers.includes(target.id)) { await sendMessage(`Cannot change bot status of qweribot managed user`, msg.messageId); return; };
const data = await redis.del(`user:${target.id}:bot`);
if (data === 1) await sendMessage(`${target.displayName} is no longer a bot`, msg.messageId);
else await sendMessage(`${target.displayName} isn't a bot`, msg.messageId);
}
name: "removebot",
aliases: ["removebot"],
usertype: "streamer",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a target", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
if (streamerUsers.includes(target.id)) {
await sendMessage(
`Cannot change bot status of qweribot managed user`,
msg.messageId,
);
return;
}
const data = await redis.del(`user:${target.id}:bot`);
if (data === 1)
await sendMessage(
`${target.displayName} is no longer a bot`,
msg.messageId,
);
else await sendMessage(`${target.displayName} isn't a bot`, msg.messageId);
},
});

View File

@@ -1,22 +1,42 @@
import { Command, sendMessage } from "lib/commandUtils";
import { streamerUsers } from "main";
import { removeInvuln } from "lib/invuln";
import parseCommandArgs from "lib/parseCommandArgs";
import { streamerUsers } from "main";
import User from "user";
export default new Command({
name: 'removeinvuln',
aliases: ['removeinvuln'],
usertype: 'moderator',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) { await sendMessage('Please specify a target', msg.messageId); return; };
const target = await User.initUsername(args[0].toLowerCase());
if (!target) { await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId); return; };
if (streamerUsers.includes(target.id)) { await sendMessage(`Can't remove invulnerability from ${target.displayName} as they are managed by the bot program`, msg.messageId); return; };
const data = await removeInvuln(target.id);
if (data === 1) await sendMessage(`${target.displayName} is no longer invulnerable`, msg.messageId);
else await sendMessage(`${target.displayName} isn't invulnerable`, msg.messageId);
}
name: "removeinvuln",
aliases: ["removeinvuln"],
usertype: "moderator",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0]) {
await sendMessage("Please specify a target", msg.messageId);
return;
}
const target = await User.initUsername(args[0].toLowerCase());
if (!target) {
await sendMessage(`Chatter ${args[0]} doesn't exist`, msg.messageId);
return;
}
if (streamerUsers.includes(target.id)) {
await sendMessage(
`Can't remove invulnerability from ${target.displayName} as they are managed by the bot program`,
msg.messageId,
);
return;
}
const data = await removeInvuln(target.id);
if (data === 1)
await sendMessage(
`${target.displayName} is no longer invulnerable`,
msg.messageId,
);
else
await sendMessage(
`${target.displayName} isn't invulnerable`,
msg.messageId,
);
},
});

View File

@@ -5,21 +5,24 @@ import { timeout } from "lib/timeout";
const barrelCount = 6;
export default new Command({
name: 'roulette',
aliases: ['roulette'],
usertype: 'chatter',
execution: async (msg, user) => {
if (!await redis.exists('rouletteCount')) await redis.set('rouletteCount', "0");
const currentChamber = Number(await redis.get('rouletteCount'));
const shot = Math.random() < 1 / (barrelCount - currentChamber);
if (!shot) await Promise.all([
redis.incr('rouletteCount'),
sendMessage("SWEAT Click SWEAT", msg.messageId)
]);
else await Promise.all([
redis.set('rouletteCount', "0"),
sendMessage("wybuh BANG!! wybuh"),
timeout(user, "You lost at russian roulette!", 5 * 60)
]);
}
name: "roulette",
aliases: ["roulette"],
usertype: "chatter",
execution: async (msg, user) => {
if (!(await redis.exists("rouletteCount")))
await redis.set("rouletteCount", "0");
const currentChamber = Number(await redis.get("rouletteCount"));
const shot = Math.random() < 1 / (barrelCount - currentChamber);
if (!shot)
await Promise.all([
redis.incr("rouletteCount"),
sendMessage("SWEAT Click SWEAT", msg.messageId),
]);
else
await Promise.all([
redis.set("rouletteCount", "0"),
sendMessage("wybuh BANG!! wybuh"),
timeout(user, "You lost at russian roulette!", 5 * 60),
]);
},
});

View File

@@ -2,23 +2,28 @@ import { Command, sendMessage } from "lib/commandUtils";
import { timeout } from "lib/timeout";
export default new Command({
name: 'seiso',
aliases: ['seiso'],
usertype: 'chatter',
execution: async (msg, user) => {
const rand = Math.floor(Math.random() * 101);
if (rand > 75) await sendMessage(`${rand}% seiso YAAAA`, msg.messageId);
else if (rand === 67) await Promise.all([
sendMessage(`KOKPEG 67 KOKPEG`),
timeout(user, 'SIX SEVEN', 67)
])
else if (rand > 50) await sendMessage(`${rand}% seiso POGGERS`, msg.messageId);
else if (rand === 50) await sendMessage(`${rand}% seiso ok`, msg.messageId);
else if (rand > 30) await sendMessage(`${rand}% seiso SWEAT`, msg.messageId);
else if (rand > 10) await sendMessage(`${rand}% seiso catErm`, msg.messageId);
else await Promise.all([
sendMessage(`${rand}% seiso RIPBOZO`),
timeout(user, 'TOO YABAI!', 60)
]);
}
name: "seiso",
aliases: ["seiso"],
usertype: "chatter",
execution: async (msg, user) => {
const rand = Math.floor(Math.random() * 101);
if (rand > 75) await sendMessage(`${rand}% seiso YAAAA`, msg.messageId);
else if (rand === 67)
await Promise.all([
sendMessage(`KOKPEG 67 KOKPEG`),
timeout(user, "SIX SEVEN", 67),
]);
else if (rand > 50)
await sendMessage(`${rand}% seiso POGGERS`, msg.messageId);
else if (rand === 50) await sendMessage(`${rand}% seiso ok`, msg.messageId);
else if (rand > 30)
await sendMessage(`${rand}% seiso SWEAT`, msg.messageId);
else if (rand > 10)
await sendMessage(`${rand}% seiso catErm`, msg.messageId);
else
await Promise.all([
sendMessage(`${rand}% seiso RIPBOZO`),
timeout(user, "TOO YABAI!", 60),
]);
},
});

View File

@@ -1,28 +1,34 @@
import { redis } from "lib/redis";
import { Command, sendMessage } from "lib/commandUtils";
import { isAdmin } from "lib/admins";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { redis } from "lib/redis";
export default new Command({
name: 'stacking',
aliases: ['stacking'],
usertype: 'chatter',
disableable: false,
execution: async msg => {
const args = parseCommandArgs(msg.messageText);
if (!args[0] || !await isAdmin(msg.chatterId)) { await sendMessage(`Timeout stacking is currently ${await redis.exists('timeoutStacking') ? "on" : "off"}`, msg.messageId); return; };
// Only admins can reach this part of code
switch (args[0]) {
case 'enable':
case 'on':
await redis.set('timeoutStacking', '1');
await sendMessage('Timeout stacking is now on')
break;
case 'disable':
case 'off':
await redis.del('timeoutStacking');
await sendMessage('Timeout stacking is now off')
break;
};
}
name: "stacking",
aliases: ["stacking"],
usertype: "chatter",
disableable: false,
execution: async (msg) => {
const args = parseCommandArgs(msg.messageText);
if (!args[0] || !(await isAdmin(msg.chatterId))) {
await sendMessage(
`Timeout stacking is currently ${(await redis.exists("timeoutStacking")) ? "on" : "off"}`,
msg.messageId,
);
return;
}
// Only admins can reach this part of code
switch (args[0]) {
case "enable":
case "on":
await redis.set("timeoutStacking", "1");
await sendMessage("Timeout stacking is now on");
break;
case "disable":
case "off":
await redis.del("timeoutStacking");
await sendMessage("Timeout stacking is now off");
break;
}
},
});

View File

@@ -1,17 +1,26 @@
import { Command, sendMessage } from "lib/commandUtils";
import { handleCheer } from "events/message";
import { Command, sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
export default new Command({
name: 'testcheer',
aliases: ['testcheer'],
usertype: 'streamer',
disableable: false,
execution: 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(parseInt(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, user);
}
name: "testcheer",
aliases: ["testcheer"],
usertype: "streamer",
disableable: false,
execution: 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 (Number.isNaN(parseInt(args[0], 10))) {
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, user);
},
});

View File

@@ -1,25 +1,56 @@
import { redis } from "lib/redis";
import { Command, sendMessage } from "lib/commandUtils";
import items from "items";
import { isInvuln, removeInvuln } from "lib/invuln";
import { streamerUsers } from "main";
import getloot from "commands/getloot";
import items from "items";
import { Command, sendMessage } from "lib/commandUtils";
import { isInvuln, removeInvuln } from "lib/invuln";
import { redis } from "lib/redis";
import { streamerUsers } from "main";
export default new Command({
name: 'use',
aliases: ['use'],
usertype: 'chatter',
disableable: false,
specialaliases: ['i'],
execution: async (msg, user, specialargs) => {
const messagequery = msg.messageText.trim().split(' ').slice(1); // This selects the item, so on "i blast mrockstar20" it would pick ["blast", "mrockstar20"]
const silent = msg.messageText.toLowerCase().startsWith('i');
if (!messagequery[0]) { if (!silent) { await sendMessage('Please specify an item you would like to use', msg.messageId); }; return; };
const selection = items.get(messagequery[0].toLowerCase());
if (messagequery[0].toLowerCase() === "lootbox") { await getloot.execute(msg, user); return; };
if (!selection) { if (!silent) { 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, specialargs);
}
name: "use",
aliases: ["use"],
usertype: "chatter",
disableable: false,
specialaliases: ["i"],
execution: async (msg, user, specialargs) => {
const messagequery = msg.messageText.trim().split(" ").slice(1); // This selects the item, so on "i blast mrockstar20" it would pick ["blast", "mrockstar20"]
const silent = msg.messageText.toLowerCase().startsWith("i");
if (!messagequery[0]) {
if (!silent) {
await sendMessage(
"Please specify an item you would like to use",
msg.messageId,
);
}
return;
}
const selection = items.get(messagequery[0].toLowerCase());
if (messagequery[0].toLowerCase() === "lootbox") {
await getloot.execute(msg, user);
return;
}
if (!selection) {
if (!silent) {
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, specialargs);
},
});

View File

@@ -1,13 +1,16 @@
import { redis } from "lib/redis";
import { Command, sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
export default new Command({
name: 'vulnchatters',
aliases: ['vulnchatters', 'vulnc', 'vc'],
usertype: 'chatter',
execution: async msg => {
const data = await redis.keys('user:*:vulnerable');
const one = data.length === 1;
await sendMessage(`There ${one ? 'is' : 'are'} ${data.length} vulnerable chatter${one ? '' : 's'}`, msg.messageId);
}
name: "vulnchatters",
aliases: ["vulnchatters", "vulnc", "vc"],
usertype: "chatter",
execution: async (msg) => {
const data = await redis.keys("user:*:vulnerable");
const one = data.length === 1;
await sendMessage(
`There ${one ? "is" : "are"} ${data.length} vulnerable chatter${one ? "" : "s"}`,
msg.messageId,
);
},
});

View File

@@ -3,22 +3,24 @@ import { timeout } from "lib/timeout";
// Remake of the !yabai command in ttv/kiara_tv
export default new Command({
name: 'yabai',
aliases: ['yabai', 'goon'],
usertype: 'chatter',
execution: async (msg, user) => {
const rand = Math.floor(Math.random() * 101);
if (rand < 25) sendMessage(`${rand}% yabai! GIGACHAD`, msg.messageId);
else if (rand < 50) sendMessage(`${rand}% yabai POGGERS`, msg.messageId);
else if (rand === 50) sendMessage(`${rand}% yabai ok`, msg.messageId);
else if (rand === 67) await Promise.all([
sendMessage(`KOKPEG 67 KOKPEG`),
timeout(user, 'SIX SEVEN', 67)
])
else if (rand < 90) sendMessage(`${rand}% yabai AINTNOWAY`, msg.messageId);
else await Promise.all([
sendMessage(`${msg.chatterDisplayName} is ${rand}% yabai CAUGHT`),
timeout(user, "TOO YABAI!", 60)
]);
}
name: "yabai",
aliases: ["yabai", "goon"],
usertype: "chatter",
execution: async (msg, user) => {
const rand = Math.floor(Math.random() * 101);
if (rand < 25) sendMessage(`${rand}% yabai! GIGACHAD`, msg.messageId);
else if (rand < 50) sendMessage(`${rand}% yabai POGGERS`, msg.messageId);
else if (rand === 50) sendMessage(`${rand}% yabai ok`, msg.messageId);
else if (rand === 67)
await Promise.all([
sendMessage(`KOKPEG 67 KOKPEG`),
timeout(user, "SIX SEVEN", 67),
]);
else if (rand < 90) sendMessage(`${rand}% yabai AINTNOWAY`, msg.messageId);
else
await Promise.all([
sendMessage(`${msg.chatterDisplayName} is ${rand}% yabai CAUGHT`),
timeout(user, "TOO YABAI!", 60),
]);
},
});

View File

@@ -4,22 +4,28 @@ import { users } from "db/schema";
import logger from "lib/logger";
export async function connectionCheck() {
let pgstatus = false;
try {
await db.select().from(users); // The query doesn't matter, only that it fails. This also fails if the migration hasn't taken place
pgstatus = true;
} catch { };
const tempclient = new RedisClient(undefined, {
connectionTimeout: 200,
maxRetries: 1,
});
let redisstatus = false;
try {
await tempclient.connect();
redisstatus = true;
} catch { };
logger.info(`Currently using the "${process.env.NODE_ENV ?? "production"}" database`);
pgstatus ? logger.ok(`Postgresql status: good`) : logger.err(`Postgresql status: bad`);
redisstatus ? logger.ok(`Redis/Valkey status: good`) : logger.err(`Redis/Valkey status: bad`);
if (!pgstatus || !redisstatus) process.exit(1);
};
let pgstatus = false;
try {
await db.select().from(users); // The query doesn't matter, only that it fails. This also fails if the migration hasn't taken place
pgstatus = true;
} catch {}
const tempclient = new RedisClient(undefined, {
connectionTimeout: 200,
maxRetries: 1,
});
let redisstatus = false;
try {
await tempclient.connect();
redisstatus = true;
} catch {}
logger.info(
`Currently using the "${process.env.NODE_ENV ?? "production"}" database`,
);
pgstatus
? logger.ok(`Postgresql status: good`)
: logger.err(`Postgresql status: bad`);
redisstatus
? logger.ok(`Redis/Valkey status: good`)
: logger.err(`Redis/Valkey status: bad`);
if (!pgstatus || !redisstatus) process.exit(1);
}

View File

@@ -6,5 +6,5 @@ export const password = process.env.POSTGRES_PASSWORD ?? "";
export const database = process.env.POSTGRES_DB ?? "";
export const url = `postgresql://${user}:${password}@${host}/${database}`;
import { drizzle } from 'drizzle-orm/bun-sql';
export default drizzle(url, { schema })
import { drizzle } from "drizzle-orm/bun-sql";
export default drizzle(url, { schema });

View File

@@ -1,32 +1,55 @@
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";
import { and, count, eq } from "drizzle-orm";
import type { anivBots } from "lib/handleAnivMessage";
import type User from "user";
/** 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,
timeout: duration !== 0
});
};
export async function createAnivTimeoutRecord(
message: string,
anivBot: anivBots,
user: User,
duration: number,
) {
await db.insert(anivTimeouts).values({
message,
anivBot,
user: parseInt(user.id, 10),
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)
]);
let [dodge, dead] = await Promise.all([
db
.select({
dodge: count(),
})
.from(anivTimeouts)
.where(
and(
eq(anivTimeouts.user, parseInt(user.id, 10)),
eq(anivTimeouts.timeout, false),
),
)
.then((a) => a[0]?.dodge),
db
.select({
dead: count(),
})
.from(anivTimeouts)
.where(
and(
eq(anivTimeouts.user, parseInt(user.id, 10)),
eq(anivTimeouts.timeout, true),
),
)
.then((a) => a[0]?.dead),
]);
if (!dodge) dodge = 0;
if (!dead) dead = 0;
if (!dodge) dodge = 0;
if (!dead) dead = 0;
return { dodge, dead };
};
return { dodge, dead };
}

View File

@@ -4,25 +4,31 @@ import { auth } from "db/schema";
import { eq } from "drizzle-orm";
export async function createAuthRecord(token: AccessToken, userId: string) {
await db.insert(auth).values({
id: parseInt(userId),
accesstoken: token
});
};
await db.insert(auth).values({
id: parseInt(userId, 10),
accesstoken: token,
});
}
export async function getAuthRecord(userId: string, requiredIntents: string[]) {
const data = await db.query.auth.findFirst({
where: eq(auth.id, parseInt(userId))
});
if (!data) return undefined;
if (!requiredIntents.every(intent => data.accesstoken.scope.includes(intent))) return undefined;
return { accesstoken: data.accesstoken };
};
const data = await db.query.auth.findFirst({
where: eq(auth.id, parseInt(userId, 10)),
});
if (!data) return undefined;
if (
!requiredIntents.every((intent) => data.accesstoken.scope.includes(intent))
)
return undefined;
return { accesstoken: data.accesstoken };
}
export async function updateAuthRecord(userId: string, newtoken: AccessToken) {
await db.update(auth).set({ accesstoken: newtoken }).where(eq(auth.id, parseInt(userId)));
};
await db
.update(auth)
.set({ accesstoken: newtoken })
.where(eq(auth.id, parseInt(userId, 10)));
}
export async function deleteAuthRecord(userId: string): Promise<void> {
await db.delete(auth).where(eq(auth.id, parseInt(userId)));
};
await db.delete(auth).where(eq(auth.id, parseInt(userId, 10)));
}

View File

@@ -1,20 +1,31 @@
import db from "db/connection";
import { cheerEvents } from "db/schema";
import { and, between, eq, SQL } from "drizzle-orm";
import { and, between, eq, type SQL } from "drizzle-orm";
import type { items } from "items";
import User from "user";
import type User from "user";
export async function createCheerEventRecord(user: User, cheer: items): Promise<void> {
await db.insert(cheerEvents).values({ user: parseInt(user.id), event: cheer });
};
export async function createCheerEventRecord(
user: User,
cheer: items,
): Promise<void> {
await db
.insert(cheerEvents)
.values({ user: parseInt(user.id, 10), event: cheer });
}
export async function getCheerEvents(user: User, monthData?: string) {
let condition: SQL<unknown> | undefined = eq(cheerEvents.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(cheerEvents.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(cheerEvents).where(condition);
return data;
};
let condition: SQL<unknown> | undefined = eq(
cheerEvents.user,
parseInt(user.id, 10),
);
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(
condition,
between(cheerEvents.created, new Date(begin), new Date(end)),
);
}
const data = await db.select().from(cheerEvents).where(condition);
return data;
}

View File

@@ -1,19 +1,28 @@
import db from "db/connection";
import { cheers } from "db/schema";
import User from "user";
import { and, between, eq, SQL } from "drizzle-orm";
import { and, between, eq, type SQL } from "drizzle-orm";
import type User from "user";
export async function createCheerRecord(user: User, amount: number): Promise<void> {
await db.insert(cheers).values({ user: parseInt(user.id), amount });
};
export async function createCheerRecord(
user: User,
amount: number,
): Promise<void> {
await db.insert(cheers).values({ user: parseInt(user.id, 10), amount });
}
export async function getCheers(user: User, monthData?: string) {
let condition: SQL<unknown> | undefined = eq(cheers.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(cheers.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(cheers).where(condition);
return data;
};
let condition: SQL<unknown> | undefined = eq(
cheers.user,
parseInt(user.id, 10),
);
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(
condition,
between(cheers.created, new Date(begin), new Date(end)),
);
}
const data = await db.select().from(cheers).where(condition);
return data;
}

View File

@@ -3,11 +3,16 @@ import { getLoots, type lootTriggers } from "db/schema";
import type { inventory } from "items";
import type User from "user";
export async function createGetLootRecord(user: User, qbucks: number, inventory: inventory, trigger: lootTriggers) {
await db.insert(getLoots).values({
user: parseInt(user.id),
qbucks: qbucks,
items: inventory,
trigger
});
};
export async function createGetLootRecord(
user: User,
qbucks: number,
inventory: inventory,
trigger: lootTriggers,
) {
await db.insert(getLoots).values({
user: parseInt(user.id, 10),
qbucks: qbucks,
items: inventory,
trigger,
});
}

View File

@@ -1,35 +1,51 @@
import db from "db/connection";
import { timeouts } from "db/schema";
import User from "user";
import type { items } from "items";
import { and, between, eq, type SQL } from "drizzle-orm";
import type { items } from "items";
import type User from "user";
export async function createTimeoutRecord(user: User, target: User, item: items): Promise<void> {
await db.insert(timeouts).values({
user: parseInt(user.id),
target: parseInt(target.id),
item
});
};
export async function createTimeoutRecord(
user: User,
target: User,
item: items,
): Promise<void> {
await db.insert(timeouts).values({
user: parseInt(user.id, 10),
target: parseInt(target.id, 10),
item,
});
}
export async function getTimeoutsAsUser(user: User, monthData?: string) {
let condition: SQL<unknown> | undefined = eq(timeouts.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(timeouts.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(timeouts).where(condition);
return data;
};
let condition: SQL<unknown> | undefined = eq(
timeouts.user,
parseInt(user.id, 10),
);
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(
condition,
between(timeouts.created, new Date(begin), new Date(end)),
);
}
const data = await db.select().from(timeouts).where(condition);
return data;
}
export async function getTimeoutsAsTarget(user: User, monthData?: string) {
let condition: SQL<unknown> | undefined = eq(timeouts.target, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(timeouts.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(timeouts).where(condition);
return data;
};
let condition: SQL<unknown> | undefined = eq(
timeouts.target,
parseInt(user.id, 10),
);
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(
condition,
between(timeouts.created, new Date(begin), new Date(end)),
);
}
const data = await db.select().from(timeouts).where(condition);
return data;
}

View File

@@ -1,20 +1,29 @@
import db from "db/connection";
import { usedItems } from "db/schema";
import User from "user";
import type { items } from "items";
import { and, between, eq, type SQL } from "drizzle-orm";
import type { items } from "items";
import type User from "user";
export async function createUsedItemRecord(user: User, item: items): Promise<void> {
await db.insert(usedItems).values({ user: parseInt(user.id), item });
};
export async function createUsedItemRecord(
user: User,
item: items,
): Promise<void> {
await db.insert(usedItems).values({ user: parseInt(user.id, 10), item });
}
export async function getItemsUsed(user: User, monthData?: string) {
let condition: SQL<unknown> | undefined = eq(usedItems.user, parseInt(user.id));
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(usedItems.created, new Date(begin), new Date(end)));
};
const data = await db.select().from(usedItems).where(condition);
return data;
};
let condition: SQL<unknown> | undefined = eq(
usedItems.user,
parseInt(user.id, 10),
);
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(
condition,
between(usedItems.created, new Date(begin), new Date(end)),
);
}
const data = await db.select().from(usedItems).where(condition);
return data;
}

View File

@@ -1,90 +1,122 @@
import db from "db/connection";
import { timeouts, users } from "db/schema";
import {
and,
between,
count,
desc,
eq,
type InferSelectModel,
inArray,
ne,
type SQL,
sql,
} from "drizzle-orm";
import { itemarray } from "items";
import type User from "user";
import { count, desc, eq, inArray, sql, ne, between, and, SQL, type InferSelectModel } from "drizzle-orm";
/** Use this function to both ensure existance and to retreive data */
export async function getUserRecord(user: User) {
const data = await db.query.users.findFirst({ where: eq(users.id, parseInt(user.id)) });
if (!data) return createUserRecord(user);
const data = await db.query.users.findFirst({
where: eq(users.id, parseInt(user.id, 10)),
});
if (!data) return createUserRecord(user);
if (Object.keys(data.inventory).sort().toString() !== itemarray.sort().toString()) { // If the items in the user inventory are missing an item.
itemarray.forEach(key => {
if (!(key in data.inventory)) data.inventory[key] = 0;
});
};
if (
Object.keys(data.inventory).sort().toString() !==
itemarray.sort().toString()
) {
// If the items in the user inventory are missing an item.
itemarray.forEach((key) => {
if (!(key in data.inventory)) data.inventory[key] = 0;
});
}
return data;
};
return data;
}
export async function getAllUserRecords() {
return await db.select().from(users);
};
return await db.select().from(users);
}
async function createUserRecord(user: User) {
return await db.insert(users).values({
id: parseInt(user.id),
username: user.username
}).returning().then(a => {
if (!a[0]) throw Error('Something went horribly wrong');
return a[0]
});
};
return await db
.insert(users)
.values({
id: parseInt(user.id, 10),
username: user.username,
})
.returning()
.then((a) => {
if (!a[0]) throw Error("Something went horribly wrong");
return a[0];
});
}
export type UserRecord = InferSelectModel<typeof users>;
export async function updateUserRecord(user: User, newData: UserRecord) {
await db.update(users).set(newData).where(eq(users.id, parseInt(user.id)));
return true;
};
await db
.update(users)
.set(newData)
.where(eq(users.id, parseInt(user.id, 10)));
return true;
}
export async function getBalanceLeaderboard() {
return await db.select().from(users).orderBy(desc(users.balance)).limit(10);
};
return await db.select().from(users).orderBy(desc(users.balance)).limit(10);
}
export async function getKDLeaderboard(monthData?: string) {
let condition: SQL<unknown> | undefined = ne(timeouts.item, 'silverbullet');
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(condition, between(timeouts.created, new Date(begin), new Date(end)));
};
let condition: SQL<unknown> | undefined = ne(timeouts.item, "silverbullet");
if (monthData) {
const begin = Date.parse(monthData);
const end = new Date(begin).setMonth(new Date(begin).getMonth() + 1);
condition = and(
condition,
between(timeouts.created, new Date(begin), new Date(end)),
);
}
const usersGotShot = await db.select({
userId: users.id,
amount: count(timeouts.target),
})
.from(users)
.innerJoin(timeouts, eq(users.id, timeouts.target))
.groupBy(users.id)
.having(sql`count(${timeouts.id}) > 5`)
.where(condition);
const usersGotShot = await db
.select({
userId: users.id,
amount: count(timeouts.target),
})
.from(users)
.innerJoin(timeouts, eq(users.id, timeouts.target))
.groupBy(users.id)
.having(sql`count(${timeouts.id}) > 5`)
.where(condition);
const usersThatShot = await db.select({
userId: users.id,
amount: count(timeouts.user)
})
.from(users)
.innerJoin(timeouts, eq(users.id, timeouts.user))
.groupBy(users.id)
.where(
and(
condition,
inArray(users.id, usersGotShot.map(a => a.userId))
)
);
const usersThatShot = await db
.select({
userId: users.id,
amount: count(timeouts.user),
})
.from(users)
.innerJoin(timeouts, eq(users.id, timeouts.user))
.groupBy(users.id)
.where(
and(
condition,
inArray(
users.id,
usersGotShot.map((a) => a.userId),
),
),
);
const lookup = new Map(usersThatShot.map(a => [a.userId, a.amount]));
const result = usersGotShot.map(user => ({
userId: user.userId,
KD: lookup.get(user.userId)! / user.amount
}));
const lookup = new Map(usersThatShot.map((a) => [a.userId, a.amount]));
const result = usersGotShot.map((user) => ({
userId: user.userId,
KD: lookup.get(user.userId)! / user.amount,
}));
result.map(user => {
if (isNaN(user.KD)) user.KD = 0;
return user
});
result.map((user) => {
if (Number.isNaN(user.KD)) user.KD = 0;
return user;
});
return result;
};
return result;
}

View File

@@ -1,125 +1,147 @@
import type { AccessToken } from "@twurple/auth";
import type { inventory, items } from "items";
import { boolean, integer, jsonb, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core";
import type { anivBots } from "lib/handleAnivMessage";
import { relations } from "drizzle-orm";
import {
boolean,
integer,
jsonb,
pgTable,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import type { inventory, items } from "items";
import type { anivBots } from "lib/handleAnivMessage";
export const auth = pgTable('auth', {
id: integer().primaryKey(),
accesstoken: jsonb().$type<AccessToken>().notNull()
export const auth = pgTable("auth", {
id: integer().primaryKey(),
accesstoken: jsonb().$type<AccessToken>().notNull(),
});
export const users = pgTable('users', {
id: integer().primaryKey().notNull(),
username: varchar().notNull(),
balance: integer().default(0).notNull(),
inventory: jsonb().$type<inventory>().default({}).notNull()
export const users = pgTable("users", {
id: integer().primaryKey().notNull(),
username: varchar().notNull(),
balance: integer().default(0).notNull(),
inventory: jsonb().$type<inventory>().default({}).notNull(),
});
export const usersRelations = relations(users, ({ many }) => ({
timeouts_target: many(timeouts),
timeouts_shooter: many(timeouts),
usedItems: many(usedItems),
cheerEvents: many(cheerEvents),
cheers: many(cheers),
anivTimeouts: many(anivTimeouts),
getLoots: many(getLoots)
timeouts_target: many(timeouts),
timeouts_shooter: many(timeouts),
usedItems: many(usedItems),
cheerEvents: many(cheerEvents),
cheers: many(cheers),
anivTimeouts: many(anivTimeouts),
getLoots: many(getLoots),
}));
export const timeouts = pgTable('timeouts', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
target: integer().notNull().references(() => users.id),
item: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull()
export const timeouts = pgTable("timeouts", {
id: uuid().defaultRandom().primaryKey(),
user: integer()
.notNull()
.references(() => users.id),
target: integer()
.notNull()
.references(() => users.id),
item: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull(),
});
export const timeoutsRelations = relations(timeouts, ({ one }) => ({
user: one(users, {
fields: [timeouts.user],
references: [users.id],
relationName: 'shooter'
}),
target: one(users, {
fields: [timeouts.target],
references: [users.id],
relationName: 'target'
})
}))
user: one(users, {
fields: [timeouts.user],
references: [users.id],
relationName: "shooter",
}),
target: one(users, {
fields: [timeouts.target],
references: [users.id],
relationName: "target",
}),
}));
export const usedItems = pgTable('usedItems', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
item: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull()
export const usedItems = pgTable("usedItems", {
id: uuid().defaultRandom().primaryKey(),
user: integer()
.notNull()
.references(() => users.id),
item: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull(),
});
export const usedItemsRelations = relations(usedItems, ({ one }) => ({
user: one(users, {
fields: [usedItems.user],
references: [users.id]
})
user: one(users, {
fields: [usedItems.user],
references: [users.id],
}),
}));
export const cheerEvents = pgTable('cheerEvents', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
event: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull()
export const cheerEvents = pgTable("cheerEvents", {
id: uuid().defaultRandom().primaryKey(),
user: integer()
.notNull()
.references(() => users.id),
event: varchar().$type<items>().notNull(),
created: timestamp().defaultNow().notNull(),
});
export const cheerEventsRelations = relations(cheerEvents, ({ one }) => ({
user: one(users, {
fields: [cheerEvents.user],
references: [users.id]
})
user: one(users, {
fields: [cheerEvents.user],
references: [users.id],
}),
}));
export const cheers = pgTable('cheers', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
amount: integer().notNull(),
created: timestamp().defaultNow().notNull()
export const cheers = pgTable("cheers", {
id: uuid().defaultRandom().primaryKey(),
user: integer()
.notNull()
.references(() => users.id),
amount: integer().notNull(),
created: timestamp().defaultNow().notNull(),
});
export const cheersRelations = relations(cheers, ({ one }) => ({
user: one(users, {
fields: [cheers.user],
references: [users.id]
})
user: one(users, {
fields: [cheers.user],
references: [users.id],
}),
}));
export const anivTimeouts = pgTable('anivTimeouts', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
message: varchar().notNull(),
anivBot: varchar().$type<anivBots>().notNull(),
duration: integer(),
created: timestamp().defaultNow().notNull(),
timeout: boolean().default(true)
export const anivTimeouts = pgTable("anivTimeouts", {
id: uuid().defaultRandom().primaryKey(),
user: integer()
.notNull()
.references(() => users.id),
message: varchar().notNull(),
anivBot: varchar().$type<anivBots>().notNull(),
duration: integer(),
created: timestamp().defaultNow().notNull(),
timeout: boolean().default(true),
});
export const anivTimeoutsRelations = relations(anivTimeouts, ({ one }) => ({
user: one(users, {
fields: [anivTimeouts.user],
references: [users.id]
})
user: one(users, {
fields: [anivTimeouts.user],
references: [users.id],
}),
}));
export type lootTriggers = "getloot" | "superloot";
export const getLoots = pgTable('getLoots', {
id: uuid().defaultRandom().primaryKey(),
user: integer().notNull().references(() => users.id),
qbucks: integer().notNull(),
items: jsonb().$type<inventory>().notNull(),
trigger: varchar().$type<lootTriggers>().notNull(),
created: timestamp().defaultNow().notNull()
export const getLoots = pgTable("getLoots", {
id: uuid().defaultRandom().primaryKey(),
user: integer()
.notNull()
.references(() => users.id),
qbucks: integer().notNull(),
items: jsonb().$type<inventory>().notNull(),
trigger: varchar().$type<lootTriggers>().notNull(),
created: timestamp().defaultNow().notNull(),
});
export const getLootsRelations = relations(getLoots, ({ one }) => ({
user: one(users, {
fields: [getLoots.user],
references: [users.id]
})
user: one(users, {
fields: [getLoots.user],
references: [users.id],
}),
}));

View File

@@ -1,17 +1,26 @@
import { api, eventSub } from "index";
import { redis } from "lib/redis";
import { streamerId } from "main";
import { deleteBannedUserMessagesFromChatWidget } from "web/chatWidget/message";
import { eventSub, api } from "index";
import { redis } from "lib/redis";
eventSub.onChannelBan(streamerId, async msg => {
deleteBannedUserMessagesFromChatWidget(msg);
const welcomemessageid = await redis.get(`user:${msg.userId}:welcomemessageid`);
if (welcomemessageid) { await api.moderation.deleteChatMessages(streamerId, welcomemessageid); await redis.del(`user:${msg.userId}:welcomemessageid`); };
await redis.set(`user:${msg.userId}:timeout`, '1');
if (msg.endDate) await redis.expire(`user:${msg.userId}:timeout`, Math.floor((msg.endDate.getTime() - Date.now()) / 1000));
eventSub.onChannelBan(streamerId, async (msg) => {
deleteBannedUserMessagesFromChatWidget(msg);
const welcomemessageid = await redis.get(
`user:${msg.userId}:welcomemessageid`,
);
if (welcomemessageid) {
await api.moderation.deleteChatMessages(streamerId, welcomemessageid);
await redis.del(`user:${msg.userId}:welcomemessageid`);
}
await redis.set(`user:${msg.userId}:timeout`, "1");
if (msg.endDate)
await redis.expire(
`user:${msg.userId}:timeout`,
Math.floor((msg.endDate.getTime() - Date.now()) / 1000),
);
});
eventSub.onChannelUnban(streamerId, async msg => {
await redis.del(`user:${msg.userId}:timeout`);
await redis.del(`user:${msg.userId}:remod`);
eventSub.onChannelUnban(streamerId, async (msg) => {
await redis.del(`user:${msg.userId}:timeout`);
await redis.del(`user:${msg.userId}:remod`);
});

View File

@@ -1,19 +1,25 @@
import { sendMessage } from "lib/commandUtils";
import { activeRedeems } from "pointRedeems";
import { eventSub } from "index";
import { sendMessage } from "lib/commandUtils";
import logger from "lib/logger";
import { streamerId } from "main";
import { activeRedeems } from "pointRedeems";
import User from "user";
eventSub.onChannelRedemptionAdd(streamerId, async msg => {
const selection = activeRedeems.get(msg.rewardId);
if (!selection) { logger.warn(`Can't find the ${msg.rewardTitle} redeem`); return; };
const user = await User.initUsername(msg.userName);
try {
await selection.execute(msg, user!);
if (process.env.NODE_ENV === 'production') await msg.updateStatus('FULFILLED'); // only on prod
} catch (err) {
await sendMessage(`[ERROR]: Something went wrong with ${user?.displayName}'s redeem!`);
logger.err(err as string);
};
eventSub.onChannelRedemptionAdd(streamerId, async (msg) => {
const selection = activeRedeems.get(msg.rewardId);
if (!selection) {
logger.warn(`Can't find the ${msg.rewardTitle} redeem`);
return;
}
const user = await User.initUsername(msg.userName);
try {
await selection.execute(msg, user!);
if (process.env.NODE_ENV === "production")
await msg.updateStatus("FULFILLED"); // only on prod
} catch (err) {
await sendMessage(
`[ERROR]: Something went wrong with ${user?.displayName}'s redeem!`,
);
logger.err(err as string);
}
});

View File

@@ -2,6 +2,6 @@ import { eventSub } from "index";
import { chatterId, streamerId } from "main";
import { deleteMessageFromChatWidget } from "web/chatWidget/message";
eventSub.onChannelChatMessageDelete(streamerId, chatterId, async msg => {
deleteMessageFromChatWidget(msg);
eventSub.onChannelChatMessageDelete(streamerId, chatterId, async (msg) => {
deleteMessageFromChatWidget(msg);
});

View File

@@ -2,34 +2,45 @@ import { api, eventSub } from "index";
import kleur from "kleur";
import logger from "lib/logger";
eventSub.onRevoke(event => {
logger.ok(`Successfully revoked EventSub subscription: ${kleur.underline(event.id)}`);
eventSub.onRevoke((event) => {
logger.ok(
`Successfully revoked EventSub subscription: ${kleur.underline(event.id)}`,
);
});
eventSub.onSubscriptionCreateSuccess(event => {
logger.ok(`Successfully created EventSub subscription: ${kleur.underline(event.id)}`);
eventSub.onSubscriptionCreateSuccess((event) => {
logger.ok(
`Successfully created EventSub subscription: ${kleur.underline(event.id)}`,
);
});
eventSub.onSubscriptionCreateFailure(event => {
logger.err(`Failed to create EventSub subscription: ${kleur.underline(event.id)}`);
eventSub.onSubscriptionCreateFailure((event) => {
logger.err(
`Failed to create EventSub subscription: ${kleur.underline(event.id)}`,
);
});
eventSub.onSubscriptionDeleteSuccess(event => {
logger.ok(`Successfully deleted EventSub subscription: ${kleur.underline(event.id)}`);
eventSub.onSubscriptionDeleteSuccess((event) => {
logger.ok(
`Successfully deleted EventSub subscription: ${kleur.underline(event.id)}`,
);
});
eventSub.onSubscriptionDeleteFailure(event => {
logger.err(`Failed to delete EventSub subscription: ${kleur.underline(event.id)}`);
eventSub.onSubscriptionDeleteFailure((event) => {
logger.err(
`Failed to delete EventSub subscription: ${kleur.underline(event.id)}`,
);
});
await api.eventSub.deleteAllSubscriptions();
import { readdir } from 'node:fs/promises';
import { readdir } from "node:fs/promises";
const files = await readdir(import.meta.dir);
for (const file of files) {
if (!file.endsWith('.ts')) continue;
if (file === import.meta.file) continue;
await import(import.meta.dir + '/' + file.slice(0, -3));
};
if (!file.endsWith(".ts")) continue;
if (file === import.meta.file) continue;
await import(`${import.meta.dir}/${file.slice(0, -3)}`);
}
eventSub.start();

View File

@@ -1,115 +1,172 @@
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base"
import { streamerId, commandPrefix, streamerUsers, chatterId } from "main";
import User from "user";
import commands, { specialAliasCommands } from "commands";
import { Command, sendMessage } from "lib/commandUtils";
import { isAdmin } from "lib/admins";
import type { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import cheers from "cheers";
import logger from "lib/logger";
import { addMessageToChatWidget } from "web/chatWidget/message";
import { isInvuln, removeInvuln, setTemporaryInvuln } from "lib/invuln";
import { getUserRecord } from "db/dbUser";
import commands, { specialAliasCommands } from "commands";
import { createCheerRecord } from "db/dbCheers";
import handleAnivMessage from "lib/handleAnivMessage";
import { Item } from "items";
import { getUserRecord } from "db/dbUser";
import { eventSub } from "index";
import { Item } from "items";
import { isAdmin } from "lib/admins";
import { type Command, sendMessage } from "lib/commandUtils";
import handleAnivMessage from "lib/handleAnivMessage";
import { isInvuln, removeInvuln, setTemporaryInvuln } from "lib/invuln";
import logger from "lib/logger";
import { redis } from "lib/redis";
import { chatterId, commandPrefix, streamerId, streamerUsers } from "main";
import User from "user";
import { addMessageToChatWidget } from "web/chatWidget/message";
eventSub.onChannelChatMessage(streamerId, chatterId, parseChatMessage);
async function parseChatMessage(msg: EventSubChannelChatMessageEvent) {
addMessageToChatWidget(msg);
addMessageToChatWidget(msg);
const user = await User.initUsername(msg.chatterName);
const user = await User.initUsername(msg.chatterName);
// Get user from cache or place user in cache
// Given the fact that this is the user that chats, this user object always exists and cannot be null
//
// One of the flaws with the user object is solved by creating the object with the name.
// This way, if a user changes their name, the original name stays in the cache for at least 1 hour (extendable by using that name as target for item)
// and both are usable to target the same user (id is the same)
// The only problem would be if a user changed their name and someone else took their name right after
// Get user from cache or place user in cache
// Given the fact that this is the user that chats, this user object always exists and cannot be null
//
// One of the flaws with the user object is solved by creating the object with the name.
// This way, if a user changes their name, the original name stays in the cache for at least 1 hour (extendable by using that name as target for item)
// and both are usable to target the same user (id is the same)
// The only problem would be if a user changed their name and someone else took their name right after
if (await redis.exists(`user:${user?.id}:bot`)) return; // Ignore all bot commands
if (await redis.exists(`user:${user?.id}:bot`)) return; // Ignore all bot commands
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);
await redis.expire(`user:${user?.id}:welcomemessageid`, 600);
if (!await isInvuln(msg.chatterId)) await setTemporaryInvuln(user?.id!); // This would set the invuln expiration lmao
};
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);
await redis.expire(`user:${user?.id}:welcomemessageid`, 600);
if (!(await isInvuln(msg.chatterId))) await setTemporaryInvuln(user?.id!); // This would set the invuln expiration lmao
}
if (!await isInvuln(user?.id!)) user?.setVulnerable(); // Make the user vulnerable to explosions if not marked as invuln
if (!(await isInvuln(user?.id!))) user?.setVulnerable(); // Make the user vulnerable to explosions if not marked as invuln
if (!msg.isCheer && !msg.isRedemption) await handleChatMessage(msg, user!)
else if (msg.isCheer && !msg.isRedemption) await handleCheer(msg, msg.bits, user!);
};
if (!msg.isCheer && !msg.isRedemption) await handleChatMessage(msg, user!);
else if (msg.isCheer && !msg.isRedemption)
await handleCheer(msg, msg.bits, user!);
}
async function handleChatMessage(msg: EventSubChannelChatMessageEvent, user: User) {
// Aniv message filter
handleAnivMessage(msg, user);
async function handleChatMessage(
msg: EventSubChannelChatMessageEvent,
user: User,
) {
// Aniv message filter
handleAnivMessage(msg, user);
// Parse commands:
const selected = selectCommand(msg.messageText);
if (!selected) return;
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); };
// Parse commands:
const selected = selectCommand(msg.messageText);
if (!selected) return;
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":
if (!await isAdmin(user.id)) return;
break;
case "streamer":
if (!streamerUsers.includes(msg.chatterId)) return;
break;
case "moderator":
if (!(await redis.exists(`user:${user.id}:mod`) || await isAdmin(user.id))) return;
break;
};
switch (selection.usertype) {
case "admin":
if (!(await isAdmin(user.id))) return;
break;
case "streamer":
if (!streamerUsers.includes(msg.chatterId)) return;
break;
case "moderator":
if (
!(
(await redis.exists(`user:${user.id}:mod`)) ||
(await isAdmin(user.id))
)
)
return;
break;
}
try {
await selection.execute(msg, user, { activation });
} catch (err) {
logger.err(err as string);
await sendMessage('ERROR: Something went wrong', msg.messageId);
await user.clearLock();
};
};
try {
await selection.execute(msg, user, { activation });
} catch (err) {
logger.err(err as string);
await sendMessage("ERROR: Something went wrong", msg.messageId);
await user.clearLock();
}
}
type selectedCommand = {
cmd: Command;
activation: string;
isitem: boolean;
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, isitem: specialcmd instanceof Item };
if (!message.startsWith(commandPrefix)) return false;
const commandSelector = message.slice(commandPrefix.length).trim().toLowerCase().split(' ')[0]!;
const normalcmd = commands.get(commandSelector);
if (normalcmd) return { cmd: normalcmd, activation: commandPrefix + commandSelector, isitem: normalcmd instanceof Item };
return false;
};
const specialcmdselector = message.trim().toLowerCase().split(" ")[0]!;
const specialcmd = specialAliasCommands.get(specialcmdselector);
if (specialcmd)
return {
cmd: specialcmd,
activation: specialcmdselector,
isitem: specialcmd instanceof Item,
};
if (!message.startsWith(commandPrefix)) return false;
const commandSelector = message
.slice(commandPrefix.length)
.trim()
.toLowerCase()
.split(" ")[0]!;
const normalcmd = commands.get(commandSelector);
if (normalcmd)
return {
cmd: normalcmd,
activation: commandPrefix + commandSelector,
isitem: normalcmd instanceof Item,
};
return false;
}
export async function handleCheer(msg: EventSubChannelChatMessageEvent, bits: number, user: User) {
if (msg.isCheer) {
await getUserRecord(user); // ensure they exist in the database
await createCheerRecord(user, bits);
}; // If this is not triggered it's because of the testcheer command. these fake bits should not be added to the database
export async function handleCheer(
msg: EventSubChannelChatMessageEvent,
bits: number,
user: User,
) {
if (msg.isCheer) {
await getUserRecord(user); // ensure they exist in the database
await createCheerRecord(user, bits);
} // If this is not triggered it's because of the testcheer command. these fake bits should not be added to the database
const selection = cheers.get(bits);
if (!selection) return;
const selection = cheers.get(bits);
if (!selection) return;
if (await redis.sismember('disabledcheers', selection.name)) { await sendMessage(`The ${selection.name} cheer is disabled! Sorry!`, msg.messageId); return; };
if (selection.isItem && await isInvuln(user.id) && !streamerUsers.includes(user.id)) { await sendMessage(`${user.displayName} Is no longer an invuln`); await removeInvuln(user.id); };
try {
await selection.execute(msg, user);
} catch (err) {
await sendMessage(`[ERROR]: Something went wrong with cheer execution`);
logger.err(err as string);
};
};
if (await redis.sismember("disabledcheers", selection.name)) {
await sendMessage(
`The ${selection.name} cheer is disabled! Sorry!`,
msg.messageId,
);
return;
}
if (
selection.isItem &&
(await isInvuln(user.id)) &&
!streamerUsers.includes(user.id)
) {
await sendMessage(`${user.displayName} Is no longer an invuln`);
await removeInvuln(user.id);
}
try {
await selection.execute(msg, user);
} catch (err) {
await sendMessage(`[ERROR]: Something went wrong with cheer execution`);
logger.err(err as string);
}
}

View File

@@ -2,10 +2,10 @@ import { eventSub } from "index";
import { redis } from "lib/redis";
import { streamerId } from "main";
eventSub.onChannelModeratorAdd(streamerId, async mod => {
await redis.set(`user:${mod.userId}:mod`, '1');
eventSub.onChannelModeratorAdd(streamerId, async (mod) => {
await redis.set(`user:${mod.userId}:mod`, "1");
});
eventSub.onChannelModeratorRemove(streamerId, async mod => {
await redis.del(`user:${mod.userId}:mod`);
eventSub.onChannelModeratorRemove(streamerId, async (mod) => {
await redis.del(`user:${mod.userId}:mod`);
});

View File

@@ -1,23 +1,37 @@
import { sendMessage } from "lib/commandUtils";
import { getUserRecord } from "db/dbUser";
import { eventSub, api } from "index";
import { api, eventSub } from "index";
import { changeItemCount } from "items";
import { sendMessage } from "lib/commandUtils";
import logger from "lib/logger";
import { redis } from "lib/redis";
import { streamerId } from "main";
import User from "user";
import { redis } from "lib/redis";
eventSub.onChannelRaidTo(streamerId, async msg => {
if (await redis.exists(`user:${msg.raidingBroadcasterId}:recentraid`)) { await sendMessage(`Another raid from ${msg.raidedBroadcasterDisplayName}??? SMH`); return; };
await redis.set(`user:${msg.raidingBroadcasterId}:recentraid`, '1');
await redis.expire(`user:${msg.raidingBroadcasterId}:recentraid`, 60 * 30); // raid cooldown is 30 minutes
await sendMessage(`Ty for raiding ${msg.raidingBroadcasterDisplayName}. You get 3 pieces of TNT. Enjoy!`);
try {
await api.chat.shoutoutUser(streamerId, msg.raidingBroadcasterId);
} catch (e) {
logger.warn(`Failed to give automatic shoutout to ${msg.raidingBroadcasterDisplayName}`);
};
const raider = await User.initUsername(msg.raidingBroadcasterName);
const result = await changeItemCount(raider!, await getUserRecord(raider!), 'tnt', 3);
if (!result) await sendMessage("oopsies, no tnt for you!");
eventSub.onChannelRaidTo(streamerId, async (msg) => {
if (await redis.exists(`user:${msg.raidingBroadcasterId}:recentraid`)) {
await sendMessage(
`Another raid from ${msg.raidedBroadcasterDisplayName}??? SMH`,
);
return;
}
await redis.set(`user:${msg.raidingBroadcasterId}:recentraid`, "1");
await redis.expire(`user:${msg.raidingBroadcasterId}:recentraid`, 60 * 30); // raid cooldown is 30 minutes
await sendMessage(
`Ty for raiding ${msg.raidingBroadcasterDisplayName}. You get 3 pieces of TNT. Enjoy!`,
);
try {
await api.chat.shoutoutUser(streamerId, msg.raidingBroadcasterId);
} catch (_e) {
logger.warn(
`Failed to give automatic shoutout to ${msg.raidingBroadcasterDisplayName}`,
);
}
const raider = await User.initUsername(msg.raidingBroadcasterName);
const result = await changeItemCount(
raider!,
await getUserRecord(raider!),
"tnt",
3,
);
if (!result) await sendMessage("oopsies, no tnt for you!");
});

View File

@@ -1,16 +1,20 @@
import { sendMessage } from "lib/commandUtils";
import { eventSub } from "index";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
import { streamerId } from "main";
import { sendDiscordMessage } from "web/discordConnection";
import { redis } from "lib/redis";
eventSub.onStreamOnline(streamerId, async msg => {
await redis.set('streamIsLive', '1');
await sendMessage(`${msg.broadcasterDisplayName.toUpperCase()} IS LIVE! START DIGGING!`);
await sendDiscordMessage({ message: 'live' });
eventSub.onStreamOnline(streamerId, async (msg) => {
await redis.set("streamIsLive", "1");
await sendMessage(
`${msg.broadcasterDisplayName.toUpperCase()} IS LIVE! START DIGGING!`,
);
await sendDiscordMessage({ message: "live" });
});
eventSub.onStreamOffline(streamerId, async msg => {
await redis.del('streamIsLive');
await sendMessage(`${msg.broadcasterDisplayName.toUpperCase()} IS OFFLINE! NO MORE FREE LOOT!`);
eventSub.onStreamOffline(streamerId, async (msg) => {
await redis.del("streamIsLive");
await sendMessage(
`${msg.broadcasterDisplayName.toUpperCase()} IS OFFLINE! NO MORE FREE LOOT!`,
);
});

View File

@@ -1,123 +1,153 @@
import { sendMessage } from "lib/commandUtils";
import { getUserRecord, updateUserRecord } from "db/dbUser";
import { eventSub } from "index";
import { changeBalance } from "lib/changeBalance";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
import { streamerId } from "main";
import User from "user";
import { redis } from "lib/redis";
eventSub.onChannelSubscription(streamerId, async msg => {
await redis.set(`user:${msg.userId}:subbed`, msg.tier.slice(0, 1));
if (msg.isGift) return;
const user = await User.initUsername(msg.userName);
const userRecord = await getUserRecord(user!);
switch (msg.tier) {
case "1000":
await Promise.all([
sendMessage(`YO THANKS FOR THE SUB ${msg.userDisplayName}! YOU GET 500 QBUCKS`),
changeBalance(user!, userRecord, 500)
]);
break;
case "2000":
userRecord.balance += 1500;
if (userRecord.inventory.silverbullet) userRecord.inventory.silverbullet += 1
else userRecord.inventory.silverbullet = 1
await Promise.all([
sendMessage(`YO THANKS FOR THE TIER 2 SUB ${msg.userDisplayName}! YOU GET 1500 QBUCKS AND A SILVER BULLET`),
updateUserRecord(user!, userRecord)
]);
break;
case "3000":
userRecord.balance += 3000;
if (userRecord.inventory.silverbullet) userRecord.inventory.silverbullet += 2
else userRecord.inventory.silverbullet = 2;
await Promise.all([
sendMessage(`YO THANKS FOR THE TIER 3 SUB ${msg.userDisplayName}! YOU GET 3000 QBUCKS AND 2 SILVER BULLETS`),
updateUserRecord(user!, userRecord)
]);
break;
};
eventSub.onChannelSubscription(streamerId, async (msg) => {
await redis.set(`user:${msg.userId}:subbed`, msg.tier.slice(0, 1));
if (msg.isGift) return;
const user = await User.initUsername(msg.userName);
const userRecord = await getUserRecord(user!);
switch (msg.tier) {
case "1000":
await Promise.all([
sendMessage(
`YO THANKS FOR THE SUB ${msg.userDisplayName}! YOU GET 500 QBUCKS`,
),
changeBalance(user!, userRecord, 500),
]);
break;
case "2000":
userRecord.balance += 1500;
if (userRecord.inventory.silverbullet)
userRecord.inventory.silverbullet += 1;
else userRecord.inventory.silverbullet = 1;
await Promise.all([
sendMessage(
`YO THANKS FOR THE TIER 2 SUB ${msg.userDisplayName}! YOU GET 1500 QBUCKS AND A SILVER BULLET`,
),
updateUserRecord(user!, userRecord),
]);
break;
case "3000":
userRecord.balance += 3000;
if (userRecord.inventory.silverbullet)
userRecord.inventory.silverbullet += 2;
else userRecord.inventory.silverbullet = 2;
await Promise.all([
sendMessage(
`YO THANKS FOR THE TIER 3 SUB ${msg.userDisplayName}! YOU GET 3000 QBUCKS AND 2 SILVER BULLETS`,
),
updateUserRecord(user!, userRecord),
]);
break;
}
});
eventSub.onChannelSubscriptionGift(streamerId, async msg => {
if (msg.isAnonymous) {
switch (msg.tier) {
case "1000":
await sendMessage(`YO THANKS ANON FOR THE SCAM SUB${msg.amount === 1 ? '' : 'S'}`);
break;
case "2000":
await sendMessage(`YO THANKS ANON FOR THE ${msg.amount} TIER 2 SCAM SUB${msg.amount === 1 ? '' : 'S'}`);
break;
case "3000":
await sendMessage(`YO THANKS ANON FOR THE ${msg.amount} TIER 3 SCAM SUB${msg.amount === 1 ? '' : 'S'}`);
break;
};
return;
};
eventSub.onChannelSubscriptionGift(streamerId, async (msg) => {
if (msg.isAnonymous) {
switch (msg.tier) {
case "1000":
await sendMessage(
`YO THANKS ANON FOR THE SCAM SUB${msg.amount === 1 ? "" : "S"}`,
);
break;
case "2000":
await sendMessage(
`YO THANKS ANON FOR THE ${msg.amount} TIER 2 SCAM SUB${msg.amount === 1 ? "" : "S"}`,
);
break;
case "3000":
await sendMessage(
`YO THANKS ANON FOR THE ${msg.amount} TIER 3 SCAM SUB${msg.amount === 1 ? "" : "S"}`,
);
break;
}
return;
}
const user = await User.initUsername(msg.gifterName);
const amount = msg.amount;
const userRecord = await getUserRecord(user!);
switch (msg.tier) {
case "1000":
await Promise.all([
sendMessage(`YO THANKS FOR THE SCAM GIFTS ${msg.gifterDisplayName}! YOU GET ${amount * 500} QBUCKS`),
changeBalance(user!, userRecord, amount * 500)
]);
break;
case "2000":
userRecord.balance += 1500 * amount;
if (userRecord.inventory.silverbullet) userRecord.inventory.silverbullet += amount
else userRecord.inventory.silverbullet = amount;
await Promise.all([
sendMessage(`YO THANKS FOR THE SCAM TIER 2 GIFTS ${msg.gifterDisplayName}! YOU GET ${amount * 1500} QBUCKS AND ${amount} SILVER BULLET${amount === 1 ? '' : 'S'}`),
updateUserRecord(user!, userRecord)
]);
break;
case "3000":
userRecord.balance += 3000 * amount;
if (userRecord.inventory.silverbullet) userRecord.inventory.silverbullet += amount * 2
else userRecord.inventory.silverbullet = amount * 2;
await Promise.all([
sendMessage(`YO THANKS FOR THE SCAM TIER 3 GIFTS ${msg.gifterDisplayName}! YOU GET ${amount * 3000} QBUCKS AND ${amount * 2} SILVER BULLETS`),
updateUserRecord(user!, userRecord)
]);
break;
}
const user = await User.initUsername(msg.gifterName);
const amount = msg.amount;
const userRecord = await getUserRecord(user!);
switch (msg.tier) {
case "1000":
await Promise.all([
sendMessage(
`YO THANKS FOR THE SCAM GIFTS ${msg.gifterDisplayName}! YOU GET ${amount * 500} QBUCKS`,
),
changeBalance(user!, userRecord, amount * 500),
]);
break;
case "2000":
userRecord.balance += 1500 * amount;
if (userRecord.inventory.silverbullet)
userRecord.inventory.silverbullet += amount;
else userRecord.inventory.silverbullet = amount;
await Promise.all([
sendMessage(
`YO THANKS FOR THE SCAM TIER 2 GIFTS ${msg.gifterDisplayName}! YOU GET ${amount * 1500} QBUCKS AND ${amount} SILVER BULLET${amount === 1 ? "" : "S"}`,
),
updateUserRecord(user!, userRecord),
]);
break;
case "3000":
userRecord.balance += 3000 * amount;
if (userRecord.inventory.silverbullet)
userRecord.inventory.silverbullet += amount * 2;
else userRecord.inventory.silverbullet = amount * 2;
await Promise.all([
sendMessage(
`YO THANKS FOR THE SCAM TIER 3 GIFTS ${msg.gifterDisplayName}! YOU GET ${amount * 3000} QBUCKS AND ${amount * 2} SILVER BULLETS`,
),
updateUserRecord(user!, userRecord),
]);
break;
}
});
eventSub.onChannelSubscriptionEnd(streamerId, async msg => {
await redis.del(`user:${msg.userId}:subbed`);
eventSub.onChannelSubscriptionEnd(streamerId, async (msg) => {
await redis.del(`user:${msg.userId}:subbed`);
});
eventSub.onChannelSubscriptionMessage(streamerId, async msg => {
await redis.set(`user:${msg.userId}:subbed`, msg.tier.slice(0, 1));
const user = await User.initUsername(msg.userName);
const userRecord = await getUserRecord(user!);
switch (msg.tier) {
case "1000":
await Promise.all([
sendMessage(`YO THANKS FOR THE RESUB ${msg.userDisplayName}! YOU GET 500 QBUCKS`),
changeBalance(user!, userRecord, 500)
]);
break;
case "2000":
userRecord.balance += 1500;
if (userRecord.inventory.silverbullet) userRecord.inventory.silverbullet += 1
else userRecord.inventory.silverbullet = 1
await Promise.all([
sendMessage(`YO THANKS FOR THE TIER 2 RESUB ${msg.userDisplayName}! YOU GET 1500 QBUCKS AND A SILVER BULLET`),
updateUserRecord(user!, userRecord)
]);
break;
case "3000":
userRecord.balance += 3000;
if (userRecord.inventory.silverbullet) userRecord.inventory.silverbullet += 2
else userRecord.inventory.silverbullet = 2;
await Promise.all([
sendMessage(`YO THANKS FOR THE TIER 3 RESUB ${msg.userDisplayName}! YOU GET 3000 QBUCKS AND 2 SILVER BULLETS`),
updateUserRecord(user!, userRecord)
]);
break;
};
eventSub.onChannelSubscriptionMessage(streamerId, async (msg) => {
await redis.set(`user:${msg.userId}:subbed`, msg.tier.slice(0, 1));
const user = await User.initUsername(msg.userName);
const userRecord = await getUserRecord(user!);
switch (msg.tier) {
case "1000":
await Promise.all([
sendMessage(
`YO THANKS FOR THE RESUB ${msg.userDisplayName}! YOU GET 500 QBUCKS`,
),
changeBalance(user!, userRecord, 500),
]);
break;
case "2000":
userRecord.balance += 1500;
if (userRecord.inventory.silverbullet)
userRecord.inventory.silverbullet += 1;
else userRecord.inventory.silverbullet = 1;
await Promise.all([
sendMessage(
`YO THANKS FOR THE TIER 2 RESUB ${msg.userDisplayName}! YOU GET 1500 QBUCKS AND A SILVER BULLET`,
),
updateUserRecord(user!, userRecord),
]);
break;
case "3000":
userRecord.balance += 3000;
if (userRecord.inventory.silverbullet)
userRecord.inventory.silverbullet += 2;
else userRecord.inventory.silverbullet = 2;
await Promise.all([
sendMessage(
`YO THANKS FOR THE TIER 3 RESUB ${msg.userDisplayName}! YOU GET 3000 QBUCKS AND 2 SILVER BULLETS`,
),
updateUserRecord(user!, userRecord),
]);
break;
}
});

View File

@@ -1,36 +1,80 @@
import { sendMessage } from "lib/commandUtils";
import { api, eventSub } from "index";
import { sendMessage } from "lib/commandUtils";
import { buildTimeString } from "lib/dateManager";
import { chatterId, commandPrefix } from "main";
import { redis } from "lib/redis";
import { chatterId, commandPrefix } from "main";
const WHISPERCOOLDOWN = 60 * 5; // 5 minutes
eventSub.onUserWhisperMessage(chatterId, async msg => {
if (!msg.messageText.startsWith(commandPrefix)) { await whisper(msg.senderUserId, `Whisper commands start with '${commandPrefix}'. All whisper commands can be found here: https://github.com/qwerinope/qweribot#whisper-commands-1`); return; };
const cmd = msg.messageText.slice(commandPrefix.length).trim().toLowerCase().split(' ')[0]!;
eventSub.onUserWhisperMessage(chatterId, async (msg) => {
if (!msg.messageText.startsWith(commandPrefix)) {
await whisper(
msg.senderUserId,
`Whisper commands start with '${commandPrefix}'. All whisper commands can be found here: https://github.com/qwerinope/qweribot#whisper-commands-1`,
);
return;
}
const cmd = msg.messageText
.slice(commandPrefix.length)
.trim()
.toLowerCase()
.split(" ")[0]!;
switch (cmd) {
case 'help':
case 'h':
await whisper(msg.senderUserId, `All whisper commands can be found here: https://github.com/qwerinope/qweribot#whisper-commands-1`);
break;
case 'ghostwhisper':
case 'ghost':
case 'g':
if (await redis.ttl(`user:${msg.senderUserId}:timeout`) < 0) { await whisper(msg.senderUserId, 'Cannot send ghost whisper while not timed out'); return; };
const cooldown = await redis.expiretime(`user:${msg.senderUserId}:whispercooldown`);
if (cooldown < 0) {
if (msg.messageText.length > 200) { await whisper(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.split(' ').slice(1).join(' ').replaceAll(/cheer[0-9]+/gi, '')}`);
await whisper(msg.senderUserId, `Message sent. You can send another ghost whisper in ${Math.floor(WHISPERCOOLDOWN / 60)} minutes.`);
} else {
await whisper(msg.senderUserId, `Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another ghost whisper.`);
};
break;
};
switch (cmd) {
case "help":
case "h":
await whisper(
msg.senderUserId,
`All whisper commands can be found here: https://github.com/qwerinope/qweribot#whisper-commands-1`,
);
break;
case "ghostwhisper":
case "ghost":
case "g": {
if ((await redis.ttl(`user:${msg.senderUserId}:timeout`)) < 0) {
await whisper(
msg.senderUserId,
"Cannot send ghost whisper while not timed out",
);
return;
}
const cooldown = await redis.expiretime(
`user:${msg.senderUserId}:whispercooldown`,
);
if (cooldown < 0) {
if (msg.messageText.length > 200) {
await whisper(
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
.split(" ")
.slice(1)
.join(" ")
.replaceAll(/cheer[0-9]+/gi, "")}`,
);
await whisper(
msg.senderUserId,
`Message sent. You can send another ghost whisper in ${Math.floor(WHISPERCOOLDOWN / 60)} minutes.`,
);
} else {
await whisper(
msg.senderUserId,
`Wait another ${buildTimeString(cooldown * 1000, Date.now())} before sending another ghost whisper.`,
);
}
break;
}
}
});
const whisper = async (target: string, message: string) => await api.whispers.sendWhisper(chatterId, target, message);
const whisper = async (target: string, message: string) =>
await api.whispers.sendWhisper(chatterId, target, message);

View File

@@ -1,128 +1,212 @@
import { chatterId, streamerId, singleUserMode, streamerUsers } from "main";
import { ApiClient } from "@twurple/api";
import { connectionCheck } from "connectionCheck";
import { ApiClient } from "@twurple/api";
import {
type ConnectionAdapter,
EventSubHttpListener,
ReverseProxyAdapter,
} from "@twurple/eventsub-http";
import { NgrokAdapter } from "@twurple/eventsub-ngrok";
import { type authProviderInstructions, createAuthProvider } from "auth";
import { database, host, password, user } from "db/connection";
import logger from "lib/logger";
import { redis } from "lib/redis";
import { createAuthProvider, type authProviderInstructions } from "auth";
import { user, password, database, host } from "db/connection";
import { ConnectionAdapter, EventSubHttpListener, ReverseProxyAdapter } from "@twurple/eventsub-http";
import { NgrokAdapter } from "@twurple/eventsub-ngrok";
import { chatterId, singleUserMode, streamerId, streamerUsers } from "main";
if (chatterId === "") { logger.enverr('CHATTER_ID'); process.exit(1); };
if (streamerId === "") { logger.enverr('STREAMER_ID'); process.exit(1); };
if (!user) { logger.enverr("POSTGRES_USER"); process.exit(1); };
if (!password) { logger.enverr("POSTGRES_USER"); process.exit(1); };
if (!database) { logger.enverr("POSTGRES_DB"); process.exit(1); };
if (!host) { logger.enverr("POSTGRES_HOST"); process.exit(1); };
if (chatterId === "") {
logger.enverr("CHATTER_ID");
process.exit(1);
}
if (streamerId === "") {
logger.enverr("STREAMER_ID");
process.exit(1);
}
if (!user) {
logger.enverr("POSTGRES_USER");
process.exit(1);
}
if (!password) {
logger.enverr("POSTGRES_USER");
process.exit(1);
}
if (!database) {
logger.enverr("POSTGRES_DB");
process.exit(1);
}
if (!host) {
logger.enverr("POSTGRES_HOST");
process.exit(1);
}
const eventSubHostName = process.env.EVENTSUB_HOSTNAME ?? (() => {
logger.enverr('EVENTSUB_HOSTNAME');
process.exit(1);
})();
const eventSubHostName =
process.env.EVENTSUB_HOSTNAME ??
(() => {
logger.enverr("EVENTSUB_HOSTNAME");
process.exit(1);
})();
const eventSubPort = process.env.EVENTSUB_PORT ?? (() => {
logger.enverr('EVENTSUB_PORT');
process.exit(1);
})();
const eventSubPort =
process.env.EVENTSUB_PORT ??
(() => {
logger.enverr("EVENTSUB_PORT");
process.exit(1);
})();
const eventSubSecret = process.env.EVENTSUB_SECRET ?? (() => {
logger.enverr('EVENTSUB_SECRET');
process.exit(1);
})();
const eventSubSecret =
process.env.EVENTSUB_SECRET ??
(() => {
logger.enverr("EVENTSUB_SECRET");
process.exit(1);
})();
const eventSubPath = process.env.EVENTSUB_PATH;
await connectionCheck();
const CHATTERINTENTS = ["user:read:chat", "user:write:chat", "user:bot", "user:manage:whispers"];
const STREAMERINTENTS = ["channel:bot", "user:read:chat", "moderation:read", "moderator:read:chatters", "channel:manage:moderators", "moderator:manage:chat_messages", "moderator:manage:banned_users", "bits:read", "channel:moderate", "moderator:manage:shoutouts", "channel:read:subscriptions", "channel:manage:redemptions"];
const users: authProviderInstructions[] = [
{
userId: streamerId,
intents: singleUserMode ? CHATTERINTENTS.concat(STREAMERINTENTS) : STREAMERINTENTS,
streamer: true
}
const CHATTERINTENTS = [
"user:read:chat",
"user:write:chat",
"user:bot",
"user:manage:whispers",
];
const STREAMERINTENTS = [
"channel:bot",
"user:read:chat",
"moderation:read",
"moderator:read:chatters",
"channel:manage:moderators",
"moderator:manage:chat_messages",
"moderator:manage:banned_users",
"bits:read",
"channel:moderate",
"moderator:manage:shoutouts",
"channel:read:subscriptions",
"channel:manage:redemptions",
];
if (!singleUserMode) users.push({
userId: chatterId,
intents: CHATTERINTENTS,
streamer: false
});
const users: authProviderInstructions[] = [
{
userId: streamerId,
intents: singleUserMode
? CHATTERINTENTS.concat(STREAMERINTENTS)
: STREAMERINTENTS,
streamer: true,
},
];
const adapter: ConnectionAdapter = process.env.NODE_ENV === 'development' ? new NgrokAdapter({
ngrokConfig: {
authtoken: process.env.EVENTSUB_NGROK_TOKEN ?? (() => {
logger.enverr('EVENTSUB_NGROK_TOKEN');
process.exit(1);
})()
}
}) : new ReverseProxyAdapter({
pathPrefix: eventSubPath, hostName: eventSubHostName, port: parseInt(eventSubPort)
});
if (!singleUserMode)
users.push({
userId: chatterId,
intents: CHATTERINTENTS,
streamer: false,
});
const adapter: ConnectionAdapter =
process.env.NODE_ENV === "development"
? new NgrokAdapter({
ngrokConfig: {
authtoken:
process.env.EVENTSUB_NGROK_TOKEN ??
(() => {
logger.enverr("EVENTSUB_NGROK_TOKEN");
process.exit(1);
})(),
},
})
: new ReverseProxyAdapter({
pathPrefix: eventSubPath,
hostName: eventSubHostName,
port: parseInt(eventSubPort, 10),
});
const authProvider = await createAuthProvider(users);
export const api = new ApiClient({ authProvider });
export const eventSub = new EventSubHttpListener({ apiClient: api, secret: eventSubSecret, adapter });
export const eventSub = new EventSubHttpListener({
apiClient: api,
secret: eventSubSecret,
adapter,
});
if (!singleUserMode) await redis.set(`user:${chatterId}:bot`, '1');
if (!singleUserMode) await redis.set(`user:${chatterId}:bot`, "1");
import { addAdmin } from "lib/admins";
import { addInvuln } from "lib/invuln";
import User from "user";
import { remodMod, timeoutDuration } from "lib/timeout";
import User from "user";
streamerUsers.forEach(async id => await Promise.all([addAdmin(id), addInvuln(id), redis.set(`user:${id}:mod`, '1')]));
streamerUsers.forEach(
async (id) =>
await Promise.all([
addAdmin(id),
addInvuln(id),
redis.set(`user:${id}:mod`, "1"),
]),
);
const banned = await api.moderation.getBannedUsers(streamerId).then(a => a.data);
const banned = await api.moderation
.getBannedUsers(streamerId)
.then((a) => a.data);
for (const ban of banned) {
await redis.set(`user:${ban.userId}:timeout`, '1');
const banlength = ban.expiryDate;
if (banlength) {
redis.expire(`user:${ban.userId}:timeout`, Math.floor((ban.expiryDate.getTime() - Date.now()) / 1000) + 1);
logger.info(`Set the timeout of ${ban.userDisplayName} in the Redis/Valkey database.`);
};
};
await redis.set(`user:${ban.userId}:timeout`, "1");
const banlength = ban.expiryDate;
if (banlength) {
redis.expire(
`user:${ban.userId}:timeout`,
Math.floor((ban.expiryDate.getTime() - Date.now()) / 1000) + 1,
);
logger.info(
`Set the timeout of ${ban.userDisplayName} in the Redis/Valkey database.`,
);
}
}
const mods = await api.moderation.getModerators(streamerId).then(a => a.data);
const mods = await api.moderation.getModerators(streamerId).then((a) => a.data);
for (const mod of mods) {
await redis.set(`user:${mod.userId}:mod`, '1');
logger.info(`Set the mod status of ${mod.userDisplayName} in the Redis/Valkey database.`);
};
await redis.set(`user:${mod.userId}:mod`, "1");
logger.info(
`Set the mod status of ${mod.userDisplayName} in the Redis/Valkey database.`,
);
}
const bannedmods = await redis.keys('user:*:remod').then(a => Array.from(a).map(b => b.slice(5, -6)));
const bannedmods = await redis
.keys("user:*:remod")
.then((a) => Array.from(a).map((b) => b.slice(5, -6)));
for (const remod of bannedmods) {
const target = await User.initUserId(remod);
const durationdata = await timeoutDuration(target!);
let duration = 0;
if (durationdata) duration = Math.floor((durationdata * 1000 - Date.now()) / 1000);
remodMod(target!, duration);
logger.info(`Set the remod timer for ${target?.displayName} to ${duration} seconds.`);
};
const target = await User.initUserId(remod);
const durationdata = await timeoutDuration(target!);
let duration = 0;
if (durationdata)
duration = Math.floor((durationdata * 1000 - Date.now()) / 1000);
remodMod(target!, duration);
logger.info(
`Set the remod timer for ${target?.displayName} to ${duration} seconds.`,
);
}
const subs = await api.subscriptions.getSubscriptions(streamerId).then(a => a.data);
const redisSubs = await redis.keys('user:*:subbed').then(a => a.map(b => b.slice(5, -7)));
const subs = await api.subscriptions
.getSubscriptions(streamerId)
.then((a) => a.data);
const redisSubs = await redis
.keys("user:*:subbed")
.then((a) => a.map((b) => b.slice(5, -7)));
for (const sub of subs) {
if (redisSubs.includes(sub.userId)) {
const index = redisSubs.indexOf(sub.userId);
redisSubs.splice(index, 1);
continue;
};
await redis.set(`user:${sub.userId}:subbed`, sub.tier.slice(0, 1));
};
if (redisSubs.includes(sub.userId)) {
const index = redisSubs.indexOf(sub.userId);
redisSubs.splice(index, 1);
continue;
}
await redis.set(`user:${sub.userId}:subbed`, sub.tier.slice(0, 1));
}
redisSubs.map(async a => await redis.del(`user:${a}:subbed`));
redisSubs.map(async (a) => await redis.del(`user:${a}:subbed`));
const streamdata = await api.streams.getStreamByUserId(streamerId);
if (streamdata) await redis.set('streamIsLive', '1')
else await redis.del('streamIsLive');
if (streamdata) await redis.set("streamIsLive", "1");
else await redis.del("streamIsLive");
await import("./events");
await import("./pointRedeems");
await import("./web");

View File

@@ -1,63 +1,89 @@
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import { createTimeoutRecord } from "db/dbTimeouts";
import { createUsedItemRecord } from "db/dbUsedItems";
import { getUserRecord } from "db/dbUser";
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { timeout } from "lib/timeout";
import User from "user";
import { playAlert } from "web/alerts/serverFunctions";
const ITEMNAME = 'blaster';
const ITEMNAME = "blaster";
export default new Item({
name: ITEMNAME,
prettyName: 'Blaster',
plural: 's',
description: 'Times a specific person out for 60 seconds',
aliases: ['blaster', 'blast'],
price: 100,
execution: async (msg, user, specialargs) => {
const messagequery = parseCommandArgs(msg.messageText, specialargs?.activation);
if (!messagequery[0]) { await sendMessage('Please specify a target'); return; };
const target = await User.initUsername(messagequery[0].toLowerCase());
if (!target) { await sendMessage(`${messagequery[0]} doesn't exist`); return; };
await getUserRecord(target); // make sure the user record exist in the database
name: ITEMNAME,
prettyName: "Blaster",
plural: "s",
description: "Times a specific person out for 60 seconds",
aliases: ["blaster", "blast"],
price: 100,
execution: async (msg, user, specialargs) => {
const messagequery = parseCommandArgs(
msg.messageText,
specialargs?.activation,
);
if (!messagequery[0]) {
await sendMessage("Please specify a target");
return;
}
const target = await User.initUsername(messagequery[0].toLowerCase());
if (!target) {
await sendMessage(`${messagequery[0]} doesn't exist`);
return;
}
await getUserRecord(target); // make sure the user record exist in the database
if (await user.itemLock()) { await sendMessage('Cannot use an item (itemlock)', msg.messageId); return; };
await user.setLock();
if (await user.itemLock()) {
await sendMessage("Cannot use an item (itemlock)", msg.messageId);
return;
}
await user.setLock();
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any blasters!`, msg.messageId); await user.clearLock(); return; };
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) {
await sendMessage(`You don't have any blasters!`, msg.messageId);
await user.clearLock();
return;
}
const result = await timeout(target, `You got blasted by ${user.displayName}!`, 60);
if (result.status) await Promise.all([
sendMessage(`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`),
changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target, ITEMNAME),
createUsedItemRecord(user, ITEMNAME),
playAlert({
name: 'userBlast',
user: user.displayName,
target: target.displayName
})
]);
else {
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;
};
};
await user.clearLock();
}
const result = await timeout(
target,
`You got blasted by ${user.displayName}!`,
60,
);
if (result.status)
await Promise.all([
sendMessage(
`GOTTEM ${target.displayName} got BLASTED by ${user.displayName} GOTTEM`,
),
changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target, ITEMNAME),
createUsedItemRecord(user, ITEMNAME),
playAlert({
name: "userBlast",
user: user.displayName,
target: target.displayName,
}),
]);
else {
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;
}
}
await user.clearLock();
},
});

View File

@@ -1,48 +1,60 @@
import { redis } from "lib/redis";
import { sendMessage } from "lib/commandUtils";
import { timeout } from "lib/timeout";
import { changeItemCount, Item } from "items";
import User from "user";
import { getUserRecord } from "db/dbUser";
import { createTimeoutRecord } from "db/dbTimeouts";
import { createUsedItemRecord } from "db/dbUsedItems";
import { getUserRecord } from "db/dbUser";
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import User from "user";
import { playAlert } from "web/alerts/serverFunctions";
const ITEMNAME = 'grenade';
const ITEMNAME = "grenade";
export default new Item({
name: ITEMNAME,
prettyName: 'Grenade',
plural: 's',
description: 'Give a random chatter a 60s timeout',
aliases: ['grenade'],
price: 99,
execution: async (msg, user) => {
const targets = await redis.keys(`user:*:vulnerable`);
if (targets.length === 0) { await sendMessage('No vulnerable chatters to blow up', msg.messageId); return; };
const selection = targets[Math.floor(Math.random() * targets.length)]!;
const target = await User.initUserId(selection.slice(5, -11));
name: ITEMNAME,
prettyName: "Grenade",
plural: "s",
description: "Give a random chatter a 60s timeout",
aliases: ["grenade"],
price: 99,
execution: async (msg, user) => {
const targets = await redis.keys(`user:*:vulnerable`);
if (targets.length === 0) {
await sendMessage("No vulnerable chatters to blow up", msg.messageId);
return;
}
const selection = targets[Math.floor(Math.random() * targets.length)]!;
const target = await User.initUserId(selection.slice(5, -11));
await getUserRecord(target!); // make sure the user record exist in the database
await getUserRecord(target!); // make sure the user record exist in the database
if (await user.itemLock()) { await sendMessage('Cannot use an item (itemlock)', msg.messageId); return; };
await user.setLock();
if (await user.itemLock()) {
await sendMessage("Cannot use an item (itemlock)", msg.messageId);
return;
}
await user.setLock();
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any grenades!`, msg.messageId); await user.clearLock(); return; };
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) {
await sendMessage(`You don't have any grenades!`, msg.messageId);
await user.clearLock();
return;
}
await Promise.all([
timeout(target!, `You got hit by ${user.displayName}'s grenade!`, 60),
sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s grenade wybuh`),
changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target!, ITEMNAME),
createUsedItemRecord(user, ITEMNAME),
playAlert({
name: 'grenadeExplosion',
user: user.displayName,
target: target?.displayName!
})
]);
await user.clearLock();
}
await Promise.all([
timeout(target!, `You got hit by ${user.displayName}'s grenade!`, 60),
sendMessage(
`wybuh ${target?.displayName} got hit by ${user.displayName}'s grenade wybuh`,
),
changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target!, ITEMNAME),
createUsedItemRecord(user, ITEMNAME),
playAlert({
name: "grenadeExplosion",
user: user.displayName,
target: target?.displayName!,
}),
]);
await user.clearLock();
},
});

View File

@@ -1,80 +1,96 @@
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import User from "user";
import { type userType, type specialExecuteArgs } from "lib/commandUtils";
import type { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import type { specialExecuteArgs, userType } from "lib/commandUtils";
import type User from "user";
type itemOptions = {
name: items;
aliases: string[];
prettyName: string;
plural: string;
description: string;
execution: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>;
specialaliases?: string[];
price: number;
name: items;
aliases: string[];
prettyName: string;
plural: string;
description: string;
execution: (
message: EventSubChannelChatMessageEvent,
sender: User,
args?: specialExecuteArgs,
) => Promise<void>;
specialaliases?: string[];
price: number;
};
export class Item {
public readonly name: items;
public readonly prettyName: string;
public readonly plural: string;
public readonly description: string;
public readonly aliases: string[];
public readonly specialaliases: string[];
public readonly usertype: userType;
public readonly price: number;
public readonly execute: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>;
public readonly disableable: boolean;
public readonly name: items;
public readonly prettyName: string;
public readonly plural: string;
public readonly description: string;
public readonly aliases: string[];
public readonly specialaliases: string[];
public readonly usertype: userType;
public readonly price: number;
public readonly execute: (
message: EventSubChannelChatMessageEvent,
sender: User,
args?: specialExecuteArgs,
) => Promise<void>;
public readonly disableable: boolean;
/** Creates an item object */
constructor(options: itemOptions) {
this.name = options.name;
this.prettyName = options.prettyName;
this.plural = options.plural;
this.description = options.description;
this.aliases = options.aliases;
this.usertype = 'chatter'; // Items are usable by everyone
this.execute = options.execution;
this.disableable = true;
this.specialaliases = options.specialaliases ?? [];
this.price = options.price;
};
};
/** Creates an item object */
constructor(options: itemOptions) {
this.name = options.name;
this.prettyName = options.prettyName;
this.plural = options.plural;
this.description = options.description;
this.aliases = options.aliases;
this.usertype = "chatter"; // Items are usable by everyone
this.execute = options.execution;
this.disableable = true;
this.specialaliases = options.specialaliases ?? [];
this.price = options.price;
}
}
import { readdir } from 'node:fs/promises';
import { updateUserRecord, type UserRecord } from "db/dbUser";
const itemAliasMap = new Map<string, Item>;
const itemObjectArray: Item[] = []
const specialAliasItems = new Map<string, Item>;
import { readdir } from "node:fs/promises";
import { type UserRecord, updateUserRecord } from "db/dbUser";
const itemAliasMap = new Map<string, Item>();
const itemObjectArray: Item[] = [];
const specialAliasItems = new Map<string, Item>();
const emptyInventory: inventory = {};
const itemarray: items[] = [];
const files = await readdir(import.meta.dir);
for (const file of files) {
if (!file.endsWith('.ts')) continue;
if (file === import.meta.file) continue;
const item: Item = await import(import.meta.dir + '/' + file.slice(0, -3)).then(a => a.default);
emptyInventory[item.name] = 0;
itemarray.push(item.name);
itemObjectArray.push(item);
for (const alias of item.aliases) {
itemAliasMap.set(alias, item); // Since it's not a primitive type the map is filled with references to the item, not the actual object
};
for (const alias of item.specialaliases) {
specialAliasItems.set(alias, item);
};
};
if (!file.endsWith(".ts")) continue;
if (file === import.meta.file) continue;
const item: Item = await import(
`${import.meta.dir}/${file.slice(0, -3)}`
).then((a) => a.default);
emptyInventory[item.name] = 0;
itemarray.push(item.name);
itemObjectArray.push(item);
for (const alias of item.aliases) {
itemAliasMap.set(alias, item); // Since it's not a primitive type the map is filled with references to the item, not the actual object
}
for (const alias of item.specialaliases) {
specialAliasItems.set(alias, item);
}
}
export default itemAliasMap;
export { emptyInventory, itemarray, specialAliasItems, itemObjectArray };
export type items = "blaster" | "silverbullet" | "grenade" | "tnt";
export type inventory = {
[key in items]?: number;
[key in items]?: number;
};
export async function changeItemCount(user: User, userRecord: UserRecord, itemname: items, amount = -1): Promise<false | UserRecord> {
userRecord.inventory[itemname] = userRecord.inventory[itemname]! += amount;
if (userRecord.inventory[itemname] < 0) return false;
await updateUserRecord(user, userRecord);
return userRecord;
};
export async function changeItemCount(
user: User,
userRecord: UserRecord,
itemname: items,
amount = -1,
): Promise<false | UserRecord> {
userRecord.inventory[itemname] = userRecord.inventory[itemname]! += amount;
if (userRecord.inventory[itemname] < 0) return false;
await updateUserRecord(user, userRecord);
return userRecord;
}

View File

@@ -1,88 +1,116 @@
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import { createTimeoutRecord } from "db/dbTimeouts";
import { createUsedItemRecord } from "db/dbUsedItems";
import { getUserRecord } from "db/dbUser";
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import parseCommandArgs from "lib/parseCommandArgs";
import { timeout } from "lib/timeout";
import { playAlert } from "web/alerts/serverFunctions";
import User from "user";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import { streamerId } from "main";
import User from "user";
import { playAlert } from "web/alerts/serverFunctions";
const ITEMNAME = 'silverbullet';
const ITEMNAME = "silverbullet";
export default new Item({
name: ITEMNAME,
prettyName: 'Silver bullet',
plural: 's',
description: 'Times targeted or random vulnerable user out for 30 minutes',
aliases: ['execute', 'silverbullet'],
specialaliases: ['blastin'],
price: 666,
execution: async (msg, user, specialargs) => {
const messagequery = parseCommandArgs(msg.messageText, specialargs?.activation);
name: ITEMNAME,
prettyName: "Silver bullet",
plural: "s",
description: "Times targeted or random vulnerable user out for 30 minutes",
aliases: ["execute", "silverbullet"],
specialaliases: ["blastin"],
price: 666,
execution: async (msg, user, specialargs) => {
const messagequery = parseCommandArgs(
msg.messageText,
specialargs?.activation,
);
if (await user.itemLock()) { await sendMessage('Cannot use an item (itemlock)', msg.messageId); return; };
await user.setLock();
if (await user.itemLock()) {
await sendMessage("Cannot use an item (itemlock)", msg.messageId);
return;
}
await user.setLock();
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1 && user.id !== streamerId) { await sendMessage(`You don't have any silver bullets!`, msg.messageId); await user.clearLock(); return; };
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1 && user.id !== streamerId) {
await sendMessage(`You don't have any silver bullets!`, msg.messageId);
await user.clearLock();
return;
}
let target: User | null;
if (!messagequery[0]) {
const vulnsids = await redis.keys('user:*:vulnerable');
const baseusers = vulnsids.map(a => User.initUserId(a.slice(5, -11)));
const users: User[] = [];
for (const user of baseusers) {
const a = await user;
if (!a) continue;
users.push(a);
};
if (users.length === 0) { await user.clearLock(); await sendMessage('No vulnerable chatters', msg.messageId); return; };
target = users[Math.floor(Math.random() * users.length)]!;
await playAlert({
name: 'blastinRoulette',
user: user.displayName,
targets: users.map(a => a.displayName),
finaltarget: target.displayName
});
await new Promise((res, _) => setTimeout(res, 4000));
} else {
target = await User.initUsername(messagequery[0].toLowerCase());
};
if (!target) { await user.clearLock(); await sendMessage(`${messagequery[0]} doesn't exist`); return; };
let target: User | null;
if (!messagequery[0]) {
const vulnsids = await redis.keys("user:*:vulnerable");
const baseusers = vulnsids.map((a) => User.initUserId(a.slice(5, -11)));
const users: User[] = [];
for (const user of baseusers) {
const a = await user;
if (!a) continue;
users.push(a);
}
if (users.length === 0) {
await user.clearLock();
await sendMessage("No vulnerable chatters", msg.messageId);
return;
}
target = users[Math.floor(Math.random() * users.length)]!;
await playAlert({
name: "blastinRoulette",
user: user.displayName,
targets: users.map((a) => a.displayName),
finaltarget: target.displayName,
});
await new Promise((res, _) => setTimeout(res, 4000));
} else {
target = await User.initUsername(messagequery[0].toLowerCase());
}
if (!target) {
await user.clearLock();
await sendMessage(`${messagequery[0]} doesn't exist`);
return;
}
await getUserRecord(target); // make sure the user record exist in the database
await getUserRecord(target); // make sure the user record exist in the database
const result = await timeout(target, `You got blasted by ${user.displayName}!`, 60 * 30);
if (result.status) await Promise.all([
sendMessage(`KEKPOINT KEKPOINT KEKPOINT ${target.displayName.toUpperCase()} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`),
changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target, ITEMNAME),
createUsedItemRecord(user, ITEMNAME),
playAlert({
name: 'userExecution',
user: user.displayName,
target: target.displayName
})
]);
else {
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;
};
};
await user.clearLock();
}
const result = await timeout(
target,
`You got blasted by ${user.displayName}!`,
60 * 30,
);
if (result.status)
await Promise.all([
sendMessage(
`KEKPOINT KEKPOINT KEKPOINT ${target.displayName.toUpperCase()} RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO RIPBOZO`,
),
changeItemCount(user, userObj, ITEMNAME),
createTimeoutRecord(user, target, ITEMNAME),
createUsedItemRecord(user, ITEMNAME),
playAlert({
name: "userExecution",
user: user.displayName,
target: target.displayName,
}),
]);
else {
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;
}
}
await user.clearLock();
},
});

View File

@@ -1,64 +1,82 @@
import { redis } from "lib/redis";
import { sendMessage } from "lib/commandUtils";
import { timeout } from "lib/timeout";
import { changeItemCount, Item } from "items";
import User from "user";
import { getUserRecord } from "db/dbUser";
import { createTimeoutRecord } from "db/dbTimeouts";
import { createUsedItemRecord } from "db/dbUsedItems";
import { getUserRecord } from "db/dbUser";
import { changeItemCount, Item } from "items";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import User from "user";
import { playAlert } from "web/alerts/serverFunctions";
const ITEMNAME = 'tnt';
const ITEMNAME = "tnt";
export default new Item({
name: ITEMNAME,
prettyName: 'TNT',
plural: 's',
description: 'Give 5-10 random chatters 60 second timeouts',
aliases: ['tnt'],
price: 1000,
execution: async (msg, user) => {
const vulntargets = await redis.keys('user:*:vulnerable').then(a => a.map(b => b.slice(5, -11)));
if (vulntargets.length === 0) { await sendMessage('No vulnerable chatters to blow up', msg.messageId); return; };
const targets = getTNTTargets(vulntargets);
name: ITEMNAME,
prettyName: "TNT",
plural: "s",
description: "Give 5-10 random chatters 60 second timeouts",
aliases: ["tnt"],
price: 1000,
execution: async (msg, user) => {
const vulntargets = await redis
.keys("user:*:vulnerable")
.then((a) => a.map((b) => b.slice(5, -11)));
if (vulntargets.length === 0) {
await sendMessage("No vulnerable chatters to blow up", msg.messageId);
return;
}
const targets = getTNTTargets(vulntargets);
if (await user.itemLock()) { await sendMessage('Cannot use an item (itemlock)', msg.messageId); return; };
await user.setLock();
if (await user.itemLock()) {
await sendMessage("Cannot use an item (itemlock)", msg.messageId);
return;
}
await user.setLock();
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) { await sendMessage(`You don't have any TNTs!`, msg.messageId); await user.clearLock(); return; };
const userObj = await getUserRecord(user);
if (userObj.inventory[ITEMNAME]! < 1) {
await sendMessage(`You don't have any TNTs!`, msg.messageId);
await user.clearLock();
return;
}
await Promise.all(targets.map(async targetid => {
const target = await User.initUserId(targetid);
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),
sendMessage(`wybuh ${target?.displayName} got hit by ${user.displayName}'s TNT wybuh`),
createTimeoutRecord(user, target!, ITEMNAME),
]);
}));
await Promise.all(
targets.map(async (targetid) => {
const target = await User.initUserId(targetid);
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),
sendMessage(
`wybuh ${target?.displayName} got hit by ${user.displayName}'s TNT wybuh`,
),
createTimeoutRecord(user, target!, ITEMNAME),
]);
}),
);
await Promise.all([
createUsedItemRecord(user, ITEMNAME),
playAlert({
name: 'tntExplosion',
user: user.displayName,
targets
}),
changeItemCount(user, userObj, ITEMNAME)
]);
await Promise.all([
createUsedItemRecord(user, ITEMNAME),
playAlert({
name: "tntExplosion",
user: user.displayName,
targets,
}),
changeItemCount(user, userObj, ITEMNAME),
]);
await user.clearLock();
await sendMessage(`RIPBOZO ${user.displayName} exploded ${targets.length} chatter${targets.length === 1 ? '' : 's'} with their TNT RIPBOZO`);
}
await user.clearLock();
await sendMessage(
`RIPBOZO ${user.displayName} exploded ${targets.length} chatter${targets.length === 1 ? "" : "s"} with their TNT RIPBOZO`,
);
},
});
export function getTNTTargets<T>(arr: T[]): T[] {
if (arr.length <= 5) {
return arr;
};
if (arr.length <= 5) {
return arr;
}
const count = Math.floor(Math.random() * 6) + 5; // Random number between 5 and 10
const shuffled = [...arr].sort(() => 0.5 - Math.random()); // Shuffle array
return shuffled.slice(0, Math.min(count, arr.length)); // Return up to `count` entries
};
const count = Math.floor(Math.random() * 6) + 5; // Random number between 5 and 10
const shuffled = [...arr].sort(() => 0.5 - Math.random()); // Shuffle array
return shuffled.slice(0, Math.min(count, arr.length)); // Return up to `count` entries
}

View File

@@ -1,15 +1,15 @@
import { redis } from "lib/redis";
export async function getAdmins() {
const data = await redis.keys('user:*:admin');
return data.map(a => a.slice(5, -6));
};
const data = await redis.keys("user:*:admin");
return data.map((a) => a.slice(5, -6));
}
export async function isAdmin(userid: string) {
return await redis.exists(`user:${userid}:admin`);
};
return await redis.exists(`user:${userid}:admin`);
}
export async function addAdmin(userid: string) {
return await redis.set(`user:${userid}:admin`, '1');
};
return await redis.set(`user:${userid}:admin`, "1");
}
export async function removeAdmin(userid: string) {
return await redis.del(`user:${userid}:admin`);
};
return await redis.del(`user:${userid}:admin`);
}

View File

@@ -1,9 +1,13 @@
import { updateUserRecord, type UserRecord } from "db/dbUser";
import User from "user";
import { type UserRecord, updateUserRecord } from "db/dbUser";
import type User from "user";
export async function changeBalance(user: User, userRecord: UserRecord, amount: number): Promise<false | UserRecord> {
userRecord.balance = userRecord.balance += amount;
if (userRecord.balance < 0) return false;
await updateUserRecord(user, userRecord);
return userRecord;
};
export async function changeBalance(
user: User,
userRecord: UserRecord,
amount: number,
): Promise<false | UserRecord> {
userRecord.balance = userRecord.balance += amount;
if (userRecord.balance < 0) return false;
await updateUserRecord(user, userRecord);
return userRecord;
}

View File

@@ -1,46 +1,59 @@
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import User from "user";
import { chatterId, streamerId } from "main";
import type { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import { api } from "index";
import { chatterId, streamerId } from "main";
import type User from "user";
export type userType = 'chatter' | 'admin' | 'streamer' | 'moderator';
export type userType = "chatter" | "admin" | "streamer" | "moderator";
export type specialExecuteArgs = {
activation?: string;
activation?: string;
};
export type commandOptions = {
name: string;
aliases: string[];
usertype: userType;
execution: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>;
disableable?: boolean;
specialaliases?: string[];
name: string;
aliases: string[];
usertype: userType;
execution: (
message: EventSubChannelChatMessageEvent,
sender: User,
args?: specialExecuteArgs,
) => Promise<void>;
disableable?: boolean;
specialaliases?: string[];
};
/** The Command class represents a command */
export class Command {
public readonly name: string;
public readonly aliases: string[];
public readonly usertype: userType;
public readonly disableable: boolean;
public readonly specialaliases: string[];
public readonly execute: (message: EventSubChannelChatMessageEvent, sender: User, args?: specialExecuteArgs) => Promise<void>;
constructor(options: commandOptions) {
this.name = options.name.toLowerCase();
this.aliases = options.aliases;
this.usertype = options.usertype;
this.execute = options.execution;
this.disableable = options.disableable ?? true;
this.specialaliases = options.specialaliases ?? [];
};
};
public readonly name: string;
public readonly aliases: string[];
public readonly usertype: userType;
public readonly disableable: boolean;
public readonly specialaliases: string[];
public readonly execute: (
message: EventSubChannelChatMessageEvent,
sender: User,
args?: specialExecuteArgs,
) => Promise<void>;
constructor(options: commandOptions) {
this.name = options.name.toLowerCase();
this.aliases = options.aliases;
this.usertype = options.usertype;
this.execute = options.execution;
this.disableable = options.disableable ?? true;
this.specialaliases = options.specialaliases ?? [];
}
}
/** Helper function to send a message to the stream */
export const sendMessage = async (message: string, replyParentMessageId?: string) => {
try {
return await api.chat.sendChatMessageAsApp(chatterId, streamerId, message, { replyParentMessageId });
} catch (e) {
return await api.chat.sendChatMessageAsApp(chatterId, streamerId, message);
};
export const sendMessage = async (
message: string,
replyParentMessageId?: string,
) => {
try {
return await api.chat.sendChatMessageAsApp(chatterId, streamerId, message, {
replyParentMessageId,
});
} catch (_e) {
return await api.chat.sendChatMessageAsApp(chatterId, streamerId, message);
}
};

View File

@@ -1,17 +1,19 @@
export function buildTimeString(time1: number, time2: number): string {
const diff = Math.abs(time1 - time2);
const timeobj = {
day: Math.floor(diff / (1000 * 60 * 60 * 24)),
hour: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minute: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)),
second: Math.floor((diff % (1000 * 60)) / 1000)
};
const stringarray: string[] = [];
for (const [unit, value] of Object.entries(timeobj)) {
if (value === 0) continue;
if (unit === 'second' && timeobj.day > 0) continue;
stringarray.push(`${value} ${unit}${value === 1 ? '' : 's'}`);
};
const last = stringarray.pop();
return stringarray.length === 0 ? last! : stringarray.join(', ') + " and " + last;
};
const diff = Math.abs(time1 - time2);
const timeobj = {
day: Math.floor(diff / (1000 * 60 * 60 * 24)),
hour: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minute: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)),
second: Math.floor((diff % (1000 * 60)) / 1000),
};
const stringarray: string[] = [];
for (const [unit, value] of Object.entries(timeobj)) {
if (value === 0) continue;
if (unit === "second" && timeobj.day > 0) continue;
stringarray.push(`${value} ${unit}${value === 1 ? "" : "s"}`);
}
const last = stringarray.pop();
return stringarray.length === 0
? last!
: `${stringarray.join(", ")} and ${last}`;
}

View File

@@ -5,57 +5,63 @@ import type { inventory } from "items";
import type User from "user";
export async function getTimeoutStats(target: User, thismonth: boolean) {
const monthdata = thismonth ? new Date().toISOString().slice(0, 7) : undefined;
const monthdata = thismonth
? new Date().toISOString().slice(0, 7)
: undefined;
const [shot, hit] = await Promise.all([
getTimeoutsAsUser(target, monthdata),
getTimeoutsAsTarget(target, monthdata)
]);
const [shot, hit] = await Promise.all([
getTimeoutsAsUser(target, monthdata),
getTimeoutsAsTarget(target, monthdata),
]);
if (!shot || !hit) return;
if (!shot || !hit) return;
const blasterhit = hit.filter(item => item.item !== 'silverbullet').length;
const silverbullethit = hit.length - blasterhit;
const blastershot = shot.filter(item => item.item !== 'silverbullet').length;
const silverbulletshot = shot.length - blastershot;
const blasterhit = hit.filter((item) => item.item !== "silverbullet").length;
const silverbullethit = hit.length - blasterhit;
const blastershot = shot.filter(
(item) => item.item !== "silverbullet",
).length;
const silverbulletshot = shot.length - blastershot;
return {
hit: {
blaster: blasterhit,
silverbullet: silverbullethit
},
shot: {
blaster: blastershot,
silverbullet: silverbulletshot
}
};
};
return {
hit: {
blaster: blasterhit,
silverbullet: silverbullethit,
},
shot: {
blaster: blastershot,
silverbullet: silverbulletshot,
},
};
}
export async function getItemStats(target: User, thismonth: boolean) {
const monthdata = thismonth ? new Date().toISOString().slice(0, 7) : undefined;
const monthdata = thismonth
? new Date().toISOString().slice(0, 7)
: undefined;
const [items, cheers] = await Promise.all([
getItemsUsed(target, monthdata),
getCheerEvents(target, monthdata)
]);
if (!items || !cheers) return;
const [items, cheers] = await Promise.all([
getItemsUsed(target, monthdata),
getCheerEvents(target, monthdata),
]);
if (!items || !cheers) return;
const returnObj: inventory = {
blaster: 0,
silverbullet: 0,
grenade: 0,
tnt: 0,
};
const returnObj: inventory = {
blaster: 0,
silverbullet: 0,
grenade: 0,
tnt: 0,
};
for (const item of items) {
if (!returnObj[item.item]) returnObj[item.item] = 0;
returnObj[item.item]! += 1;
};
for (const item of items) {
if (!returnObj[item.item]) returnObj[item.item] = 0;
returnObj[item.item]! += 1;
}
for (const cheer of cheers) {
if (!returnObj[cheer.event]) returnObj[cheer.event] = 0;
returnObj[cheer.event]! += 1
};
for (const cheer of cheers) {
if (!returnObj[cheer.event]) returnObj[cheer.event] = 0;
returnObj[cheer.event]! += 1;
}
return returnObj;
};
return returnObj;
}

View File

@@ -1,60 +1,71 @@
import { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base"
import { redis } from "lib/redis";
import type User from "user";
import { timeout } from "lib/timeout";
import { sendMessage } from "lib/commandUtils";
import type { EventSubChannelChatMessageEvent } from "@twurple/eventsub-base";
import { createAnivTimeoutRecord } from "db/dbAnivTimeouts";
import { sendMessage } from "lib/commandUtils";
import { redis } from "lib/redis";
import { timeout } from "lib/timeout";
import type User from "user";
const ANIVNAMES: anivBots[] = ['a_n_e_e_v', 'a_n_i_v'];
const ANIVNAMES: anivBots[] = ["a_n_e_e_v", "a_n_i_v"];
type anivMessageStore = {
[key: string]: string;
[key: string]: string;
};
type IsAnivMessage = {
isAnivMessage: true;
message: string;
anivbot: anivBots;
isAnivMessage: true;
message: string;
anivbot: anivBots;
};
type isNotAnivMessage = {
isAnivMessage: false;
isAnivMessage: false;
};
export type anivBots = 'a_n_i_v' | 'a_n_e_e_v';
export type anivBots = "a_n_i_v" | "a_n_e_e_v";
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 };
};
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.map(a => a.toLowerCase()).includes(user.username)) {
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) {
if (Math.random() > 0.5) { // 1/2 chance to dodge aniv timeout
await createAnivTimeoutRecord(msg.messageText, data.anivbot, user, 0)
return;
};
export default async function handleMessage(
msg: EventSubChannelChatMessageEvent,
user: User,
) {
if (ANIVNAMES.map((a) => a.toLowerCase()).includes(user.username)) {
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) {
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
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)
]);
};
};
};
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),
]);
}
}
}

View File

@@ -2,22 +2,23 @@ import { redis } from "lib/redis";
import { streamerUsers } from "main";
export async function getInvulns() {
const data = await redis.keys('user:*:invulnerable');
return data.map(a => a.slice(5, -13));
};
const data = await redis.keys("user:*:invulnerable");
return data.map((a) => a.slice(5, -13));
}
export async function isInvuln(userid: string) {
return await redis.exists(`user:${userid}:invulnerable`);
};
return await redis.exists(`user:${userid}:invulnerable`);
}
export async function addInvuln(userid: string) {
await redis.del(`user:${userid}:vulnerable`);
if (await redis.ttl(`user:${userid}:invulnerable`) > 0) await redis.del(`user:${userid}:invulnerable`);
return await redis.set(`user:${userid}:invulnerable`, '1');
};
await redis.del(`user:${userid}:vulnerable`);
if ((await redis.ttl(`user:${userid}:invulnerable`)) > 0)
await redis.del(`user:${userid}:invulnerable`);
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`);
};
if (streamerUsers.includes(userid)) return;
return await redis.del(`user:${userid}:invulnerable`);
}
export async function setTemporaryInvuln(userid: string, duration = 600) {
await redis.set(`user:${userid}:invulnerable`, '1');
await redis.expire(`user:${userid}:invulnerable`, duration);
};
await redis.set(`user:${userid}:invulnerable`, "1");
await redis.expire(`user:${userid}:invulnerable`, duration);
}

View File

@@ -1,11 +1,18 @@
import kleur from "kleur";
const logger = {
err: (arg: string) => console.error(kleur.red().bold().italic('[ERROR] ') + kleur.red().bold(arg)),
warn: (arg: string) => console.warn(kleur.yellow().bold().italic('[WARN] ') + kleur.yellow().bold(arg)),
info: (arg: string) => console.info(kleur.white().bold().italic('[INFO] ') + kleur.white(arg)),
ok: (arg: string) => console.info(kleur.green().bold(arg)),
enverr: (arg: string) => logger.err(`Please provide a ${arg} in the .env`)
err: (arg: string) =>
console.error(
kleur.red().bold().italic("[ERROR] ") + kleur.red().bold(arg),
),
warn: (arg: string) =>
console.warn(
kleur.yellow().bold().italic("[WARN] ") + kleur.yellow().bold(arg),
),
info: (arg: string) =>
console.info(kleur.white().bold().italic("[INFO] ") + kleur.white(arg)),
ok: (arg: string) => console.info(kleur.green().bold(arg)),
enverr: (arg: string) => logger.err(`Please provide a ${arg} in the .env`),
};
export default logger
export default logger;

View File

@@ -2,24 +2,40 @@ import { commandPrefix } from "main";
/** Helper function to extract arguments from commands */
export default function parseCommandArgs(input: string, specialAlias?: string) {
let nice = '';
let sliceLength = 0;
if (specialAlias) {
nice = input.toLowerCase().slice(specialAlias.length).replace(/[^\x00-\x7F]/g, '').trim();
sliceLength = input.toLowerCase().startsWith('i') ? 1 : 0;
} else {
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, ''));
};
let nice = "";
let sliceLength = 0;
if (specialAlias) {
nice = input
.toLowerCase()
.slice(specialAlias.length)
.replace(/[^\x00-\x7F]/g, "")
.trim();
sliceLength = input.toLowerCase().startsWith("i") ? 1 : 0;
} else {
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, ""));
}
export function parseCheerArgs(input: string) {
const nice = input.toLowerCase().trim();
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).replace(/[^\x00-\x7F]/g, '').split(' ').slice(1);
// 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)
.replace(/[^\x00-\x7F]/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));
};
// This is for actual cheers. Remove all 'cheerx' parts of the message
return nice.split(" ").filter((a) => !/cheer[0-9]+/i.test(a));
}

View File

@@ -1,3 +1,3 @@
import { RedisClient } from "bun";
export const redis = new RedisClient();
export const redis = new RedisClient();

View File

@@ -1,70 +1,90 @@
import { streamerId } from "main";
import logger from "lib/logger";
import User from "user";
import { isInvuln } from "lib/invuln";
import { api } from "index";
import { isInvuln } from "lib/invuln";
import logger from "lib/logger";
import { redis } from "lib/redis";
import { streamerId } from "main";
import type User from "user";
type SuccessfulTimeout = { status: true; };
type UnSuccessfulTimeout = { status: false; reason: 'banned' | 'unknown' | 'illegal'; };
type SuccessfulTimeout = { status: true };
type UnSuccessfulTimeout = {
status: false;
reason: "banned" | "unknown" | "illegal";
};
type TimeoutResult = SuccessfulTimeout | UnSuccessfulTimeout;
/** Give a user a timeout/ban
* @param user - user class of target to timeout/ban
* @param reason - reason for timeout/ban
* @param duration - duration of timeout. don't specifiy for ban */
export const timeout = async (user: User, reason: string, duration?: number): Promise<TimeoutResult> => {
if (await isInvuln(user.id) && duration || await redis.exists(`user:${user.id}:bot`)) return { status: false, reason: 'illegal' }; // Don't timeout invulnerable chatters and bots
export const timeout = async (
user: User,
reason: string,
duration?: number,
): Promise<TimeoutResult> => {
if (
((await isInvuln(user.id)) && duration) ||
(await redis.exists(`user:${user.id}:bot`))
)
return { status: false, reason: "illegal" }; // Don't timeout invulnerable chatters and bots
// Check if user already has a timeout and handle stacking
const banStatus = await timeoutDuration(user);
if (banStatus) {
if (await redis.exists('timeoutStacking')) {
if (duration) duration += Math.floor((banStatus * 1000 - Date.now()) / 1000); // the target is timed out and stacking is on
} else return { status: false, reason: 'banned' }; // the target is timed out, but stacking is off
} else if (banStatus === null) return { status: false, reason: 'banned' }; // target is perma banned
// Check if user already has a timeout and handle stacking
const banStatus = await timeoutDuration(user);
if (banStatus) {
if (await redis.exists("timeoutStacking")) {
if (duration)
duration += Math.floor((banStatus * 1000 - Date.now()) / 1000); // the target is timed out and stacking is on
} else return { status: false, reason: "banned" }; // the target is timed out, but stacking is off
} else if (banStatus === null) return { status: false, reason: "banned" }; // target is perma banned
if (await redis.exists(`user:${user.id}:mod`)) {
if (!duration) duration = 60; // make sure that mods don't get perma-banned
await redis.set(`user:${user.id}:remod`, '1');
remodMod(user, duration);
await api.moderation.removeModerator(streamerId, user.id!);
};
if (await redis.exists(`user:${user.id}:mod`)) {
if (!duration) duration = 60; // make sure that mods don't get perma-banned
await redis.set(`user:${user.id}:remod`, "1");
remodMod(user, duration);
await api.moderation.removeModerator(streamerId, user.id!);
}
try {
await api.moderation.banUser(streamerId, { user: user.id, reason, duration });
} catch (err) {
logger.err(err as string);
return { status: false, reason: 'unknown' };
};
try {
await api.moderation.banUser(streamerId, {
user: user.id,
reason,
duration,
});
} catch (err) {
logger.err(err as string);
return { status: false, reason: "unknown" };
}
await user.clearVulnerable();
await redis.set(`user:${user.id}:timeout`, '1');
if (duration) await redis.expire(`user:${user.id}:timeout`, duration);
await user.clearVulnerable();
await redis.set(`user:${user.id}:timeout`, "1");
if (duration) await redis.expire(`user:${user.id}:timeout`, duration);
return { status: true };
return { status: true };
};
/** Give the target mod status back after timeout */
export function remodMod(target: User, duration: number) {
setTimeout(async () => {
const bandata = await timeoutDuration(target);
if (bandata) { // If the target is still timed out, try again when new timeout expires
const timeoutleft = bandata * 1000 - Date.now(); // date when timeout expires - current date
remodMod(target, timeoutleft); // Call the current function with new time (recursion)
} else {
try {
await api.moderation.addModerator(streamerId, target.id);
await redis.del(`user:${target.id}:remod`);
} catch (err) { }; // This triggers when the timeout got shortened. try/catch so no runtime error
};
}, duration + 3000); // callback gets called after duration of timeout + 3 seconds
};
setTimeout(async () => {
const bandata = await timeoutDuration(target);
if (bandata) {
// If the target is still timed out, try again when new timeout expires
const timeoutleft = bandata * 1000 - Date.now(); // date when timeout expires - current date
remodMod(target, timeoutleft); // Call the current function with new time (recursion)
} else {
try {
await api.moderation.addModerator(streamerId, target.id);
await redis.del(`user:${target.id}:remod`);
} catch (_err) {} // This triggers when the timeout got shortened. try/catch so no runtime error
}
}, duration + 3000); // callback gets called after duration of timeout + 3 seconds
}
/** This returns number if there is a duration of time for the timeout, false if not banned and null if perma banned */
export async function timeoutDuration(user: User): Promise<number | null | false> {
const data = await redis.expiretime(`user:${user.id}:timeout`);
if (data === -1) return null; // Perma banned
else if (data === -2) return false; // Not banned
return data;
};
export async function timeoutDuration(
user: User,
): Promise<number | null | false> {
const data = await redis.expiretime(`user:${user.id}:timeout`);
if (data === -1)
return null; // Perma banned
else if (data === -2) return false; // Not banned
return data;
}

View File

@@ -1,4 +1,4 @@
export const singleUserMode = process.env.CHATTER_IS_STREAMER === 'true';
export const singleUserMode = process.env.CHATTER_IS_STREAMER === "true";
export const chatterId = process.env.CHATTER_ID ?? "";
export const streamerId = process.env.STREAMER_ID ?? "";
export const commandPrefix = process.env.COMMAND_PREFIX ?? "!";

View File

@@ -1,112 +1,123 @@
import { EventSubChannelRedemptionAddEvent } from "@twurple/eventsub-base";
import User from "user";
import type { EventSubChannelRedemptionAddEvent } from "@twurple/eventsub-base";
import type User from "user";
export type pointRedeemOptions = {
name: string;
title: string;
prompt?: string;
cost: number;
color?: string;
sfxredeem?: boolean;
input?: boolean;
execution: (message: EventSubChannelRedemptionAddEvent, sender: User) => Promise<void>;
name: string;
title: string;
prompt?: string;
cost: number;
color?: string;
sfxredeem?: boolean;
input?: boolean;
execution: (
message: EventSubChannelRedemptionAddEvent,
sender: User,
) => Promise<void>;
};
/** The Command class represents a command */
export default class PointRedeem {
public readonly name: string;
public readonly title: string;
public readonly prompt?: string;
public readonly cost: number;
public readonly color?: string;
public readonly sfxredeem?: boolean;
public readonly input?: boolean;
public readonly execute: (message: EventSubChannelRedemptionAddEvent, sender: User) => Promise<void>;
constructor(options: pointRedeemOptions) {
this.name = options.name.toLowerCase();
this.title = options.title;
this.prompt = options.prompt;
this.cost = options.cost;
this.color = options.color;
this.execute = options.execution;
this.sfxredeem = options.sfxredeem;
this.input = options.input;
};
};
public readonly name: string;
public readonly title: string;
public readonly prompt?: string;
public readonly cost: number;
public readonly color?: string;
public readonly sfxredeem?: boolean;
public readonly input?: boolean;
public readonly execute: (
message: EventSubChannelRedemptionAddEvent,
sender: User,
) => Promise<void>;
constructor(options: pointRedeemOptions) {
this.name = options.name.toLowerCase();
this.title = options.title;
this.prompt = options.prompt;
this.cost = options.cost;
this.color = options.color;
this.execute = options.execution;
this.sfxredeem = options.sfxredeem;
this.input = options.input;
}
}
import { readdir } from 'node:fs/promises';
import { readdir } from "node:fs/promises";
/** A map of all (including inactive) redeems mapped to names */
const namedRedeems = new Map<string, PointRedeem>;
const namedRedeems = new Map<string, PointRedeem>();
const sfxRedeems = new Map<string, PointRedeem>;
const sfxRedeems = new Map<string, PointRedeem>();
const files = await readdir(import.meta.dir);
for (const file of files) {
if (!file.endsWith('.ts')) continue;
if (file === import.meta.file) continue;
const redeem: PointRedeem = await import(import.meta.dir + '/' + file.slice(0, -3)).then(a => a.default);
namedRedeems.set(redeem.name, redeem);
if (redeem.sfxredeem) sfxRedeems.set(redeem.name, redeem);
};
if (!file.endsWith(".ts")) continue;
if (file === import.meta.file) continue;
const redeem: PointRedeem = await import(
`${import.meta.dir}/${file.slice(0, -3)}`
).then((a) => a.default);
namedRedeems.set(redeem.name, redeem);
if (redeem.sfxredeem) sfxRedeems.set(redeem.name, redeem);
}
export { namedRedeems, sfxRedeems };
const activeRedeems = new Map<string, PointRedeem>;
const activeRedeems = new Map<string, PointRedeem>();
/** Map of redeemname to twitch redeem ID */
const idMap = new Map<string, string>;
const idMap = new Map<string, string>();
import { streamerId } from "main";
import logger from "lib/logger";
import { api } from "index";
import logger from "lib/logger";
import { streamerId } from "main";
const currentRedeems = new Map<string, string>;
await api.channelPoints.getCustomRewards(streamerId).then(a => a.map(b => currentRedeems.set(b.title, b.id)));
const currentRedeems = new Map<string, string>();
await api.channelPoints
.getCustomRewards(streamerId)
.then((a) => a.map((b) => currentRedeems.set(b.title, b.id)));
for (const [_, redeem] of Array.from(namedRedeems)) {
const selection = currentRedeems.get(redeem.title);
if (selection) {
currentRedeems.delete(redeem.title);
idMap.set(redeem.name, selection);
activeRedeems.set(selection, redeem);
} else {
if (process.env.NODE_ENV !== 'production') continue; // If created with dev-app we won't be able to change it with prod app
const creation = await api.channelPoints.createCustomReward(streamerId, {
title: redeem.title,
prompt: redeem.prompt,
cost: redeem.cost,
backgroundColor: redeem.color,
userInputRequired: redeem.input
});
logger.ok(`Created custom point redeem ${redeem.title}`);
idMap.set(redeem.name, creation.id);
activeRedeems.set(creation.id, redeem);
};
};
const selection = currentRedeems.get(redeem.title);
if (selection) {
currentRedeems.delete(redeem.title);
idMap.set(redeem.name, selection);
activeRedeems.set(selection, redeem);
} else {
if (process.env.NODE_ENV !== "production") continue; // If created with dev-app we won't be able to change it with prod app
const creation = await api.channelPoints.createCustomReward(streamerId, {
title: redeem.title,
prompt: redeem.prompt,
cost: redeem.cost,
backgroundColor: redeem.color,
userInputRequired: redeem.input,
});
logger.ok(`Created custom point redeem ${redeem.title}`);
idMap.set(redeem.name, creation.id);
activeRedeems.set(creation.id, redeem);
}
}
Array.from(currentRedeems).map(async ([title, redeem]) => {
if (process.env.NODE_ENV !== 'production') return;
await api.channelPoints.deleteCustomReward(streamerId, redeem); logger.ok(`Deleted custom point redeem ${title}`);
if (process.env.NODE_ENV !== "production") return;
await api.channelPoints.deleteCustomReward(streamerId, redeem);
logger.ok(`Deleted custom point redeem ${title}`);
});
logger.ok("Successfully synced all custom point redeems");
export async function enableRedeem(redeem: PointRedeem, id: string) {
if (process.env.NODE_ENV !== 'production') return;
await api.channelPoints.updateCustomReward(streamerId, id, {
isEnabled: true
});
activeRedeems.set(id, redeem);
logger.ok(`Enabled the ${redeem.name} point redeem`);
};
if (process.env.NODE_ENV !== "production") return;
await api.channelPoints.updateCustomReward(streamerId, id, {
isEnabled: true,
});
activeRedeems.set(id, redeem);
logger.ok(`Enabled the ${redeem.name} point redeem`);
}
export async function disableRedeem(redeem: PointRedeem, id: string) {
if (process.env.NODE_ENV !== 'production') return;
await api.channelPoints.updateCustomReward(streamerId, id, {
isEnabled: false
});
activeRedeems.delete(id);
logger.ok(`Disabled the ${redeem.name} point redeem`);
};
if (process.env.NODE_ENV !== "production") return;
await api.channelPoints.updateCustomReward(streamerId, id, {
isEnabled: false,
});
activeRedeems.delete(id);
logger.ok(`Disabled the ${redeem.name} point redeem`);
}
export { activeRedeems, idMap };
export { activeRedeems, idMap };

View File

@@ -1,16 +1,18 @@
import { sendMessage } from "lib/commandUtils";
import PointRedeem from "pointRedeems";
import { getUserRecord } from "db/dbUser";
import { changeBalance } from "lib/changeBalance";
import PointRedeem from "pointRedeems";
import { sendMessage } from "lib/commandUtils";
export default new PointRedeem({
name: "qbucksredeem",
title: "FREE MONEY",
prompt: "GET 100 QBUCKS!",
color: '#00FF00',
cost: 1000,
execution: async (_msg, user) => {
await changeBalance(user, await getUserRecord(user), 100);
await sendMessage(`${user.displayName} got 100 qbucks for their point redeem`);
}
})
name: "qbucksredeem",
title: "FREE MONEY",
prompt: "GET 100 QBUCKS!",
color: "#00FF00",
cost: 1000,
execution: async (_msg, user) => {
await changeBalance(user, await getUserRecord(user), 100);
await sendMessage(
`${user.displayName} got 100 qbucks for their point redeem`,
);
},
});

View File

@@ -2,15 +2,16 @@ import PointRedeem from "pointRedeems";
import { playAlert } from "web/alerts/serverFunctions";
export default new PointRedeem({
name: "sfxEddieScream",
title: "Eddie scream",
cost: 100,
color: "#A020F0",
prompt: "Eddie screaming",
sfxredeem: true,
execution: async msg => await playAlert({
name: 'sound',
user: msg.userDisplayName,
sound: 'eddiescream'
})
name: "sfxEddieScream",
title: "Eddie scream",
cost: 100,
color: "#A020F0",
prompt: "Eddie screaming",
sfxredeem: true,
execution: async (msg) =>
await playAlert({
name: "sound",
user: msg.userDisplayName,
sound: "eddiescream",
}),
});

View File

@@ -2,15 +2,16 @@ import PointRedeem from "pointRedeems";
import { playAlert } from "web/alerts/serverFunctions";
export default new PointRedeem({
name: "sfxMrockMadhouse",
title: "Welcome to the Madhouse",
cost: 100,
color: "#A020F0",
prompt: "mrockstar20 saying 'Welcome to the Madhouse'",
sfxredeem: true,
execution: async msg => await playAlert({
name: 'sound',
user: msg.userDisplayName,
sound: 'mrockmadhouse'
})
name: "sfxMrockMadhouse",
title: "Welcome to the Madhouse",
cost: 100,
color: "#A020F0",
prompt: "mrockstar20 saying 'Welcome to the Madhouse'",
sfxredeem: true,
execution: async (msg) =>
await playAlert({
name: "sound",
user: msg.userDisplayName,
sound: "mrockmadhouse",
}),
});

View File

@@ -2,15 +2,16 @@ import PointRedeem from "pointRedeems";
import { playAlert } from "web/alerts/serverFunctions";
export default new PointRedeem({
name: "sfxRipBozo",
title: "RIP BOZO",
cost: 500,
color: "#A020F0",
prompt: "Coffeezilla calls me a conman",
sfxredeem: true,
execution: async msg => await playAlert({
name: 'sound',
user: msg.userDisplayName,
sound: 'ripbozo'
})
name: "sfxRipBozo",
title: "RIP BOZO",
cost: 500,
color: "#A020F0",
prompt: "Coffeezilla calls me a conman",
sfxredeem: true,
execution: async (msg) =>
await playAlert({
name: "sound",
user: msg.userDisplayName,
sound: "ripbozo",
}),
});

View File

@@ -1,9 +1,9 @@
import { redis } from "lib/redis";
import type { HelixUser } from "@twurple/api";
import { api } from "index";
import { HelixUser } from "@twurple/api"
import logger from "lib/logger";
import { redis } from "lib/redis";
const EXPIRETIME = 60 * 60 // 60 minutes
const EXPIRETIME = 60 * 60; // 60 minutes
// The objective of this class is to:
// store displayname, username and id to reduce api calls
@@ -16,108 +16,111 @@ const EXPIRETIME = 60 * 60 // 60 minutes
// vulnchatters only gets set when user chats
export default class User {
public username!: string;
public id!: string;
public displayName!: string;
public username!: string;
public id!: string;
public displayName!: string;
static async initUsername(dirtyUsername: string): Promise<User | null> {
try {
const userObj = new User();
const username = dirtyUsername.replaceAll(/@/gi, '');
userObj.username = username;
const userid = await redis.get(`userlookup:${username}`);
if (!userid) {
const userdata = await api.users.getUserByName(username);
if (!userdata) return null;
userObj._setCache(userdata);
userObj.id = userdata.id;
userObj.displayName = userdata.displayName;
} else {
const displayname = await redis.get(`user:${userid}:displayName`);
userObj._setExpire(userid, username);
userObj.id = userid;
userObj.displayName = displayname!;
};
return userObj;
} catch {
logger.err(`Failed to initialize user with name: ${dirtyUsername}`);
return null;
};
};
static async initUsername(dirtyUsername: string): Promise<User | null> {
try {
const userObj = new User();
const username = dirtyUsername.replaceAll(/@/gi, "");
userObj.username = username;
const userid = await redis.get(`userlookup:${username}`);
if (!userid) {
const userdata = await api.users.getUserByName(username);
if (!userdata) return null;
userObj._setCache(userdata);
userObj.id = userdata.id;
userObj.displayName = userdata.displayName;
} else {
const displayname = await redis.get(`user:${userid}:displayName`);
userObj._setExpire(userid, username);
userObj.id = userid;
userObj.displayName = displayname!;
}
return userObj;
} catch {
logger.err(`Failed to initialize user with name: ${dirtyUsername}`);
return null;
}
}
static async initUserId(userId: string): Promise<User | null> {
try {
const userObj = new User();
userObj.id = userId;
if (!await redis.exists(`user:${userId}:displayName`)) {
const userdata = await api.users.getUserById(userId);
if (!userdata) return null;
userObj._setCache(userdata);
userObj.username = userdata.name;
userObj.displayName = userdata.displayName;
} else {
const [displayName, username] = await Promise.all([
redis.get(`user:${userId}:displayName`),
redis.get(`user:${userId}:username`)
]);
userObj._setExpire(userId, username!);
userObj.username = username!;
userObj.displayName = displayName!;
};
return userObj;
} catch {
logger.err(`Failed to initializer user with id: ${userId}`);
return null;
};
};
static async initUserId(userId: string): Promise<User | null> {
try {
const userObj = new User();
userObj.id = userId;
if (!(await redis.exists(`user:${userId}:displayName`))) {
const userdata = await api.users.getUserById(userId);
if (!userdata) return null;
userObj._setCache(userdata);
userObj.username = userdata.name;
userObj.displayName = userdata.displayName;
} else {
const [displayName, username] = await Promise.all([
redis.get(`user:${userId}:displayName`),
redis.get(`user:${userId}:username`),
]);
userObj._setExpire(userId, username!);
userObj.username = username!;
userObj.displayName = displayName!;
}
return userObj;
} catch {
logger.err(`Failed to initializer user with id: ${userId}`);
return null;
}
}
private async _setCache(userdata: HelixUser) {
await Promise.all([
redis.set(`user:${userdata.id}:displayName`, userdata.displayName),
redis.set(`user:${userdata.id}:username`, userdata.name),
redis.set(`userlookup:${userdata.name}`, userdata.id)
]);
await this._setExpire(userdata.id, userdata.name);
};
private async _setCache(userdata: HelixUser) {
await Promise.all([
redis.set(`user:${userdata.id}:displayName`, userdata.displayName),
redis.set(`user:${userdata.id}:username`, userdata.name),
redis.set(`userlookup:${userdata.name}`, userdata.id),
]);
await this._setExpire(userdata.id, userdata.name);
}
private async _setExpire(userId: string, userName: string) {
await Promise.all([
redis.expire(`user:${userId}:displayName`, EXPIRETIME),
redis.expire(`user:${userId}:username`, EXPIRETIME),
redis.expire(`userlookup:${userName}`, EXPIRETIME)
]);
};
private async _setExpire(userId: string, userName: string) {
await Promise.all([
redis.expire(`user:${userId}:displayName`, EXPIRETIME),
redis.expire(`user:${userId}:username`, EXPIRETIME),
redis.expire(`userlookup:${userName}`, EXPIRETIME),
]);
}
public async itemLock(): Promise<boolean> {
return await redis.exists(`user:${this.id}:itemlock`);
};
public async itemLock(): Promise<boolean> {
return await redis.exists(`user:${this.id}:itemlock`);
}
public async setLock(): Promise<void> {
await redis.set(`user:${this.id}:itemlock`, '1');
};
public async setLock(): Promise<void> {
await redis.set(`user:${this.id}:itemlock`, "1");
}
public async clearLock(): Promise<void> {
await redis.del(`user:${this.id}:itemlock`);
};
public async clearLock(): Promise<void> {
await redis.del(`user:${this.id}:itemlock`);
}
public async setVulnerable(): Promise<void> {
await redis.set(`user:${this.id}:vulnerable`, '1');
await redis.expire(`user:${this.id}:vulnerable`, Math.floor(EXPIRETIME / 2)); // Vulnerable chatter gets removed from the pool after 30 minutes
};
public async setVulnerable(): Promise<void> {
await redis.set(`user:${this.id}:vulnerable`, "1");
await redis.expire(
`user:${this.id}:vulnerable`,
Math.floor(EXPIRETIME / 2),
); // Vulnerable chatter gets removed from the pool after 30 minutes
}
public async clearVulnerable(): Promise<void> {
await redis.del(`user:${this.id}:vulnerable`);
};
public async clearVulnerable(): Promise<void> {
await redis.del(`user:${this.id}:vulnerable`);
}
public async setGreed(): Promise<void> {
await redis.set(`user:${this.id}:greedy`, '1');
};
public async setGreed(): Promise<void> {
await redis.set(`user:${this.id}:greedy`, "1");
}
public async clearGreed(): Promise<void> {
await redis.del(`user:${this.id}:greedy`);
};
public async clearGreed(): Promise<void> {
await redis.del(`user:${this.id}:greedy`);
}
public async greedy(): Promise<boolean> {
return await redis.exists(`user:${this.id}:greedy`);
};
};
public async greedy(): Promise<boolean> {
return await redis.exists(`user:${this.id}:greedy`);
}
}

Some files were not shown because too many files have changed in this diff Show More