GODDESS OF DISCORD, YEAH

This commit is contained in:
Emily 2020-10-20 16:47:05 +11:00
parent 00bbebcf4a
commit ee5de0ccaa
19 changed files with 2640 additions and 8 deletions

23
bot/base/Command.js Normal file
View file

@ -0,0 +1,23 @@
class Command {
constructor (client, {
name = null,
description = 'No description provided.',
category = 'Miscellaneous',
usage = 'No usage provided.',
parameters = '',
examples = '',
enabled = true,
guildOnly = false,
devOnly = false,
aliases = new Array(),
userPerms = new Array(),
botPerms = new Array (),
cooldown = 2000
}) {
this.client = client;
this.conf = { enabled, guildOnly, devOnly, aliases, userPerms, botPerms, cooldown };
this.help = { name, description, category, usage, parameters, examples };
}
}
module.exports = Command;

View file

161
bot/extenders/Embed.js Normal file
View file

@ -0,0 +1,161 @@
/**
* Custom Discord Embed builder with color resolving from the Chariot.js Client framework
* Link: https://github.com/riyacchi/chariot.js/blob/master/structures/ChariotEmbed.js
*/
class Embed {
constructor (data = {}) {
this.fields = [];
Object.assign(this, data);
return this;
}
/**
* Set the author of this Embed
* @param {string} name Name of the Author
* @param {string} icon URL of the icon that should be used
* @param {string} url URL of the author if clicked
*/
setAuthor (name, icon, url) {
this.author = { name, icon_url: icon, url };
return this;
}
/**
* Resolves a color to a usable "Discord readable" color
* @private
* @param {*} color Any color which can be resolved
*/
_resolveColor (color) {
if (typeof color === 'string') {
if (color === 'RANDOM') return Math.floor(Math.random() * (0xFFFFFF + 1));
color = Colors[color.toUpperCase()] || parseInt(color.replace('#', ''), 16);
}
return color;
}
/**
* Set the color of this Embed
* @param {*} color Any color resolvable by this._resolveColor
*/
setColor (color) {
this.color = this._resolveColor(color);
return this;
}
/**
* Set the description of this Embed
* @param {string} desc A description
*/
setDescription (desc) {
this.description = desc.toString().substring(0, 2048);
return this;
}
/**
* Add a field to the Embed
* @param {string} name The name (or title if you will) of the field
* @param {string} value The content of the field
* @param {boolean} inline Whether this field should be inline or not
*/
addField (name, value, inline = false) {
if (this.fields.length >= 25) {
return this;
} else if (!name) {
return this;
} else if (!value) {
return false;
}
this.fields.push({ name: name.toString().substring(0, 256), value: value.toString().substring(0, 1024), inline });
return this;
}
/**
* Add a blank field to the Embed
* @param {boolean} inline Whether this field should be inline or not
*/
addBlankField (inline = false) {
return this.addField('\u200B', '\u200B', inline);
}
/**
* Attaches a file to the Embed
* @param {*} file The file to be attached
*/
attachFile (file) {
this.file = file;
return this;
}
/**
* Set the footer of this Embed
* @param {string} text The footer's text
* @param {string} icon The URL of an icon that should be used in the footer
*/
setFooter (text, icon) {
this.footer = { text: text.toString().substring(0, 2048), icon_url: icon };
return this;
}
/**
* Set the image of this Embed
* @param {string} url The image url
*/
setImage (url) {
this.image = { url };
return this;
}
/**
* Set the timestamp of this Embed
* @param {*} time A time resolvable, e.g. a UTC timestamp or epoch timestamps in MS
*/
setTimestamp (time = new Date()) {
this.timestamp = time;
return this;
}
/**
* Set the title of this Embed
* @param {string} title The title this Embed should have
*/
setTitle (title) {
this.title = title.toString().substring(0, 256);
return this;
}
/**
* Set the thumbnail of this Embed
* @param {string} url The thumbnail URL of this Embed
*/
setThumbnail (url) {
this.thumbnail = { url };
return this;
}
/**
* Set the URL of this Embed
* @param {string} url The URL of this Embed
*/
setUrl (url) {
this.url = url;
return this;
}
}
module.exports = Embed;

142
bot/index.js Normal file
View file

