Did a number of things.

- Upgraded dependencies
- Added eval command (admin)
- Added bot info command (info)
This commit is contained in:
Keanu Timmermans 2020-10-22 13:41:02 +00:00 committed by GitHub
parent cdf2c47f0c
commit beab6cb1fc
5 changed files with 2262 additions and 2411 deletions

3215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,41 @@
{ {
"name": "d.js-v12-bot", "name": "d.js-v12-bot",
"version": "0.0.1", "version": "0.0.1",
"description": "A Discord bot built on Discord.JS v12", "description": "A Discord bot built on Discord.JS v12",
"main": "dist/index.js", "main": "dist/index.js",
"private": true, "private": true,
"dependencies": { "dependencies": {
"chalk": "^4.1.0", "chalk": "^4.1.0",
"discord.js": "^12.4.0", "discord.js": "^12.4.0",
"inquirer": "^7.3.1", "discord.js-lavalink-lib": "^0.1.7",
"moment": "^2.27.0" "inquirer": "^7.3.3",
}, "moment": "^2.29.1",
"devDependencies": { "ms": "^2.1.2",
"@types/inquirer": "^6.5.0", "os": "^0.1.1"
"@types/mocha": "^8.0.3", },
"@types/node": "^14.0.22", "devDependencies": {
"@types/ws": "^7.2.6", "@types/inquirer": "^6.5.0",
"mocha": "^8.1.2", "@types/mocha": "^8.0.3",
"prettier": "2.1.2", "@types/ms": "^0.7.31",
"ts-node": "^9.0.0", "@types/node": "^14.14.2",
"tsc-watch": "^4.2.9", "@types/ws": "^7.2.7",
"typescript": "^3.9.6" "mocha": "^8.2.0",
}, "prettier": "2.1.2",
"scripts": { "ts-node": "^9.0.0",
"build": "tsc && npm prune --production", "tsc-watch": "^4.2.9",
"start": "node dist/index.js", "typescript": "^3.9.7"
"once": "tsc && npm start", },
"dev": "tsc-watch --onSuccess \"node dist/index.js dev\"", "scripts": {
"test": "mocha --require ts-node/register --extension ts --recursive" "build": "tsc && npm prune --production",
}, "start": "node dist/index.js",
"keywords": [ "once": "tsc && npm start",
"discord.js", "dev": "tsc-watch --onSuccess \"node dist/index.js dev\"",
"bot" "test": "mocha --require ts-node/register --extension ts --recursive"
], },
"author": "Keanu Timmermans", "keywords": [
"license": "MIT" "discord.js",
} "bot"
],
"author": "Keanu Timmermans",
"license": "MIT"
}

View File

