diff --git a/Xiao.js b/Xiao.js
index 118fb623..407839ab 100644
--- a/Xiao.js
+++ b/Xiao.js
@@ -1,7 +1,7 @@
const { XIAO_TOKEN, OWNERS, XIAO_PREFIX, INVITE } = process.env;
const path = require('path');
-const { CommandoClient } = require('discord.js-commando');
-const client = new CommandoClient({
+const XiaoClient = require('./structures/Client');
+const client = new XiaoClient({
commandPrefix: XIAO_PREFIX,
owner: OWNERS.split(','),
invite: INVITE,
@@ -9,6 +9,7 @@ const client = new CommandoClient({
unknownCommandResponse: false,
disabledEvents: ['TYPING_START']
});
+const SequelizeProvider = require('./providers/Sequelize');
const activities = require('./assets/json/activity');
client.registry
@@ -31,12 +32,12 @@ client.registry
])
.registerDefaultCommands({
help: false,
- ping: false,
- prefix: false,
- commandState: false
+ ping: false
})
.registerCommandsIn(path.join(__dirname, 'commands'));
+client.setProvider(new SequelizeProvider(client.database));
+
client.on('ready', () => {
console.log(`[READY] Logged in as ${client.user.tag}! (${client.user.id})`);
client.setInterval(() => {
diff --git a/assets/json/hat.json b/assets/json/hat.json
new file mode 100644
index 00000000..e9172d58
--- /dev/null
+++ b/assets/json/hat.json
@@ -0,0 +1,7 @@
+[
+ "christmas",
+ "anime",
+ "tophat",
+ "pilgrim",
+ "birthday"
+]
diff --git a/commands/avatar-edit/hat.js b/commands/avatar-edit/hat.js
index d461865c..8afa9b51 100644
--- a/commands/avatar-edit/hat.js
+++ b/commands/avatar-edit/hat.js
@@ -3,7 +3,7 @@ const { createCanvas, loadImage } = require('canvas');
const snekfetch = require('snekfetch');
const path = require('path');
const { list } = require('../../util/Util');
-const hats = ['christmas', 'anime', 'tophat', 'pilgrim', 'birthday'];
+const hats = require('../../assets/json/hat');
module.exports = class HatCommand extends Command {
constructor(client) {
@@ -12,6 +12,7 @@ module.exports = class HatCommand extends Command {
group: 'avatar-edit',
memberName: 'hat',
description: 'Draws a hat over a user\'s avatar.',
+ details: `**Hats**: ${hats.join(', ')}`,
throttling: {
usages: 1,
duration: 10
diff --git a/commands/games/akinator.js b/commands/games/akinator.js
index 62fb42f9..793c84c4 100644
--- a/commands/games/akinator.js
+++ b/commands/games/akinator.js
@@ -30,7 +30,7 @@ module.exports = class AkinatorCommand extends Command {
answers.push('end');
await msg.say(stripIndents`
**${++data.step}.** ${data.question}
- ${data.answers.map(answer => answer.answer).join(' | ')} | End
+ ${data.answers.map(answer => answer.answer).join(' | ')}
`);
const filter = res => res.author.id === msg.author.id && answers.includes(res.content.toLowerCase());
const msgs = await msg.channel.awaitMessages(filter, {
diff --git a/commands/games/balloon-pop.js b/commands/games/balloon-pop.js
index 58e11e99..0134ab29 100644
--- a/commands/games/balloon-pop.js
+++ b/commands/games/balloon-pop.js
@@ -51,7 +51,7 @@ module.exports = class BalloonPopCommand extends Command {
}
if (pump) {
await msg.say(`${user} pumps the balloon!`);
- remains -= randomRange(50, 100);
+ remains -= randomRange(25, 75);
const popped = Math.floor(Math.random() * remains);
if (popped <= 0) {
await msg.say('The balloon pops!');
diff --git a/commands/games/math-quiz.js b/commands/games/math-quiz.js
index e9695811..02c85902 100644
--- a/commands/games/math-quiz.js
+++ b/commands/games/math-quiz.js
@@ -19,6 +19,7 @@ module.exports = class MathQuizCommand extends Command {
group: 'games',
memberName: 'math-quiz',
description: 'See how fast you can answer a math problem in a given time limit.',
+ details: `**Difficulties**: ${difficulties.join(', ')}`,
args: [
{
key: 'difficulty',
diff --git a/commands/games/quiz.js b/commands/games/quiz.js
index 13eb36f2..8031aabb 100644
--- a/commands/games/quiz.js
+++ b/commands/games/quiz.js
@@ -13,6 +13,10 @@ module.exports = class QuizCommand extends Command {
group: 'games',
memberName: 'quiz',
description: 'Answer a quiz question.',
+ details: stripIndents`
+ **Types**: ${types.join(', ')}
+ **Difficulties**: ${difficulties.join(', ')}
+ `,
args: [
{
key: 'type',
diff --git a/commands/games/sorting-hat-quiz.js b/commands/games/sorting-hat-quiz.js
index c6a2bfa2..a85a8431 100644
--- a/commands/games/sorting-hat-quiz.js
+++ b/commands/games/sorting-hat-quiz.js
@@ -11,8 +11,7 @@ module.exports = class SortingHatQuizCommand extends Command {
aliases: ['sorting-hat', 'pottermore', 'hogwarts'],
group: 'games',
memberName: 'sorting-hat-quiz',
- description: 'Take a quiz to determine your Hogwarts house.',
- details: '**Source**: '
+ description: 'Take a quiz to determine your Hogwarts house.'
});
this.playing = new Set();
diff --git a/commands/games/typing-test.js b/commands/games/typing-test.js
index 9e10fcc2..284ed17d 100644
--- a/commands/games/typing-test.js
+++ b/commands/games/typing-test.js
@@ -19,6 +19,7 @@ module.exports = class TypingTestCommand extends Command {
group: 'games',
memberName: 'typing-test',
description: 'See how fast you can type a sentence in a given time limit.',
+ details: `**Difficulties**: ${difficulties.join(', ')}`,
args: [
{
key: 'difficulty',
diff --git a/commands/number-edit/temperature.js b/commands/number-edit/temperature.js
index 32d6deda..443494a4 100644
--- a/commands/number-edit/temperature.js
+++ b/commands/number-edit/temperature.js
@@ -10,6 +10,7 @@ module.exports = class TemperatureCommand extends Command {
group: 'number-edit',
memberName: 'temperature',
description: `Converts temperatures to/from ${list(units, 'or')}.`,
+ details: `**Units**: ${units.join(', ')}`,
args: [
{
key: 'base',
diff --git a/commands/other/coolness.js b/commands/other/coolness.js
index 090d0525..ea8266e4 100644
--- a/commands/other/coolness.js
+++ b/commands/other/coolness.js
@@ -28,6 +28,7 @@ module.exports = class CoolnessCommand extends Command {
if (coolness < 1.2) return msg.say(`${user.username} is okay, nothing special.`);
if (coolness < 1.4) return msg.say(`${user.username} is just not all that neat.`);
if (coolness < 1.6) return msg.say(`${user.username} is awful, honestly.`);
- return msg.say(`${user.username} smells like a sack of diapers.`);
+ if (coolness < 1.8) return msg.say(`${user.username} smells like a sack of diapers.`);
+ return msg.say(`${user.username} is terrible in every way.`);
}
};
diff --git a/commands/random/reddit.js b/commands/random/reddit.js
index 4389ad02..8bcd2794 100644
--- a/commands/random/reddit.js
+++ b/commands/random/reddit.js
@@ -1,6 +1,6 @@
const { Command } = require('discord.js-commando');
-const { MessageEmbed } = require('discord.js');
const snekfetch = require('snekfetch');
+const { stripIndents } = require('common-tags');
module.exports = class RedditCommand extends Command {
constructor(client) {
@@ -10,7 +10,6 @@ module.exports = class RedditCommand extends Command {
group: 'random',
memberName: 'reddit',
description: 'Responds with a random post from a subreddit.',
- clientPermissions: ['EMBED_LINKS'],
args: [
{
key: 'subreddit',
@@ -30,18 +29,13 @@ module.exports = class RedditCommand extends Command {
const allowed = msg.channel.nsfw ? body.data.children : body.data.children.filter(post => !post.data.over_18);
if (!allowed.length) return msg.say('Could not find any results.');
const post = allowed[Math.floor(Math.random() * allowed.length)].data;
- const embed = new MessageEmbed()
- .setColor(0xFF4500)
- .setAuthor('Reddit', 'https://i.imgur.com/DSBOK0P.png')
- .setURL(`https://www.reddit.com${post.permalink}`)
- .setTitle(post.title)
- .addField('❯ Upvotes',
- post.ups, true)
- .addField('❯ Downvotes',
- post.downs, true)
- .addField('❯ Score',
- post.score, true);
- return msg.embed(embed);
+ return msg.say(stripIndents`
+ **${post.title}**
+
+
+ ⬆ ${post.ups}
+ ⬇ ${post.downs}
+ `);
} catch (err) {
if (err.status === 403) return msg.say('This subreddit is private.');
if (err.status === 404) return msg.say('Could not find any results.');
diff --git a/commands/search/deviantart.js b/commands/search/deviantart.js
index d4b2b2ab..0ac50d58 100644
--- a/commands/search/deviantart.js
+++ b/commands/search/deviantart.js
@@ -11,6 +11,7 @@ module.exports = class DeviantartCommand extends Command {
group: 'search',
memberName: 'deviantart',
description: 'Responds with an image from a DeviantArt section, with optional query.',
+ details: `**Sections**: ${sections.join(', ')}`,
args: [
{
key: 'section',
diff --git a/commands/search/periodic-table.js b/commands/search/periodic-table.js
index b4a84885..559de8c0 100644
--- a/commands/search/periodic-table.js
+++ b/commands/search/periodic-table.js
@@ -3,8 +3,6 @@ const { createCanvas, loadImage, registerFont } = require('canvas');
const path = require('path');
const { elements, colors } = require('../../assets/json/periodic-table');
registerFont(path.join(__dirname, '..', '..', 'assets', 'fonts', 'Noto-Regular.ttf'), { family: 'Noto' });
-registerFont(path.join(__dirname, '..', '..', 'assets', 'fonts', 'Noto-CJK.otf'), { family: 'Noto' });
-registerFont(path.join(__dirname, '..', '..', 'assets', 'fonts', 'Noto-Emoji.ttf'), { family: 'Noto' });
module.exports = class PeriodicTableCommand extends Command {
constructor(client) {
diff --git a/commands/search/urban-dictionary.js b/commands/search/urban-dictionary.js
index 91c8ce1d..395a16d3 100644
--- a/commands/search/urban-dictionary.js
+++ b/commands/search/urban-dictionary.js
@@ -1,7 +1,8 @@
const { Command } = require('discord.js-commando');
const { MessageEmbed } = require('discord.js');
const snekfetch = require('snekfetch');
-const { shorten } = require('../../util/Util');
+const { shorten, list } = require('../../util/Util');
+const types = ['random', 'top'];
module.exports = class UrbanDictionaryCommand extends Command {
constructor(client) {
@@ -11,24 +12,36 @@ module.exports = class UrbanDictionaryCommand extends Command {
group: 'search',
memberName: 'urban-dictionary',
description: 'Defines a word, but with Urban Dictionary.',
+ details: `**Types**: ${types.join(', ')}`,
clientPermissions: ['EMBED_LINKS'],
args: [
{
key: 'word',
prompt: 'What word would you like to look up?',
type: 'string'
+ },
+ {
+ key: 'type',
+ prompt: 'Do you want to get the top answer or a random one?',
+ type: 'string',
+ default: 'top',
+ validate: type => {
+ if (types.includes(type.toLowerCase())) return true;
+ return `Invalid type, please enter either ${list(types, 'or')}.`;
+ },
+ parse: type => type.toLowerCase()
}
]
});
}
- async run(msg, { word }) {
+ async run(msg, { word, type }) {
try {
const { body } = await snekfetch
.get('http://api.urbandictionary.com/v0/define')
.query({ term: word });
if (!body.list.length) return msg.say('Could not find any results.');
- const data = body.list[Math.floor(Math.random() * body.list.length)];
+ const data = body.list[type === 'top' ? 0 : Math.floor(Math.random() * body.list.length)];
const embed = new MessageEmbed()
.setColor(0x32A8F0)
.setAuthor('Urban Dictionary', 'https://i.imgur.com/Fo0nRTe.png')
diff --git a/commands/text-edit/binary.js b/commands/text-edit/binary.js
index 9d0ec314..6ba97084 100644
--- a/commands/text-edit/binary.js
+++ b/commands/text-edit/binary.js
@@ -9,6 +9,7 @@ module.exports = class BinaryCommand extends Command {
group: 'text-edit',
memberName: 'binary',
description: 'Converts text to binary.',
+ details: `**Modes**: ${modes.join(', ')}`,
args: [
{
key: 'mode',
diff --git a/commands/util/reload.js b/commands/util/reload.js
deleted file mode 100644
index cd64abbb..00000000
--- a/commands/util/reload.js
+++ /dev/null
@@ -1,27 +0,0 @@
-const { Command } = require('discord.js-commando');
-
-module.exports = class ReloadCommand extends Command {
- constructor(client) {
- super(client, {
- name: 'reload',
- group: 'util',
- memberName: 'reload',
- description: 'Reloads a command.',
- details: 'Only the bot owner(s) may use this command.',
- ownerOnly: true,
- guarded: true,
- args: [
- {
- key: 'command',
- prompt: 'Which command would you like to reload?',
- type: 'command'
- }
- ]
- });
- }
-
- run(msg, { command }) {
- command.reload();
- return msg.say(`Reloaded \`${command.name}\`.`);
- }
-};
diff --git a/package.json b/package.json
index e4797c6c..0d45f364 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "xiao",
- "version": "64.0.2",
+ "version": "64.1.0",
"description": "Your personal server companion.",
"main": "Xiao.js",
"scripts": {
diff --git a/providers/Sequelize.js b/providers/Sequelize.js
new file mode 100644
index 00000000..1811d38d
--- /dev/null
+++ b/providers/Sequelize.js
@@ -0,0 +1,244 @@
+// Credit: https://github.com/iCrawl/Tohru/blob/master/src/providers/Sequelize.js
+
+const { SettingProvider } = require('discord.js-commando');
+const Sequelize = require('sequelize');
+
+/**
+ * Uses an PostgreSQL database to store settings with guilds
+ * @extends {SettingProvider}
+ */
+class SequelizeProvider extends SettingProvider {
+ /**
+ * @external PostgreSQLDatabase
+ * @see {@link https://www.npmjs.com/package/sequelize}
+ */
+
+ /**
+ * @param {SQLDatabase} db - Database for the provider
+ */
+ constructor(db) {
+ super();
+
+ /**
+ * Database that will be used for storing/retrieving settings
+ * @type {SQLDatabase}
+ */
+ this.db = db;
+
+ /**
+ * Client that the provider is for (set once the client is ready, after using {@link CommandoClient#setProvider})
+ * @name SequelizeProvider#client
+ * @type {CommandoClient}
+ * @readonly
+ */
+ Object.defineProperty(this, 'client', { value: null, writable: true });
+
+ /**
+ * Settings cached in memory, mapped by guild ID (or 'global')
+ * @type {Map}
+ * @private
+ */
+ this.settings = new Map();
+
+ /**
+ * Listeners on the Client, mapped by the event name
+ * @type {Map}
+ * @private
+ */
+ this.listeners = new Map();
+
+ /**
+ * Sequelize Model Object
+ * @type {SequelizeModel}
+ * @private
+ */
+ this.model = this.db.define('settings', {
+ guild: {
+ type: Sequelize.BIGINT,
+ allowNull: false,
+ unique: true,
+ primaryKey: true
+ },
+ settings: { type: Sequelize.TEXT }
+ }, { freezeTableName: true, timestamps: false });
+
+ /**
+ * @external SequelizeModel
+ * @see {@link http://docs.sequelizejs.com/en/latest/api/model/}
+ */
+ }
+
+ async init(client) {
+ this.client = client;
+ await this.db.sync();
+
+ // Load all settings
+ const rows = await this.model.findAll();
+ for (const row of rows) {
+ let settings;
+ try {
+ settings = JSON.parse(row.dataValues.settings);
+ } catch (err) {
+ client.emit('warn', `SequelizeProvider couldn't parse the settings stored for guild ${row.dataValues.guild}.`);
+ continue;
+ }
+
+ const guild = row.dataValues.guild !== '0' ? row.dataValues.guild : 'global';
+
+ this.settings.set(guild, settings);
+ if (guild !== 'global' && !client.guilds.has(row.dataValues.guild)) continue;
+ this.setupGuild(guild, settings);
+ }
+
+ // Listen for changes
+ this.listeners
+ .set('commandPrefixChange', (guild, prefix) => this.set(guild, 'prefix', prefix))
+ .set('commandStatusChange', (guild, command, enabled) => this.set(guild, `cmd-${command.name}`, enabled))
+ .set('groupStatusChange', (guild, group, enabled) => this.set(guild, `grp-${group.id}`, enabled))
+ .set('guildCreate', guild => {
+ const settings = this.settings.get(guild.id);
+ if (!settings) return;
+ this.setupGuild(guild.id, settings);
+ })
+ .set('commandRegister', command => {
+ for (const [guild, settings] of this.settings) {
+ if (guild !== 'global' && !client.guilds.has(guild)) continue;
+ this.setupGuildCommand(client.guilds.get(guild), command, settings);
+ }
+ })
+ .set('groupRegister', group => {
+ for (const [guild, settings] of this.settings) {
+ if (guild !== 'global' && !client.guilds.has(guild)) continue;
+ this.setupGuildGroup(client.guilds.get(guild), group, settings);
+ }
+ });
+ for (const [event, listener] of this.listeners) client.on(event, listener);
+ }
+
+ destroy() {
+ // Remove all listeners from the client
+ for (const [event, listener] of this.listeners) this.client.removeListener(event, listener);
+ this.listeners.clear();
+ }
+
+ get(guild, key, defVal) {
+ const settings = this.settings.get(this.constructor.getGuildID(guild));
+ return settings ? typeof settings[key] !== 'undefined' ? settings[key] : defVal : defVal;
+ }
+
+ async set(guild, key, val) {
+ guild = this.constructor.getGuildID(guild);
+ let settings = this.settings.get(guild);
+ if (!settings) {
+ settings = {};
+ this.settings.set(guild, settings);
+ }
+
+ settings[key] = val;
+ await this.model.upsert(
+ { guild: guild !== 'global' ? guild : '0', settings: JSON.stringify(settings) }
+ );
+ if (guild === 'global') this.updateOtherShards(key, val);
+ return val;
+ }
+
+ async remove(guild, key) {
+ guild = this.constructor.getGuildID(guild);
+ const settings = this.settings.get(guild);
+ if (!settings || typeof settings[key] === 'undefined') return undefined;
+
+ const val = settings[key];
+ settings[key] = undefined;
+ await this.model.upsert(
+ { guild: guild !== 'global' ? guild : '0', settings: JSON.stringify(settings) }
+ );
+ if (guild === 'global') this.updateOtherShards(key, undefined);
+ return val;
+ }
+
+ async clear(guild) {
+ guild = this.constructor.getGuildID(guild);
+ if (!this.settings.has(guild)) return;
+ this.settings.delete(guild);
+ await this.model.destroy({ where: { guild: guild !== 'global' ? guild : '0' } });
+ }
+
+ /**
+ * Loads all settings for a guild
+ * @param {string} guild - Guild ID to load the settings of (or 'global')
+ * @param {Object} settings - Settings to load
+ * @private
+ */
+ setupGuild(guild, settings) {
+ if (typeof guild !== 'string') throw new TypeError('The guild must be a guild ID or "global".');
+ guild = this.client.guilds.get(guild) || null;
+
+ // Load the command prefix
+ if (typeof settings.prefix !== 'undefined') {
+ if (guild) guild._commandPrefix = settings.prefix;
+ else this.client._commandPrefix = settings.prefix;
+ }
+
+ // Load all command/group statuses
+ for (const command of this.client.registry.commands.values()) this.setupGuildCommand(guild, command, settings);
+ for (const group of this.client.registry.groups.values()) this.setupGuildGroup(guild, group, settings);
+ }
+
+ /**
+ * Sets up a command's status in a guild from the guild's settings
+ * @param {?Guild} guild - Guild to set the status in
+ * @param {Command} command - Command to set the status of
+ * @param {Object} settings - Settings of the guild
+ * @private
+ */
+ setupGuildCommand(guild, command, settings) {
+ if (typeof settings[`cmd-${command.name}`] === 'undefined') return;
+ if (guild) {
+ if (!guild._commandsEnabled) guild._commandsEnabled = {};
+ guild._commandsEnabled[command.name] = settings[`cmd-${command.name}`];
+ } else {
+ command._globalEnabled = settings[`cmd-${command.name}`];
+ }
+ }
+
+ /**
+ * Sets up a group's status in a guild from the guild's settings
+ * @param {?Guild} guild - Guild to set the status in
+ * @param {CommandGroup} group - Group to set the status of
+ * @param {Object} settings - Settings of the guild
+ * @private
+ */
+ setupGuildGroup(guild, group, settings) {
+ if (typeof settings[`grp-${group.id}`] === 'undefined') return;
+ if (guild) {
+ if (!guild._groupsEnabled) guild._groupsEnabled = {};
+ guild._groupsEnabled[group.id] = settings[`grp-${group.id}`];
+ } else {
+ group._globalEnabled = settings[`grp-${group.id}`];
+ }
+ }
+
+ /**
+ * Updates a global setting on all other shards if using the {@link ShardingManager}.
+ * @param {string} key - Key of the setting to update
+ * @param {*} val - Value of the setting
+ * @private
+ */
+ updateOtherShards(key, val) {
+ if (!this.client.shard) return;
+ key = JSON.stringify(key);
+ val = typeof val !== 'undefined' ? JSON.stringify(val) : 'undefined';
+ this.client.shard.broadcastEval(`
+ if(this.shard.id !== ${this.client.shard.id} && this.provider && this.provider.settings) {
+ let global = this.provider.settings.get('global');
+ if (!global) {
+ global = {};
+ this.provider.settings.set('global', global)
+ }
+ global[${key}] = ${val};
+ }
+ `);
+ }
+}
+
+module.exports = SequelizeProvider;
diff --git a/structures/Client.js b/structures/Client.js
new file mode 100644
index 00000000..aa93b8e0
--- /dev/null
+++ b/structures/Client.js
@@ -0,0 +1,13 @@
+const { CommandoClient } = require('discord.js-commando');
+const Database = require('../structures/PostgreSQL');
+
+class XiaoClient extends CommandoClient {
+ constructor(options) {
+ super(options);
+ this.database = Database.db;
+
+ Database.start();
+ }
+}
+
+module.exports = XiaoClient;
diff --git a/structures/PostgreSQL.js b/structures/PostgreSQL.js
new file mode 100644
index 00000000..ce89e8fa
--- /dev/null
+++ b/structures/PostgreSQL.js
@@ -0,0 +1,23 @@
+const Sequelize = require('sequelize');
+const { DB_URL } = process.env;
+const db = new Sequelize(DB_URL, { logging: false, operatorsAliases: false });
+
+class PostgreSQL {
+ static get db() {
+ return db;
+ }
+
+ static async start() {
+ try {
+ await db.authenticate();
+ console.log('[DATABASE] Connection established! Syncing...');
+ await db.sync();
+ console.log('[DATABASE] Database sync complete!');
+ } catch (err) {
+ console.error('[DATABASE] Unable to connect to database:', err);
+ setTimeout(() => PostgreSQL.start(), 5000);
+ }
+ }
+}
+
+module.exports = PostgreSQL;