@ -0,0 +1,142 @@
// Copyright 2020 Emily J. / mudkipscience and contributors. Subject to the AGPLv3 license.
const Eris = require('eris-additions')(require('eris'));
const EventHandler = require('./util/handlers/eventHandler');
const messageHandler = require('./util/handlers/messageHandler');
const Helpers = require('./util/helpers');
const Database = require('./util/database');
const Logger = require('./util/logger');
const read = require('fs-readdir-recursive');
const sentry = require('@sentry/node');
const config = require('../botconfig.yml');
const pkg = require('../package.json');
class WoomyClient extends Eris.Client {
constructor (token, options) {
super(token, options);
this.config = config;
this.path = __dirname;
this.version = pkg.version;
this.commandFiles = read('./commands').filter(file => file.endsWith('.js'));
this.eventFiles = read('./event_modules').filter(file => file.endsWith('.js'));
this.logger = Logger;
//this.helpers = new Helpers(this);
this.db = new Database(this);
this.commands = new Eris.Collection();
this.aliases = new Eris.Collection();
this.cooldowns = new Eris.Collection();
this.eventModules = new Eris.Collection();
}
loadCommands () {
const nameRegexp = /[^/]*$/;
const catRegexp = /.+?(?=\/)/;
for (const file of this.commandFiles) {
try {
const props = require(this.path + '/commands/' + file)(this);
props.help.name = nameRegexp.exec(file);
props.help.category = catRegexp.exec(file);
this.commands.set(props.help.name, props);
this.cooldowns.set(props.help.name, new Map());
props.conf.aliases.forEach(alias => {
this.aliases.set(alias, props.help.name);
});
} catch (error) {
this.logger.error('COMMAND_LOADER', error);
}
}
this.logger.success('COMMAND_LOADER', `Successfully loaded ${this.commands.size}/${this.commandFiles.length} commands.`);
}
loadEventModules () {
const nameRegexp = /[^/]*$/;
for (const file of this.eventFiles) {
try {
const event = require(this.path + '/event_modules/' + file)(this);
this.eventModules.set(nameRegexp.exec(file), event);
} catch (error) {
this.logger.error('EVENT_LOADER', error);
}
}
this.logger.success('EVENT_LOADER', `Successfully loaded ${this.eventModules.size}/${this.eventFiles.length} event modules.`);
}
mainEventListener (wsEvent, message, other) {
}
runReadyEvents () {
this.mainEventListener('ready');
}
runErrorEvents (error) {
this.mainEventListener('error', null, error);
}
runGuildCreateEvents (guild) {
this.mainEventListener('guildCreate', null, guild);
}
runGuildDeleteEvents (guild) {
this.mainEventListener('guildDelete', null, guild);
}
runGuildMemberAddEvents () {
}
createEventListeners () {
this.on('ready', );
this.on('error')
this.on('messageCreate', this.mainEventLIstener('message', message));
this.on('guildCreate', );
this.on('guildDelete', );
this.on('guildMemberAdd', );
this.on('guildMemberRemove', );
this.on('voiceStateUpdate', );
}
}
async function init () {
const client = new WoomyClient(config.token, {
defaultImageFormat: 'png',
defaultImageSize: 2048,
intents: [
'guilds',
'guildMembers',
'guildEmojis',
'guildVoiceStates',
'guildMessages',
'guildMessageReactions',
'directMessages',
'directMessageReactions'
]
});
require('./util/prototypes');
client.loadCommands();
client.loadEventModules();
client.createEventListeners();
if (client.config.devmode === true) {
try {
// sentry.init({ dsn: client.config.keys.sentry });
} catch (err) {
client.logger.error('SENTRY', `Sentry failed to start: ${err}`);
}
} else {
client.logger.warning('DEVMODE', 'Running in development mode, some features have been disabled.');
}
}
init ();

160
bot/util/database.js Normal file
View file