@ -1,192 +1,211 @@
import Command from '../core/command'; import Command from '../core/command';
import { CommonLibrary, logs, botHasPermission } from '../core/lib'; import { CommonLibrary, logs, botHasPermission, clean } from '../core/lib';
import { Config, Storage } from '../core/structures'; import { Config, Storage } from '../core/structures';
import { PermissionNames, getPermissionLevel } from '../core/permissions'; import { PermissionNames, getPermissionLevel } from '../core/permissions';
import { Permissions } from 'discord.js'; import { Permissions } from 'discord.js';
import * as discord from 'discord.js'; import * as discord from 'discord.js';
function getLogBuffer(type: string) { function getLogBuffer(type: string) {
return { return {
files: [ files: [
{ {
attachment: Buffer.alloc(logs[type].length, logs[type]), attachment: Buffer.alloc(logs[type].length, logs[type]),
name: `${Date.now()}.${type}.log`, name: `${Date.now()}.${type}.log`,
}, },
], ],
}; };
} }
const activities = ['playing', 'listening', 'streaming', 'watching']; const activities = ['playing', 'listening', 'streaming', 'watching'];
const statuses = ['online', 'idle', 'dnd', 'invisible'];
export default new Command({
description: export default new Command({
"An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.", description:
async run($: CommonLibrary): Promise<any> { "An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.",
if (!$.member) async run($: CommonLibrary): Promise<any> {
return $.channel.send( if (!$.member)
"Couldn't find a member object for you! Did you make sure you used this in a server?", return $.channel.send(
); "Couldn't find a member object for you! Did you make sure you used this in a server?",
const permLevel = getPermissionLevel($.member); );
$.channel.send( const permLevel = getPermissionLevel($.member);
`${$.author.toString()}, your permission level is \`${ $.channel.send(
PermissionNames[permLevel] `${$.author.toString()}, your permission level is \`${
}\` (${permLevel}).`, PermissionNames[permLevel]
); }\` (${permLevel}).`,
}, );
subcommands: { },
set: new Command({ subcommands: {
description: 'Set different per-guild settings for the bot.', set: new Command({
run: 'You have to specify the option you want to set.', description: 'Set different per-guild settings for the bot.',
permission: Command.PERMISSIONS.ADMIN, run: 'You have to specify the option you want to set.',
subcommands: { permission: Command.PERMISSIONS.ADMIN,
prefix: new Command({ subcommands: {
description: prefix: new Command({
'Set a custom prefix for your guild. Removes your custom prefix if none is provided.', description:
usage: '(<prefix>)', 'Set a custom prefix for your guild. Removes your custom prefix if none is provided.',
async run($: CommonLibrary): Promise<any> { usage: '(<prefix>)',
Storage.getGuild($.guild?.id || 'N/A').prefix = null; async run($: CommonLibrary): Promise<any> {
Storage.save(); Storage.getGuild($.guild?.id || 'N/A').prefix = null;
$.channel.send( Storage.save();
`The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`, $.channel.send(
); `The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`,
}, );
any: new Command({ },
async run($: CommonLibrary): Promise<any> { any: new Command({
Storage.getGuild($.guild?.id || 'N/A').prefix = $.args[0]; async run($: CommonLibrary): Promise<any> {
Storage.save(); Storage.getGuild($.guild?.id || 'N/A').prefix = $.args[0];
$.channel.send( Storage.save();
`The custom prefix for this guild is now \`${$.args[0]}\`.`, $.channel.send(
); `The custom prefix for this guild is now \`${$.args[0]}\`.`,
}, );
}), },
}), }),
}, }),
}), },
diag: new Command({ }),
description: 'Requests a debug log with the "info" verbosity level.', diag: new Command({
permission: Command.PERMISSIONS.BOT_SUPPORT, description: 'Requests a debug log with the "info" verbosity level.',
async run($: CommonLibrary): Promise<any> { permission: Command.PERMISSIONS.BOT_SUPPORT,
$.channel.send(getLogBuffer('info')); async run($: CommonLibrary): Promise<any> {
}, $.channel.send(getLogBuffer('info'));
any: new Command({ },
description: `Select a verbosity to listen to. Available levels: \`[${Object.keys( any: new Command({
logs, description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(
).join(', ')}]\``, logs,
async run($: CommonLibrary): Promise<any> { ).join(', ')}]\``,
const type = $.args[0]; async run($: CommonLibrary): Promise<any> {
const type = $.args[0];
if (type in logs) $.channel.send(getLogBuffer(type));
else if (type in logs) $.channel.send(getLogBuffer(type));
$.channel.send( else
`Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys( $.channel.send(
logs, `Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys(
).join(', ')}]\`.`, logs,
); ).join(', ')}]\`.`,
}, );
}), },
}), }),
status: new Command({ }),
description: "Changes the bot's status.", status: new Command({
permission: Command.PERMISSIONS.BOT_SUPPORT, description: "Changes the bot's status.",
async run($: CommonLibrary): Promise<any> { permission: Command.PERMISSIONS.BOT_SUPPORT,
$.channel.send('Setting status to `online`...'); async run($: CommonLibrary): Promise<any> {
}, $.channel.send('Setting status to `online`...');
any: new Command({ },
description: `Select a status to set to. Available statuses: \`online\`, \`idle\`, \`dnd\`, \`invisible\``, any: new Command({
async run($: CommonLibrary): Promise<any> { description: `Select a status to set to. Available statuses: \`[${statuses.join(
let statuses = ['online', 'idle', 'dnd', 'invisible']; ', ',
if (!statuses.includes($.args[0])) )}]\`.`,
return $.channel.send("That status doesn't exist!"); async run($: CommonLibrary): Promise<any> {
else { if (!statuses.includes($.args[0]))
$.client.user?.setStatus($.args[0]); return $.channel.send("That status doesn't exist!");
$.channel.send(`Setting status to \`${$.args[0]}\`...`); else {
} $.client.user?.setStatus($.args[0]);
}, $.channel.send(`Setting status to \`${$.args[0]}\`...`);
}), }
}), },
purge: new Command({ }),
description: 'Purges bot messages.', }),
permission: Command.PERMISSIONS.BOT_SUPPORT, purge: new Command({
async run($: CommonLibrary): Promise<any> { description: 'Purges bot messages.',
if ($.message.channel instanceof discord.DMChannel) { permission: Command.PERMISSIONS.BOT_SUPPORT,
return; async run($: CommonLibrary): Promise<any> {
} if ($.message.channel instanceof discord.DMChannel) {
$.message.delete(); return;
const msgs = await $.channel.messages.fetch({ }
limit: 100, $.message.delete();
}); const msgs = await $.channel.messages.fetch({
const travMessages = msgs.filter( limit: 100,
(m) => m.author.id === $.client.user?.id, });
); const travMessages = msgs.filter(
(m) => m.author.id === $.client.user?.id,
await $.message.channel );
.send(`Found ${travMessages.size} messages to delete.`)
.then((m) => await $.message.channel
m.delete({ .send(`Found ${travMessages.size} messages to delete.`)
timeout: 5000, .then((m) =>
}), m.delete({
); timeout: 5000,
await $.message.channel.bulkDelete(travMessages); }),
}, );
}), await $.message.channel.bulkDelete(travMessages);
nick: new Command({ },
description: "Change the bot's nickname.", }),
permission: Command.PERMISSIONS.BOT_SUPPORT, eval: new Command({
async run($: CommonLibrary): Promise<any> { description: 'Evaluate code.',
const nickName = $.args.join(' '); usage: '<code>',
const trav = $.guild?.members.cache.find( permission: Command.PERMISSIONS.BOT_OWNER,
(member) => member.id === $.client.user?.id, async run($: CommonLibrary): Promise<any> {
); try {
await trav?.setNickname(nickName); const code = $.args.join(' ');
if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES)) let evaled = eval(code);
$.message.delete({ timeout: 5000 }).catch($.handler.bind($));
$.channel if (typeof evaled !== 'string')
.send(`Nickname set to \`${nickName}\``) evaled = require('util').inspect(evaled);
.then((m) => m.delete({ timeout: 5000 })); $.channel.send(clean(evaled), { code: 'x1' });
}, } catch (err) {
}), $.channel.send(`\`ERROR\` \`\`\`x1\n${clean(err)}\n\`\`\``);
guilds: new Command({ }
description: 'Shows a list of all guilds the bot is a member of.', },
permission: Command.PERMISSIONS.BOT_SUPPORT, }),
async run($: CommonLibrary): Promise<any> { nick: new Command({
const guildList = $.client.guilds.cache.array().map((e) => e.name); description: "Change the bot's nickname.",
$.channel.send(guildList); permission: Command.PERMISSIONS.BOT_SUPPORT,
}, async run($: CommonLibrary): Promise<any> {
}), const nickName = $.args.join(' ');
activity: new Command({ const trav = $.guild?.members.cache.find(
description: 'Set the activity of the bot.', (member) => member.id === $.client.user?.id,
permission: Command.PERMISSIONS.BOT_SUPPORT, );
usage: '<type> <string>', await trav?.setNickname(nickName);
async run($: CommonLibrary): Promise<any> { if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES))
$.client.user?.setActivity('.help', { $.message.delete({ timeout: 5000 }).catch($.handler.bind($));
type: 'LISTENING', $.channel
}); .send(`Nickname set to \`${nickName}\``)
$.channel.send('Activity set to default.'); .then((m) => m.delete({ timeout: 5000 }));
}, },
any: new Command({ }),
description: `Select an activity type to set. Available levels: \`[${activities.join( guilds: new Command({
', ', description: 'Shows a list of all guilds the bot is a member of.',
)}]\``, permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {
const type = $.args[0]; const guildList = $.client.guilds.cache.array().map((e) => e.name);
$.channel.send(guildList);
if (activities.includes(type)) { },
$.client.user?.setActivity($.args.slice(1).join(' '), { }),
type: $.args[0].toUpperCase(), activity: new Command({
}); description: 'Set the activity of the bot.',
$.channel.send( permission: Command.PERMISSIONS.BOT_SUPPORT,
`Set activity to \`${$.args[0].toUpperCase()}\` \`${$.args usage: '<type> <string>',
.slice(1) async run($: CommonLibrary): Promise<any> {
.join(' ')}\`.`, $.client.user?.setActivity('.help', {
); type: 'LISTENING',
} else });
$.channel.send( $.channel.send('Activity set to default.');
`Couldn't find an activity type named \`${type}\`! The available types are \`[${activities.join( },
', ', any: new Command({
)}]\`.`, description: `Select an activity type to set. Available levels: \`[${activities.join(
); ', ',
}, )}]\``,
}), async run($: CommonLibrary): Promise<any> {
}), const type = $.args[0];
},
}); if (activities.includes(type)) {
$.client.user?.setActivity($.args.slice(1).join(' '), {
type: $.args[0].toUpperCase(),
});
$.channel.send(
`Set activity to \`${$.args[0].toUpperCase()}\` \`${$.args
.slice(1)
.join(' ')}\`.`,
);
} else
$.channel.send(
`Couldn't find an activity type named \`${type}\`! The available types are \`[${activities.join(
', ',
)}]\`.`,
);
},
}),
}),
},
});

View File

@ -1,17 +1,13 @@
import { Guild, MessageEmbed } from 'discord.js'; import { MessageEmbed, version as djsversion } from 'discord.js';
import moment from 'moment'; /// @ts-ignore
import { version } from '../../package.json';
import ms from 'ms';
import os from 'os';
import Command from '../core/command'; import Command from '../core/command';
import { CommonLibrary } from '../core/lib'; import { CommonLibrary, formatBytes, trimArray } from '../core/lib';
import { verificationLevels, filterLevels, regions, flags } from '../defs/info'; import { verificationLevels, filterLevels, regions, flags } from '../defs/info';
import moment from 'moment';
function trimArray(arr: any, maxLen = 10) { import utc from 'moment';
if (arr.length > maxLen) {
const len = arr.length - maxLen;
arr = arr.slice(0, maxLen);
arr.push(`${len} more...`);
}
return arr;
}
export default new Command({ export default new Command({
description: description:
@ -36,6 +32,47 @@ export default new Command({
}), }),
}), }),
bot: new Command({
description: 'Displays info about the bot.',
async run($: CommonLibrary): Promise<any> {
const core = os.cpus()[0];
const embed = new MessageEmbed()
.setThumbnail(
/// @ts-ignore
$.client.user?.displayAvatarURL({ dynamic: true, size: 2048 }),
)
.setColor($.guild?.me?.displayHexColor || 'BLUE')
.addField('General', [
`** Client:** ${$.client.user?.tag} (${$.client.user?.id})`,
`** Servers:** ${$.client.guilds.cache.size.toLocaleString()}`,
`** Users:** ${$.client.guilds.cache
.reduce((a: any, b: { memberCount: any }) => a + b.memberCount, 0)
.toLocaleString()}`,
`** Channels:** ${$.client.channels.cache.size.toLocaleString()}`,
`** Creation Date:** ${utc($.client.user?.createdTimestamp).format(
'Do MMMM YYYY HH:mm:ss',
)}`,
`** Node.JS:** ${process.version}`,
`** Version:** v${version}`,
`** Discord.JS:** ${djsversion}`,
'\u200b',
])
.addField('System', [
`** Platform:** ${process.platform}`,
`** Uptime:** ${ms(os.uptime() * 1000, { long: true })}`,
`** CPU:**`,
`\u3000 • Cores: ${os.cpus().length}`,
`\u3000 • Model: ${core.model}`,
`\u3000 • Speed: ${core.speed}MHz`,
`** Memory:**`,
`\u3000 • Total: ${formatBytes(process.memoryUsage().heapTotal)}`,
`\u3000 • Used: ${formatBytes(process.memoryUsage().heapTotal)}`,
])
.setTimestamp();
$.channel.send(embed);
},
}),
guild: new Command({ guild: new Command({
description: 'Displays info about the current guild.', description: 'Displays info about the current guild.',
async run($: CommonLibrary): Promise<any> { async run($: CommonLibrary): Promise<any> {

View File

@ -1,446 +1,470 @@
import { import {
GenericWrapper, GenericWrapper,
NumberWrapper, NumberWrapper,
StringWrapper, StringWrapper,
ArrayWrapper, ArrayWrapper,
} from './wrappers'; } from './wrappers';
import { import {
Client, Client,
Message, Message,
TextChannel, TextChannel,
DMChannel, DMChannel,
NewsChannel, NewsChannel,
Guild, Guild,
User, User,
GuildMember, GuildMember,
Permissions, Permissions,
} from 'discord.js'; } from 'discord.js';
import chalk from 'chalk'; import chalk from 'chalk';
import FileManager from './storage'; import FileManager from './storage';
import { eventListeners } from '../events/messageReactionRemove'; import { eventListeners } from '../events/messageReactionRemove';
import { client } from '../index'; import { client } from '../index';
/** A type that describes what the library module does. */ /** A type that describes what the library module does. */
export interface CommonLibrary { export interface CommonLibrary {
// Wrapper Object // // Wrapper Object //
/** Wraps the value you enter with an object that provides extra functionality and provides common utility functions. */ /** Wraps the value you enter with an object that provides extra functionality and provides common utility functions. */
(value: number): NumberWrapper; (value: number): NumberWrapper;
(value: string): StringWrapper; (value: string): StringWrapper;
<T>(value: T[]): ArrayWrapper<T>; <T>(value: T[]): ArrayWrapper<T>;
<T>(value: T): GenericWrapper<T>; <T>(value: T): GenericWrapper<T>;
// Common Library Functions // // Common Library Functions //
/** <Promise>.catch($.handler.bind($)) or <Promise>.catch(error => $.handler(error)) */ /** <Promise>.catch($.handler.bind($)) or <Promise>.catch(error => $.handler(error)) */
handler: (error: Error) => void; handler: (error: Error) => void;
log: (...args: any[]) => void; log: (...args: any[]) => void;
warn: (...args: any[]) => void; warn: (...args: any[]) => void;
error: (...args: any[]) => void; error: (...args: any[]) => void;
debug: (...args: any[]) => void; debug: (...args: any[]) => void;
ready: (...args: any[]) => void; ready: (...args: any[]) => void;
paginate: ( paginate: (
message: Message, message: Message,
senderID: string, senderID: string,
total: number, total: number,
callback: (page: number) => void, callback: (page: number) => void,
duration?: number, duration?: number,
) => void; ) => void;
prompt: ( prompt: (
message: Message, message: Message,
senderID: string, senderID: string,
onConfirm: () => void, onConfirm: () => void,
duration?: number, duration?: number,
) => void; ) => void;
getMemberByUsername: ( getMemberByUsername: (
guild: Guild, guild: Guild,
username: string, username: string,
) => Promise<GuildMember | undefined>; ) => Promise<GuildMember | undefined>;
callMemberByUsername: ( callMemberByUsername: (
message: Message, message: Message,
username: string, username: string,
onSuccess: (member: GuildMember) => void, onSuccess: (member: GuildMember) => void,
) => Promise<void>; ) => Promise<void>;
// Dynamic Properties // // Dynamic Properties //
args: any[]; args: any[];
client: Client; client: Client;
message: Message; message: Message;
channel: TextChannel | DMChannel | NewsChannel; channel: TextChannel | DMChannel | NewsChannel;
guild: Guild | null; guild: Guild | null;
author: User; author: User;
member: GuildMember | null; member: GuildMember | null;
} }
export default function $(value: number): NumberWrapper; export default function $(value: number): NumberWrapper;
export default function $(value: string): StringWrapper; export default function $(value: string): StringWrapper;
export default function $<T>(value: T[]): ArrayWrapper<T>; export default function $<T>(value: T[]): ArrayWrapper<T>;
export default function $<T>(value: T): GenericWrapper<T>; export default function $<T>(value: T): GenericWrapper<T>;
export default function $(value: any) { export default function $(value: any) {
if (isType(value, Number)) return new NumberWrapper(value); if (isType(value, Number)) return new NumberWrapper(value);
else if (isType(value, String)) return new StringWrapper(value); else if (isType(value, String)) return new StringWrapper(value);
else if (isType(value, Array)) return new ArrayWrapper(value); else if (isType(value, Array)) return new ArrayWrapper(value);
else return new GenericWrapper(value); else return new GenericWrapper(value);
} }
// If you use promises, use this function to display the error in chat. // If you use promises, use this function to display the error in chat.
// Case #1: await $.channel.send(""); --> Automatically caught by Command.execute(). // Case #1: await $.channel.send(""); --> Automatically caught by Command.execute().
// Case #2: $.channel.send("").catch($.handler.bind($)); --> Manually caught by the user. // Case #2: $.channel.send("").catch($.handler.bind($)); --> Manually caught by the user.
$.handler = function (this: CommonLibrary, error: Error) { $.handler = function (this: CommonLibrary, error: Error) {
if (this) if (this)
this.channel.send( this.channel.send(
`There was an error while trying to execute that command!\`\`\`${ `There was an error while trying to execute that command!\`\`\`${
error.stack ?? error error.stack ?? error
}\`\`\``, }\`\`\``,
); );
else else
$.warn( $.warn(
'No context was attached to $.handler! Make sure to use .catch($.handler.bind($)) or .catch(error => $.handler(error)) instead!', 'No context was attached to $.handler! Make sure to use .catch($.handler.bind($)) or .catch(error => $.handler(error)) instead!',
); );
$.error(error); $.error(error);
}; };
// Logs with different levels of verbosity. // Logs with different levels of verbosity.
export const logs: { [type: string]: string } = { export const logs: { [type: string]: string } = {
error: '', error: '',
warn: '', warn: '',
info: '', info: '',
verbose: '', verbose: '',
}; };
let enabled = true; let enabled = true;
export function setConsoleActivated(activated: boolean) { export function setConsoleActivated(activated: boolean) {
enabled = activated; enabled = activated;
} }
// The custom console. In order of verbosity, error, warn, log, and debug. Ready is a variation of log. // The custom console. In order of verbosity, error, warn, log, and debug. Ready is a variation of log.
// General Purpose Logger // General Purpose Logger
$.log = (...args: any[]) => { $.log = (...args: any[]) => {
if (enabled) if (enabled)
console.log( console.log(
chalk.white.bgGray(formatTimestamp()), chalk.white.bgGray(formatTimestamp()),
chalk.black.bgWhite('INFO'), chalk.black.bgWhite('INFO'),
...args, ...args,
); );
const text = `[${formatUTCTimestamp()}] [INFO] ${args.join(' ')}\n`; const text = `[${formatUTCTimestamp()}] [INFO] ${args.join(' ')}\n`;
logs.info += text; logs.info += text;
logs.verbose += text; logs.verbose += text;
}; };
// "It'll still work, but you should really check up on this." // "It'll still work, but you should really check up on this."
$.warn = (...args: any[]) => { $.warn = (...args: any[]) => {
if (enabled) if (enabled)
console.warn( console.warn(
chalk.white.bgGray(formatTimestamp()), chalk.white.bgGray(formatTimestamp()),
chalk.black.bgYellow('WARN'), chalk.black.bgYellow('WARN'),
...args, ...args,
); );
const text = `[${formatUTCTimestamp()}] [WARN] ${args.join(' ')}\n`; const text = `[${formatUTCTimestamp()}] [WARN] ${args.join(' ')}\n`;
logs.warn += text; logs.warn += text;
logs.info += text; logs.info += text;
logs.verbose += text; logs.verbose += text;
}; };
// Used for anything which prevents the program from actually running. // Used for anything which prevents the program from actually running.
$.error = (...args: any[]) => { $.error = (...args: any[]) => {
if (enabled) if (enabled)
console.error( console.error(
chalk.white.bgGray(formatTimestamp()), chalk.white.bgGray(formatTimestamp()),
chalk.white.bgRed('ERROR'), chalk.white.bgRed('ERROR'),
...args, ...args,
); );
const text = `[${formatUTCTimestamp()}] [ERROR] ${args.join(' ')}\n`; const text = `[${formatUTCTimestamp()}] [ERROR] ${args.join(' ')}\n`;
logs.error += text; logs.error += text;
logs.warn += text; logs.warn += text;
logs.info += text; logs.info += text;
logs.verbose += text; logs.verbose += text;
}; };
// Be as verbose as possible. If anything might help when debugging an error, then include it. This only shows in your console if you run this with "dev", but you can still get it from "logs.verbose". // Be as verbose as possible. If anything might help when debugging an error, then include it. This only shows in your console if you run this with "dev", but you can still get it from "logs.verbose".
// $.debug(`core/lib::parseArgs("testing \"in progress\"") = ["testing", "in progress"]`) --> <path>/::(<object>.)<function>(<args>) = <value> // $.debug(`core/lib::parseArgs("testing \"in progress\"") = ["testing", "in progress"]`) --> <path>/::(<object>.)<function>(<args>) = <value>
// Would probably be more suited for debugging program logic rather than function logic, which can be checked using unit tests. // Would probably be more suited for debugging program logic rather than function logic, which can be checked using unit tests.
$.debug = (...args: any[]) => { $.debug = (...args: any[]) => {
if (process.argv[2] === 'dev' && enabled) if (process.argv[2] === 'dev' && enabled)
console.debug( console.debug(
chalk.white.bgGray(formatTimestamp()), chalk.white.bgGray(formatTimestamp()),
chalk.white.bgBlue('DEBUG'), chalk.white.bgBlue('DEBUG'),
...args, ...args,
); );
const text = `[${formatUTCTimestamp()}] [DEBUG] ${args.join(' ')}\n`; const text = `[${formatUTCTimestamp()}] [DEBUG] ${args.join(' ')}\n`;
logs.verbose += text; logs.verbose += text;
}; };
// Used once at the start of the program when the bot loads. // Used once at the start of the program when the bot loads.
$.ready = (...args: any[]) => { $.ready = (...args: any[]) => {
if (enabled) if (enabled)
console.log( console.log(
chalk.white.bgGray(formatTimestamp()), chalk.white.bgGray(formatTimestamp()),
chalk.black.bgGreen('READY'), chalk.black.bgGreen('READY'),
...args, ...args,
); );
const text = `[${formatUTCTimestamp()}] [READY] ${args.join(' ')}\n`; const text = `[${formatUTCTimestamp()}] [READY] ${args.join(' ')}\n`;
logs.info += text; logs.info += text;
logs.verbose += text; logs.verbose += text;
}; };
export function formatTimestamp(now = new Date()) { export function formatTimestamp(now = new Date()) {
const year = now.getFullYear(); const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0'); const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0'); const day = now.getDate().toString().padStart(2, '0');
const hour = now.getHours().toString().padStart(2, '0'); const hour = now.getHours().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0'); const minute = now.getMinutes().toString().padStart(2, '0');
const second = now.getSeconds().toString().padStart(2, '0'); const second = now.getSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`; return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
} }
export function formatUTCTimestamp(now = new Date()) { export function formatUTCTimestamp(now = new Date()) {
const year = now.getUTCFullYear(); const year = now.getUTCFullYear();
const month = (now.getUTCMonth() + 1).toString().padStart(2, '0'); const month = (now.getUTCMonth() + 1).toString().padStart(2, '0');
const day = now.getUTCDate().toString().padStart(2, '0'); const day = now.getUTCDate().toString().padStart(2, '0');
const hour = now.getUTCHours().toString().padStart(2, '0'); const hour = now.getUTCHours().toString().padStart(2, '0');
const minute = now.getUTCMinutes().toString().padStart(2, '0'); const minute = now.getUTCMinutes().toString().padStart(2, '0');
const second = now.getUTCSeconds().toString().padStart(2, '0'); const second = now.getUTCSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`; return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
} }
export function botHasPermission( export function botHasPermission(
guild: Guild | null, guild: Guild | null,
permission: number, permission: number,
): boolean { ): boolean {
return !!( return !!(
client.user && client.user &&
guild?.members.resolve(client.user)?.hasPermission(permission) guild?.members.resolve(client.user)?.hasPermission(permission)
); );
} }
// Pagination function that allows for customization via a callback. // Pagination function that allows for customization via a callback.
// Define your own pages outside the function because this only manages the actual turning of pages. // Define your own pages outside the function because this only manages the actual turning of pages.
$.paginate = async ( $.paginate = async (
message: Message, message: Message,
senderID: string, senderID: string,
total: number, total: number,
callback: (page: number) => void, callback: (page: number) => void,
duration = 60000, duration = 60000,
) => { ) => {
let page = 0; let page = 0;
const turn = (amount: number) => { const turn = (amount: number) => {
page += amount; page += amount;
if (page < 0) page += total; if (page < 0) page += total;
else if (page >= total) page -= total; else if (page >= total) page -= total;
callback(page); callback(page);
}; };
const handle = (emote: string, reacterID: string) => { const handle = (emote: string, reacterID: string) => {
switch (emote) { switch (emote) {
case '⬅️': case '⬅️':
turn(-1); turn(-1);
break; break;
case '➡️': case '➡️':
turn(1); turn(1);
break; break;
} }
}; };
// Listen for reactions and call the handler. // Listen for reactions and call the handler.
await message.react('⬅️'); await message.react('⬅️');
await message.react('➡️'); await message.react('➡️');
eventListeners.set(message.id, handle); eventListeners.set(message.id, handle);
await message.awaitReactions( await message.awaitReactions(
(reaction, user) => { (reaction, user) => {
if (user.id === senderID) { if (user.id === senderID) {
// The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error. // The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error.
// This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission. // This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission.
const canDeleteEmotes = botHasPermission( const canDeleteEmotes = botHasPermission(
message.guild, message.guild,
Permissions.FLAGS.MANAGE_MESSAGES, Permissions.FLAGS.MANAGE_MESSAGES,
); );
handle(reaction.emoji.name, user.id); handle(reaction.emoji.name, user.id);
if (canDeleteEmotes) reaction.users.remove(user); if (canDeleteEmotes) reaction.users.remove(user);
} }
return false; return false;
}, },
{ time: duration }, { time: duration },
); );
// When time's up, remove the bot's own reactions. // When time's up, remove the bot's own reactions.
eventListeners.delete(message.id); eventListeners.delete(message.id);
message.reactions.cache.get('⬅️')?.users.remove(message.author); message.reactions.cache.get('⬅️')?.users.remove(message.author);
message.reactions.cache.get('➡️')?.users.remove(message.author); message.reactions.cache.get('➡️')?.users.remove(message.author);
}; };
// Waits for the sender to either confirm an action or let it pass (and delete the message). // Waits for the sender to either confirm an action or let it pass (and delete the message).
$.prompt = async ( $.prompt = async (
message: Message, message: Message,
senderID: string, senderID: string,
onConfirm: () => void, onConfirm: () => void,
duration = 10000, duration = 10000,
) => { ) => {
let isDeleted = false; let isDeleted = false;
message.react('✅'); message.react('✅');
await message.awaitReactions( await message.awaitReactions(
(reaction, user) => { (reaction, user) => {
if (user.id === senderID) { if (user.id === senderID) {
if (reaction.emoji.name === '✅') onConfirm(); if (reaction.emoji.name === '✅') onConfirm();
isDeleted = true; isDeleted = true;
message.delete(); message.delete();
} }
// CollectorFilter requires a boolean to be returned. // CollectorFilter requires a boolean to be returned.
// My guess is that the return value of awaitReactions can be altered by making a boolean filter. // My guess is that the return value of awaitReactions can be altered by making a boolean filter.
// However, because that's not my concern with this command, I don't have to worry about it. // However, because that's not my concern with this command, I don't have to worry about it.
// May as well just set it to false because I'm not concerned with collecting any reactions. // May as well just set it to false because I'm not concerned with collecting any reactions.
return false; return false;
}, },
{ time: duration }, { time: duration },
); );
if (!isDeleted) message.delete(); if (!isDeleted) message.delete();
}; };
$.getMemberByUsername = async (guild: Guild, username: string) => { $.getMemberByUsername = async (guild: Guild, username: string) => {
return ( return (
await guild.members.fetch({ await guild.members.fetch({
query: username, query: username,
limit: 1, limit: 1,
}) })
).first(); ).first();
}; };
/** Convenience function to handle false cases automatically. */ /** Convenience function to handle false cases automatically. */
$.callMemberByUsername = async ( $.callMemberByUsername = async (
message: Message, message: Message,
username: string, username: string,
onSuccess: (member: GuildMember) => void, onSuccess: (member: GuildMember) => void,
) => { ) => {
const guild = message.guild; const guild = message.guild;
const send = message.channel.send; const send = message.channel.send;
if (guild) { if (guild) {
const member = await $.getMemberByUsername(guild, username); const member = await $.getMemberByUsername(guild, username);
if (member) onSuccess(member); if (member) onSuccess(member);
else send(`Couldn't find a user by the name of \`${username}\`!`); else send(`Couldn't find a user by the name of \`${username}\`!`);
} else send('You must execute this command in a server!'); } else send('You must execute this command in a server!');
}; };
/** /**
* Splits a command by spaces while accounting for quotes which capture string arguments. * Splits a command by spaces while accounting for quotes which capture string arguments.
* - `\"` = `"` * - `\"` = `"`
* - `\\` = `\` * - `\\` = `\`
*/ */
export function parseArgs(line: string): string[] { export function parseArgs(line: string): string[] {
let result = []; let result = [];
let selection = ''; let selection = '';
let inString = false; let inString = false;
let isEscaped = false; let isEscaped = false;
for (let c of line) { for (let c of line) {
if (isEscaped) { if (isEscaped) {
if (['"', '\\'].includes(c)) selection += c; if (['"', '\\'].includes(c)) selection += c;
else selection += '\\' + c; else selection += '\\' + c;
isEscaped = false; isEscaped = false;
} else if (c === '\\') isEscaped = true; } else if (c === '\\') isEscaped = true;
else if (c === '"') inString = !inString; else if (c === '"') inString = !inString;
else if (c === ' ' && !inString) { else if (c === ' ' && !inString) {
result.push(selection); result.push(selection);
selection = ''; selection = '';
} else selection += c; } else selection += c;
} }
if (selection.length > 0) result.push(selection); if (selection.length > 0) result.push(selection);
return result; return result;
} }
/** /**
* Allows you to store a template string with variable markers and parse it later. * Allows you to store a template string with variable markers and parse it later.
* - Use `%name%` for variables * - Use `%name%` for variables
* - `%%` = `%` * - `%%` = `%`
* - If the invalid token is null/undefined, nothing is changed. * - If the invalid token is null/undefined, nothing is changed.
*/ */
export function parseVars( export function parseVars(
line: string, line: string,
definitions: { [key: string]: string }, definitions: { [key: string]: string },
invalid: string | null = '', invalid: string | null = '',
): string { ): string {
let result = ''; let result = '';
let inVariable = false; let inVariable = false;
let token = ''; let token = '';
for (const c of line) { for (const c of line) {
if (c === '%') { if (c === '%') {
if (inVariable) { if (inVariable) {
if (token === '') result += '%'; if (token === '') result += '%';
else { else {
if (token in definitions) result += definitions[token]; if (token in definitions) result += definitions[token];
else if (invalid === null) result += `%${token}%`; else if (invalid === null) result += `%${token}%`;
else result += invalid; else result += invalid;
token = ''; token = '';
} }
} }
inVariable = !inVariable; inVariable = !inVariable;
} else if (inVariable) token += c; } else if (inVariable) token += c;
else result += c; else result += c;
} }
return result; return result;
} }
export function isType(value: any, type: any): boolean { export function isType(value: any, type: any): boolean {
if (value === undefined && type === undefined) return true; if (value === undefined && type === undefined) return true;
else if (value === null && type === null) return true; else if (value === null && type === null) return true;
else else
return value !== undefined && value !== null && value.constructor === type; return value !== undefined && value !== null && value.constructor === type;
} }
/** /**
* Checks a value to see if it matches the fallback's type, otherwise returns the fallback. * Checks a value to see if it matches the fallback's type, otherwise returns the fallback.
* For the purposes of the templates system, this function will only check array types, objects should be checked under their own type (as you'd do anyway with something like a User object). * For the purposes of the templates system, this function will only check array types, objects should be checked under their own type (as you'd do anyway with something like a User object).
* If at any point the value doesn't match the data structure provided, the fallback is returned. * If at any point the value doesn't match the data structure provided, the fallback is returned.
* Warning: Type checking is based on the fallback's type. Be sure that the "type" parameter is accurate to this! * Warning: Type checking is based on the fallback's type. Be sure that the "type" parameter is accurate to this!
*/ */
export function select<T>( export function select<T>(
value: any, value: any,
fallback: T, fallback: T,
type: Function, type: Function,
isArray = false, isArray = false,
): T { ): T {
if (isArray && isType(value, Array)) { if (isArray && isType(value, Array)) {
for (let item of value) if (!isType(item, type)) return fallback; for (let item of value) if (!isType(item, type)) return fallback;
return value; return value;
} else { } else {
if (isType(value, type)) return value; if (isType(value, type)) return value;
else return fallback; else return fallback;
} }
} }
export interface GenericJSON { export function clean(text: any) {
[key: string]: any; if (typeof text === 'string')
} return text
.replace(/`/g, '`' + String.fromCharCode(8203))
export abstract class GenericStructure { .replace(/@/g, '@' + String.fromCharCode(8203));
private __meta__ = 'generic'; else return text;
}
constructor(tag?: string) {
this.__meta__ = tag || this.__meta__; export function trimArray(arr: any, maxLen = 10) {
} if (arr.length > maxLen) {
const len = arr.length - maxLen;
public save(asynchronous = true) { arr = arr.slice(0, maxLen);
const tag = this.__meta__; arr.push(`${len} more...`);
/// @ts-ignore }
delete this.__meta__; return arr;
FileManager.write(tag, this, asynchronous); }
this.__meta__ = tag;
} export function formatBytes(bytes: any) {
} if (bytes === 0) return '0 Bytes';
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
// A 50% chance would be "Math.random() < 0.5" because Math.random() can be [0, 1), so to make two equal ranges, you'd need [0, 0.5)U[0.5, 1). const i = Math.floor(Math.log(bytes) / Math.log(1024));
// Similar logic would follow for any other percentage. Math.random() < 1 is always true (100% chance) and Math.random() < 0 is always false (0% chance). return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`;
export const Random = { }
num: (min: number, max: number) => Math.random() * (max - min) + min,
int: (min: number, max: number) => Math.floor(Random.num(min, max)), export interface GenericJSON {
chance: (decimal: number) => Math.random() < decimal, [key: string]: any;
sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1), }
deviation: (base: number, deviation: number) =>
Random.num(base - deviation, base + deviation), export abstract class GenericStructure {
}; private __meta__ = 'generic';
constructor(tag?: string) {
this.__meta__ = tag || this.__meta__;
}
public save(asynchronous = true) {
const tag = this.__meta__;
/// @ts-ignore
delete this.__meta__;
FileManager.write(tag, this, asynchronous);
this.__meta__ = tag;
}
}
// A 50% chance would be "Math.random() < 0.5" because Math.random() can be [0, 1), so to make two equal ranges, you'd need [0, 0.5)U[0.5, 1).
// Similar logic would follow for any other percentage. Math.random() < 1 is always true (100% chance) and Math.random() < 0 is always false (0% chance).
export const Random = {
num: (min: number, max: number) => Math.random() * (max - min) + min,
int: (min: number, max: number) => Math.floor(Random.num(min, max)),
chance: (decimal: number) => Math.random() < decimal,
sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1),
deviation: (base: number, deviation: number) =>
Random.num(base - deviation, base + deviation),
};