@ -0,0 +1,160 @@
/* eslint-disable no-unused-vars */
const { Pool } = require('pg');
const format = require('pg-format');
class Database {
constructor (client) {
this.client = client;
this.pool = new Pool(client.config.postgresOptions);
this.pool.on('error', err => {
this.client.logger.error('Postgres error: ' + err);
});
}
async getGuild (id) {
const res = await this.pool.query('SELECT * FROM guilds WHERE guild_id = $1;', [id]);
return res.rows[0];
}
async getMember (guild_id, user_id) {
const key = guild_id + ':' + user_id;
const res = await this.pool.query('SELECT * FROM members WHERE member_id = $1;', [key]);
return res.rows[0];
}
async getUser (id) {
const res = await this.pool.query('SELECT * FROM users WHERE user_id = $1;', [id]);
return res.rows[0];
}
async getGuildField (id, column) {
const sql = format('SELECT %I FROM guilds WHERE guild_id = $1;', column);
const query = {
text: sql,
values: [id],
rowMode: 'array'
};
const res = await this.pool.query(query);
return res.rows[0][0];
}
async getMemberField (guild_id, user_id, column) {
const key = guild_id + ':' + user_id;
const sql = format('SELECT %I FROM members WHERE member_id = $1;', column);
const query = {
text: sql,
values: [key],
rowMode: 'array'
};
const res = await this.pool.query(query);
return res.rows[0][0];
}
async getUserField (id, column) {
const sql = format('SELECT %I FROM users WHERE user_id = $1;', column);
const query = {
text: sql,
values: [id],
rowMode: 'array'
};
const res = await this.pool.query(query);
return res.rows[0][0];
}
async updateGuild (id, column, newValue) {
const sql = format('UPDATE guilds SET %I = $1 WHERE guild_id = $2;', column);
await this.pool.query(sql, [newValue, id]);
return;
}
async updateMember (guild_id, user_id, column, newValue) {
const key = guild_id + ':' + user_id;
const sql = format('UPDATE members SET %I = $1 WHERE member_id = $2;', column);
await this.pool.query(sql, [newValue, key]);
return;
}
async updateUser (id, column, newValue) {
const sql = format('UPDATE users SET %I = $1 WHERE user_id = $2;', column);
await this.pool.query(sql, [newValue, id]);
return;
}
async resetGuild (id, column) {
const regexp = /(?<=\')(.*?)(?=\')/; //eslint-disable-line no-useless-escape
const res = await this.client.db.pool.query(
'SELECT column_default FROM information_schema.columns WHERE table_name=\'guilds\' AND column_name = $1;', [column]);
const def = res.rows[0].column_default.match(regexp)[0];
await this.updateGuild(id, column, def);
return;
}
async resetMember (guild_id, user_id, column) {
const key = guild_id + ':' + user_id;
const regexp = /(?<=\')(.*?)(?=\')/; //eslint-disable-line no-useless-escape
const res = await this.client.db.pool.query(
'SELECT column_default FROM information_schema.columns WHERE table_name=\'members\' AND column_name = $1;', [column]);
const def = res.rows[0].column_default.match(regexp)[0];
await this.updateGuild(key, column, def);
return;
}
async resetUser (id, column) {
const regexp = /(?<=\')(.*?)(?=\')/; //eslint-disable-line no-useless-escape
const res = await this.client.db.pool.query(
'SELECT column_default FROM information_schema.columns WHERE table_name=\'users\' AND column_name = $1;', [column]);
const def = res.rows[0].column_default.match(regexp)[0];
await this.updateGuild(id, column, def);
return;
}
async deleteGuild (id) {
await this.pool.query('DELETE FROM guilds WHERE guild_id = $1;', [id]);
await this.pool.query('DELETE FROM members WHERE member_id LIKE $1;', [`${id}%`]);
return;
}
async deleteMember (guild_id, user_id) {
const key = guild_id + ':' + user_id;
await this.pool.query('DELETE FROM members WHERE member_id = $1;', [key]);
return;
}
async deleteUser (id) {
await this.pool.query('DELETE FROM users WHERE user_id = $1;', [id]);
await this.pool.query('DELETE FROM members WHERE member_id LIKE $1;', [`${id}%`]);
return;
}
async createGuild (id) {
const res = await this.pool.query('INSERT INTO guilds (guild_id) VALUES ($1) RETURNING *;', [id]);
return res;
}
async createMember (guild_id, user_id) {
const key = guild_id + ':' + user_id;
const res = await this.pool.query('INSERT INTO members (member_id) VALUES ($1) RETURNING *;', [key]);
return res.rows[0];
}
async createUser (id) {
const res = await this.pool.query('INSERT INTO users (user_id) VALUES ($1) RETURNING *;', [id]);
return res.rows[0];
}
}
module.exports = Database;

0
bot/util/dbvalidator.js Normal file
View file

View file

@ -0,0 +1,47 @@
class EventHandler {
constructor (client) {
this.client = client;
}
ready () {
const readyModules = this.client.eventModules.filter(module => module.wsEvent = 'ready');
readyModules.forEach(module => module.execute(this.client));
}
error () {
const errorModules = this.client.eventModules.filter(module => module.wsEvent = 'error');
errorModules.forEach(module => module.execute(this.client));
}
messageCreate () {
const mCreateModules = this.client.eventModules.filter(module => module.wsEvent = 'messageCreate');
mCreateModules.forEach(module => module.execute(this.client));
}
guildCreate () {
const gCreateModules = this.client.eventModules.filter(module => module.wsEvent = 'guildCreate');
gCreateModules.forEach(module => module.execute(this.client));
}
guildDelete () {
const gDeleteModules = this.client.eventModules.filter(module => module.wsEvent = 'guildDelete');
gDeleteModules.forEach(module => module.execute(this.client));
}
guildMemberAdd () {
const gMemberAddModules = this.client.eventModules.filter(module => module.wsEvent = 'guildMemberAdd');
gMemberAddModules.forEach(module => module.execute(this.client));
}
guildMemberRemove () {
const gMemberRemoveModules = this.client.eventModules.filter(module => module.wsEvent = 'guildMemberRemove');
gMemberRemoveModules.forEach(module => module.execute(this.client));
}
voiceStateUpdate () {
const vStateUpdateModules = this.client.eventModules.filter(module => module.wsEvent = 'voiceStateUpdate');
vStateUpdateModules.forEach(module => module.execute(this.client));
}
}
module.exports = EventHandler;

View file

147
bot/util/helpers.js Normal file
View file

@ -0,0 +1,147 @@
const { MessageEmbed } = require('discord.js');
const { inspect, promisify } = require('util');
class Helpers {
constructor (client) {
this.client = client;
}
userError (channel, cmd, error) {
const embed = new MessageEmbed()
.setColor('#EF5350')
.setTitle(`${cmd.help.name}:${cmd.help.category.toLowerCase()}`)
.setDescription(error)
.addField('**Usage**', cmd.help.usage)
.setFooter(`Run 'help ${cmd.help.name}' for more information.`);
channel.send(embed);
}
async getLastMessage (channel) {
const messages = await channel.messages.fetch({ limit: 2 });
return messages.last().content;
}
async awaitReply (message, question, limit = 60000) {
const filter = (m) => m.author.id === message.author.id;
await message.channel.send(question);
try {
const collected = await message.channel.awaitMessages(filter, {
max: 1,
time: limit,
errors: ['time']
});
return collected.first().content;
} catch (err) {
return false;
}
}
searchForMembers (guild, query) {
query = query.toLowerCase();
const matches = [];
let match;
try {
match = guild.members.cache.find(x => x.displayName.toLowerCase() == query);
if (!match) guild.members.cache.find(x => x.user.username.toLowerCase() == query);
} catch (err) {} //eslint-disable-line no-empty
if (match) matches.push(match);
guild.members.cache.forEach(member => {
if (
(member.displayName.toLowerCase().startsWith(query) ||
member.user.tag.toLowerCase().startsWith(query)) &&
member.id != (match && match.id)
) {
matches.push(member);
}
});
return matches;
}
findRole (input, message) {
let role;
role = message.guild.roles.cache.find(r => r.name.toLowerCase() === input.toLowerCase());
if (!role) {
role = message.guild.roles.cache.get(input.toLowerCase());
}
if (!role) return;
return role;
}
checkPermissions (command, message, member) {
const missingPerms = [];
if (member.user.bot) {
command.conf.botPerms.forEach(p => {
if (!message.channel.permissionsFor(member).has(p)) missingPerms.push(p);
});
} else {
command.conf.userPerms.forEach(p => {
if (!message.channel.permissionsFor(member).has(p)) missingPerms.push(p);
});
}
if (missingPerms.length > 0) return missingPerms;
}
intBetween (min, max) {
return Math.round((Math.random() * (max - min) + min));
}
isDeveloper (id) {
if (this.client.config.ownerIDs.includes(id)) {
return true;
} else {
return false;
}
}
shutdown () {
const exitQuotes = [
'Shutting down.',
'I don\'t blame you.',
'I don\'t hate you.',
'Whyyyyy',
'Goodnight.',
'Goodbye'
];
this.client.db.pool.end().then(() => {
this.client.logger.info('Connection to database closed.');
});
this.client.destroy();
console.log(exitQuotes);
}
async clean (text) {
if (text && text.constructor.name === 'Promise') {
text = await text;
}
if (typeof text !== 'string') {
text = inspect(text, { depth: 1});
}
text = text
.replace(/`/g, '`' + String.fromCharCode(8203))
.replace(/@/g, '@' + String.fromCharCode(8203))
.replace(this.client.token, 'mfa.VkO_2G4Qv3T--NO--lWetW_tjND--TOKEN--QFTm6YGtzq9PH--4U--tG0');
return text;
}
wait () {
promisify(setTimeout);
}
}
module.exports = Helpers;

0
bot/util/loaders.js Normal file
View file

93
bot/util/logger.js Normal file
View file

@ -0,0 +1,93 @@
/**
* Colorful logger class from the Chariot.js Client framework featuring various log levels.
* Link: https://github.com/riyacchi/chariot.js/master/helpers/Logger.js
*/
const chalk = require('chalk');
class Logger {
/**
* Pads a single number for unified looks in the console
* @param {number} number The number that should be force-padded
* @returns {number} The padded number
*/
static _forcePadding (number) {
return (number < 10 ? '0' : '') + number;
}
/**
* Gets the full current system time and date for logging purposes
* @returns {string} The formatted current time
*/
static _getCurrentTime () {
const now = new Date();
const day = this._forcePadding(now.getDate());
const month = this._forcePadding(now.getMonth() + 1);
const year = this._forcePadding(now.getFullYear());
const hour = this._forcePadding(now.getHours());
const minute = this._forcePadding(now.getMinutes());
const second = this._forcePadding(now.getSeconds());
return `${day}.${month}.${year} ${hour}:${minute}:${second}`;
}
/**
* Log something related to being successful
* @param {string} title The title of the log enty
* @param {string} body The body of the log entry
* @returns {void}
*/
static success (title, body) {
console.log(chalk.bold.green(`[ ${this._getCurrentTime()} ] [ ${title} ] `) + body);
}
/**
* Log something related to a warning
* @param {string} title The title of the log enty
* @param {string} body The body of the log entry
* @returns {void}
*/
static warning (title, body) {
console.log(chalk.bold.yellow(`[ ${this._getCurrentTime()} ] [ ${title} ] `) + body);
}
/**
* Log something related to an error
* @param {string} title The title of the log enty
* @param {string} body The body of the log entry
* @returns {void}
*/
static error (title, body) {
console.log(chalk.bold.red(`[ ${this._getCurrentTime()} ] [ ${title} ] `) + body);
}
/**
* Log something related to debugging
* @param {string} title The title of the log enty
* @param {string} body The body of the log entry
* @returns {void}
*/
static debug (title, body) {
console.log(chalk.bold.magenta(`[ ${this._getCurrentTime()} ] [ ${title} ] `) + body);
}
/**
* Log something related to an event
* @param {string} body The body of the log entry
* @returns {void}
*/
static event (body) {
console.log(chalk.bold.yellow(`[ ${this._getCurrentTime()} ] [ EVENT ] `) + body);
}
/**
* Log something related to command usage
* @param {string} body The body of the log entry
* @returns {void}
*/
static command (body) {
console.log(chalk.bold.green(`[ ${this._getCurrentTime()} ] [ COMMAND ] `) + body);
}
}
module.exports = Logger;

View file

15
bot/util/prototypes.js vendored Normal file
View file

@ -0,0 +1,15 @@
// YEAH IM EXTENDING PROTOTYPES FUCK YOU
String.prototype.toProperCase = function () {
return this.replace(/([^\W_]+[^\s-]*) */g, function (txt) {return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
};
Array.prototype.random = function () {
return this[Math.floor(Math.random() * this.length)];
};
Array.prototype.remove = function (element) {
const index = this.indexOf(element);
if (index !== -1) this.splice(index, 1);
return this;
};