├── .gitattributes ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── resources ├── imgs │ ├── back0.jpg │ ├── back1.jpg │ └── card-back0.png └── polices │ └── Digitalt.ttf ├── .gitignore ├── test ├── getRandomNumber.test.js ├── moduleIsInstalled.test.js ├── isWholeNumber.test.js └── timeConverter.test.js ├── CONTRIBUTING.md ├── utils ├── sleep.js ├── getRandomNumber.js ├── moduleIsInstalled.js ├── isWholeNumber.js ├── paginate.js ├── prompt.js ├── databaseUpdater.js ├── traverse.js ├── index.js ├── TimeConverter.js └── log.js ├── tsconfig.json ├── events ├── voiceChannelLeave.js ├── guildCreate.js ├── voiceChannelSwitch.js ├── guildMemberRemove.js ├── guildMemberAdd.js └── error.js ├── commands ├── admin │ ├── dummy.js │ ├── removedonator.js │ ├── connect.js │ ├── execute.js │ ├── eval.js │ ├── registerdonator.js │ ├── bumpversion.js │ └── clientstats.js ├── generic │ ├── ping.js │ ├── prefix.js │ ├── avatar.js │ ├── invite.js │ └── sinfo.js ├── economy │ ├── balance.js │ ├── navalbase.js │ ├── give.js │ ├── inventory.js │ ├── transactions.js │ ├── market.js │ └── daily.js ├── music │ ├── leave.js │ ├── pause.js │ ├── forceskip.js │ ├── shuffle.js │ ├── nowplaying.js │ ├── removesong.js │ ├── clearqueue.js │ ├── forceskipto.js │ ├── deleteplaylist.js │ ├── setvolume.js │ ├── seeplaylists.js │ ├── repeat.js │ ├── saveplaylist.js │ ├── playafter.js │ ├── play.js │ ├── skip.js │ ├── addplaylist.js │ └── skipto.js ├── fun │ ├── choose.js │ ├── udefine.js │ └── love.js ├── image │ ├── triggered_gen.js │ ├── shitwaifu.js │ └── loveship.js ├── moderation │ ├── clearpermissions.js │ ├── announce.js │ └── clear.js ├── settings │ ├── setprefix.js │ ├── simulatefarewells.js │ └── simulategreetings.js ├── utility │ ├── mdn.js │ ├── translate.js │ └── npm.js └── misc │ └── setrankbg.js ├── .eslintrc.json ├── structures ├── CommandCategories │ ├── FunCommands.js │ ├── UtilityCommands.js │ ├── GenericCommands.js │ ├── EconomyCommands.js │ ├── SettingsCommands.js │ ├── ImageCommands.js │ ├── AdminCommands.js │ ├── MiscCommands.js │ └── ModerationCommands.js ├── ExtendedStructures │ ├── ExtendedMessage.js │ └── ExtendedUser.js ├── Contexts │ ├── FunContext.js │ ├── AdminContext.js │ ├── ImageContext.js │ ├── MiscContext.js │ ├── EconomyContext.js │ ├── GenericContext.js │ ├── UtilityContext.js │ ├── SettingsContext.js │ ├── ModerationContext.js │ └── MusicContext.js ├── index.js └── HandlersStructures │ └── marketItems.js ├── .circleci └── config.yml ├── handlers ├── index.js ├── MessageCollector.js ├── ReactionCollector.js ├── RedisManager.js └── EconomyManager.js ├── package.json ├── README.md └── index.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: paradoxorigins 4 | -------------------------------------------------------------------------------- /resources/imgs/back0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadoxalCorp/felix-production/HEAD/resources/imgs/back0.jpg -------------------------------------------------------------------------------- /resources/imgs/back1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadoxalCorp/felix-production/HEAD/resources/imgs/back1.jpg -------------------------------------------------------------------------------- /resources/imgs/card-back0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadoxalCorp/felix-production/HEAD/resources/imgs/card-back0.png -------------------------------------------------------------------------------- /resources/polices/Digitalt.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ParadoxalCorp/felix-production/HEAD/resources/polices/Digitalt.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /config.js 3 | /package-lock.json 4 | .vscode 5 | .nyc_output 6 | 7 | codealike.json 8 | config.withmusic.js 9 | config.withoutmusic.js -------------------------------------------------------------------------------- /test/getRandomNumber.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | const getRandomNumber = require('../utils/getRandomNumber'); 4 | const assert = require('assert').strict; 5 | 6 | describe('getRandomNumber()', function () { 7 | it('Should return a number of type number', function () { 8 | assert.equal(typeof getRandomNumber(0, 100), 'number'); 9 | }); 10 | }); 11 | 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 13 | **Screenshots** 14 | If applicable, add screenshots to help explain your problem. 15 | 16 | **Additional context** 17 | Add any other context about the problem here. 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Any contribution is more than welcome, here are the steps toward a "cool" PR: 4 | 5 | * Fork the repository 6 | * Code as your heart wishes and, as i try to force a consistent code style in Felix, comply to ESLint 7 | * Open a pull request ! 8 | 9 | You can see the ESLint rules enabled on this project [here](https://github.com/ParadoxalCorp/felix-production/blob/master/.eslintrc.json) 10 | -------------------------------------------------------------------------------- /utils/sleep.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Function} sleep 3 | * Equivalent for other languages sleep function, use promises to reproduce though 4 | * @param {Number} ms - Time in milliseconds to wait 5 | * @returns {Promise} Promise that will be resolved once the specified milliseconds are elapsed 6 | */ 7 | async function sleep(ms) { 8 | return new Promise(resolve => setTimeout(resolve, ms)); 9 | } 10 | 11 | module.exports = sleep; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "noEmit": true, 5 | "checkJs": true, 6 | "allowJs": true, 7 | "module": "commonjs", 8 | "watch": true, 9 | "skipLibCheck": false, 10 | "lib": [ 11 | "esnext" 12 | ], 13 | "types": [ 14 | "node", 15 | "sharp", 16 | "ioredis", 17 | "mocha" 18 | ] 19 | }, 20 | "exclude": [ 21 | "node_modules/*", 22 | "tests", 23 | "commands" 24 | ] 25 | } -------------------------------------------------------------------------------- /utils/getRandomNumber.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Function} getRandomNumber 3 | * Get a random number in the specified interval 4 | * @param {number} min - The minimum 5 | * @param {number} max - The maximum 6 | * @returns {number} A random number between min (inclusive) and max (exclusive) 7 | */ 8 | function getRandomNumber(min, max) { 9 | return Math.floor(Math.random() * (max - min + 1)) + min; 10 | } 11 | module.exports = getRandomNumber; -------------------------------------------------------------------------------- /utils/moduleIsInstalled.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Function} moduleIsInstalled 3 | * Check if a NPM module is installed 4 | * @param {string} name - The module name 5 | * @returns {boolean} Whether the module is installed or not 6 | */ 7 | const moduleIsInstalled = (name) => { 8 | try { 9 | require(name); 10 | return true; 11 | } catch (err) { 12 | return false; 13 | } 14 | }; 15 | 16 | module.exports = moduleIsInstalled; -------------------------------------------------------------------------------- /test/moduleIsInstalled.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | const moduleIsInstalled = require('../utils/moduleIsInstalled'); 4 | const assert = require('assert').strict; 5 | 6 | describe('moduleIsInstalled()', function() { 7 | it('Should return true if the given module is installed and false otherwise', function() { 8 | assert.equal(moduleIsInstalled("baguette"), false); 9 | assert.equal(moduleIsInstalled("mocha"), true); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature for felix 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the feature you'd like** 11 | A clear and concise description of what you want 12 | 13 | **Additional context** 14 | Add any other context or screenshots about the feature request here. 15 | -------------------------------------------------------------------------------- /events/voiceChannelLeave.js: -------------------------------------------------------------------------------- 1 | class VoiceChannelLeave { 2 | constructor() {} 3 | 4 | async handle(client, member, channel) { 5 | const musicConnection = client.handlers.MusicManager.connections.get(channel.guild.id); 6 | if (!musicConnection || musicConnection.player.channelId !== channel.id) { 7 | return; 8 | } 9 | if (channel.voiceMembers.size === 1) { 10 | return musicConnection.startInactivityTimeout(); 11 | } 12 | } 13 | } 14 | 15 | module.exports = new VoiceChannelLeave(); -------------------------------------------------------------------------------- /test/isWholeNumber.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | const isWholeNumber = require('../utils/isWholeNumber'); 4 | const assert = require('assert').strict; 5 | 6 | describe('isWholeNumber()', function () { 7 | it('Should return true for a whole number and false for a non-whole number', function () { 8 | assert.equal(isWholeNumber(1), true); 9 | assert.equal(isWholeNumber(1.5), false); 10 | assert.equal(isWholeNumber("1.5"), false); 11 | assert.equal(isWholeNumber("1"), true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /utils/isWholeNumber.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Function} isWholeNumber 3 | * Check if the given number is a whole number 4 | * @param {number|string} number - The number to check if its a whole number or not 5 | * @returns {boolean} A boolean representing whether it is a whole number or not 6 | */ 7 | const isWholeNumber = (number) => { 8 | if (typeof number !== 'string') { 9 | // @ts-ignore 10 | number = new String(number); 11 | } 12 | // @ts-ignore 13 | return new RegExp(/[^0-9]/gi).test(number) ? false : true; 14 | }; 15 | 16 | module.exports = isWholeNumber; -------------------------------------------------------------------------------- /commands/admin/dummy.js: -------------------------------------------------------------------------------- 1 | const AdminCommands = require('../../structures/CommandCategories/AdminCommands'); 2 | 3 | class Dummy extends AdminCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'dummy', 8 | description: 'dummy', 9 | usage: '{prefix}dummy' 10 | } 11 | }); 12 | } 13 | /** @param {import("../../structures/Contexts/AdminContext")} context */ 14 | 15 | async run(context) { 16 | return context.message.channel.createMessage('not used atm'); 17 | } 18 | } 19 | 20 | module.exports = Dummy; -------------------------------------------------------------------------------- /utils/paginate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Split the provided array into multiple arrays of the specified size 3 | * @param {Array} array The array to split 4 | * @param {Number} size The required size for a new array to be created 5 | * @returns {Array} An array of arrays, where each arrays represent a "page" 6 | */ 7 | function paginate(array, size) { 8 | let result = []; 9 | let j = 0; 10 | for (let i = 0; i < Math.ceil(array.length / (size || 10)); i++) { 11 | result.push(array.slice(j, j + (size || 10))); 12 | j = j + (size || 10); 13 | } 14 | return result; 15 | } 16 | 17 | module.exports = paginate; -------------------------------------------------------------------------------- /utils/prompt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Function} prompt 3 | * Prompt the user through the terminal 4 | * @param {string} question - The question to prompt 5 | * @returns {Promise} The answer 6 | */ 7 | const prompt = (question) => { 8 | const rl = require('readline'); 9 | const r = rl.createInterface({ 10 | input: process.stdin, 11 | output: process.stdout, 12 | terminal: false 13 | }); 14 | return new Promise((resolve) => { 15 | r.question(question, answer => { 16 | r.close(); 17 | resolve(answer); 18 | }); 19 | }); 20 | }; 21 | 22 | module.exports = prompt; -------------------------------------------------------------------------------- /events/guildCreate.js: -------------------------------------------------------------------------------- 1 | class GuildCreateHandler { 2 | constructor() {} 3 | 4 | async handle(client, guild) { 5 | if (!client.handlers.DatabaseWrapper || !client.handlers.DatabaseWrapper.healthy) { 6 | return; 7 | } 8 | const guildIsInDb = await client.handlers.DatabaseWrapper.getGuild(guild.id); 9 | if (!guildIsInDb) { 10 | client.handlers.DatabaseWrapper.set(client.structures.References.guildEntry(guild.id)) 11 | .catch(err => { 12 | client.bot.emit('error', err); 13 | }); 14 | } 15 | } 16 | } 17 | 18 | module.exports = new GuildCreateHandler(); -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 9, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "impliedStrict": true, 7 | "jsx": true 8 | } 9 | }, 10 | "env": { 11 | "node": true 12 | }, 13 | "globals": { 14 | "Map": true, 15 | "Promise": true 16 | }, 17 | "rules": { 18 | "eqeqeq": "warn", 19 | "semi": "warn", 20 | "curly": "warn", 21 | "no-empty": "warn", 22 | "valid-jsdoc": "warn", 23 | "no-extra-semi": "warn", 24 | "no-unused-vars": "warn", 25 | "no-undef": "error", 26 | "indent": "warn" 27 | } 28 | } -------------------------------------------------------------------------------- /utils/databaseUpdater.js: -------------------------------------------------------------------------------- 1 | const references = require('../structures/References'); 2 | 3 | const databaseUpdater = (data, type, source) => { 4 | const defaultDataModel = source ? source : (type === "guild" ? references.guildEntry(data.id) : references.userEntry(data.id)); 5 | for (const key in data) { 6 | if (typeof defaultDataModel[key] === typeof data[key] && typeof defaultDataModel[key] === "object" && !Array.isArray(defaultDataModel[key])) { 7 | databaseUpdater(data[key], null, defaultDataModel[key]); 8 | } else if (typeof defaultDataModel[key] !== "undefined") { 9 | defaultDataModel[key] = data[key]; 10 | } 11 | } 12 | return defaultDataModel; 13 | }; 14 | 15 | module.exports = databaseUpdater; -------------------------------------------------------------------------------- /commands/generic/ping.js: -------------------------------------------------------------------------------- 1 | const GenericCommands = require('../../structures/CommandCategories/GenericCommands'); 2 | 3 | class Ping extends GenericCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'ping', 8 | description: 'pong', 9 | usage: '{prefix}ping', 10 | } 11 | }); 12 | } 13 | /** @param {import("../../structures/Contexts/GenericContext")} context */ 14 | 15 | async run(context) { 16 | const startTime = Date.now(); 17 | const messageSent = await context.message.channel.createMessage(context.emote('ping')); 18 | return messageSent.edit(`~~Baguette~~ Pong | \`${Date.now() - startTime}\`ms`); 19 | } 20 | } 21 | 22 | module.exports = Ping; -------------------------------------------------------------------------------- /commands/economy/balance.js: -------------------------------------------------------------------------------- 1 | const EconomyCommands = require('../../structures/CommandCategories/EconomyCommands'); 2 | 3 | class Balance extends EconomyCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'balance', 8 | description: 'Check your balance', 9 | usage: '{prefix}balance' 10 | }, 11 | conf: { 12 | aliases: ['coins'] 13 | }, 14 | }); 15 | } 16 | /** @param {import("../../structures/Contexts/EconomyContext")} context */ 17 | 18 | async run(context) { 19 | return context.message.channel.createMessage(`Hai ! You currently have \`${context.userEntry.economy.coins}\` holy coins`); 20 | } 21 | } 22 | 23 | module.exports = Balance; -------------------------------------------------------------------------------- /commands/generic/prefix.js: -------------------------------------------------------------------------------- 1 | const GenericCommands = require("../../structures/CommandCategories/GenericCommands"); 2 | 3 | class Prefix extends GenericCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'prefix', 8 | description: `See ${client.config.codename}'s prefix on this server`, 9 | usage: '{prefix}prefix' 10 | }, 11 | conf: { 12 | requireDB: true 13 | } 14 | }); 15 | } 16 | /** @param {import("../../structures/Contexts/GenericContext")} context */ 17 | 18 | async run(context) { 19 | return context.message.channel.createMessage(`My prefix here is \`${context.prefix}\`, you can use commands like \`${context.prefix}ping\``); 20 | } 21 | } 22 | 23 | module.exports = Prefix; -------------------------------------------------------------------------------- /commands/music/leave.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class Leave extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'leave', 8 | description: 'Stop playing and leave the voice channel', 9 | usage: '{prefix}leave' 10 | } 11 | }, { userInVC: true }); 12 | } 13 | /** 14 | * @param {import("../../structures/Contexts/MusicContext")} context The context 15 | */ 16 | 17 | async run(context) { 18 | if (!context.clientVC) { 19 | return context.message.channel.createMessage(':x: I am not in any voice channel'); 20 | } 21 | return context.connection ? context.connection.leave() : context.clientVC.leave(); 22 | } 23 | } 24 | 25 | module.exports = Leave; -------------------------------------------------------------------------------- /commands/music/pause.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class Pause extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'pause', 8 | description: 'Pause or resume the playback', 9 | usage: '{prefix}pause' 10 | } 11 | }, { userInVC: true, playing: true }); 12 | } 13 | /** 14 | * @param {import("../../structures/Contexts/MusicContext")} context The context 15 | */ 16 | 17 | async run(context) { 18 | context.connection.player.setPause(context.connection.player.paused ? false : true); 19 | return context.message.channel.createMessage(`:white_check_mark: Successfully ${context.connection.player.paused ? 'paused' : 'resumed'} the playback`); 20 | } 21 | } 22 | 23 | module.exports = Pause; -------------------------------------------------------------------------------- /events/voiceChannelSwitch.js: -------------------------------------------------------------------------------- 1 | class VoiceChannelSwitch { 2 | constructor() {} 3 | 4 | async handle(client, member, newChannel, oldChannel) { 5 | const musicConnection = client.handlers.MusicManager.connections.get(oldChannel.guild.id); 6 | if (!musicConnection || (member.id !== client.bot.user.id && musicConnection.player.channelId !== oldChannel.id)) { 7 | return; 8 | } 9 | //Clear the timeout if there is one and that the bot is switching to a channel with users in 10 | if (musicConnection.inactivityTimeout && client.bot.user.id === member.id && newChannel.voiceMembers.size > 1) { 11 | return clearTimeout(musicConnection.inactivityTimeout); 12 | } else if (oldChannel.voiceMembers.size === 1) { 13 | return musicConnection.startInactivityTimeout(); 14 | } 15 | } 16 | } 17 | 18 | module.exports = new VoiceChannelSwitch(); -------------------------------------------------------------------------------- /commands/music/forceskip.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class ForceSkip extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'forceskip', 8 | description: 'Force skip the currently playing song', 9 | usage: '{prefix}forceskip' 10 | }, 11 | conf: { aliases: ['fskip'] } 12 | }, { userInVC: true, playing: true }); 13 | } 14 | /** 15 | * @param {import("../../structures/Contexts/MusicContext")} context The context 16 | */ 17 | 18 | async run(context) { 19 | const skippedSong = context.connection.skipTrack(); 20 | return context.message.channel.createMessage(`:white_check_mark: Skipped \`${skippedSong.info.title}\` by \`${skippedSong.info.author}\``); 21 | } 22 | } 23 | 24 | module.exports = ForceSkip; -------------------------------------------------------------------------------- /structures/CommandCategories/FunCommands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../../main.js").Client} Client 3 | * @typedef {import("../Command.js").PartialCommandOptions} PartialCommandOptions 4 | */ 5 | 6 | const Command = require('../Command'); 7 | 8 | class FunCommands extends Command { 9 | /** 10 | * 11 | * @param {Client} client - The client instance 12 | * @param {PartialCommandOptions} commandOptions - The general command configuration 13 | * @param {{noArgs: string}} [options] - `noArgs` specify a message to return if no arguments are provided. 14 | * These args will make the command handler act before running the command 15 | */ 16 | constructor(client, commandOptions, options = {}) { 17 | super(client, { ...commandOptions, category: { 18 | name: 'Fun', 19 | emote: 'tada' 20 | }}); 21 | this.options = options; 22 | } 23 | } 24 | 25 | module.exports = FunCommands; -------------------------------------------------------------------------------- /structures/ExtendedStructures/ExtendedMessage.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("./ExtendedUser")} ExtendedUser 3 | */ 4 | 5 | const Message = require('eris').Message; //eslint-disable-line no-unused-vars 6 | 7 | /** @typedef {Object} AdditionalProperties 8 | * @prop {Client} client The client instance 9 | * @prop {ExtendedUser} author The message author 10 | */ 11 | 12 | /** 13 | * @class ExtendedMessage 14 | * @typedef {Message & AdditionalProperties} ExtendedMessage 15 | */ 16 | 17 | class ExtendedMessage { 18 | /** 19 | * 20 | * @param {Message} message - The message 21 | * @param {Client} client - The client instance 22 | */ 23 | constructor(message, client) { 24 | Object.assign(this, { 25 | client, 26 | author: new client.structures.ExtendedUser(message.author, client) 27 | }, message); 28 | } 29 | } 30 | 31 | module.exports = ExtendedMessage; -------------------------------------------------------------------------------- /structures/CommandCategories/UtilityCommands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../../main.js").Client} Client 3 | * @typedef {import("../Command.js").PartialCommandOptions} PartialCommandOptions 4 | */ 5 | 6 | const Command = require('../Command'); 7 | 8 | class UtilityCommands extends Command { 9 | /** 10 | * 11 | * @param {Client} client - The client instance 12 | * @param {PartialCommandOptions} commandOptions - The general command configuration 13 | * @param {{noArgs: string}} [options] - `noArgs` specify a message to return if no arguments are provided. 14 | * These args will make the command handler act before running the command 15 | */ 16 | constructor(client, commandOptions, options = {}) { 17 | super(client, { ...commandOptions, category: { 18 | name: 'Utility', 19 | emote: 'tools' 20 | }}); 21 | this.options = options; 22 | } 23 | } 24 | 25 | module.exports = UtilityCommands; -------------------------------------------------------------------------------- /structures/CommandCategories/GenericCommands.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @typedef {import("../../main.js").Client} Client 5 | * @typedef {import("../Command.js").PartialCommandOptions} PartialCommandOptions 6 | */ 7 | 8 | const Command = require('../Command'); 9 | 10 | class GenericCommands extends Command { 11 | /** 12 | * 13 | * @param {Client} client - The client instance 14 | * @param {PartialCommandOptions} commandOptions - The general command configuration 15 | * @param {{noArgs: string}} [options] - `noArgs` specify a message to return if no arguments are provided. 16 | * These args will make the command handler act before running the command 17 | */ 18 | constructor(client, commandOptions, options = {}) { 19 | super(client, { ...commandOptions, category: { 20 | name: 'Generic', 21 | emote: 'barChart' 22 | }}); 23 | this.options = options; 24 | } 25 | } 26 | 27 | module.exports = GenericCommands; -------------------------------------------------------------------------------- /structures/Contexts/FunContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry")} GuildEntry 4 | * @typedef {import("../References").UserEntry & import("../ExtendedStructures/ExtendedUserEntry")} UserEntry 5 | */ 6 | 7 | const BaseContext = require('./BaseContext'); 8 | 9 | class FunContext extends BaseContext { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {Message} message - The message 14 | * @param {Array} args - The parsed args 15 | * @param {GuildEntry} guildEntry - The guild database entry, if any 16 | * @param {UserEntry} userEntry - The user database entry 17 | */ 18 | constructor(client, message, args, guildEntry, userEntry) { 19 | super(client, message, args, guildEntry, userEntry); 20 | } 21 | } 22 | 23 | module.exports = FunContext; 24 | 25 | -------------------------------------------------------------------------------- /structures/Contexts/AdminContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry")} GuildEntry 4 | * @typedef {import("../References").UserEntry & import("../ExtendedStructures/ExtendedUserEntry")} UserEntry 5 | */ 6 | 7 | const BaseContext = require('./BaseContext'); 8 | 9 | class AdminContext extends BaseContext { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {Message} message - The message 14 | * @param {Array} args - The parsed args 15 | * @param {GuildEntry} guildEntry - The guild database entry, if any 16 | * @param {UserEntry} userEntry - The user database entry 17 | */ 18 | constructor(client, message, args, guildEntry, userEntry) { 19 | super(client, message, args, guildEntry, userEntry); 20 | } 21 | } 22 | 23 | module.exports = AdminContext; 24 | 25 | -------------------------------------------------------------------------------- /structures/Contexts/ImageContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry")} GuildEntry 4 | * @typedef {import("../References").UserEntry & import("../ExtendedStructures/ExtendedUserEntry")} UserEntry 5 | */ 6 | 7 | const BaseContext = require('./BaseContext'); 8 | 9 | class ImageContext extends BaseContext { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {Message} message - The message 14 | * @param {Array} args - The parsed args 15 | * @param {GuildEntry} guildEntry - The guild database entry, if any 16 | * @param {UserEntry} userEntry - The user database entry 17 | */ 18 | constructor(client, message, args, guildEntry, userEntry) { 19 | super(client, message, args, guildEntry, userEntry); 20 | } 21 | } 22 | 23 | module.exports = ImageContext; 24 | 25 | -------------------------------------------------------------------------------- /structures/Contexts/MiscContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry")} GuildEntry 4 | * @typedef {import("../References").UserEntry & import("../ExtendedStructures/ExtendedUserEntry")} UserEntry 5 | */ 6 | 7 | const BaseContext = require('./BaseContext'); 8 | 9 | class MiscContext extends BaseContext { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {Message} message - The message 14 | * @param {Array} args - The parsed args 15 | * @param {GuildEntry} guildEntry - The guild database entry, if any 16 | * @param {UserEntry} userEntry - The user database entry 17 | */ 18 | constructor(client, message, args, guildEntry, userEntry) { 19 | super(client, message, args, guildEntry, userEntry); 20 | } 21 | } 22 | 23 | module.exports = MiscContext; 24 | 25 | -------------------------------------------------------------------------------- /structures/Contexts/EconomyContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry")} GuildEntry 4 | * @typedef {import("../References").UserEntry & import("../ExtendedStructures/ExtendedUserEntry")} UserEntry 5 | */ 6 | 7 | const BaseContext = require('./BaseContext'); 8 | 9 | class EconomyContext extends BaseContext { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {Message} message - The message 14 | * @param {Array} args - The parsed args 15 | * @param {GuildEntry} guildEntry - The guild database entry, if any 16 | * @param {UserEntry} userEntry - The user database entry 17 | */ 18 | constructor(client, message, args, guildEntry, userEntry) { 19 | super(client, message, args, guildEntry, userEntry); 20 | } 21 | } 22 | 23 | module.exports = EconomyContext; 24 | 25 | -------------------------------------------------------------------------------- /structures/Contexts/GenericContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry")} GuildEntry 4 | * @typedef {import("../References").UserEntry & import("../ExtendedStructures/ExtendedUserEntry")} UserEntry 5 | */ 6 | 7 | const BaseContext = require('./BaseContext'); 8 | 9 | class GenericContext extends BaseContext { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {Message} message - The message 14 | * @param {Array} args - The parsed args 15 | * @param {GuildEntry} guildEntry - The guild database entry, if any 16 | * @param {UserEntry} userEntry - The user database entry 17 | */ 18 | constructor(client, message, args, guildEntry, userEntry) { 19 | super(client, message, args, guildEntry, userEntry); 20 | } 21 | } 22 | 23 | module.exports = GenericContext; 24 | 25 | -------------------------------------------------------------------------------- /structures/Contexts/UtilityContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry")} GuildEntry 4 | * @typedef {import("../References").UserEntry & import("../ExtendedStructures/ExtendedUserEntry")} UserEntry 5 | */ 6 | 7 | const BaseContext = require('./BaseContext'); 8 | 9 | class UtilityContext extends BaseContext { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {Message} message - The message 14 | * @param {Array} args - The parsed args 15 | * @param {GuildEntry} guildEntry - The guild database entry, if any 16 | * @param {UserEntry} userEntry - The user database entry 17 | */ 18 | constructor(client, message, args, guildEntry, userEntry) { 19 | super(client, message, args, guildEntry, userEntry); 20 | } 21 | } 22 | 23 | module.exports = UtilityContext; 24 | 25 | -------------------------------------------------------------------------------- /utils/traverse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Function} traverse 3 | * Traverse through a given object, calling the callback function with each key that is not an object 4 | * @param {object} object - The object to traverse in 5 | * @param {function} callback - A function that will be called for each key of the object that is not an object 6 | * @param {Array} [ignore] - Optional, an array of strings, being the names of the objects to not traverse through 7 | * @returns {object} - The given object 8 | */ 9 | const traverse = (object, callback, ignore) => { 10 | for (const key in object) { 11 | if (typeof object[key] !== "object" || !ignore || !ignore.includes(key)) { 12 | if (typeof object[key] !== 'object' || Array.isArray(object[key])) { 13 | object[key] = callback(object[key]); 14 | } else { 15 | traverse(object[key], callback, ignore); 16 | } 17 | } 18 | } 19 | return object; 20 | }; 21 | 22 | module.exports = traverse; -------------------------------------------------------------------------------- /structures/Contexts/SettingsContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry")} GuildEntry 4 | * @typedef {import("../References").UserEntry & import("../ExtendedStructures/ExtendedUserEntry")} UserEntry 5 | */ 6 | 7 | const BaseContext = require('./BaseContext'); 8 | 9 | class SettingsContext extends BaseContext { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {Message} message - The message 14 | * @param {Array} args - The parsed args 15 | * @param {GuildEntry} guildEntry - The guild database entry, if any 16 | * @param {UserEntry} userEntry - The user database entry 17 | */ 18 | constructor(client, message, args, guildEntry, userEntry) { 19 | super(client, message, args, guildEntry, userEntry); 20 | } 21 | } 22 | 23 | module.exports = SettingsContext; 24 | 25 | -------------------------------------------------------------------------------- /structures/Contexts/ModerationContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry")} GuildEntry 4 | * @typedef {import("../References").UserEntry & import("../ExtendedStructures/ExtendedUserEntry")} UserEntry 5 | */ 6 | 7 | const BaseContext = require('./BaseContext'); 8 | 9 | class ModerationContext extends BaseContext { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {Message} message - The message 14 | * @param {Array} args - The parsed args 15 | * @param {GuildEntry} guildEntry - The guild database entry, if any 16 | * @param {UserEntry} userEntry - The user database entry 17 | */ 18 | constructor(client, message, args, guildEntry, userEntry) { 19 | super(client, message, args, guildEntry, userEntry); 20 | } 21 | } 22 | 23 | module.exports = ModerationContext; 24 | 25 | -------------------------------------------------------------------------------- /commands/music/shuffle.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class Shuffle extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'shuffle', 8 | description: 'Shuffle the queue', 9 | usage: '{prefix}shuffle ' 10 | } 11 | }, { userInVC: true }); 12 | } 13 | /** 14 | * @param {import("../../structures/Contexts/MusicContext")} context The context 15 | */ 16 | 17 | async run(context) { 18 | if (!context.connection || !context.connection.queue[0]) { 19 | return context.message.channel.createMessage(`:x: There is nothing in the queue to shuffle`); 20 | } 21 | context.connection.editQueue([...context.connection.queue].sort(() => Math.random() - Math.random())); 22 | return context.message.channel.createMessage(`:musical_note: Successfully shuffled the queue`); 23 | } 24 | } 25 | 26 | module.exports = Shuffle; -------------------------------------------------------------------------------- /structures/CommandCategories/EconomyCommands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../../main.js").Client} Client 3 | * @typedef {import("../Command.js").PartialCommandOptions} PartialCommandOptions 4 | */ 5 | 6 | const Command = require('../Command'); 7 | 8 | class EconomyCommands extends Command { 9 | /** 10 | * 11 | * @param {Client} client - The client instance 12 | * @param {PartialCommandOptions} commandOptions - The general command configuration 13 | * @param {{noArgs: string}} [options] - `noArgs` specify a message to return if no arguments are provided. 14 | * These args will make the command handler act before running the command 15 | */ 16 | constructor(client, commandOptions, options = {}) { 17 | super(client, { ...commandOptions, category: { 18 | name: 'Economy', 19 | emote: 'moneybag', 20 | conf: { 21 | requireDB: true 22 | } 23 | }}); 24 | this.options = options; 25 | } 26 | } 27 | 28 | module.exports = EconomyCommands; -------------------------------------------------------------------------------- /structures/CommandCategories/SettingsCommands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../../main.js").Client} Client 3 | * @typedef {import("../Command.js").PartialCommandOptions} PartialCommandOptions 4 | */ 5 | 6 | const Command = require('../Command'); 7 | 8 | class SettingsCommands extends Command { 9 | /** 10 | * 11 | * @param {Client} client - The client instance 12 | * @param {PartialCommandOptions} commandOptions - The general command configuration 13 | * @param {{noArgs: string}} [options] - `noArgs` specify a message to return if no arguments are provided. 14 | * These args will make the command handler act before running the command 15 | */ 16 | constructor(client, commandOptions, options = {}) { 17 | super(client, { ...commandOptions, category: { 18 | name: 'Settings', 19 | emote: 'gear', 20 | conf: { 21 | requireDB: true, 22 | guildOnly: true 23 | } 24 | }}); 25 | this.options = options; 26 | } 27 | } 28 | 29 | module.exports = SettingsCommands; -------------------------------------------------------------------------------- /commands/music/nowplaying.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class NowPlaying extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'nowplaying', 8 | description: 'Check the currently playing song', 9 | usage: '{prefix}nowplaying' 10 | }, 11 | conf: { aliases: ['np'] } 12 | }, { playing: true }); 13 | } 14 | 15 | /** 16 | * @param {import("../../structures/Contexts/MusicContext")} context The context 17 | */ 18 | 19 | async run(context) { 20 | const genericEmbed = await this.genericEmbed(context.currentTrack, context.connection, 'Now playing'); 21 | const node = context.client.config.options.music.nodes.find(n => n.host === context.connection.player.node.host); 22 | genericEmbed.fields.push({ 23 | name: 'Node', 24 | value: `${node.countryEmote} ${node.location}`, 25 | inline: true 26 | }); 27 | return context.message.channel.createMessage({ embed: genericEmbed }); 28 | } 29 | } 30 | 31 | module.exports = NowPlaying; -------------------------------------------------------------------------------- /commands/fun/choose.js: -------------------------------------------------------------------------------- 1 | const FunCommands = require('../../structures/CommandCategories/FunCommands'); 2 | 3 | class Choose extends FunCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'choose', 8 | description: 'Make felix choose between some stuff', 9 | usage: '{prefix}choose ; ; ', 10 | }, 11 | }, { noArgs: `:x: Well, I need some stuff to choose from, I can't choose from nothing sadly :v` }); 12 | } 13 | 14 | /** @param {import("../../structures/Contexts/FunContext")} context */ 15 | 16 | async run(context) { 17 | let choices = context.args.join(' ').split(/;/g).filter(c => c && c !== ' '); //Filter empty choices :^) 18 | if (choices.length < 2) { 19 | return context.message.channel.createMessage(`:x: Welp I need to choose from at least two things, I mean what's the point in choosing between only one thing?`); 20 | } 21 | let choice = choices[Math.floor(Math.random() * choices.length)].trim(); 22 | context.message.channel.createMessage(`I choose \`${choice}\`!`); 23 | } 24 | } 25 | 26 | module.exports = Choose; -------------------------------------------------------------------------------- /commands/admin/removedonator.js: -------------------------------------------------------------------------------- 1 | const AdminCommands = require('../../structures/CommandCategories/AdminCommands'); 2 | 3 | class RemoveDonator extends AdminCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'removedonator', 8 | description: 'Remove the premium status of a user', 9 | usage: '{prefix}removedonator ' 10 | }, 11 | conf: { 12 | aliases: ['removedonor', 'removepatron', "rmdonor", "rmpatron"], 13 | requireDB: true 14 | } 15 | }); 16 | } 17 | /** @param {import("../../structures/Contexts/AdminContext")} context */ 18 | 19 | async run(context) { 20 | const donator = await context.client.handlers.DatabaseWrapper.getUser(context.args[0]); 21 | donator.premium = context.client.structures.References.userEntry(context.args[0]).premium; 22 | await context.client.handlers.DatabaseWrapper.set(donator); 23 | const user = await context.client.utils.helpers.fetchUser(context.args[0]); 24 | return context.message.channel.createMessage(`:white_check_mark: Successfully disabled the premium status of the user **${user.tag}**`); 25 | } 26 | } 27 | 28 | module.exports = RemoveDonator; -------------------------------------------------------------------------------- /commands/admin/connect.js: -------------------------------------------------------------------------------- 1 | const AdminCommands = require('../../structures/CommandCategories/AdminCommands'); 2 | 3 | class Connect extends AdminCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'connect', 8 | description: 'Connect to the database, in case the bot was launched with the --no-db arg, this allow for a connection to the db', 9 | usage: '{prefix}connect' 10 | } 11 | }); 12 | } 13 | /** @param {import("../../structures/Contexts/AdminContext")} context */ 14 | 15 | async run(context) { 16 | if (context.client.handlers.DatabaseWrapper && context.client.handlers.DatabaseWrapper.healthy) { 17 | return context.message.channel.createMessage('Are you a baka? Im already connected to the database'); 18 | } 19 | this.client.handlers.DatabaseWrapper = this.client.handlers.DatabaseWrapper 20 | ? this.client.handlers.DatabaseWrapper._reload() 21 | : new(require('../../handlers/DatabaseWrapper'))(this.client); 22 | return context.message.channel.createMessage('Welp I launched the connection process, can\'t do much more tho so check the console to see if it worked lul'); 23 | } 24 | } 25 | 26 | module.exports = Connect; -------------------------------------------------------------------------------- /structures/ExtendedStructures/ExtendedUser.js: -------------------------------------------------------------------------------- 1 | const User = require("eris").User; 2 | 3 | /** 4 | * @extends User 5 | */ 6 | class ExtendedUser extends User { 7 | /** 8 | * 9 | * @param {User} user 10 | * @param {import("../../main.js")} client - The client instance 11 | */ 12 | 13 | constructor(user, client) { 14 | super(user, client.bot); 15 | } 16 | 17 | /** 18 | * @returns {String} Returns the Username#Discriminator of the user 19 | */ 20 | get tag() { 21 | return `${this.username}#${this.discriminator}`; 22 | } 23 | 24 | /** 25 | * Sends a DM to the user 26 | * @param {import("eris").MessageContent} message The message content 27 | * @param {import("eris").MessageFile} file A file to upload along 28 | * @returns {Promise} The sent message 29 | */ 30 | 31 | createMessage(message, file) { 32 | return this.getDMChannel().then(c => { 33 | return c.createMessage(message, file); 34 | }); 35 | } 36 | 37 | /** 38 | * @returns {String} The CDN URL of the default avatar of this user 39 | */ 40 | get defaultCDNAvatar() { 41 | return `https://cdn.discordapp.com/embed/avatars/${this.discriminator % 5}.png`; 42 | } 43 | } 44 | 45 | module.exports = ExtendedUser; -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: npm i --production 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: npm test 38 | - run: npm run eslint-check 39 | 40 | notify: 41 | webhooks: 42 | - url: https://skyhook.glitch.me/api/webhooks/464471103289425929/vbShJ1KL9rHMo5KII2LSwkYtkhirhhxwRPOC9iH_bPwln-QOc2XUoICphFaTtkjqhyO3/circleci -------------------------------------------------------------------------------- /commands/music/removesong.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class RemoveSong extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'removesong', 8 | description: 'Remove the song at the specified position in the queue', 9 | usage: '{prefix}removesong ' 10 | }, 11 | conf: { aliases: ['rs'] } 12 | }, { userInVC: true, playing: true }); 13 | } 14 | /** 15 | * @param {import("../../structures/Contexts/MusicContext")} context The context 16 | */ 17 | 18 | async run(context) { 19 | let position = context.args[0]; 20 | if (!this.isValidPosition(position, context.connection.queue)) { 21 | return context.message.channel.createMessage(':x: You did not specify a valid number ! You must specify a number corresponding to the position in the queue of the song you want to skip to'); 22 | } 23 | position = parseInt(position) - 1; 24 | const removedTrack = context.connection.removeTrack(position); 25 | return context.message.channel.createMessage(`:white_check_mark: Successfully removed the track \`${removedTrack.info.title}\` by \`${removedTrack.info.author}\``); 26 | } 27 | } 28 | 29 | module.exports = RemoveSong; -------------------------------------------------------------------------------- /commands/music/clearqueue.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class ClearQueue extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'clearqueue', 8 | description: 'Clear the queue', 9 | usage: '{prefix}clearqueue' 10 | }, 11 | conf: { aliases: ['cq'] } 12 | }); 13 | } 14 | /** 15 | * @param {import("../../structures/Contexts/MusicContext")} context The context 16 | */ 17 | 18 | async run(context) { 19 | if (!context.connection || !context.connection.queue[0]) { 20 | const queue = await this.client.handlers.MusicManager.getQueueOf(context.message.channel.guild.id); 21 | if (queue[0]) { 22 | await this.client.handlers.RedisManager.del(`${context.message.channel.guild.id}-queue`); 23 | return context.message.channel.createMessage(':white_check_mark: Successfully cleared the queue'); 24 | } 25 | return context.message.channel.createMessage(':x: There is nothing in the queue'); 26 | } 27 | await context.connection.clearQueue(); 28 | return context.message.channel.createMessage(`:white_check_mark: Successfully cleared the queue `); 29 | } 30 | } 31 | 32 | module.exports = ClearQueue; -------------------------------------------------------------------------------- /commands/admin/execute.js: -------------------------------------------------------------------------------- 1 | const { inspect } = require('util'); 2 | const AdminCommands = require('../../structures/CommandCategories/AdminCommands'); 3 | const { exec } = require('child_process'); 4 | 5 | class Execute extends AdminCommands { 6 | constructor(client) { 7 | super(client, { 8 | help: { 9 | name: 'execute', 10 | description: 'execute, i think it\'s fairly obvious at this point', 11 | usage: '{prefix}execute' 12 | }, 13 | conf: { 14 | aliases: ['exec', 'shell'], 15 | } 16 | }, { noArgs: 'baguette tbh' }); 17 | } 18 | /** @param {import("../../structures/Contexts/AdminContext")} context */ 19 | 20 | async run(context) { 21 | exec(context.args.join(' '), (error, stdout) => { 22 | const outputType = error || stdout; 23 | let output = outputType; 24 | if (typeof outputType === 'object') { 25 | output = inspect(outputType, { 26 | depth: this.getMaxDepth(outputType, context.args.join(' ')) 27 | }); 28 | } 29 | output = context.client.utils.helpers.redact(output.length > 1980 ? output.substr(0, 1977) + '...' : output); 30 | return context.message.channel.createMessage('```\n' + output + '```'); 31 | exec.kill(); 32 | }); 33 | } 34 | } 35 | 36 | module.exports = Execute; -------------------------------------------------------------------------------- /commands/music/forceskipto.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class ForceSkipTo extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'forceskipto', 8 | description: 'Force-skip to the specified position in the queue', 9 | usage: '{prefix}forceskipto ' 10 | }, 11 | conf: { 12 | aliases: ['fskipto'], 13 | expectedArgs: [{ 14 | description: 'Please specify the position in the queue of the song you want to skip to' 15 | }] 16 | } 17 | }, { userInVC: true, playing: true }); 18 | } 19 | /** @param {import("../../structures/Contexts/MusicContext.js")} context */ 20 | 21 | async run(context) { 22 | if (!this.isValidPosition(context.args[0], context.connection.queue)) { 23 | return context.message.channel.createMessage(':x: The specified position isn\'t valid :v'); 24 | } 25 | const skippedTo = context.connection.queue[parseInt(context.args[0]) - 1]; 26 | context.connection.skipTrack(parseInt(context.args[0]) - 1); 27 | return context.message.channel.createMessage(`:white_check_mark: Succesfully skipped to the song \`${skippedTo.info.title}\` by \`${skippedTo.info.author}\``); 28 | } 29 | } 30 | 31 | module.exports = ForceSkipTo; -------------------------------------------------------------------------------- /commands/music/deleteplaylist.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class DeletePlaylist extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'deleteplaylist', 8 | description: 'Delete one of your saved playlists', 9 | usage: '{prefix}deleteplaylist ' 10 | }, 11 | conf: { requireDB: true, expectedArgs: [{description: 'Please specify the ID of the playlist to delete'}] } 12 | }); 13 | } 14 | /** 15 | * @param {import("../../structures/Contexts/MusicContext")} context The context 16 | */ 17 | 18 | async run(context) { 19 | const playlist = await this.client.handlers.DatabaseWrapper.rethink.table("playlists").get(context.args[0]).run(); 20 | if (!playlist) { 21 | return context.message.channel.createMessage(':x: I couldn\'t find any playlist with this ID'); 22 | } else if (playlist.userID !== context.message.author.id) { 23 | return context.message.channel.createMessage(':x: You can\'t delete a playlist of someone else, that\'s like, bad'); 24 | } 25 | await this.client.handlers.DatabaseWrapper.rethink.table("playlists").get(playlist.id).delete(playlist.id).run(); 26 | return context.message.channel.createMessage(`:white_check_mark: Successfully deleted the playlist \`${playlist.name}\``); 27 | } 28 | } 29 | 30 | module.exports = DeletePlaylist; -------------------------------------------------------------------------------- /handlers/index.js: -------------------------------------------------------------------------------- 1 | /** @typedef {Object} Handlers 2 | * @prop {import("./DatabaseWrapper.js")} DatabaseWrapper The database wrapper 3 | * @prop {import("./EconomyManager.js")} EconomyManager The economy handler 4 | * @prop {import("./ExperienceHandler.js")} ExperienceHandler The experience handler 5 | * @prop {import("./ImageHandler.js")} ImageHandler The image handler 6 | * @prop {import("./InteractiveList.js")} InteractiveList The interactive list interface 7 | * @prop {import("./IPCHandler.js")} IPCHandler The IPC handler 8 | * @prop {import("./MessageCollector.js")} MessageCollector The message collector 9 | * @prop {import("./MusicManager.js")} MusicManager The music manager 10 | * @prop {import("./ReactionCollector.js")} ReactionCollector The reaction collector 11 | * @prop {import("./RedisManager.js")} RedisManager The redis handler 12 | * @prop {import("./Reloader.js")} Reloader The reload handler 13 | */ 14 | 15 | module.exports = { 16 | DatabaseWrapper: require('./DatabaseWrapper.js'), 17 | EconomyManager: require('./EconomyManager.js'), 18 | ExperienceHandler: require('./ExperienceHandler.js'), 19 | ImageHandler: require('./ImageHandler.js'), 20 | InteractiveList: require('./InteractiveList.js'), 21 | IPCHandler: require('./IPCHandler.js'), 22 | MessageCollector: require('./MessageCollector.js'), 23 | MusicManager: require('./MusicManager.js'), 24 | ReactionCollector: require('./ReactionCollector.js'), 25 | RedisManager: require('./RedisManager.js'), 26 | Reloader: require('./Reloader.js') 27 | }; -------------------------------------------------------------------------------- /commands/music/setvolume.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class SetVolume extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'setvolume', 8 | description: 'Donator only. Set the volume of the current playback, Accepted values are between 1 and 200', 9 | usage: '{prefix}setvolume ' 10 | }, 11 | aliases: ['setvol'] 12 | }, { userInVC: true, playing: true, noArgs: ':x: You did not specify the volume to set' }); 13 | } 14 | /** 15 | * @param {import("../../structures/Contexts/MusicContext")} context The context 16 | */ 17 | 18 | async run(context) { 19 | if (!context.userEntry.hasPremiumStatus()) { 20 | return context.message.channel.createMessage(':x: I am terribly sorry but this command is restricted to donators :v, you can get a link to my patreon with the `bot` command'); 21 | } 22 | let volume = context.args[0]; 23 | if (!this.client.utils.isWholeNumber(volume) || volume > 200 || volume < 1) { 24 | return context.message.channel.createMessage(':x: The specified volume is invalid ! It must be a whole number between 1 and 200'); 25 | } 26 | context.connection.player.setVolume(parseInt(volume)); 27 | return context.message.channel.createMessage(`:white_check_mark: Successfully set the volume at \`${volume}\``); 28 | } 29 | } 30 | 31 | module.exports = SetVolume; -------------------------------------------------------------------------------- /commands/image/triggered_gen.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const GenericCommands = require('../../structures/CommandCategories/ImageCommands'); 3 | 4 | class TriggeredGen extends GenericCommands { 5 | constructor(client) { 6 | super(client, { 7 | help: { 8 | name: 'triggered_gen', 9 | description: 'Generate a triggered image with the avatar of the specified user, or yours if nobody is specified', 10 | usage: '{prefix}triggered_gen ', 11 | subCategory: 'image-generation' 12 | }, 13 | conf: { 14 | aliases: ['trig_gen', 'triggeredgen', 'triggen'], 15 | requirePerms: ['attachFiles'], 16 | guildOnly: true, 17 | require: ['weebSH', 'taihou'] 18 | }, 19 | }); 20 | } 21 | /** @param {import("../../structures/Contexts/ImageContext")} context */ 22 | 23 | async run(context) { 24 | const user = await this.getUserFromText({ message: context.message, client: context.client, text: context.args.join(' ') }); 25 | const target = user || context.message.author; 26 | const image = await axios.get(`https://cute-api.tk/v1/generate/triggered?url=${target.avatarURL || target.defaultCDNAvatar}`, {responseType: 'arraybuffer'}); 27 | return context.message.channel.createMessage(``, { 28 | file: image.data, 29 | name: `${Date.now()}-${context.message.author.id}.gif` 30 | }); 31 | } 32 | } 33 | 34 | module.exports = TriggeredGen; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "felix", 3 | "version": "4.4.16", 4 | "description": "felix-production", 5 | "main": "index.js", 6 | "author": "ParadoxOrigins", 7 | "contributors": [ 8 | { 9 | "name": "Niputi" 10 | }, { 11 | "name": "Otaku17" 12 | } 13 | ], 14 | "repository": "github:ParadoxalCorp/felix-production", 15 | "license": "GPL-3.0", 16 | "scripts": { 17 | "start": "node index.js", 18 | "eslint-check": "eslint .", 19 | "test": "nyc mocha" 20 | }, 21 | "dependencies": { 22 | "axios": "^0.18.0", 23 | "chalk": "^2.3.1", 24 | "draftlog": "^1.0.12", 25 | "eris": "^0.8.5", 26 | "eris-sharder": "^1.7.9", 27 | "moment": "^2.22.1", 28 | "npm": "^6.0.0", 29 | "rethinkdbdash": "^2.3.31" 30 | }, 31 | "optionalDependencies": { 32 | "canvas": "2.0.0-alpha.12", 33 | "canvas-constructor": "^1.0.1", 34 | "eris-lavalink": "^1.0.2", 35 | "eventemitter3": "^3.1.0", 36 | "fs-nextra": "^0.3.5", 37 | "google-translate-api": "^2.3.0", 38 | "ioredis": "^3.2.2", 39 | "mocha": "^5.2.0", 40 | "mock-stdin": "^0.3.1", 41 | "nyc": "^12.0.2", 42 | "mal-scraper": "^2.4.2", 43 | "raven": "^2.6.0", 44 | "taihou": "^2.0.2", 45 | "sharp": "^0.20.5" 46 | }, 47 | "devDependencies": { 48 | "@types/ioredis": "^3.2.11", 49 | "@types/mocha": "^5.2.5", 50 | "@types/node": "^10.0.0", 51 | "@types/sharp": "^0.17.9", 52 | "eslint": "^5.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/timeConverter.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | const timeConverter = require('../utils/TimeConverter'); 4 | const assert = require('assert').strict; 5 | 6 | describe('TimeConverter', function () { 7 | describe('#toElapsedTime()', function () { 8 | it('Should return the expected object structure', function () { 9 | assert.deepEqual(timeConverter.toElapsedTime(0), { 10 | days: 0, 11 | hours: 0, 12 | minutes: 0, 13 | seconds: 0 14 | }); 15 | }); 16 | 17 | it("typeof non-readable format", () => { 18 | assert.deepEqual(typeof timeConverter.toElapsedTime(1934329731689, true), "string"); 19 | }); 20 | }); 21 | 22 | describe('#toHumanDate()', function () { 23 | it('Should return the expected object structure', function () { 24 | assert.deepEqual(timeConverter.toHumanDate(0, false), { 25 | seconds: 0, 26 | minutes: 0, 27 | hours: timeConverter.toHumanDate(0, false).hours === 0 ? 0 : 1, 28 | day: 1, 29 | month: "January", 30 | year: 1970, 31 | }); 32 | }); 33 | 34 | it('typeof toHumanDate() formatted string', function () { 35 | assert.deepEqual(typeof timeConverter.toHumanDate(1534249942186, true), "string"); 36 | }); 37 | }); 38 | 39 | describe('#getMonth()', function () { 40 | it('get month name getMonth()', function () { 41 | assert.deepEqual(timeConverter.getMonth(new Date(1934329731689)), "April"); 42 | }); 43 | }); 44 | }); -------------------------------------------------------------------------------- /commands/admin/eval.js: -------------------------------------------------------------------------------- 1 | const { inspect } = require('util'); 2 | const AdminCommands = require('../../structures/CommandCategories/AdminCommands'); 3 | 4 | class Eval extends AdminCommands { 5 | constructor(client) { 6 | super(client, { 7 | help: { 8 | name: 'eval', 9 | description: 'eval, i think it\'s fairly obvious at this point', 10 | usage: '{prefix}eval' 11 | } 12 | }); 13 | } 14 | /** @param {import("../../structures/Contexts/AdminContext")} context */ 15 | 16 | async run(context) { 17 | if (!context.args[0]) { 18 | return context.message.channel.createMessage('baguette tbh'); 19 | } 20 | let toEval = context.args.join(' ').replace(/;\s+/g, ';\n').trim(); 21 | const parsedArgs = this.parseArguments(context.args); 22 | for (const arg in parsedArgs) { 23 | toEval = toEval.replace(`--${arg + (typeof parsedArgs[arg] !== 'boolean' ? '=' + parsedArgs[arg] : '')}`, ''); 24 | } 25 | try { 26 | let evaluated = parsedArgs['await'] ? await eval(toEval) : eval(toEval); 27 | throw evaluated; 28 | } catch (err) { 29 | if (typeof err !== 'string') { 30 | err = inspect(err, { 31 | depth: parsedArgs['depth'] ? parseInt(parsedArgs['depth']) : this.getMaxDepth(err, toEval), 32 | showHidden: true 33 | }); 34 | } 35 | return context.message.channel.createMessage("**Input:**\n```js\n" + toEval + "```\n**Output:**\n```js\n" + context.client.utils.helpers.redact(err) + "```"); 36 | } 37 | } 38 | } 39 | 40 | module.exports = Eval; -------------------------------------------------------------------------------- /commands/image/shitwaifu.js: -------------------------------------------------------------------------------- 1 | const GenericCommands = require('../../structures/CommandCategories/ImageCommands'); 2 | 3 | class ShitWaifu extends GenericCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'shitwaifu', 8 | description: 'Uh well, I think the name is pretty self-explanatory', 9 | usage: '{prefix}shitwaifu ', 10 | subCategory: 'image-generation' 11 | }, 12 | conf: { 13 | requirePerms: ['attachFiles'], 14 | guildOnly: true, 15 | require: ['weebSH', 'taihou'] 16 | }, 17 | }); 18 | } 19 | /** @param {import("../../structures/Contexts/ImageContext")} context */ 20 | 21 | async run(context) { 22 | const user = context.args[0] ? await this.getUserFromText({client: context.client, message: context.message, text: context.args[0]}) : context.message.author; 23 | let typing = false; 24 | //If the queue contains 2 items or more, expect that this request will take some seconds and send typing to let the user know 25 | if (context.client.weebSH.korra.requestHandler.queue.length >= 2) { 26 | context.client.bot.sendChannelTyping(context.message.channel.id); 27 | typing = true; 28 | } 29 | const generatedInsult = await context.client.weebSH.korra.generateWaifuInsult(this.useWebpFormat(user)).catch(this.handleError.bind(this, context, typing)); 30 | return context.message.channel.createMessage(typing ? `<@!${context.message.author.id}> ` : '', {file: generatedInsult, name: `${Date.now()}-${context.message.author.id}.png`}); 31 | } 32 | } 33 | 34 | module.exports = ShitWaifu; -------------------------------------------------------------------------------- /commands/moderation/clearpermissions.js: -------------------------------------------------------------------------------- 1 | const ModerationCommands = require('../../structures/CommandCategories/ModerationCommands'); 2 | 3 | class ClearPermissions extends ModerationCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'clearpermissions', 8 | description: 'Clear all the permissions set until now, global, channels, roles and users permissions included', 9 | usage: '{prefix}clearpermissions', 10 | externalDoc: 'https://github.com/ParadoxalCorp/felix-production/blob/master/usage.md#permissions-system' 11 | }, 12 | conf: { 13 | aliases: ['clearperms', 'nukeperms', 'cp'], 14 | requireDB: true, 15 | guildOnly: true, 16 | }, 17 | }); 18 | } 19 | 20 | /** @param {import("../../structures/Contexts/ModerationContext")} context */ 21 | 22 | async run(context) { 23 | await context.message.channel.createMessage('Are you sure you want to do that? Reply with `yes` to confirm or anything else to abort'); 24 | const confirmation = await context.client.handlers.MessageCollector.awaitMessage(context.message.channel.id, context.message.author.id); 25 | if (!confirmation || confirmation.content.toLowerCase().trim() !== 'yes') { 26 | return context.message.channel.createMessage(':x: Command aborted'); 27 | } 28 | context.guildEntry.permissions = context.client.structures.References.guildEntry('1').permissions; 29 | await context.client.handlers.DatabaseWrapper.set(context.guildEntry, 'guild'); 30 | return context.message.channel.createMessage(':white_check_mark: Successfully cleared all permissions'); 31 | } 32 | } 33 | 34 | module.exports = ClearPermissions; -------------------------------------------------------------------------------- /commands/music/seeplaylists.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class SeePlaylists extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'seeplaylists', 8 | description: 'See your saved playlists', 9 | usage: '{prefix}seeplaylists' 10 | }, 11 | conf: { requireDB: true } 12 | }); 13 | } 14 | /** 15 | * @param {import("../../structures/Contexts/MusicContext")} context The context 16 | */ 17 | 18 | async run(context) { 19 | const userPlaylists = await this.client.handlers.DatabaseWrapper.rethink.table("playlists").filter({userID: context.message.author.id}).run(); 20 | if (!userPlaylists[0]) { 21 | return context.message.channel.createMessage(':x: You do not have any saved playlists :v, save one with the `saveplaylist` command'); 22 | } 23 | return context.message.channel.createMessage({ 24 | embed: { 25 | title: `:musical_note: ${context.message.author.username}'s saved playlists`, 26 | description: (() => { 27 | let playlists = '\n'; 28 | for (const playlist of userPlaylists) { 29 | playlists += `\`${playlist.name}\` (**${playlist.tracks.length}** tracks)\n**=>** ID: \`${playlist.id}\`\n\n`; 30 | } 31 | return playlists; 32 | })(), 33 | footer: { 34 | text: `To load a saved playlist, use "${this.getPrefix(context.guildEntry)}addplaylist "` 35 | }, 36 | color: 0x36393f 37 | } 38 | }); 39 | } 40 | } 41 | 42 | module.exports = SeePlaylists; -------------------------------------------------------------------------------- /events/guildMemberRemove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("eris").Guild} Guild 3 | * @typedef {import("eris").Member} Member 4 | * @typedef {import("../main.js")} Client 5 | */ 6 | 7 | /** 8 | * 9 | * 10 | * @class GuildMemberRemoveHandler 11 | */ 12 | class GuildMemberRemoveHandler { 13 | constructor() {} 14 | /** 15 | * 16 | * @param {Client} client Felix's client 17 | * @param {Guild} guild eris member 18 | * @param {Member} member Eris member 19 | * @returns {Promise} hi 20 | * @memberof GuildMemberRemoveHandler 21 | */ 22 | async handle(client, guild, member) { 23 | if (member.user.bot) { 24 | return; 25 | } 26 | const guildEntry = await client.handlers.DatabaseWrapper.getGuild(guild.id); 27 | if (!guildEntry) { 28 | return; 29 | } 30 | const user = new client.structures.ExtendedUser(member.user, client); 31 | //Farewells 32 | if (!guildEntry.farewells.channel || !guildEntry.farewells.enabled || !guildEntry.farewells.message) { 33 | return; 34 | } 35 | let message = this.replaceFarewellTags(guild, user, guildEntry.farewells.message); 36 | 37 | let channel = guild.channels.get(guildEntry.farewells.channel); 38 | if (!channel || channel.type !== 0) { 39 | return; 40 | } 41 | //@ts-ignore 42 | channel.createMessage(message).catch(() => {}); 43 | } 44 | 45 | replaceFarewellTags(guild, user, message) { 46 | return message.replace(/\%USERNAME\%/gim, `${user.username}`) 47 | .replace(/\%USERTAG%/gim, `${user.tag}`) 48 | .replace(/\%GUILD\%/gim, `${guild.name}`) 49 | .replace(/\%MEMBERCOUNT%/gim, guild.memberCount); 50 | } 51 | } 52 | 53 | module.exports = new GuildMemberRemoveHandler(); -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../main.js")} Client 3 | */ 4 | 5 | /** 6 | * @typedef Utils 7 | * @property {import("./log.js").Log} log Custom logger module 8 | * @property {import("./timeConverter.js").TimeConverter} timeConverter Provides methods to parse UNIX timestamps 9 | * @property {import("./sleep.js").sleep} sleep Reproduce PHP's sleep function 10 | * @property {import("./getRandomNumber.js").getRandomNumber} getRandomNumber Get a random integer between the specified interval 11 | * @property {import("./helpers.js")} helpers Some utility methods 12 | * @property {import("./paginate.js").paginate} paginate Split an array into multiples, used for pagination purposes 13 | * @property {import("./traverse.js").traverse} traverse Traverse through a given object 14 | * @property {import("./prompt.js").prompt} prompt Create a prompt in the command-line 15 | * @property {import("./isWholeNumber.js").isWholeNumber} isWholeNumber Check if the given number is a whole number 16 | * @property {import("./moduleIsInstalled.js").moduleIsInstalled} moduleIsInstalled Checks if the specified module is installed in node_modules 17 | */ 18 | 19 | /** 20 | * @param {Client} client client 21 | * @returns {Object} object of modules 22 | */ 23 | module.exports = (client) => { 24 | return { 25 | //In case of a complete reload of the modules, ignore the critical modules 26 | log: require('./log'), 27 | timeConverter: require('./TimeConverter.js'), 28 | sleep: require('./sleep.js'), 29 | getRandomNumber: require('./getRandomNumber'), 30 | paginate: require('./paginate'), 31 | traverse: require('./traverse'), 32 | prompt: require('./prompt'), 33 | isWholeNumber: require('./isWholeNumber'), 34 | moduleIsInstalled: require('./moduleIsInstalled'), 35 | helpers: new(require('./helpers'))(client) 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /commands/generic/avatar.js: -------------------------------------------------------------------------------- 1 | const GenericCommands = require('../../structures/CommandCategories/GenericCommands'); 2 | 3 | class Avatar extends GenericCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'avatar', 8 | description: 'Display and give a link to the avatar of the specified user, or to yours if nobody is specified', 9 | usage: '{prefix}avatar ', 10 | }, 11 | conf: { 12 | guildOnly: true 13 | } 14 | }); 15 | } 16 | /** @param {import("../../structures/Contexts/GenericContext")} context */ 17 | 18 | async run(context) { 19 | const user = await this.getUserFromText({ message: context.message, client: context.client, text: context.args.join(' ') }); 20 | const target = user || context.message.author; 21 | return context.message.channel.createMessage({ 22 | content: `${context.emote('picture')} The avatar of **${target.username}**`, 23 | embed: { 24 | color: context.client.config.options.embedColor.generic, 25 | author: { 26 | name: `Requested by: ${context.message.author.username}#${context.message.author.discriminator}`, 27 | icon_url: context.message.author.avatarURL 28 | }, 29 | title: `Link to the avatar`, 30 | url: target.avatarURL || target.defaultCDNAvatar, 31 | image: { 32 | url: target.avatarURL || target.defaultCDNAvatar 33 | }, 34 | timestamp: new Date(), 35 | footer: { 36 | text: context.client.bot.user.username, 37 | icon_url: context.client.bot.user.avatarURL 38 | } 39 | } 40 | }); 41 | } 42 | } 43 | 44 | module.exports = Avatar; 45 | -------------------------------------------------------------------------------- /structures/CommandCategories/ImageCommands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../../main.js").Client} Client 3 | * @typedef {import("../Command.js").PartialCommandOptions} PartialCommandOptions 4 | * @typedef {import("../ExtendedStructures/ExtendedUser")} ExtendedUser 5 | */ 6 | 7 | const Command = require('../Command'); 8 | 9 | class ImageCommands extends Command { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {PartialCommandOptions} commandOptions - The general command configuration 14 | * @param {{noArgs: string}} [options] - `noArgs` specify a message to return if no arguments are provided. 15 | * These args will make the command handler act before running the command 16 | */ 17 | constructor(client, commandOptions, options = {}) { 18 | super(client, { ...commandOptions, category: { 19 | name: 'Image', 20 | conf: { 21 | require: ['weebSH', 'taihou'], 22 | requirePerms: ['embedLinks'], 23 | }, 24 | emote: 'picture' 25 | }}); 26 | this.options = options; 27 | } 28 | 29 | /** 30 | * 31 | * @param {ImageContext} context - The context 32 | * @param {Boolean} typing - Whether the bot is typing right now 33 | * @param {*} error - The error 34 | * @returns {void} 35 | */ 36 | handleError(context, typing, error) { 37 | if (typing) { 38 | context.client.bot.sendChannelTyping(context.message.channel.id); 39 | } 40 | throw error; 41 | } 42 | 43 | /** 44 | * If possible, get the user's avatar URL in `.webp` format 45 | * @param {ExtendedUser} user - The user 46 | * @returns {String} The user's avatar URL 47 | */ 48 | useWebpFormat(user) { 49 | return user.avatarURL ? user.avatarURL.replace(/.jpeg|.jpg|.png|.gif/g, '.webp') : user.defaultCDNAvatar; 50 | } 51 | 52 | } 53 | 54 | module.exports = ImageCommands; -------------------------------------------------------------------------------- /commands/generic/invite.js: -------------------------------------------------------------------------------- 1 | const GenericCommands = require('../../structures/CommandCategories/GenericCommands'); 2 | 3 | class Invite extends GenericCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'invite', 8 | description: 'Get Felix\'s invite link', 9 | usage: '{prefix}invite', 10 | } 11 | }); 12 | } 13 | /** @param {import("../../structures/Contexts/GenericContext")} context */ 14 | 15 | async run(context) { 16 | context.message.channel.createMessage({ 17 | content: `Here's my invite link :wave:`, 18 | embed: { 19 | color: context.client.config.options.embedColor.generic, 20 | author: { 21 | name: `Requested by: ${context.message.author.username}#${context.message.author.discriminator}`, 22 | icon_url: context.message.author.avatarURL 23 | }, 24 | description: `[Invitation link](https://discordapp.com/oauth2/authorize?&client_id=${context.client.bot.user.id}&scope=bot&permissions=2146950271)\n**Please remember that I might not work perfectly if I dont have all permissions~**`, 25 | thumbnail: { 26 | url: context.client.bot.user.avatarURL 27 | }, 28 | fields: [{ 29 | name: "Servers/Guilds", 30 | value: context.client.bot.guilds.size, 31 | inline: true 32 | },{ 33 | name: "Users/Members", 34 | value: context.client.bot.users.size, 35 | inline: true 36 | }], 37 | timestamp: new Date(), 38 | footer: { 39 | text: context.client.bot.user.username, 40 | icon_url: context.client.bot.user.avatarURL 41 | } 42 | } 43 | }); 44 | } 45 | } 46 | 47 | module.exports = Invite; 48 | -------------------------------------------------------------------------------- /structures/Contexts/MusicContext.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main.js").Client} Client 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("../References.js").GuildEntry & import("../ExtendedStructures/ExtendedGuildEntry.js")} GuildEntry 4 | * @typedef {import("../References.js").UserEntry & import("../ExtendedStructures/ExtendedUserEntry.js")} UserEntry 5 | * @typedef {import("../HandlersStructures/MusicConnection.js")} MusicConnection 6 | * @typedef {import("../HandlersStructures/MusicConnection.js").FelixTrack} FelixTrack 7 | * @typedef {import("eris").VoiceChannel} VoiceChannel 8 | */ 9 | 10 | const BaseContext = require('./BaseContext'); 11 | 12 | class MusicContext extends BaseContext { 13 | /** 14 | * 15 | * @param {Client} client - The client instance 16 | * @param {Message} message - The message 17 | * @param {Array} args - The parsed args 18 | * @param {GuildEntry} guildEntry - The guild database entry, if any 19 | * @param {UserEntry} userEntry - The user database entry 20 | */ 21 | constructor(client, message, args, guildEntry, userEntry) { 22 | super(client, message, args, guildEntry, userEntry); 23 | /** @type {MusicConnection} The MusicConnection instance for this guild, if any */ 24 | this.connection = client.handlers.MusicManager.connections.get(message.channel.guild.id); 25 | /** @type {FelixTrack} The now playing track on this guild, if any */ 26 | this.currentTrack = this.connection ? this.connection.nowPlaying : null; 27 | /** @type {VoiceChannel} The voice channel the bot is in, if any */ 28 | this.clientVC = message.channel.guild.channels.get(message.channel.guild.members.get(this.client.bot.user.id).voiceState.channelID); 29 | /** @type {VoiceChannel} The voice channel the user is in, if any */ 30 | this.userVC = message.channel.guild.channels.get(message.channel.guild.members.get(message.author.id).voiceState.channelID); 31 | } 32 | } 33 | 34 | module.exports = MusicContext; 35 | 36 | -------------------------------------------------------------------------------- /commands/settings/setprefix.js: -------------------------------------------------------------------------------- 1 | const SettingsCommands = require("../../structures/CommandCategories/SettingsCommands"); 2 | 3 | class SetPrefix extends SettingsCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: "setprefix", 8 | description: "Set a custom prefix for commands, if you want the prefix to not contain a space between the prefix and the command, use `{prefix}setprefix unspaced` so like `{prefix}setprefix ! unspaced` will make commands look like `!ping`", 9 | usage: "{prefix}setprefix " 10 | } 11 | }); 12 | } 13 | 14 | /** @param {import("../../structures/Contexts/SettingsContext")} context */ 15 | 16 | async run(context) { 17 | const spaced = ( 18 | context.args[1] 19 | ? context.args[1].toLowerCase() 20 | : context.args[1]) === "unspaced" 21 | ? false 22 | : true; 23 | if (!context.args[0]) { 24 | return context.message.channel.createMessage(`The current prefix on this server is \`${context.guildEntry.getPrefix}\``); 25 | } 26 | if (context.args[0] === `<@${context.client.bot.user.id}>` || context.args[0] === `<@!${context.client.bot.user.id}>`) { 27 | return context.message.channel.createMessage(`:x: Ahhh yes but no im sorry this prefix cannot be chosen as it is a default prefix`); 28 | } 29 | context.guildEntry.prefix = context.args[0] === context.client.config.prefix && spaced 30 | ? "" 31 | : context.args[0]; 32 | context.guildEntry.spacedPrefix = spaced; 33 | await context.client.handlers.DatabaseWrapper.set(context.guildEntry, "guild"); 34 | return context.message.channel.createMessage( 35 | `:white_check_mark: Alright, the prefix has successfully been set as a ${spaced 36 | ? "spaced" 37 | : "unspaced"} prefix to \`${context.args[0]}\`, commands will now look like \`${context.args[0] + ( 38 | spaced 39 | ? " " 40 | : "")}ping\``); 41 | } 42 | } 43 | 44 | module.exports = SetPrefix; -------------------------------------------------------------------------------- /events/guildMemberAdd.js: -------------------------------------------------------------------------------- 1 | class GuildMemberAddHandler { 2 | constructor() {} 3 | 4 | async handle(client, guild, member) { 5 | if (member.user.bot) { 6 | return; 7 | } 8 | const guildEntry = await client.handlers.DatabaseWrapper.getGuild(guild.id); 9 | if (!guildEntry) { 10 | return; 11 | } 12 | const clientMember = guild.members.get(client.bot.user.id); 13 | const user = new client.structures.ExtendedUser(member.user, client ); 14 | //On join role 15 | if (guildEntry.onJoinRoles[0] && clientMember.permission.has('manageRoles')) { 16 | this.addRoles(guild, member, guildEntry).catch(() => {}); 17 | } 18 | //Greetings 19 | if (!guildEntry.greetings.channel || !guildEntry.greetings.enabled || !guildEntry.greetings.message) { 20 | return; 21 | } 22 | let message = this.replaceGreetingsTags(guild, user, guildEntry.greetings.message); 23 | let channel = guildEntry.greetings.channel === "dm" ? undefined : guild.channels.get(guildEntry.greetings.channel); 24 | if (guildEntry.greetings.channel !== 'dm' && (!channel || channel.type !== 0)) { 25 | return; 26 | } 27 | if (guildEntry.greetings.channel === "dm") { 28 | user.createMessage(message).catch(() => {}); 29 | } else { 30 | channel.createMessage(message).catch(() => {}); 31 | } 32 | } 33 | 34 | async addRoles(guild, member, guildEntry) { 35 | const existingRoles = guildEntry.onJoinRoles.filter(r => guild.roles.has(r)); //Filter roles that are no more 36 | if (existingRoles[0]) { 37 | await Promise.all(existingRoles.map(r => member.addRole(r, `The role is set to be given to new members`))).catch(() => {}); 38 | } 39 | } 40 | 41 | replaceGreetingsTags(guild, user, message) { 42 | return message.replace(/\%USER\%/gim, `<@!${user.id}>`) 43 | .replace(/\%USERNAME\%/gim, `${user.username}`) 44 | .replace(/\%USERTAG%/gim, `${user.tag}`) 45 | .replace(/\%GUILD\%/gim, `${guild.name}`) 46 | .replace(/\%MEMBERCOUNT%/gim, guild.memberCount); 47 | } 48 | } 49 | 50 | module.exports = new GuildMemberAddHandler(); -------------------------------------------------------------------------------- /commands/economy/navalbase.js: -------------------------------------------------------------------------------- 1 | const EconomyCommands = require('../../structures/CommandCategories/EconomyCommands'); 2 | 3 | class NavalBase extends EconomyCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'navalbase', 8 | description: 'Check your fleet', 9 | usage: '{prefix}navalbase' 10 | }, 11 | conf: { 12 | aliases: ['fleet', 'port', 'nb', 'base'] 13 | }, 14 | }); 15 | } 16 | /** @param {import("../../structures/Contexts/EconomyContext")} context */ 17 | 18 | async run(context) { 19 | if (!context.userEntry.economy.items.filter(i => context.client.handlers.EconomyManager.getItem(i.id).family === "Ships")[0]) { 20 | return context.message.channel.createMessage(`:x: Sorry, but it seems like you don't own any ship yet :c`); 21 | } 22 | return context.message.channel.createMessage({ 23 | embed: { 24 | title: ':ship: Naval Base - Fleet overview', 25 | fields: (() => { 26 | let typesOwned = []; 27 | for (const item of context.userEntry.economy.items) { 28 | if (context.client.handlers.EconomyManager.getItem(item.id).data && !typesOwned.includes(context.client.handlers.EconomyManager.getItem(item.id).data.type) && context.client.handlers.EconomyManager.getItem(item.id).family === 'Ships') { 29 | typesOwned.push(context.client.handlers.EconomyManager.getItem(item.id).data.type); 30 | } 31 | } 32 | typesOwned = typesOwned.map(t => { 33 | return { 34 | name: `${t}(s)`, 35 | value: context.client.handlers.EconomyManager.marketItems.filter(i => i.data && i.data.type === t && context.userEntry.hasItem(i.id)).map(i => i.name).join(', ') 36 | }; 37 | }); 38 | 39 | return typesOwned; 40 | })(), 41 | color: context.client.config.options.embedColor.generic 42 | } 43 | }); 44 | } 45 | } 46 | 47 | module.exports = NavalBase; -------------------------------------------------------------------------------- /structures/index.js: -------------------------------------------------------------------------------- 1 | /** @typedef {Object} Structures 2 | * @prop {import("./ExtendedStructures/ExtendedUser.js")} ExtendedUser An extended eris user 3 | * @prop {import("./ExtendedStructures/ExtendedMessage.js").ExtendedMessage} ExtendedMessage An extended eris message 4 | * @prop {import("./ExtendedStructures/ExtendedUserEntry")} ExtendedUserEntry An extended user database entry 5 | * @prop {import("./ExtendedStructures/ExtendedGuildEntry")} ExtendedGuildEntry An extended guild database entry 6 | * @prop {import("./CommandCategories/MusicCommands.js")} MusicCommands The MusicCommands category 7 | * @prop {import("./HandlersStructures/MusicConnection.js")} MusicConnection The MusicConnection class 8 | * @prop {import("./References.js").References} References The generic data models references 9 | * @prop {import("./HandlersStructures/TableInterface.js")} TableInterface The rethink table interface 10 | * @prop {import("./Contexts/BaseContext.js")} BaseContext The base context for all commands 11 | * @prop {import("./HandlersStructures/dailyEvents.js")} dailyEvents An object representing the existing daily events 12 | * @prop {import("./HandlersStructures/marketItems.js")} marketItems An object representing the existing market items 13 | * @prop {import("./HandlersStructures/slotsEvents.js")} slotsEvents An object representing the existing slots events 14 | */ 15 | 16 | module.exports = { 17 | ExtendedUser: require('./ExtendedStructures/ExtendedUser.js'), 18 | ExtendedUserEntry: require('./ExtendedStructures/ExtendedUserEntry.js'), 19 | ExtendedGuildEntry: require('./ExtendedStructures/ExtendedGuildEntry.js'), 20 | ExtendedMessage: require('./ExtendedStructures/ExtendedMessage'), 21 | MusicCommands: require('./CommandCategories/MusicCommands.js'), 22 | MusicConnection: require('./HandlersStructures/MusicConnection.js'), 23 | //Backward compatibility 24 | refs: require('./References.js'), 25 | References: require('./References.js'), 26 | TableInterface: require('./HandlersStructures/TableInterface.js'), 27 | BaseContext: require('./Contexts/BaseContext.js'), 28 | dailyEvents: require('./HandlersStructures/dailyEvents.js'), 29 | marketItems: require('./HandlersStructures/marketItems.js'), 30 | slotsEvents: require('./HandlersStructures/slotsEvents.js') 31 | }; -------------------------------------------------------------------------------- /structures/CommandCategories/AdminCommands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../../main.js").Client} Client 3 | * @typedef {import("../Command.js").PartialCommandOptions} PartialCommandOptions 4 | */ 5 | 6 | const { inspect } = require('util'); 7 | const Command = require('../Command'); 8 | 9 | class AdminCommands extends Command { 10 | /** 11 | * 12 | * @param {Client} client - The client instance 13 | * @param {PartialCommandOptions} commandOptions - The general command configuration 14 | * @param {{noArgs: string}} [options] - `noArgs` specify a message to return if no arguments are provided. 15 | * These args will make the command handler act before running the command 16 | */ 17 | constructor(client, commandOptions, options = {}) { 18 | super(client, { ...commandOptions, category: { 19 | name: 'Admin', 20 | conf: { 21 | hidden: true 22 | }, 23 | emote: 'heart' 24 | }}); 25 | this.options = options; 26 | } 27 | 28 | /** 29 | * Parse CLI-like arguments 30 | * @param {Array} args - The arguments to parse 31 | * @returns {Object} An object following the structure { `argName`: `value` } 32 | */ 33 | parseArguments(args) { 34 | const parsedArgs = {}; 35 | args.forEach(arg => { 36 | if (!arg.includes('--')) { 37 | return; 38 | } 39 | parsedArgs[arg.split('--')[1].split('=')[0].toLowerCase()] = arg.includes('=') ? arg.split('=')[1] : true; 40 | }); 41 | return parsedArgs; 42 | } 43 | 44 | /** 45 | * Get the max depth at which an element can be inspected according to discord's regular message limit 46 | * @param {*} toInspect - The element to inspect 47 | * @param {String} [additionalText] - An additional text that should be taken into account 48 | * @returns {Number} The max depth 49 | */ 50 | getMaxDepth(toInspect, additionalText = "") { 51 | let maxDepth = 0; 52 | for (let i = 0; i < 10; i++) { 53 | if (inspect(toInspect, { depth: i }).length > (1950 - additionalText.length)) { 54 | return i - 1; 55 | } else { 56 | maxDepth++; 57 | } 58 | } 59 | return maxDepth; 60 | } 61 | } 62 | 63 | module.exports = AdminCommands; -------------------------------------------------------------------------------- /commands/settings/simulatefarewells.js: -------------------------------------------------------------------------------- 1 | const SettingsCommands = require("../../structures/CommandCategories/SettingsCommands"); 2 | 3 | class SimulateFarewells extends SettingsCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: "simulatefarewells", 8 | description: "Simulate the farewells with you as the leaving member", 9 | usage: "{prefix}simulatefarewells" 10 | }, 11 | conf: { 12 | requireDB: true, 13 | guildOnly: true 14 | } 15 | }); 16 | } 17 | 18 | /** @param {import("../../structures/Contexts/SettingsContext")} context */ 19 | 20 | async run(context) { 21 | if (!context.guildEntry.farewells.enabled) { 22 | return context.message.channel.createMessage(":x: The farewell are not enabled"); 23 | } 24 | if (!context.guildEntry.farewells.message) { 25 | return context.message.channel.createMessage( 26 | ":x: There is no farewell message set" 27 | ); 28 | } 29 | if ( 30 | !context.guildEntry.farewells.channel || 31 | (context.guildEntry.farewells.channel !== "dm" && 32 | !context.message.channel.guild.channels.has(context.guildEntry.farewells.channel)) 33 | ) { 34 | return context.message.channel.createMessage( 35 | ":x: The farewell's message target is not set" 36 | ); 37 | } 38 | //Backward compatibility, see issue #33 (https://github.com/ParadoxalCorp/felix-production/issues/33) 39 | if ( 40 | context.message.channel.guild.channels.get(context.guildEntry.farewells.channel).type !== 41 | 0 42 | ) { 43 | return context.message.channel.createMessage( 44 | ":x: The farewell's message target is not a text channel, you should change it to a text channel in order for farewells to work" 45 | ); 46 | } 47 | context.client.bot.emit( 48 | "guildMemberRemove", 49 | context.message.channel.guild, 50 | context.message.channel.guild.members.get(context.message.author.id) 51 | ); 52 | return context.message.channel.createMessage( 53 | this.client.commands 54 | .get("setfarewells") 55 | ._checkPermissions(context) 56 | ); 57 | } 58 | } 59 | 60 | module.exports = SimulateFarewells; 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Felix production 2 | 3 | [![CircleCI](https://circleci.com/gh/ParadoxalCorp/felix-production.svg?style=svg)](https://circleci.com/gh/ParadoxalCorp/felix-production) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/70cc8d49e16c4b928bb75be87f5e2f59)](https://www.codacy.com/app/paradoxalcorp/felix-production?utm_source=github.com&utm_medium=referral&utm_content=ParadoxalCorp/felix-production&utm_campaign=Badge_Grade) 5 | [![donate](https://img.shields.io/badge/donate-patreon-F96854.svg)](https://www.patreon.com/paradoxorigins) 6 | [![discord](https://discordapp.com/api/guilds/328842643746324481/embed.png)](https://discord.gg/Ud49hQJ) 7 | [![Discord Bots](https://discordbots.org/api/widget/status/327144735359762432.svg)](https://discordbots.org/bot/327144735359762432) 8 | 9 | [![Discord Bot List](https://discordbotlist.com/bots/327144735359762432/widget.svg)](https://discordbotlist.com/bots/327144735359762432/) 10 | 11 | This repository contains the main source code of Felix, the Discord bot. 12 | 13 | As Felix rely on third-party services and other micro-services, not everything is in this repository 14 | 15 | If you seek to self-host Felix, you might want to look at [this repository](https://github.com/ParadoxalCorp/FelixBot) instead, which provides self-host support 16 | 17 | ## Dependencies 18 | 19 | As of version 4.1.3, Felix use Redis, available [here](https://redis.io/download) 20 | 21 | ## Development cycle 22 | 23 | ![develoment cycle](https://cdn.discordapp.com/attachments/358212785181556739/461835951199485952/unknown.png) 24 | 25 | ## Versioning 26 | 27 | Felix's versioning works like semantic versioning, in the `major.minor.patch` format 28 | 29 | Bug fixes increments `patch` and new features/enhancements increments `minor` 30 | 31 | Major rewrites of the back-end/interface increments `major`, each `major` increment comes with a reset of `minor` and `patch` 32 | 33 | ## Credits 34 | 35 | * Niputi#2490 (162325985079984129) 36 | 37 | - * Awesome person 38 | - * Various contributions to the code & active co-dev 39 | - * Has covered by himself Felix's servers cost until now 40 | - * Very cute 41 | 42 | * Aetheryx#2222 (284122164582416385) 43 | 44 | - * Original author of a few modules Felix still actively use 45 | - * Awesome person too 46 | 47 | * Ota#1354 (285837115206402049) 48 | 49 | - * Various contributions to the code & active co-dev 50 | 51 | * Emy#0001 (285057692354215936) 52 | 53 | - * Very cute & Felix's first donator 54 | 55 | * All of the donators ❤ -------------------------------------------------------------------------------- /commands/fun/udefine.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const FunCommands = require('../../structures/CommandCategories/FunCommands'); 3 | 4 | class Udefine extends FunCommands { 5 | constructor(client) { 6 | super(client, { 7 | help: { 8 | name: 'udefine', 9 | description: 'Search definitions through urbandictionary', 10 | usage: 'udefine pizza', 11 | }, 12 | conf: { 13 | aliases: ["urdef", "define", "urban"], 14 | }, 15 | }); 16 | } 17 | 18 | /** @param {import("../../structures/Contexts/GenericContext")} context */ 19 | 20 | async run(context) { 21 | if (!context.args[0]) { 22 | return context.message.channel.createMessage(":x: No search term specified"); 23 | } 24 | if (!context.message.channel.nsfw) { 25 | return context.message.channel.createMessage(":x: This command can only be used in a channel set as NSFW"); 26 | } 27 | const result = await axios.default.get(`https://api.urbandictionary.com/v0/define?term=${encodeURIComponent(context.args.join(' '))}`); 28 | if (!result.data) { 29 | return context.message.channel.createMessage(":x: an error occurred"); 30 | } 31 | if (!result.data.list[0]) { 32 | return context.message.channel.createMessage(":x: I couldn't find any results :c"); 33 | } 34 | const firstResult = result.data.list[0]; 35 | return context.message.channel.createMessage({ 36 | embed: { 37 | color: context.client.config.options.embedColor.generic, 38 | title: `Results`, 39 | url: firstResult.permalink, 40 | fields: [{ 41 | name: "**Definition:**", 42 | value: firstResult.definition.length > 1000 ? firstResult.definition.substr(0, 990) + '...' : firstResult.definition 43 | }, { 44 | name: "**Example:**", 45 | value: '*' + firstResult.example + '*' 46 | }, { 47 | name: "**Author:**", 48 | value: firstResult.author 49 | }], 50 | footer: { 51 | text: `👍${firstResult.thumbs_up} | ${firstResult.thumbs_down}👎` 52 | }, 53 | timestamp: new Date() 54 | } 55 | }); 56 | } 57 | } 58 | 59 | module.exports = Udefine; -------------------------------------------------------------------------------- /commands/music/repeat.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class Repeat extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'repeat', 8 | description: 'Set the repeat to repeat the queue, the current song or turn it off', 9 | usage: '{prefix}repeat ' 10 | }, 11 | conf: { 12 | expectedArgs: [{ 13 | description: 'Please choose what repeat mode to toggle, can be either `queue` to repeat the queue, `song` to repeat the current song or `off` to disable the repeat', 14 | possibleValues: [{ 15 | name: 'queue' 16 | }, { 17 | name: 'song' 18 | }, { 19 | name: 'off' 20 | }] 21 | }] 22 | } 23 | }, { userInVC: true, playing: true }); 24 | this.extra = { 25 | off: { 26 | sentence: 'turned off the repeat', 27 | emote: ':arrow_forward:' 28 | }, 29 | song: { 30 | sentence: 'set to repeat the current song', 31 | emote: ':repeat_one:' 32 | }, 33 | queue: { 34 | sentence: 'set to repeat the queue', 35 | emote: ':repeat:' 36 | } 37 | }; 38 | } 39 | /** 40 | * @param {import("../../structures/Contexts/MusicContext")} context The context 41 | */ 42 | 43 | async run(context) { 44 | if (!context.args[0] || !['off', 'queue', 'song'].includes(context.args[0].toLowerCase())) { 45 | return context.message.channel.createMessage(':x: Please specify the repeat mode to toggle, can be either `queue` to repeat the queue, `song` to repeat the current song or `off` to disable the repeat'); 46 | } 47 | context.connection.repeat = context.args[0].toLowerCase(); 48 | if (context.connection.repeat === "queue") { 49 | if (context.connection.nowPlaying) { 50 | context.connection.addTrack(context.connection.nowPlaying); 51 | } 52 | } 53 | return context.message.channel.createMessage(`${this.extra[context.connection.repeat].emote} Successfully ${this.extra[context.connection.repeat].sentence}`); 54 | } 55 | } 56 | 57 | module.exports = Repeat; -------------------------------------------------------------------------------- /commands/settings/simulategreetings.js: -------------------------------------------------------------------------------- 1 | const SettingsCommands = require("../../structures/CommandCategories/SettingsCommands"); 2 | 3 | class SimulateGreetings extends SettingsCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: "simulategreetings", 8 | description: "Simulate the greetings with you as the new member", 9 | usage: "{prefix}simulategreetings" 10 | }, 11 | conf: { 12 | requireDB: true, 13 | guildOnly: true 14 | } 15 | }); 16 | } 17 | 18 | /** @param {import("../../structures/Contexts/SettingsContext")} context */ 19 | 20 | async run(context) { 21 | if (!context.guildEntry.greetings.enabled) { 22 | return context.message.channel.createMessage(":x: The greetings are not enabled"); 23 | } 24 | if (!context.guildEntry.greetings.message) { 25 | return context.message.channel.createMessage( 26 | ":x: There is no greetings message set" 27 | ); 28 | } 29 | if ( 30 | !context.guildEntry.greetings.channel || 31 | (context.guildEntry.greetings.channel !== "dm" && 32 | !context.message.channel.guild.channels.has(context.guildEntry.greetings.channel)) 33 | ) { 34 | return context.message.channel.createMessage( 35 | ":x: The greetings's message target is not set" 36 | ); 37 | } 38 | //Backward compatibility, see issue #33 (https://github.com/ParadoxalCorp/felix-production/issues/33) 39 | if ( 40 | context.guildEntry.greetings.channel !== "dm" && 41 | context.message.channel.guild.channels.get(context.guildEntry.greetings.channel).type !== 42 | 0 43 | ) { 44 | return context.message.channel.createMessage( 45 | ":x: The greetings's message target is not a text channel, you should change it to a text channel in order for greetings to work" 46 | ); 47 | } 48 | context.client.bot.emit( 49 | "guildMemberAdd", 50 | context.message.channel.guild, 51 | context.message.channel.guild.members.get(context.message.author.id) 52 | ); 53 | return context.message.channel.createMessage( 54 | this.client.commands 55 | .get("setgreetings") 56 | ._checkPermissions(context) 57 | ); 58 | } 59 | } 60 | 61 | module.exports = SimulateGreetings; 62 | -------------------------------------------------------------------------------- /commands/admin/registerdonator.js: -------------------------------------------------------------------------------- 1 | const AdminCommands = require('../../structures/CommandCategories/AdminCommands'); 2 | 3 | class RegisterDonator extends AdminCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'registerdonator', 8 | description: 'Register a new donator and give them premium status, omit the `` parameter to not set any expiration date', 9 | usage: '{prefix}registerdonator | | ' 10 | }, 11 | conf: { 12 | aliases: ['registerdonor', 'registerpatron', "regpatron", "regdonor"], 13 | requireDB: true, 14 | } 15 | }); 16 | } 17 | /** @param {import("../../structures/Contexts/AdminContext")} context */ 18 | 19 | async run(context) { 20 | if (!context.args[0] || !context.args[1]) { 21 | return context.message.channel.createMessage(`:x: Missing context.args`); 22 | } 23 | if (!context.client.utils.isWholeNumber(context.args[0])) { 24 | return context.message.channel.createMessage('The specified tier is not a whole number :angery:'); 25 | } 26 | const newDonator = await context.client.handlers.DatabaseWrapper.getUser(context.args[1]); 27 | newDonator.premium.tier = parseInt(context.args[0]); 28 | newDonator.premium.expire = context.args[2] ? Date.now() + parseInt(context.args[2]) : true; 29 | if (newDonator.premium.tier >= 4) { 30 | newDonator.cooldowns.loveCooldown.max = newDonator.cooldowns.loveCooldown.max + (newDonator.premium.tier - 3); 31 | newDonator.addCoins(5e7); 32 | if (newDonator.premium.tier >= 5) { 33 | newDonator.addCoins(1e9); 34 | } 35 | } 36 | await context.client.handlers.DatabaseWrapper.set(newDonator); 37 | const user = await context.client.utils.helpers.fetchUser(context.args[1]); 38 | let res = `:white_check_mark: Successfully given premium status to the user \`${user.tag}\` at tier \`${context.args[0]}\`\n\n`; 39 | if (context.args[2]) { 40 | res += `The premium status of this user will expire in **${context.client.utils.timeConverter.toElapsedTime(context.args[2], true)}** the **${context.client.utils.timeConverter.toHumanDate(newDonator.premium.expire, true)}**`; 41 | } 42 | return context.message.channel.createMessage(res); 43 | } 44 | } 45 | 46 | module.exports = RegisterDonator; -------------------------------------------------------------------------------- /commands/music/saveplaylist.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class SavePlaylist extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'saveplaylist', 8 | description: 'Save the current queue in your playlists, allowing you to load it whenever you want', 9 | usage: '{prefix}saveplaylist ' 10 | }, 11 | conf: { 12 | requireDB: true, 13 | expectedArgs: [{ 14 | description: 'Please specify the name you want to give to this playlist' 15 | }] 16 | } 17 | }); 18 | } 19 | /** 20 | * @param {import("../../structures/Contexts/MusicContext")} context The context 21 | */ 22 | 23 | async run(context) { 24 | const queue = await this.client.handlers.MusicManager.getQueueOf(context.message.channel.guild.id); 25 | if (!queue[0]) { 26 | return context.message.channel.createMessage(`:x: There's nothing in the queue to save :v`); 27 | } 28 | const userPlaylists = await this.client.handlers.DatabaseWrapper.rethink.table("playlists").filter({userID: context.message.author.id}).run(); 29 | if (userPlaylists.length >= context.userEntry.tierLimits.maxSavedPlaylists) { 30 | return context.message.channel.createMessage(`:x: You have reached the limit of \`${context.userEntry.tierLimits.maxSavedPlaylists}\` saved playlists :v, you can delete one to save a new one or become a donator to increase the limit`); 31 | } else if (queue.length > context.userEntry.tierLimits.playlistSaveLimit) { 32 | return context.message.channel.createMessage(`:x: You can't save a playlist bigger than \`${context.userEntry.tierLimits.playlistSaveLimit}\` tracks :v, donators can save bigger playlists`); 33 | } else if (context.args.join(" ") > 84) { 34 | return context.message.channel.createMessage(`:x: The playlist name can't be longer than \`84\` characters :v`); 35 | } 36 | const playlistID = `${context.message.author.id}-${Date.now()}-${process.pid}`; 37 | await this.client.handlers.DatabaseWrapper.set(this.client.structures.References.userPlaylist(context.args.join(" "), playlistID, context.message.author.id, queue), 'playlists'); 38 | return context.message.channel.createMessage(`:white_check_mark: Saved the queue as a playlist with the name \`${context.args.join(" ")}\`. This playlist's id is \`${playlistID}\``); 39 | } 40 | } 41 | 42 | module.exports = SavePlaylist; -------------------------------------------------------------------------------- /commands/admin/bumpversion.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios').default; 2 | const AdminCommands = require('../../structures/CommandCategories/AdminCommands'); 3 | 4 | class BumpVersion extends AdminCommands { 5 | constructor(client) { 6 | super(client, { 7 | help: { 8 | name: 'bumpversion', 9 | description: 'Bump Felix\'s version and create a new release on Sentry', 10 | usage: '{prefix}bumpversion | ' 11 | }, 12 | conf: { 13 | aliases: ["bump"], 14 | } 15 | }); 16 | } 17 | /** @param {import("../../structures/Contexts/AdminContext")} context */ 18 | 19 | async run(context) { 20 | if (!context.args[0]) { 21 | return context.message.channel.createMessage(':x: You must specify if its a major, minor or patch bump, or at least the specific version'); 22 | } 23 | let newRelease = context.args[0]; 24 | if (['major', 'minor', 'patch'].includes(context.args[0])) { 25 | const versions = context.client.package.version.split('.'); 26 | switch (context.args[0]) { 27 | case 'major': 28 | versions[0] = `${parseInt(versions[0]) + 1}`; 29 | break; 30 | case 'minor': 31 | versions[1] = `${parseInt(versions[1]) + 1}`; 32 | break; 33 | case 'patch': 34 | versions[2] = `${parseInt(versions[2]) + 1}`; 35 | break; 36 | } 37 | newRelease = versions.join('.'); 38 | } 39 | context.client.package.version = newRelease; 40 | if (context.client.config.apiKeys.sentryAPI && context.args[1]) { 41 | await this.postRelease(context.args[1]); 42 | } 43 | return context.message.channel.createMessage(`:white_check_mark: Successfully bumped the version to \`${context.client.package.version}\``); 44 | } 45 | 46 | async postRelease(commitID) { 47 | return axios.post('https://app.getsentry.com/api/0/organizations/paradoxcorp/releases/', { 48 | version: this.client.package.version, 49 | ref: commitID, 50 | projects: ['felix'], 51 | url: `https://github.com/ParadoxalCorp/felix-production/tree/v${this.client.package.version}`, 52 | commits: [{id: commitID, repository: 'ParadoxalCorp/felix-production'}] 53 | }, { 54 | headers: {'Content-Type': 'application/json', 'Authorization': `Bearer ${this.client.config.apiKeys.sentryAPI}`} 55 | }); 56 | } 57 | } 58 | 59 | module.exports = BumpVersion; -------------------------------------------------------------------------------- /commands/economy/give.js: -------------------------------------------------------------------------------- 1 | const EconomyCommands = require('../../structures/CommandCategories/EconomyCommands'); 2 | 3 | class Give extends EconomyCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'give', 8 | description: 'Give some of your holy coins to the specified user', 9 | usage: '{prefix}give ' 10 | }, 11 | conf: { 12 | aliases: ['transfer'], 13 | guildOnly: true, 14 | expectedArgs: [{ 15 | description: 'To which user should i give the coins? You can specify a nickname, a username or a user ID' 16 | }, { 17 | description: 'How many coins do you want to give?' 18 | }] 19 | }, 20 | }); 21 | } 22 | /** @param {import("../../structures/Contexts/EconomyContext")} context */ 23 | 24 | async run(context) { 25 | const userInput = context.args.length >= 2 ? context.args.slice(0, context.args.length - 1) : false; 26 | if (!userInput) { 27 | return context.message.channel.createMessage(`:x: Invalid syntax or missing parameters, the correct syntax should be \`${this.help.usage.replace(/{prefix}/gim, context.guildEntry.prefix || context.client.config.prefix)}\``); 28 | } 29 | const receiver = await this.getUserFromText({ message: context.message, client: context.client, text: userInput.join(" ") }); 30 | const coins = context.client.utils.isWholeNumber(context.args[context.args.length - 1]) ? Number(context.args[context.args.length - 1]) : false; 31 | if (!receiver || !coins) { 32 | return context.message.channel.createMessage(!receiver ? ':x: I couldn\'t find the user you specified' : ':x: Please specify a whole number !'); 33 | } else if (coins > context.userEntry.economy.coins) { 34 | return context.message.channel.createMessage(':x: Yeah well, how to say this.. you can\'t give more coins than you have..'); 35 | } 36 | //@ts-ignore 37 | const receiverEntry = await context.client.handlers.DatabaseWrapper.getUser(receiver.id) || context.client.structures.References.userEntry(receiver.id); 38 | const transaction = await context.client.handlers.EconomyManager.transfer({ from: context.userEntry, to: receiverEntry, amount: coins }); 39 | //@ts-ignore 40 | return context.message.channel.createMessage(`:white_check_mark: You just transferred \`${transaction.donor.debited}\` of your holy coins to \`${receiver.username + '#' + receiver.discriminator}\``); 41 | } 42 | } 43 | 44 | module.exports = Give; -------------------------------------------------------------------------------- /commands/economy/inventory.js: -------------------------------------------------------------------------------- 1 | const EconomyCommands = require('../../structures/CommandCategories/EconomyCommands'); 2 | 3 | class Inventory extends EconomyCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'inventory', 8 | description: 'Check the items you possess', 9 | usage: '{prefix}inventory' 10 | } 11 | }); 12 | } 13 | /** @param {import("../../structures/Contexts/EconomyContext")} context */ 14 | 15 | async run(context) { 16 | if (!context.userEntry.economy.items[0]) { 17 | return context.message.channel.createMessage(`:x: Sorry, but it seems like you don't own any item yet :c`); 18 | } 19 | let ownedItemsWorth = 0; 20 | for (const item of context.client.handlers.EconomyManager.marketItems) { 21 | if (context.userEntry.hasItem(item.id)) { 22 | ownedItemsWorth = ownedItemsWorth + item.price; 23 | } 24 | } 25 | return context.message.channel.createMessage({ 26 | embed: { 27 | title: ':package: Inventory', 28 | description: `Your owned items are worth a total of \`${ownedItemsWorth}\` holy coins (including ships).\n\nIf you are looking for your ships, you should check your naval base with the \`navalbase\` command instead`, 29 | fields: (() => { 30 | let familiesOwned = []; 31 | for (const item of context.userEntry.economy.items) { 32 | if (!familiesOwned.includes(context.client.handlers.EconomyManager.getItem(item.id).family) && context.client.handlers.EconomyManager.getItem(item.id).family !== 'Ships') { 33 | familiesOwned.push(context.client.handlers.EconomyManager.getItem(item.id).family); 34 | } 35 | } 36 | familiesOwned = familiesOwned.map(f => { 37 | return { 38 | name: `${context.client.handlers.EconomyManager.marketItems.filter(i => i.family === f)[0].emote} ${f}`, 39 | value: context.client.handlers.EconomyManager.marketItems.filter(i => i.family === f && context.userEntry.hasItem(i.id)).map(i => `${i.emote} ${i.name} (x${context.userEntry.economy.items.find(item => item.id === i.id).count})`).join(', ') 40 | }; 41 | }); 42 | 43 | return familiesOwned; 44 | })(), 45 | color: context.client.config.options.embedColor.generic 46 | 47 | } 48 | }); 49 | } 50 | } 51 | 52 | module.exports = Inventory; -------------------------------------------------------------------------------- /handlers/MessageCollector.js: -------------------------------------------------------------------------------- 1 | //Stolen from Tweetcord (https://github.com/Aetheryx/tweetcord) the 20/03/18 2 | //With some JSDoc added cuz its useful kek 3 | 4 | /** @typedef {import("eris").Message} Message **/ 5 | /** @typedef {import("../main.js").Client} Client */ 6 | 7 | /** 8 | * A message collector which does not create a new event listener each collectors, but rather only use one added when its instantiated 9 | * @prop {Object} collectors An object representing all the ongoing collectors 10 | * @prop {Client} client The client instance 11 | */ 12 | class MessageCollector { 13 | /** 14 | * Instantiating this class create a new messageCreate listener, which will be used for all calls to awaitMessage 15 | * @param {Client} client - The client instance 16 | * @param {{collectors: Object}} [options={}] - An additional object of options 17 | */ 18 | constructor(client, options = {}) { 19 | this.collectors = options.collectors || {}; 20 | this.client = client; 21 | client.bot.on('messageCreate', this.verify.bind(this)); 22 | } 23 | 24 | /** 25 | * Verify if the message pass the condition of the filter function 26 | * @param {*} msg The message to verify 27 | * @returns {Promise} verify 28 | * @private 29 | */ 30 | async verify(msg) { 31 | if (!msg.author) { 32 | return; 33 | } 34 | const collector = this.collectors[msg.channel.id + msg.author.id]; 35 | if (collector && collector.filter(msg)) { 36 | collector.resolve(msg); 37 | } 38 | } 39 | 40 | /** 41 | * Await a message from the specified user in the specified channel 42 | * @param {object} channelID - The ID of the channel to await a message in 43 | * @param {object} userID - The ID of the user to await a message from 44 | * @param {number} [timeout=60000] - Time in milliseconds before the collect should be aborted 45 | * @param {function} [filter] - A function that will be tested against the messages of the user, by default always resolve to true 46 | * @returns {Promise} The message, or false if the timeout has elapsed 47 | */ 48 | awaitMessage(channelID, userID, timeout = 60000, filter = () => true) { 49 | return new Promise(resolve => { 50 | if (this.collectors[channelID + userID]) { 51 | delete this.collectors[channelID + userID]; 52 | } 53 | 54 | this.collectors[channelID + userID] = { resolve, filter }; 55 | 56 | setTimeout(resolve.bind(null, false), timeout); 57 | }); 58 | } 59 | 60 | _reload() { 61 | delete require.cache[module.filename]; 62 | this.client.bot.removeListener('messageCreate', this.verify.bind(this)); 63 | return new(require(module.filename))(this.client, this.collectors); 64 | } 65 | } 66 | 67 | module.exports = MessageCollector; -------------------------------------------------------------------------------- /commands/economy/transactions.js: -------------------------------------------------------------------------------- 1 | const EconomyCommands = require('../../structures/CommandCategories/EconomyCommands'); 2 | 3 | class Transactions extends EconomyCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'transactions', 8 | description: 'See the 10 latest transactions of your account', 9 | usage: '{prefix}transactions' 10 | }, 11 | conf: { 12 | requireDB: true, 13 | } 14 | }); 15 | } 16 | /** @param {import("../../structures/Contexts/EconomyContext")} context */ 17 | 18 | async run(context) { 19 | if (!context.userEntry.economy.transactions[0]) { 20 | return context.message.channel.createMessage(':x: It seems you did not transfer or receive holy coins yet, so there\'s no transactions to display :v'); 21 | } 22 | const splicedTransactions = await this.mapSplicedTransactions(context, context.client.utils.paginate(context.userEntry.economy.transactions, 4)); 23 | if (splicedTransactions.length < 2) { 24 | return context.message.channel.createMessage(splicedTransactions[0]); 25 | } else { 26 | return context.client.handlers.InteractiveList.createPaginatedMessage({ 27 | channel: context.message.channel, 28 | messages: splicedTransactions, 29 | userID: context.message.author.id 30 | }); 31 | } 32 | } 33 | 34 | async mapSplicedTransactions(context, splicedTransactions) { 35 | let i = 0; 36 | for (let transactionGroup of splicedTransactions) { 37 | const fields = []; 38 | for (const transaction of transactionGroup) { 39 | const transactionUsers = await Promise.all([context.client.utils.helpers.fetchUser(transaction.from).then(u => u.tag), context.client.utils.helpers.fetchUser(transaction.to).then(u => u.tag)]); 40 | fields.push({ 41 | name: context.client.utils.timeConverter.toHumanDate(transaction.date), 42 | value: '```diff\n' + `From: ${transactionUsers[0]}\nTo: ${transactionUsers[1]}\nCoins: ${transaction.amount < 0 ? transaction.amount : '+' + transaction.amount}` + '```', 43 | }); 44 | } 45 | splicedTransactions[i] = { 46 | embed: { 47 | title: 'Recent transactions', 48 | fields, 49 | footer: { 50 | text: `Showing page ${!splicedTransactions[1] ? '1/1' : '{index}/' + splicedTransactions.length }` 51 | }, 52 | color: context.client.config.options.embedColor.generic 53 | } 54 | }; 55 | i++; 56 | } 57 | return splicedTransactions; 58 | } 59 | } 60 | 61 | module.exports = Transactions; -------------------------------------------------------------------------------- /commands/utility/mdn.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios').default; 2 | const UtilityCommands = require('../../structures/CommandCategories/UtilityCommands'); 3 | 4 | class MDN extends UtilityCommands { 5 | constructor(client) { 6 | super(client, { 7 | help: { 8 | name: 'mdn', 9 | description: 'Search something through the Mozilla Developer Network', 10 | usage: '{prefix}mdn arrays', 11 | }, 12 | }); 13 | } 14 | 15 | /** @param {import("../../structures/Contexts/UtilityContext")} context */ 16 | 17 | async run(context) { 18 | if (!context.args[0]) { 19 | return context.message.channel.createMessage(":x: You must specify something to search"); 20 | } 21 | if (context.args.join(" ").length > 100) { 22 | return context.message.channel.createMessage(":x: You can't search for something over 100 characters"); 23 | } 24 | const result = await axios.get( 25 | `https://developer.mozilla.org/en-US/search.json?locale=en-US&q=${encodeURIComponent(context.args.join(" "))}`, { headers: { 'Content-Type': 'application/json' } }) 26 | .then(r => r.data); 27 | if (!result.documents || !result.documents[0]) { 28 | return context.message.channel.createMessage(":x: Your search did not returned any result"); 29 | } 30 | result.documents = result.documents.map(document => { 31 | return { 32 | embed: { 33 | color: context.client.config.options.embedColor.generic, 34 | title: context.args.join().substr(0, 124), 35 | url: `https://developer.mozilla.org/en-US/search?locale=en-US&q=${encodeURIComponent(context.args.join(" "))}`, 36 | thumbnail: { 37 | url: "https://developer.cdn.mozilla.net/static/img/opengraph-logo.dc4e08e2f6af.png" 38 | }, 39 | fields: [{ 40 | name: document.title, 41 | value: `${document.excerpt.replace(/\/gm, '`').replace(/\<\/mark\>/gm, '`').substr(0, 1000)}..` 42 | }, { 43 | name: 'Tags', 44 | value: document.tags.join(', ') 45 | }], 46 | timestamp: new Date(), 47 | footer: { 48 | icon_url: context.client.bot.user.avatarURL, 49 | text: "Showing page {index}/{length}" 50 | } 51 | } 52 | }; 53 | }); 54 | return context.client.handlers.InteractiveList.createPaginatedMessage({ 55 | channel: context.message.channel, 56 | messages: result.documents, 57 | userID: context.message.author.id 58 | }); 59 | } 60 | } 61 | 62 | module.exports = MDN; -------------------------------------------------------------------------------- /commands/utility/translate.js: -------------------------------------------------------------------------------- 1 | const UtilityCommands = require("../../structures/CommandCategories/UtilityCommands"); 2 | 3 | class Translate extends UtilityCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: "translate", 8 | description: "Translate the provided text to the specified language using google translate", 9 | usage: '{prefix}translate "hello" en:fr' 10 | } 11 | }); 12 | } 13 | 14 | /** @param {import("../../structures/Contexts/UtilityContext")} context */ 15 | 16 | async run(context) { 17 | const googleTranslate = require("google-translate-api"); 18 | context.args = context.args.join(" ").split('"').filter(a => a !== "").map(a => a.trim()); 19 | if (context.args[0] && context.args[0].includes(":") && !new RegExp(/\s+/g).test(context.args[0])) { 20 | context.args.push(context.args.shift().trim()); 21 | } 22 | let textToTranslate = context.args[0]; 23 | if (!context.args[1] || !textToTranslate) { 24 | return context.message.channel.createMessage(`:x: You need to at least specify the text to translate and the language to which i should translate it`); 25 | } 26 | let sourceLang = context.args[1].split(":")[0].toLowerCase().trim(); 27 | let targetLang = context.args[1].split(":")[1] 28 | ? context.args[ 29 | context.args[1].includes('"') 30 | ? 0 31 | : 1 32 | ].split(":")[1].toLowerCase().trim() 33 | : false; 34 | //If only one language iso is specified, take it as the target 35 | if (!targetLang) { 36 | targetLang = sourceLang; 37 | sourceLang = undefined; 38 | } 39 | let translated = await googleTranslate(textToTranslate, { 40 | from: sourceLang, 41 | to: targetLang 42 | }).catch(() => { 43 | return false; 44 | }); 45 | if (!translated) { 46 | return context.message.channel.createMessage( 47 | `:x: One of the specified language is not supported or the syntax is incorrect, it must be the following syntax: \`${context.message.guild 48 | ? context.client.guildData.get(context.message.guild.id).generalSettings.prefix 49 | : context.client.config.prefix}translate "text to translate" SOURCE_LANGUAGE_ISO:TARGET_LANGUAGE_ISO\` (see the help for examples)`); 50 | } 51 | return context.message.channel.createMessage({ 52 | embed: { 53 | title: `:white_check_mark: Text translated from ${translated.from.language.iso.toUpperCase()} to ${targetLang.toUpperCase()}\n`, 54 | description: "```" + translated.text + "```" 55 | } 56 | }); 57 | } 58 | } 59 | 60 | module.exports = Translate; -------------------------------------------------------------------------------- /commands/image/loveship.js: -------------------------------------------------------------------------------- 1 | const GenericCommands = require('../../structures/CommandCategories/ImageCommands'); 2 | 3 | class LoveShip extends GenericCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'loveship', 8 | description: 'Ship a user with another user !', 9 | usage: '{prefix}loveship ', 10 | subCategory: 'image-generation' 11 | }, 12 | conf: { 13 | aliases: ['ship'], 14 | requirePerms: ['attachFiles'], 15 | guildOnly: true, 16 | require: ['weebSH', 'taihou'] 17 | }, 18 | }, { noArgs: ':x: You need to specify at least one user to ship' }); 19 | } 20 | /** @param {import("../../structures/Contexts/ImageContext")} context */ 21 | 22 | async run(context) { 23 | const firstUser = await this.getUserFromText({client: context.client, message: context.message, text: context.args[0]}); 24 | const secondUser = context.args[1] ? await this.getUserFromText({client: context.client, message: context.message, text: context.args.splice(1).join(' ')}) : context.message.author; 25 | if ((!firstUser && secondUser.id === context.message.author.id) || (!secondUser && firstUser)) { 26 | return context.message.channel.createMessage(':x: I\'m sorry but I couldn\'t find the users you specified :c'); 27 | } else if (firstUser.id === secondUser.id) { 28 | return context.message.channel.createMessage(`:x: You can't match a user with themselves, like, why?`); 29 | } 30 | let typing = false; 31 | //If the queue contains 2 items or more, expect that this request will take some seconds and send typing to let the user know 32 | if (context.client.weebSH.korra.requestHandler.queue.length >= 2) { 33 | context.client.bot.sendChannelTyping(context.message.channel.id); 34 | typing = true; 35 | } 36 | const generatedShip = await context.client.weebSH.korra.generateLoveShip(this.useWebpFormat(firstUser), this.useWebpFormat(secondUser)).catch(this.handleError.bind(this, context, typing)); 37 | const match = (() => { 38 | let msg = typing ? `<@!${context.message.author.id}> ` : ''; 39 | msg += `I, Felix von Trap, by the powers bestowed upon me, declare this a **${this.calculateMatch(firstUser.id, secondUser.id)}** match`; 40 | return msg; 41 | })(); 42 | return context.message.channel.createMessage(match, {file: generatedShip, name: `${Date.now()}-${context.message.author.id}.png`}); 43 | } 44 | 45 | calculateMatch(firstID, secondID) { 46 | const total = parseInt(firstID) + parseInt(secondID); 47 | const sliced = total.toString().split(''); 48 | return `${sliced[6]}${sliced[15]}%`; 49 | } 50 | } 51 | 52 | module.exports = LoveShip; -------------------------------------------------------------------------------- /commands/music/playafter.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class PlayAfter extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'playafter', 8 | description: 'Push to the first position in the queue a song. You can input: A `YouTube` URL (including livestreams), a `Soundcloud` URL, a `Twitch` channel URL (the channel must be live);\n\nOr a search term to search through `YouTube` or `Soundcloud`, by default the search is done on `YouTube`, to search through `Soundcloud`, you must specify it like `{prefix}queue soundcloud `', 9 | usage: '{prefix}playafter ' 10 | }, 11 | conf: { aliases: ['playneft'] } 12 | }, { userInVC: true, playing: true, noArgs: ':x: You didn\'t specify any song to play after this one' }); 13 | } 14 | /** 15 | * @param {import("../../structures/Contexts/MusicContext")} context The context 16 | */ 17 | 18 | async run(context) { 19 | const resolveTracks = await this.client.handlers.MusicManager.resolveTracks(context.connection.player.node, context.args.join(' ')); 20 | if (resolveTracks.loadType === this.client.handlers.MusicManager.constants.loadTypes.playlist) { 21 | return context.message.channel.createMessage(':x: Oops, this looks like a playlist to me, please use the `addplaylist` command instead'); 22 | } 23 | let queued; 24 | let track = resolveTracks.tracks[0]; 25 | if (!track) { 26 | return context.message.channel.createMessage(`:x: I could not find any song :c, please make sure to:\n- Follow the syntax (check \`${this.getPrefix(context.guildEntry)}help ${this.help.name}\`)\n- Use HTTPS links, unsecured HTTP links aren't supported\n- If a YouTube video, I can't play it if it is age-restricted\n - If a YouTube video, it might be blocked in the country my servers are`); 27 | } 28 | if (resolveTracks.tracks.length > 1) { 29 | track = await this.selectTrack(context, resolveTracks.tracks); 30 | if (!track) { 31 | return; 32 | } 33 | } 34 | if (track.info.isStream) { 35 | return context.message.channel.createMessage(':x: I am sorry but you cannot add live streams to the queue, you can only play them immediately'); 36 | } 37 | if (!context.connection.player.playing && !context.connection.player.paused) { 38 | context.connection.play(track, context.message.author.id); 39 | } else { 40 | context.connection.addTrack(track, context.message.author.id, true); 41 | queued = true; 42 | } 43 | return context.message.channel.createMessage({embed: await this.genericEmbed(track, context.connection, queued ? 'Successfully enqueued to first position' : 'Now playing')}); 44 | } 45 | } 46 | 47 | module.exports = PlayAfter; -------------------------------------------------------------------------------- /commands/music/play.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands.js'); 2 | 3 | class Play extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'play', 8 | description: 'Play a song, you can input: A `YouTube` URL (including livestreams), a `Soundcloud` URL, a `Twitch` channel URL (the channel must be live);\n\nOr a search term to search through `YouTube` or `Soundcloud`, by default the search is done on `YouTube`, to search through `Soundcloud`, you must specify it like `{prefix}play soundcloud `', 9 | usage: '{prefix}play ' 10 | }, 11 | }, { userInVC: true, autoJoin: true }); 12 | } 13 | /** 14 | * @param {import("../../structures/Contexts/MusicContext.js")} context The context 15 | */ 16 | 17 | async run(context) { 18 | let track; 19 | if (!context.args[0]) { 20 | if (context.connection.queue[0]) { 21 | track = context.connection.queue[0]; 22 | if (context.connection.player.paused) { 23 | context.connection.player.setPause(false); 24 | } else if (context.connection.nowPlaying) { 25 | return context.message.channel.createMessage(':x: You should specify something to play'); 26 | } else { 27 | context.connection.queue.shift(); 28 | } 29 | } else { 30 | return context.message.channel.createMessage(':x: You didn\'t specified any songs to play and there is nothing in the queue'); 31 | } 32 | } 33 | let tracks = track ? [] : await this.client.handlers.MusicManager.resolveTracks(context.connection.player.node, context.args.join(' ')); 34 | if (tracks.loadType === this.client.handlers.MusicManager.constants.loadTypes.playlist) { 35 | return context.message.channel.createMessage(':x: Oops, this looks like a playlist to me, please use the `addplaylist` command instead'); 36 | } 37 | track = track ? track : tracks.tracks[0]; 38 | if (!track) { 39 | return context.message.channel.createMessage(`:x: I could not find any song :c, please make sure to:\n- Follow the syntax (check \`${this.getPrefix(context.guildEntry)}help ${this.help.name}\`)\n- Use HTTPS links, unsecured HTTP links aren't supported\n- If a YouTube video, I can't play it if it is age-restricted\n - If a YouTube video, it might be blocked in the country my servers are`); 40 | } 41 | if (tracks.length > 1) { 42 | track = await this.selectTrack(context, tracks); 43 | if (!track) { 44 | return; 45 | } 46 | } 47 | context.connection.play(track, context.message.author.id); 48 | return context.message.channel.createMessage({embed: await this.genericEmbed(track, context.connection, 'Now playing', true)}); 49 | } 50 | } 51 | 52 | module.exports = Play; -------------------------------------------------------------------------------- /structures/CommandCategories/MiscCommands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../../main.js").Client} Client 3 | * @typedef {import("../Command.js").PartialCommandOptions} PartialCommandOptions 4 | * @typedef {import("eris").User} User 5 | * @typedef {import("../ExtendedStructures/ExtendedUser")} ExtendedUser 6 | */ 7 | 8 | /** @typedef {Object} TopUserData 9 | * @prop {String} id The ID of the user 10 | * @prop {Number} amount The amount of love/experience/coins of the user 11 | */ 12 | 13 | /** @typedef {Object} LeaderboardData 14 | * @prop {Array} leaderboard The 10 first in the leaderboard, ordered from highest to lowest. May be empty if Redis is unavailable/leaderboard isn't yet populated 15 | * @prop {Number} userIndex The given user's position in this leaderboard 16 | * @prop {Number} size The amount of entries in the leaderboard 17 | */ 18 | 19 | const Command = require('../Command'); 20 | 21 | class MiscCommands extends Command { 22 | /** 23 | * 24 | * @param {Client} client - The client instance 25 | * @param {PartialCommandOptions} commandOptions - The general command configuration 26 | * @param {{noArgs: string}} [options] - `noArgs` specify a message to return if no arguments are provided. 27 | * These args will make the command handler act before running the command 28 | */ 29 | constructor(client, commandOptions, options = {}) { 30 | super(client, { ...commandOptions, category: { 31 | name: 'Misc', 32 | conf: { 33 | guildOnly: true, 34 | requireDB: true 35 | }, 36 | emote: 'bookmark' 37 | }}); 38 | this.options = options; 39 | } 40 | 41 | /** 42 | * 43 | * @param {String} leaderboard - The leaderboard to get, can be either `experience`, `coins` or `love` 44 | * @param {User | ExtendedUser} user - A user from who to get the position, may be -1 if the user isn't in the leaderboard 45 | * @returns {Promise} The leaderboard data 46 | */ 47 | async getLeaderboard(leaderboard, user) { 48 | if (!this.client.handlers.RedisManager.healthy) { 49 | return { 50 | leaderboard: [], 51 | userIndex: -1 52 | }; 53 | } 54 | const pipeline = this.client.handlers.RedisManager.pipeline(); 55 | pipeline.zrevrange(`${leaderboard}-leaderboard`, 0, 9, 'WITHSCORES'); 56 | pipeline.zrevrank(`${leaderboard}-leaderboard`, user.id); 57 | pipeline.zcount(`${leaderboard}-leaderboard`, '-inf', '+inf'); 58 | return pipeline.exec().then(results => { 59 | return { 60 | leaderboard: this.client.utils.paginate(results[0][1], 2).map(entry => { 61 | return { 62 | id: entry[0], 63 | amount: entry[1], 64 | }; 65 | }), 66 | userIndex: results[1][1] || (results[1][1] === 0) ? results[1][1] : -1, 67 | size: results[2][1] 68 | }; 69 | }); 70 | } 71 | } 72 | 73 | module.exports = MiscCommands; -------------------------------------------------------------------------------- /handlers/ReactionCollector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("eris").Message} Message 3 | * @typedef {import("eris").Emoji} Emoji 4 | * @typedef {import("../main.js").Client} Client 5 | */ 6 | 7 | /** @typedef {Object} CollectedEmoji 8 | * @prop {String} name The name of the emoji 9 | * @prop {String} id The ID of the emoji 10 | * @prop {Boolean} animated Whether the emoji is animated 11 | */ 12 | 13 | 14 | /** 15 | * A reaction collector which does not create a new event listener each collectors, but rather only use one added when its instantiated 16 | * @prop {object} collectors An object representing all the ongoing collectors 17 | * @prop {Client} client The client instance 18 | */ 19 | class ReactionCollector { 20 | /** 21 | * Instantiating this class create a new messageReactionAdd listener, which will be used for all calls to awaitReaction 22 | * @param {Client} client - The client instance 23 | */ 24 | constructor(client) { 25 | this.collectors = {}; 26 | this.client = client; 27 | client.bot.on('messageReactionAdd', this.verify.bind(this)); 28 | } 29 | 30 | /** 31 | * Verify if the reaction pass the condition of the filter function 32 | * @param {Message} msg - The message 33 | * @param {Emoji} emoji - The emoji 34 | * @param {String} userID - the ID of the user 35 | * @returns {Promise} returns object 36 | * @private 37 | */ 38 | async verify(msg, emoji, userID) { 39 | const collector = this.collectors[msg.channel.id + msg.id + userID]; 40 | if (collector && collector.filter(msg, emoji, userID)) { 41 | collector.resolve({ 42 | message: msg, 43 | emoji: emoji, 44 | userID: userID 45 | }); 46 | } 47 | } 48 | 49 | /** 50 | * Await a reaction from the specified user in the specified channel 51 | * @param {String} channelID - The ID of the channel to await a reaction in 52 | * @param {String} messageID - The ID of the message to await a reaction on 53 | * @param {String} userID - The ID of the user to await a reaction from 54 | * @param {Number} [timeout=60000] - Time in milliseconds before the collect should be aborted 55 | * @param {Function} [filter] - A function that will be tested against the reactions of the user, by default always resolve to true 56 | * @returns {Promise<{message: Message, emoji: CollectedEmoji, userID: String}>} An object with the message, emoji and userID properties, or false if the timeout has elapsed 57 | */ 58 | awaitReaction(channelID, messageID, userID, timeout = 60000, filter = () => true) { 59 | return new Promise(resolve => { 60 | if (this.collectors[channelID + messageID + userID]) { 61 | delete this.collectors[channelID + messageID + userID]; 62 | } 63 | 64 | this.collectors[channelID + messageID + userID] = { resolve, filter }; 65 | 66 | setTimeout(resolve.bind(null, false), timeout); 67 | }); 68 | } 69 | 70 | _reload() { 71 | delete require.cache[module.filename]; 72 | this.client.bot.removeListener('messageReactionAdd', this.verify.bind(this)); 73 | return new(require(module.filename))(this.client, this.collectors); 74 | } 75 | } 76 | 77 | module.exports = ReactionCollector; -------------------------------------------------------------------------------- /commands/music/skip.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class Skip extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'skip', 8 | description: 'Start a vote to skip the currently playing song', 9 | usage: '{prefix}skip' 10 | }, 11 | conf: { aliases: ['voteskip'] } 12 | }, { userInVC: true, playing: true }); 13 | } 14 | /** 15 | * @param {import("../../structures/Contexts/MusicContext")} context The context 16 | */ 17 | 18 | async run(context) { 19 | if (!context.connection.skipVote.count) { 20 | context.connection.skipVote.count = 1; 21 | context.connection.skipVote.callback = this.handleVoteEnd.bind(this, this.client, context); 22 | context.connection.skipVote.timeout = setTimeout(this.handleVoteEnd.bind(this, context, 'timeout'), this.client.config.options.music.voteSkipDuration); 23 | } else { 24 | if (context.connection.skipVote.id) { 25 | return context.message.channel.createMessage(`:x: Another vote to skip to the song **${context.connection.queue.find(t => t.voteID === context.connection.skipVote.id).info.title}** is already ongoing`); 26 | } 27 | if (context.connection.skipVote.voted.includes(context.message.author.id)) { 28 | return context.message.channel.createMessage(':x: You already voted to skip this song'); 29 | } 30 | context.connection.skipVote.count = context.connection.skipVote.count + 1; 31 | } 32 | context.connection.skipVote.voted.push(context.message.author.id); 33 | return this.processVote(context); 34 | } 35 | 36 | async processVote(context) { 37 | const voiceChannel = context.message.channel.guild.channels.get(context.message.channel.guild.members.get(this.client.bot.user.id).voiceState.channelID); 38 | const userCount = voiceChannel.voiceMembers.filter(m => !m.bot).length; 39 | if (context.connection.skipVote.count >= (userCount === 2 ? 2 : (Math.ceil(userCount / 2)))) { 40 | context.connection.resetVote(); 41 | const skippedSong = context.connection.skipTrack(); 42 | return context.message.channel.createMessage(`:white_check_mark: Skipped **${skippedSong.info.title}**`); 43 | } 44 | return context.message.channel.createMessage(`:white_check_mark: Successfully registered the vote to skip the song, as there is \`${userCount}\` users listening and already \`${context.connection.skipVote.count}\` voted, \`${userCount === 2 ? 1 : Math.ceil(userCount / 2) - context.connection.skipVote.count}\` more vote(s) are needed`); 45 | } 46 | 47 | async handleVoteEnd(context, reason) { 48 | switch (reason) { 49 | case 'timeout': 50 | context.connection.resetVote(); 51 | return context.message.channel.createMessage(':x: The vote to skip the current song ended, not enough users voted'); 52 | break; 53 | case 'ended': 54 | return context.message.channel.createMessage(':x: The vote to skip the current song has been cancelled because the song just ended'); 55 | break; 56 | } 57 | } 58 | } 59 | 60 | module.exports = Skip; -------------------------------------------------------------------------------- /handlers/RedisManager.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../main.js")} Client */ 2 | 3 | const Redis = require('ioredis'); 4 | 5 | class RedisManager extends Redis { 6 | 7 | 8 | /** 9 | *Creates an instance of RedisManager. 10 | * @param {Client} client Client 11 | * @memberof RedisManager 12 | */ 13 | constructor(client) { 14 | super({ 15 | port: client.config.redis.port, 16 | host: client.config.redis.host, 17 | family: client.config.redis.family, 18 | db: client.config.redis.db, 19 | password: client.config.redis.password, 20 | lazyConnect: true 21 | }); 22 | this.felix = client; 23 | this.on('connect', this._handleConnection.bind(this)); 24 | this.on('error', this._handleError.bind(this)); 25 | this.on('close', this._handleClosedConnection.bind(this)); 26 | this.on('reconnecting', this._handleReconnection.bind(this)); 27 | this.on('ready', this._ready.bind(this)); 28 | this.on('end', this._handleEnd.bind(this)); 29 | if (client.config.redis.enabled) { 30 | this.connect(); 31 | } 32 | this.failing = false; 33 | this.knownErrors = { 34 | "err-invalid-password": true, 35 | "noauth-authentification-required": true 36 | }; 37 | } 38 | 39 | _handleConnection() { 40 | process.send({name: 'info', msg: `Successfully reached the Redis server at ${this.felix.config.redis.host}:${this.felix.config.redis.port}`}); 41 | } 42 | 43 | _ready() { 44 | process.send({name: 'info', msg: `Successfully connected to the Redis server at ${this.felix.config.redis.host}:${this.felix.config.redis.port}`}); 45 | } 46 | 47 | _handleError(err) { 48 | process.send({name: 'error', msg: `Failed to connect to the Redis server at ${this.felix.config.redis.host}:${this.felix.config.redis.port}`}); 49 | if (this.knownErrors[err.message.toLowerCase().replace(/\s+/g, '-').replace(/\./g, '')]) { 50 | this.disconnect(); 51 | } 52 | if (!this.failing) { 53 | this.felix.bot.emit('error', err); 54 | this.failing = true; 55 | } 56 | } 57 | 58 | _handleClosedConnection() { 59 | process.send({name: 'warn', msg: `The connection with the Redis server at ${this.felix.config.redis.host}:${this.felix.config.redis.port} has been closed`}); 60 | } 61 | 62 | _handleReconnection(ms) { 63 | process.send({name: 'warn', msg: `Attempting to re-connect to the Redis server in ${ms}ms`}); 64 | } 65 | 66 | _handleEnd() { 67 | process.send({name: 'error', msg: `Failed to connect to the Redis server at ${this.felix.config.redis.host}:${this.felix.config.redis.port}, no more re-connection attempts will be made`}); 68 | } 69 | 70 | get healthy() { 71 | return this.status === 'ready' ? true : false; 72 | } 73 | 74 | _reload() { 75 | const listeners = ['end', 'ready', 'reconnecting', 'close', 'error', 'connect']; 76 | for (const listener of listeners) { 77 | this.removeAllListeners(listener); 78 | } 79 | this.disconnect(); 80 | delete require.cache[module.filename]; 81 | return new(require(module.filename))(this.felix); 82 | } 83 | } 84 | 85 | module.exports = RedisManager; -------------------------------------------------------------------------------- /utils/TimeConverter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides some utility methods to parse time 3 | * @typedef {TimeConverter} TimeConverter 4 | */ 5 | class TimeConverter { 6 | constructor() { } 7 | 8 | /** 9 | * @typedef {object} ElapsedTime 10 | * @property {number} elapsedTime.days - Number of days elapsed with the given milliseconds 11 | * @property {number} elapsedTime.hours - Number of hours elapsed with the given milliseconds 12 | * @property {number} elapsedTime.minutes - Number of minutes elapsed with the given milliseconds 13 | * @property {number} elapsedTime.seconds - Number of seconds elapsed with the given milliseconds 14 | */ 15 | 16 | /** 17 | * @typedef {object} HumanDate 18 | * @property {number} seconds - The second 19 | * @property {number} minutes - The minute 20 | * @property {number} hours - The hour 21 | * @property {number} day - The day 22 | * @property {string} month - The month 23 | * @property {number} year - The year 24 | */ 25 | 26 | /** 27 | * Calculate and return how many elapsed seconds, minutes, hours and days the given milliseconds represent 28 | * @param {number} ms The milliseconds to calculate 29 | * @param {boolean} [formatted=false] Whether or not the elapsed time should be returned already in a readable string format 30 | * @returns {ElapsedTime | string} An object or a string depending on if formatted is true or false 31 | */ 32 | toElapsedTime(ms, formatted = false) { 33 | return formatted ? `${Math.floor((ms / (60 * 60 * 24 * 1000)))}d ${Math.floor((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))}h ${Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60))}m ${Math.floor((ms % (1000 * 60)) / 1000)}s` : { 34 | days: Math.floor((ms / (60 * 60 * 24 * 1000))), 35 | hours: Math.floor((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), 36 | minutes: Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)), 37 | seconds: Math.floor((ms % (1000 * 60)) / 1000) 38 | }; 39 | } 40 | 41 | /** 42 | * @param {any} Date new Date(timestamp) 43 | * @returns {String} month in string 44 | * @memberof TimeConverter 45 | */ 46 | getMonth(Date) { 47 | return new Intl.DateTimeFormat('en-DE', { month: 'long' }).format(Date); 48 | } 49 | 50 | /** 51 | * Convert a UNIX timestamp(in ms) to human date 52 | * @param {number} timestamp The UNIX timestamp in ms to convert 53 | * @param {boolean} [formatted=true] Whether or not the date should be returned already in a readable string format 54 | * @returns {HumanDate | string} An object or a string depending on if formatted is true or false 55 | */ 56 | toHumanDate(timestamp, formatted = true) { 57 | 58 | let date = new Date(timestamp); 59 | return formatted ? `${date.getDate()} ${this.getMonth(date)} ${date.getFullYear()}, ${new String(date.getHours()).length < 2 ? "0" + date.getHours() : date.getHours()}:${new String(date.getMinutes()).length < 2 ? "0" + date.getMinutes() : date.getMinutes()}:${new String(date.getSeconds()).length < 2 ? "0" + date.getSeconds() : date.getSeconds()}` : { 60 | seconds: date.getSeconds(), 61 | minutes: date.getMinutes(), 62 | hours: date.getHours(), 63 | day: date.getDate(), 64 | month: this.getMonth(date), 65 | year: date.getFullYear() 66 | }; 67 | } 68 | 69 | } 70 | 71 | module.exports = new TimeConverter(); -------------------------------------------------------------------------------- /commands/utility/npm.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const moment = require("moment"); 3 | const UtilityCommands = require("../../structures/CommandCategories/UtilityCommands"); 4 | 5 | class Npm extends UtilityCommands { 6 | constructor(client) { 7 | super(client, { 8 | help: { 9 | name: "npm", 10 | description: "Search something through NPM", 11 | usage: "{prefix}npm hapi" 12 | } 13 | }); 14 | } 15 | 16 | /** @param {import("../../structures/Contexts/UtilityContext")} context */ 17 | 18 | async run(context) { 19 | if (!context.args[0]) { 20 | return context.message.channel.createMessage(":x: You must specify at least one keyword"); 21 | } 22 | const results = await axios.default({ 23 | method: "get", 24 | url: `https://www.npmjs.com/search/suggestions?q=${encodeURIComponent(context.args.join(" "))}&size=${ 20}`, 25 | headers: { 26 | "Content-Type": "application/json" 27 | } 28 | }).then(r => r.data); 29 | if (!results[0]) { 30 | return context.message.channel.createMessage(":x: Your search did not return any result"); 31 | } 32 | let embedFields = []; 33 | if (results[0].name) { 34 | embedFields.push({name: "Name", value: `[${results[0].name}](https://www.npmjs.com/package/${results[0].name})`, inline: true}); 35 | } 36 | if (results[0].version) { 37 | embedFields.push({name: "Version", value: results[0].version, 38 | inline: true}); 39 | } 40 | if (results[0].publisher) { 41 | embedFields.push({ 42 | name: "Author", 43 | value: typeof results[0].publisher === "string" 44 | ? results[0].publisher 45 | : results[0].publisher.username, 46 | inline: true 47 | }); 48 | } 49 | if (results[0].description) { 50 | embedFields.push({name: "Description", value: results[0].description}); 51 | } 52 | if (results[0].links) { 53 | embedFields.push({ 54 | name: "Links", value: (() => { 55 | const links = []; 56 | for (const key in results[0].links) { 57 | links.push(`[${key}](${results[0].links[key]})`); 58 | } 59 | return links.join(", "); 60 | })() 61 | }); 62 | } 63 | if (results[0].date) { 64 | embedFields.push({ 65 | name: "Latest release", 66 | value: `${context.client.utils.timeConverter.toHumanDate(new Date(results[0].date).getTime())} (${moment().to(new Date(results[0].date).getTime())})` 67 | }); 68 | } 69 | return context.message.channel.createMessage({ 70 | embed: { 71 | color: context.client.config.options.embedColor.generic, 72 | title: "NPM", 73 | url: `https://www.npmjs.com/search?q=${encodeURIComponent(context.args.join("+"))}`, 74 | thumbnail: { 75 | url: "https://raw.githubusercontent.com/isaacs/npm/master/html/npm-256-square.png" 76 | }, 77 | fields: embedFields, 78 | timestamp: new Date() 79 | } 80 | }); 81 | } 82 | } 83 | 84 | module.exports = Npm; 85 | -------------------------------------------------------------------------------- /commands/economy/market.js: -------------------------------------------------------------------------------- 1 | const EconomyCommands = require('../../structures/CommandCategories/EconomyCommands'); 2 | 3 | class Market extends EconomyCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'market', 8 | description: 'The place to see and purchase available items with holy coins', 9 | usage: '{prefix}market' 10 | } 11 | }); 12 | } 13 | /** @param {import("../../structures/Contexts/EconomyContext")} context */ 14 | 15 | async run(context) { 16 | return context.client.handlers.InteractiveList.createPaginatedMessage({ 17 | channel: context.message.channel, 18 | userID: context.message.author.id, 19 | reactions: [{ 20 | unicode: '🛒', 21 | callback: this.buyItem.bind(null, context) 22 | }], 23 | messages: this.mapItems(context) 24 | }); 25 | } 26 | 27 | mapItems(context) { 28 | return context.client.handlers.EconomyManager.marketItems.map(item => { 29 | const price = typeof item.price === 'function' ? item.price(context) : item.price; 30 | return { 31 | embed: { 32 | title: `Market | ${item.name} ${item.emote ? item.emote : ''}`, 33 | description: `**Description** :notepad_spiral:\n${item.description}`, 34 | fields: [{ 35 | name: 'Price :moneybag:', 36 | value: `${price} holy coins`, 37 | inline: true 38 | }, { 39 | name: 'Unique possession :question:', 40 | value: item.buyableOnce ? ':white_check_mark:' : ':x:', 41 | inline: true 42 | }], 43 | footer: { 44 | text: `Showing page {index}/${context.client.handlers.EconomyManager.marketItems.length} ${context.client.config.admins.includes(context.message.author.id) ? '| Item ID: ' + item.id : ''}` 45 | }, 46 | image: { 47 | url: item.image 48 | }, 49 | color: context.client.config.options.embedColor.generic 50 | 51 | }, 52 | item //Will be used by buyItem 53 | }; 54 | }); 55 | } 56 | 57 | async buyItem(context, message, marketPage) { 58 | const item = marketPage.item; 59 | const price = typeof item.price === 'function' ? item.price(context) : item.price; 60 | if (item.buyableOnce && context.userEntry.hasItem(item.id)) { 61 | return context.message.channel.createMessage(':x: Sorry but this item is a unique possession and you already own one :v'); 62 | } else if (price > context.userEntry.economy.coins) { 63 | return context.message.channel.createMessage(`:x: You need **${price - context.userEntry.economy.coins}** more holy coins to purchase that`); 64 | } 65 | context.userEntry.subtractCoins(price); 66 | context.userEntry.addItem(item); 67 | if (item.run) { 68 | item.run(context); 69 | } 70 | await context.userEntry.save(); 71 | message.exit(); 72 | return context.message.channel.createMessage(`:white_check_mark: The \`${item.name}\` has been added to your belongings, you now have \`${context.userEntry.economy.coins}\` holy coins`); 73 | } 74 | } 75 | 76 | module.exports = Market; -------------------------------------------------------------------------------- /commands/moderation/announce.js: -------------------------------------------------------------------------------- 1 | const ModerationCommands = require('../../structures/CommandCategories/ModerationCommands'); 2 | 3 | class Announce extends ModerationCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'announce', 8 | description: 'Announce something with a beautiful (or smth) embed', 9 | usage: '{prefix}announce', 10 | }, 11 | conf: { 12 | expectedArgs: [ 13 | { 14 | description: "What's the title of this announcement (max: 256 characters)?" 15 | }, 16 | { 17 | description: "What's the color of this announcement? You can choose between the 3 predefined ones: `red`, `orange`, `lightblue` or use a custom HEX color in the format `#000000`" 18 | }, 19 | { 20 | description: "What's the content of this announcement? You can use the usual markdown, and even masked links using `[masked link](https://google.com)`" 21 | }, 22 | { 23 | description: "Finally, in which channel should I send the announcement?" 24 | } 25 | ] 26 | }, 27 | }); 28 | } 29 | /** @param {import("../../structures/Contexts/ModerationContext")} context */ 30 | 31 | async run(context) { 32 | let embedObject = { 33 | title: '', 34 | description: '', 35 | footer: { 36 | icon_url: context.message.author.avatarURL, 37 | text: `${context.message.author.username}#${context.message.author.discriminator}` 38 | }, 39 | color: 0x000, 40 | timestamp: new Date() 41 | }; 42 | 43 | embedObject.title = context.args[0].substr(0, 256); 44 | if (!context.args[1]) { 45 | return context.message.channel.createMessage(':x: You did not specify the color this announcement should take'); 46 | } 47 | if (context.args[1].trim() === "red") { 48 | embedObject.color = 0xff0000; 49 | } 50 | else if (context.args[1].trim() === "orange") { 51 | embedObject.color = 0xff6600; 52 | } 53 | else if (context.args[1].trim() === "lightblue") { 54 | embedObject.color = 0x33ccff; 55 | } 56 | else if (context.args[1].trim() !== "none") { 57 | embedObject.color = parseInt(`0x${context.args[1].split("#")[1]}`); 58 | embedObject.color = embedObject.color === NaN ? 0x000 : embedObject.color; 59 | } 60 | else { 61 | embedObject.color = parseInt(`0x${context.args[1].trim().substr(0,7)}`); 62 | } 63 | embedObject.description = context.args[2]; 64 | if (!embedObject.description) { 65 | return context.message.channel.createMessage(':x: You did not specify the description this announcement should have'); 66 | } 67 | if (!context.args[3]) { 68 | return context.message.channel.createMessage(':x: You did not specify where I should send this announcement'); 69 | } 70 | const channel = await this.getChannelFromText({client: this.client, message: context.message, text: context.args[3]}); 71 | if (!channel) { 72 | return context.message.channel.createMessage(':x: I couldn\'t find the channel you specified :v'); 73 | } 74 | return channel.createMessage({ 75 | embed: embedObject 76 | }); 77 | } 78 | } 79 | 80 | module.exports = Announce; -------------------------------------------------------------------------------- /commands/music/addplaylist.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class AddPlaylist extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'addplaylist', 8 | description: 'Add a YouTube playlist to the queue, note that the link must be the link to the playlist, not to the first song of the playlist', 9 | usage: '{prefix}addplaylist ' 10 | }, 11 | conf: { 12 | aliases: ['ap'], 13 | expectedArgs: [{description: 'Please specify a playlist link or a saved playlist ID'}], 14 | requireDB: true 15 | } 16 | }, { userInVC: true, autoJoin: true }); 17 | } 18 | /** 19 | * @param {import("../../structures/Contexts/MusicContext.js")} context The context 20 | */ 21 | 22 | async run(context) { 23 | if (context.client.utils.isWholeNumber(context.args[0].replace(/\-/g, ""))) { 24 | const savedPlaylist = await context.client.handlers.DatabaseWrapper.rethink.table("playlists").get(context.args[0]).run(); 25 | if (!savedPlaylist) { 26 | return context.message.channel.createMessage(`:x: I couldn't find any playlist with that ID :v`); 27 | } 28 | savedPlaylist.tracks.map(t => { 29 | t.info = { 30 | ...t.info, 31 | requestedBy: context.message.author.id, 32 | position: 0, 33 | isSeekable: true, 34 | isStream: false 35 | }; 36 | }); 37 | if (!context.connection.player.playing) { 38 | context.connection.play(savedPlaylist.tracks[0], context.message.author.id); 39 | savedPlaylist.tracks.shift(); 40 | } 41 | context.connection.addTracks(savedPlaylist.tracks); 42 | const playlistAuthor = await context.client.utils.helpers.fetchUser(savedPlaylist.userID) || `Unknown user (${savedPlaylist.userID})`; 43 | return context.message.channel.createMessage(`:white_check_mark: Successfully loaded the playlist \`${savedPlaylist.name}\` by \`${playlistAuthor.tag || playlistAuthor}\``); 44 | } 45 | 46 | const resolvedTracks = await context.client.handlers.MusicManager.resolveTracks(context.connection.player.node, context.args.join(' ')); 47 | if (resolvedTracks.loadType !== context.client.handlers.MusicManager.constants.loadTypes.playlist) { 48 | return context.message.channel.createMessage(':x: Oops, this doesn\'t looks like a playlist to me, please use the `queue`, `play` and `playafter` commands for single tracks'); 49 | } 50 | if (!resolvedTracks.tracks[0]) { 51 | return context.message.channel.createMessage(`:x: I could not load this playlist :c`); 52 | } else if (context.userEntry.tierLimits.playlistLoadLimit < resolvedTracks.tracks.length) { 53 | return context.message.channel.createMessage(`:x: You cannot load a playlist of over \`${context.userEntry.tierLimits.playlistLoadLimit}\` songs :v, you can increase this limit by becoming a donator`); 54 | } 55 | if (!context.connection.player.playing) { 56 | context.connection.play(resolvedTracks.tracks[0], context.message.author.id); 57 | resolvedTracks.tracks.shift(); 58 | } 59 | context.connection.addTracks(resolvedTracks.tracks, context.message.author.id); 60 | return context.message.channel.createMessage(':musical_note: Successfully enqueued the playlist `' + resolvedTracks.playlistInfo.name + '`'); 61 | } 62 | } 63 | 64 | module.exports = AddPlaylist; -------------------------------------------------------------------------------- /commands/fun/love.js: -------------------------------------------------------------------------------- 1 | const FunCommands = require('../../structures/CommandCategories/FunCommands'); 2 | 3 | class Love extends FunCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'love', 8 | description: 'Love someone, bring some love to this world !', 9 | usage: '{prefix}love ', 10 | }, 11 | conf: { 12 | requireDB: true, 13 | aliases: ['luv'], 14 | guildOnly: true, 15 | }, 16 | }); 17 | } 18 | 19 | /** @param {import("../../structures/Contexts/FunContext")} context */ 20 | 21 | async run(context) { 22 | let lp = context.client.utils.isWholeNumber(context.args[0]) && context.args[1] ? parseInt(context.args[0]) : 1; 23 | const remainingLps = this.getRemainingLps(context); 24 | if (!context.args[0]) { 25 | if (!remainingLps) { 26 | const remainingTime = context.client.utils.timeConverter.toElapsedTime(context.userEntry.getNearestCooldown('loveCooldown') - Date.now()); 27 | return context.message.channel.createMessage(`:x: You already used all your love points, time remaining: ${remainingTime.days}d ${remainingTime.hours}h ${remainingTime.minutes}m ${remainingTime.seconds}s`); 28 | } 29 | return context.message.channel.createMessage(`You have \`${remainingLps}\` love point(s) available`); 30 | } else if (context.userEntry.isInCooldown('loveCooldown')) { 31 | const remainingTime = context.client.utils.timeConverter.toElapsedTime(context.userEntry.getNearestCooldown('loveCooldown') - Date.now()); 32 | return context.message.channel.createMessage(`:x: You already used all your love points, time remaining: ${remainingTime.days}d ${remainingTime.hours}h ${remainingTime.minutes}m ${remainingTime.seconds}s`); 33 | } 34 | const user = lp === parseInt(context.args[0]) ? context.args.splice(1).join(' ') : context.args.join(' '); 35 | const targetUser = await this.getUserFromText({ client: this.client, message: context.message, text: user }); 36 | if (!targetUser) { 37 | return context.message.channel.createMessage(`:x: I couldn't find the user you specified :v`); 38 | } else if (targetUser.id === context.message.author.id) { 39 | return context.message.channel.createMessage(`:x: Trying to love yourself eh? :eyes:`); 40 | } 41 | if (remainingLps < lp) { 42 | lp = remainingLps; 43 | } 44 | const targetEntry = await context.client.handlers.DatabaseWrapper.getUser(targetUser.id); 45 | targetEntry.love.amount = targetEntry.love.amount + lp; 46 | for (let i = 0; i < lp; i++) { 47 | context.userEntry.addCooldown('loveCooldown', context.client.config.options.loveCooldown); 48 | } 49 | await Promise.all([context.client.handlers.DatabaseWrapper.set(context.userEntry, 'user'), context.client.handlers.DatabaseWrapper.set(targetEntry, 'user')]); 50 | return context.message.channel.createMessage(`:heart: Haii ! You just gave **${lp}** love point to **${new context.client.structures.ExtendedUser(targetUser, context.client).tag}**`); 51 | } 52 | 53 | getRemainingLps(context) { 54 | const cooldownObj = context.userEntry.cooldowns.loveCooldown; 55 | let remainingLps = cooldownObj.max - cooldownObj.cooldowns.length; //In case the user is new and hasn't received the max cooldowns yet 56 | for (const cooldown of cooldownObj.cooldowns) { 57 | if (cooldown < Date.now()) { 58 | remainingLps++; 59 | } 60 | } 61 | return remainingLps; 62 | } 63 | } 64 | 65 | module.exports = Love; -------------------------------------------------------------------------------- /events/error.js: -------------------------------------------------------------------------------- 1 | const { inspect } = require('util'); 2 | 3 | class ErrorHandler { 4 | constructor() { 5 | this.discordErrorCodes = { 6 | '10018': { 7 | message: false, 8 | discard: true 9 | }, 10 | '50001': { 11 | message: 'I don\'t have enough permissions to perform this action', 12 | discard: true 13 | }, 14 | '50007': { 15 | message: 'I tried to send a DM but you/the given user have your/their DMs disabled', 16 | discard: true 17 | }, 18 | '50013': { 19 | message: 'I don\'t have enough permissions to perform this action', 20 | discard: true 21 | }, 22 | }; 23 | this.sentry; 24 | this.lastRelease; 25 | } 26 | 27 | async handle(client, err, message, sendMessage = true) { 28 | if ((typeof this.sentry === 'undefined' && client.config.apiKeys.sentryDSN) || (client.config.apiKeys.sentryDSN && this.lastRelease && this.lastRelease !== client.package.version)) { 29 | this.initSentry(client); 30 | } 31 | if (typeof err === 'object') { 32 | err = inspect(err, {depth: 5}); 33 | } 34 | const error = this.identifyError(err); 35 | process.send({ name: 'error', msg: `Error: ${err}\nStacktrace: ${err.stack}\nMessage: ${message ? message.content : 'None'}` }); 36 | if (message && sendMessage && message.author) { 37 | if (client.config.admins.includes(message.author.id)) { 38 | message.channel.createMessage({ 39 | embed: { 40 | title: ':x: An error occurred', 41 | description: '```js\n' + (err.stack || err) + '```' 42 | } 43 | }).catch(() => {}); 44 | } else { 45 | message.channel.createMessage({ 46 | embed: { 47 | title: ':x: An error occurred :v', 48 | description: error ? `${error.message}\n\nFor more information, don't hesitate to join the [support server]()` : 'This is most likely because i lack the permission to do that. However, if the issue persist, this might be a bug. In that case, please don\'t hesitate to join the [support server]() and report it' 49 | } 50 | }).catch(() => {}); 51 | } 52 | } 53 | if (this.sentry && (!error || (error && !error.discard))) { 54 | this.sentry.captureException(err, { 55 | extra: { 56 | message: message ? message.content : 'None', 57 | guild: message && message.channel.guild ? `${message.channel.guild.name} | ${message.channel.guild.id}` : 'None', 58 | cluster: client.clusterID 59 | } 60 | }); 61 | } 62 | } 63 | 64 | identifyError(err) { 65 | for (const key in this.discordErrorCodes) { 66 | if (err.includes(key)) { 67 | return this.discordErrorCodes[key]; 68 | } 69 | } 70 | return false; 71 | } 72 | 73 | initSentry(client) { 74 | let raven = client.utils.moduleIsInstalled('raven') ? require('raven') : false; 75 | if (!raven) { 76 | return this.sentry = false; 77 | } 78 | this.sentry = raven.config(client.config.apiKeys.sentryDSN, { 79 | environment: client.config.process.environment, 80 | release: client.package.version 81 | }).install(); 82 | this.lastRelease = client.package.version; 83 | } 84 | } 85 | 86 | module.exports = new ErrorHandler(); -------------------------------------------------------------------------------- /commands/generic/sinfo.js: -------------------------------------------------------------------------------- 1 | const TimeConverter = require(`../../utils/TimeConverter`); 2 | const GenericCommands = require('../../structures/CommandCategories/GenericCommands'); 3 | 4 | class Sinfo extends GenericCommands { 5 | constructor(client) { 6 | super(client, { 7 | help: { 8 | name: 'sinfo', 9 | description: 'Display some ~~useless~~ info about this server', 10 | usage: '{prefix}sinfo' 11 | }, 12 | config: { 13 | aliases: ["serverinfo"], 14 | guildOnly: true 15 | } 16 | }); 17 | } 18 | /** @param {import("../../structures/Contexts/GenericContext")} context */ 19 | 20 | async run(context) { 21 | const embedFields = [{ 22 | name: 'Name', 23 | value: context.message.channel.guild.name, 24 | inline: true 25 | },{ 26 | name: 'Owner', 27 | value: `<@!${context.message.channel.guild.ownerID}>`, 28 | inline: true 29 | },{ 30 | name: 'Region', 31 | value: context.message.channel.guild.region, 32 | inline: true 33 | },{ 34 | name: 'Shard', 35 | value: context.message.channel.guild.shard.id, 36 | inline: true 37 | },{ 38 | name: 'Created the', 39 | value: TimeConverter.toHumanDate(context.message.channel.guild.createdAt, true), 40 | inline: true 41 | },{ 42 | name: 'I\'m here since the', 43 | value: TimeConverter.toHumanDate(context.message.channel.guild.joinedAt, true), 44 | inline: true 45 | },{ 46 | name: 'Members', 47 | value: `Users: ${context.message.channel.guild.members.filter(m => !m.user.bot).length}\nBots: ${context.message.channel.guild.members.filter(m => m.user.bot).length}`, 48 | inline: true 49 | },{ 50 | name: 'Channels', 51 | value: `Texts: ${context.message.channel.guild.channels.filter(c => c.type === 0).length}\nVoices: ${context.message.channel.guild.channels.filter(c => c.type === 2).length}`, 52 | inline: true 53 | },{ 54 | name: 'Roles', 55 | value: context.message.channel.guild.roles.size, 56 | inline: true 57 | },{ 58 | name: '2FA', 59 | value: context.message.channel.guild.mfaLevel === 0 ? `:x:` : `:white_check_mark:`, 60 | inline: true 61 | },{ 62 | name: 'Latest members', 63 | value: Array.from(context.message.channel.guild.members.values()).sort((a, b) => b.joinedAt - a.joinedAt).map(m => `\`${m.username}#${m.discriminator}\``).splice(0, 5).join(` **>** `) 64 | }]; 65 | context.message.channel.createMessage({ 66 | content: `${context.message.channel.guild.name}'s info`, 67 | embed: { 68 | color: context.client.config.options.embedColor.generic, 69 | author: { 70 | name: `Requested by: ${context.message.author.username}#${context.message.author.discriminator}`, 71 | icon_url: context.message.author.avatarURL 72 | }, 73 | thumbnail: { 74 | url: context.message.channel.guild.iconURL ? context.message.channel.guild.iconURL : 'https://cdn.discordapp.com/attachments/480710816136560651/480710970243547144/defautIcon.png' 75 | }, 76 | fields: embedFields, 77 | timestamp: new Date(), 78 | footer: { 79 | text: context.client.bot.user.username, 80 | icon_url: context.client.bot.user.avatarURL 81 | } 82 | } 83 | }); 84 | } 85 | } 86 | module.exports = Sinfo; 87 | -------------------------------------------------------------------------------- /structures/CommandCategories/ModerationCommands.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../../main.js").Client} Client 3 | * @typedef {import("../Command.js").PartialCommandOptions} PartialCommandOptions 4 | * @typedef {import("eris").Role} Role 5 | * @typedef {import("eris").TextChannel} TextChannel 6 | * @typedef {import("../ExtendedStructures/ExtendedUser")} ExtendedUser 7 | * @typedef {import("eris").CategoryChannel} CategoryChannel 8 | */ 9 | 10 | const Command = require('../Command'); 11 | 12 | class ModerationCommands extends Command { 13 | /** 14 | * 15 | * @param {Client} client - The client instance 16 | * @param {PartialCommandOptions} commandOptions - The general command configuration 17 | * @param {{noArgs: string}} [options] - `noArgs` specify a message to return if no arguments are provided. 18 | * These args will make the command handler act before running the command 19 | */ 20 | constructor(client, commandOptions, options = {}) { 21 | super(client, { ...commandOptions, category: { 22 | name: 'Moderation', 23 | conf: { 24 | guildOnly: true 25 | }, 26 | emote: 'hammerPick' 27 | }}); 28 | this.options = options; 29 | } 30 | 31 | /** 32 | * Get a permission's target object 33 | * @param {ModerationContext} context - The context 34 | * @param {Number} [startsAt=0] - An optional parameter defining at what index is the target in the `args` array 35 | * @returns {TextChannel | Role | ExtendedUser | CategoryChannel} The target, or null if none is found 36 | */ 37 | async getPermissionTarget(context, startsAt = 0) { 38 | let target = context.args[startsAt].toLowerCase() === 'global' ? 'global' : null; 39 | let targetType = context.args[startsAt].toLowerCase(); 40 | if (['category', 'channel'].includes(targetType)) { 41 | target = await this.getChannelFromText({client: context.client, message: context.message, text: context.args.slice(startsAt + 1).join(' '), type: targetType === 'channel' ? 'text' : 'category'}); 42 | } else if (targetType === 'role') { 43 | target = await this.getRoleFromText({client: context.client, message: context.message, text: context.args.slice(startsAt + 1).join(' ')}); 44 | } else if (targetType === 'user') { 45 | target = await this.getUserFromText({client: context.client, message: context.message, text: context.args.slice(startsAt + 1).join(' ')}); 46 | } 47 | return target; 48 | } 49 | 50 | /** 51 | * Checks if a given string is a valid permission permission target 52 | * @param {String} arg - The string to validate 53 | * @returns {Boolean} Whether the given string is a valid permission target 54 | */ 55 | validatePermissionTarget(arg) { 56 | return arg ? ['global', 'category', 'channel', 'role', 'user'].includes(arg.toLowerCase()) : false; 57 | } 58 | 59 | /** 60 | * Checks if a given string is a valid permission 61 | * @param {String} arg - The string on which to perform the check 62 | * @returns {Boolean} Whether the given string is a valid permission 63 | */ 64 | validatePermission(arg) { 65 | let categories = []; 66 | arg = arg ? arg.toLowerCase() : ''; 67 | //eslint-disable-next-line no-unused-vars 68 | for (const [key, command] of this.client.commands) { 69 | if (!categories.includes(command.category.name) && command.category.name !== 'admin') { 70 | categories.push(`${command.category.name.toLowerCase()}*`); 71 | } 72 | } 73 | let command = this.client.commands.get(arg) || this.client.commands.get(this.client.aliases.get(arg)); 74 | if (command && command.category.name === 'admin') { 75 | return false; 76 | } 77 | return (!command && !categories.includes(arg) && arg !== '*') ? false : true; 78 | } 79 | 80 | } 81 | 82 | module.exports = ModerationCommands; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const config = require('./config'); 2 | const { Master: Sharder } = require('eris-sharder'); 3 | const axios = require('axios').default; 4 | const log = require('./utils/log'); 5 | const r = process.argv.includes('--no-db') ? false : require('rethinkdbdash')({ 6 | servers: [ 7 | { host: config.database.host, port: config.database.port } 8 | ], 9 | silent: true 10 | }); 11 | 12 | process.on('beforeExit', () => { 13 | setTimeout(() => { 14 | process.exit(); 15 | }, 10000); 16 | }); 17 | process.on('SIGINT', () => { 18 | setTimeout(() => { 19 | process.exit(); 20 | }, 10000); 21 | }); 22 | 23 | const master = new Sharder(config.token, '/main.js', { 24 | stats: true, 25 | name: 'Felix', 26 | clientOptions: { 27 | disableEvents: { 28 | TYPING_START: true 29 | }, 30 | messageLimit: 0, 31 | defaultImageSize: 1024, 32 | }, 33 | guildsPerShards: config.process.guildsPerShards, 34 | debug: config.process.debug, 35 | shards: config.process.shards, 36 | clusters: config.process.clusters 37 | }); 38 | 39 | master.on('stats', res => { 40 | if (r) { 41 | r.db(config.database.database).table('stats') 42 | .insert({ id: 1, stats: res }, { conflict: 'update' }) 43 | .run(); 44 | } 45 | 46 | master.broadcast(1, { type: 'statsUpdate', data: res }); 47 | }); 48 | 49 | if (require('cluster').isMaster) { 50 | if (r) { 51 | const postGuilds = async() => { 52 | const { stats } = await r.db(config.database.database).table('stats') 53 | .get(1); 54 | 55 | for (const botList in config.botLists) { 56 | if (config.botLists[botList].token && !config.botLists[botList].disabled) { 57 | axios({ 58 | method: 'POST', 59 | url: `http://${config.proxy.host}:${config.proxy.port}/`, 60 | data: { 61 | data: { 62 | server_count: stats.guilds 63 | }, 64 | url: config.botLists[botList].url, 65 | headers: { 'Authorization': config.botLists[botList].token, 'Content-Type': 'application/json' }, 66 | method: 'POST' 67 | }, 68 | headers: { 'Authorization': config.proxy.auth, 'Content-Type': 'application/json' }, 69 | timeout: 15000 70 | }).then(() => { 71 | log.info(`Successfully posted guild stats to ${botList}`); 72 | }).catch(err => { 73 | log.error(`Failed to post guild stats to ${botList}: ${err}`); 74 | }); 75 | } 76 | } 77 | if (config.botLists.dbl) { 78 | axios.post(`http://${config.proxy.host}:${config.proxy.port}/`, { 79 | data: { 80 | guilds: stats.guilds, 81 | users: stats.users, 82 | 'voice_connections': stats.voice 83 | }, 84 | url: config.botLists.dbl.url, 85 | headers: { 'Authorization': `Bot ${config.botLists.dbl.token}`, 'Content-Type': 'application/json' }, 86 | method: 'POST' 87 | }, { 88 | headers: { 'Authorization': config.proxy.auth, 'Content-Type': 'application/json' }, 89 | timeout: 15000 90 | }).then(() => { 91 | log.info(`Successfully posted stats to DBL`); 92 | }).catch(err => { 93 | log.error(`Failed to post stats to DBL: ${err}`); 94 | }); 95 | } 96 | }; 97 | setTimeout(postGuilds, 180000); 98 | setInterval(postGuilds, 3600000); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /commands/economy/daily.js: -------------------------------------------------------------------------------- 1 | const EconomyCommands = require('../../structures/CommandCategories/EconomyCommands'); 2 | 3 | class Daily extends EconomyCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'daily', 8 | description: 'Get your daily holy coins', 9 | usage: '{prefix}daily' 10 | } 11 | }); 12 | } 13 | /** @param {import("../../structures/Contexts/EconomyContext")} context */ 14 | 15 | async run(context) { 16 | if (context.userEntry.isInCooldown('dailyCooldown')) { 17 | return context.message.channel.createMessage(`Ahhh, I am very sorry but you still have to wait \`${context.client.utils.timeConverter.toElapsedTime(context.userEntry.cooldowns.dailyCooldown - Date.now(), true)}\` before using daily again`); 18 | } 19 | let randomEvent = context.client.config.options.economyEvents.dailyEvents ? context.client.utils.getRandomNumber(1, 100) <= context.client.config.options.economyEvents.dailyEventsRate : false; 20 | if (randomEvent) { 21 | randomEvent = this.runRandomDailyEvent(context); 22 | } else { 23 | context.userEntry.addCoins(context.client.config.options.dailyCoins); 24 | } 25 | context.userEntry.addCooldown('dailyCooldown', context.client.config.options.dailyCooldown); 26 | await context.client.handlers.DatabaseWrapper.set(randomEvent ? randomEvent.user : context.userEntry, "user"); 27 | return context.message.channel.createMessage(randomEvent ? randomEvent.text : `Hai ! You received \`${context.client.config.options.dailyCoins}\` holy coins, you now have \`${context.userEntry.economy.coins}\` holy coins`); 28 | } 29 | 30 | runRandomDailyEvent(context) { 31 | const dailyEvent = context.client.handlers.EconomyManager.dailyEvents[context.client.utils.getRandomNumber(0, context.client.handlers.EconomyManager.dailyEvents.length - 1)]; 32 | const eventCoinsChangeRate = Array.isArray(dailyEvent.changeRate) ? context.client.utils.getRandomNumber(dailyEvent.changeRate[0], dailyEvent.changeRate[1]) : dailyEvent.changeRate; 33 | const eventCoinsChange = Math.round(Math.abs(context.client.config.options.dailyCoins / 100 * eventCoinsChangeRate)); 34 | const conditionalVariant = (() => { 35 | const conditionalVariants = dailyEvent.conditionalVariants.filter(v => v.condition(context.userEntry)); 36 | const randomVariant = conditionalVariants[context.client.utils.getRandomNumber(0, conditionalVariants.length - 1)]; 37 | return randomVariant && randomVariant.context ? randomVariant.context(context.userEntry) : randomVariant; 38 | })(); 39 | const conditionalVariantSuccess = conditionalVariant ? context.client.utils.getRandomNumber(0, 100) < conditionalVariant.successRate : false; 40 | let resultText = 'Hai ! Here\'s your daily holy coins... Wait... '; 41 | if (conditionalVariant) { 42 | resultText += conditionalVariantSuccess ? conditionalVariant.success.replace(/{value}/gim, eventCoinsChange) : conditionalVariant.fail.replace(/{value}/gim, eventCoinsChange); 43 | } else { 44 | resultText += dailyEvent.message.replace(/{value}/gim, eventCoinsChange); 45 | } 46 | const coinsChange = conditionalVariantSuccess ? context.client.config.options.dailyCoins : eventCoinsChangeRate > 0 ? context.client.config.options.dailyCoins + eventCoinsChange : context.client.config.options.dailyCoins - eventCoinsChange; 47 | resultText += `\n\n\`${Math.ceil(Math.abs(coinsChange))}\` holy coins have been ${coinsChange > 0 ? 'credited to' : 'debited from'} your account, you now have \`${context.userEntry.economy.coins + Math.ceil(coinsChange)}\` holy coins`; 48 | if (coinsChange > 0) { 49 | context.userEntry.addCoins(Math.ceil(coinsChange)); 50 | } else { 51 | context.userEntry.subtractCoins(Math.ceil(coinsChange)); 52 | } 53 | return { 54 | text: resultText, 55 | user: context.userEntry 56 | }; 57 | } 58 | } 59 | 60 | module.exports = Daily; -------------------------------------------------------------------------------- /commands/misc/setrankbg.js: -------------------------------------------------------------------------------- 1 | const MiscCommands = require('../../structures/CommandCategories/MiscCommands'); 2 | const axios = require('axios').default; 3 | const sharp = require('sharp'); 4 | 5 | class SetRankBackground extends MiscCommands { 6 | constructor(client) { 7 | super(client, { 8 | help: { 9 | name: 'setrankbg', 10 | description: 'Set a custom background for your profile on the `rank` command, you can either specify a direct link to the image you want to use or upload it along.\n\nAdditionally, you can do `{prefix}setrankbg reset` to remove your custom background', 11 | usage: `{prefix}setrankbg ` 12 | }, 13 | conf: { 14 | aliases: ['setrankbackground', 'srb'], 15 | requireDB: true 16 | } 17 | }); 18 | } 19 | /** @param {import("../../structures/Contexts/MiscContext")} context */ 20 | 21 | async run(context) { 22 | if (context.args[0] === "reset") { 23 | const notSet = await context.client.handlers.DatabaseWrapper.rethink.table("user_profiles").get(context.message.author.id).run().then(bg => bg ? false : true); 24 | if (notSet) { 25 | return context.message.channel.createMessage(':x: You don\'t have any custom background set, so i can\'t reset it'); 26 | } else { 27 | await context.client.handlers.DatabaseWrapper.rethink.table("user_profiles").get(context.message.author.id).delete().run(); 28 | return context.message.channel.createMessage(':white_check_mark: Successfully reset your custom background'); 29 | } 30 | } 31 | const isLink = context.args[0] && new RegExp(/http:\/\/|https:\/\//g).test(context.args[0]) ? true : false; 32 | if (!isLink && !context.message.attachments[0]) { 33 | return context.message.channel.createMessage(`:x: You need to either specify a link to an image or upload one :v`); 34 | } 35 | let image; 36 | if (isLink) { 37 | image = await context.client.utils.helpers.fetchFromUntrustedSource(context.args[0].replace(/<|>/g, ''), true).then(res => res.data).catch(this.handleErr.bind(this)); 38 | } else { 39 | image = await axios.get(context.message.attachments[0].url, { responseType: 'arraybuffer' }).then(res => res.data).catch(this.handleErr.bind(this)); 40 | } 41 | if (!image) { 42 | return context.message.channel.createMessage(`:x: Oops, seems like i couldn't download the image, make sure the link is valid and if it is try uploading the image instead`); 43 | } 44 | image = await this.resizeImage(image); 45 | if (!image) { 46 | return context.message.channel.createMessage(':x: Oi, this doesn\'t looks like a valid image to me, make sure it is either `.jpeg/.jpg`, `.png` or `.webp`'); 47 | } 48 | if (image.length > context.userEntry.tierLimits.profileBgSize) { 49 | return context.message.channel.createMessage(`:x: I am very sorry but you can't use an image bigger than \`${context.userEntry.tierLimits.profileBgSize / 1000 / 1000}MB\` :v, you can increase this limit by becoming a donator`); 50 | } 51 | await context.client.handlers.DatabaseWrapper.set(context.client.structures.References.userRankBackground(context.message.author.id, image), 'user_profiles'); 52 | return context.message.channel.createMessage(`:white_check_mark: Successfully changed your custom rank background image`); 53 | } 54 | 55 | handleErr(err) { 56 | this.client.bot.emit("error", err); 57 | return false; 58 | } 59 | 60 | resizeImage(buffer) { 61 | return new Promise((resolve) => { 62 | return sharp(buffer) 63 | .resize(300, 300) 64 | .toFormat('png') 65 | .toBuffer((err, buf) => { 66 | if (err) { 67 | return resolve(false); 68 | } 69 | return resolve(buf.toString('base64')); 70 | }); 71 | }); 72 | } 73 | } 74 | 75 | module.exports = SetRankBackground; -------------------------------------------------------------------------------- /commands/music/skipto.js: -------------------------------------------------------------------------------- 1 | const MusicCommands = require('../../structures/CommandCategories/MusicCommands'); 2 | 3 | class SkipTo extends MusicCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'skipto', 8 | description: 'Start a vote to skip to the specified position in the queue', 9 | usage: '{prefix}skipto ' 10 | }, 11 | conf: { aliases: ['voteskipto'] } 12 | }, { userInVC: true, playing: true }); 13 | } 14 | /** 15 | * @param {import("../../structures/Contexts/MusicContext")} context The context 16 | */ 17 | 18 | async run(context) { 19 | let position = context.args[0]; 20 | if (!this.isValidPosition(position, context.connection.queue)) { 21 | return context.message.channel.createMessage(':x: You did not specify a valid number ! You must specify a number corresponding to the position in the queue of the song you want to skip to'); 22 | } 23 | position = parseInt(position) - 1; 24 | if (!context.connection.skipVote.count) { 25 | context.connection.skipVote.count = 1; 26 | context.connection.skipVote.id = Date.now(); 27 | context.connection.queue[position].voteID = Date.now(); 28 | context.connection.skipVote.callback = this.handleVoteEnd.bind(this, context, context.connection.queue[position]); 29 | context.connection.skipVote.timeout = setTimeout(this.handleVoteEnd.bind(this, context, context.connection.queue[position], 'timeout'), this.client.config.options.music.voteSkipDuration); 30 | } else { 31 | if (!context.connection.skipVote.id) { 32 | return context.message.channel.createMessage(':x: A vote to skip the current song is already ongoing'); 33 | } 34 | if (context.connection.skipVote.voted.includes(context.message.author.id)) { 35 | return context.message.channel.createMessage(':x: You already voted to skip this song'); 36 | } 37 | context.connection.skipVote.count = context.connection.skipVote.count + 1; 38 | } 39 | context.connection.skipVote.voted.push(context.message.author.id); 40 | return this.processVote(context); 41 | } 42 | 43 | async processVote(context) { 44 | const voiceChannel = context.message.channel.guild.channels.get(context.message.channel.guild.members.get(this.client.bot.user.id).voiceState.channelID); 45 | const userCount = voiceChannel.voiceMembers.filter(m => !m.bot).length; 46 | const trackIndex = context.connection.queue.findIndex(track => track.voteID === context.connection.skipVote.id); 47 | const track = context.connection.queue[trackIndex]; 48 | if (context.connection.skipVote.count >= (userCount === 2 ? 2 : (Math.ceil(userCount / 2)))) { 49 | context.connection.resetVote(); 50 | context.connection.skipTrack(trackIndex); 51 | return context.message.channel.createMessage(`:white_check_mark: Successfully skipped to the song **${track.info.title}**`); 52 | } 53 | return context.message.channel.createMessage(`:white_check_mark: Successfully registered the vote to skip to the song **${track.info.title}**, as there is \`${userCount}\` users listening and already \`${context.connection.skipVote.count}\` voted, \`${userCount === 2 ? 1 : Math.ceil(userCount / 2) - context.connection.skipVote.count}\` more vote(s) are needed`); 54 | } 55 | 56 | async handleVoteEnd(context, song, reason) { 57 | switch (reason) { 58 | case 'timeout': 59 | context.connection.resetVote(); 60 | return context.message.channel.createMessage(`:x: The vote to the song **${song.info.title}** ended because not enough users voted`); 61 | break; 62 | case 'deleted': 63 | return context.message.channel.createMessage(`:x: The vote to skip to the song **${song.info.title}** ended because the song was removed from the queue`); 64 | case 'started': 65 | return context.message.channel.createMessage(`:x: The vote to skip to the song **${song.info.title}** ended because the song just started`); 66 | } 67 | } 68 | } 69 | 70 | module.exports = SkipTo; -------------------------------------------------------------------------------- /structures/HandlersStructures/marketItems.js: -------------------------------------------------------------------------------- 1 | /** @typedef {import("../../main.js")} Client 2 | * @typedef {import("../../handlers/economyManager.js")} EconomyManager 3 | * @typedef {import("../ExtendedStructures/extendedUserEntry.js")} UserEntry 4 | * @typedef {import("../ExtendedStructures/extendedGuildEntry.js").GuildEntry} GuildEntry 5 | * @typedef {import("../Contexts/BaseContext")} BaseContext 6 | */ 7 | 8 | /** @typedef {Object} ShipData 9 | * @prop {String} type The type of the ship, can be `Destroyer` or `Battleship` at the moment 10 | * @prop {Boolean} flagship Whether the ship can be a flagship 11 | */ 12 | 13 | /** @typedef {Object} MarketItem 14 | * @prop {Number} id The ID of the item 15 | * @prop {String} name The name of the item 16 | * @prop {String} description The description of the item 17 | * @prop {Boolean} buyableOnce Whether this item can only be bought once 18 | * @prop {String} family The family, or category, of this item 19 | * @prop {Number|Function} price The price of the item, if a function, it should be called like `.price()` with an instance of the context 20 | * @prop {String} emote The corresponding emote for this item 21 | * @prop {String} [image] The URL to a fitting image, if any 22 | * @prop {Function} [run] If the item has just been purchased and this function exist, this should be ran like `.run()` with an instance of the context 23 | * @prop {ShipData} [data] If a ship, the corresponding data 24 | */ 25 | 26 | 27 | const marketItems = [{ 28 | id: 1000, 29 | name: 'dog', 30 | description: 'A dog, the legend says that dogs are relatively effective against cats', 31 | buyableOnce: true, 32 | family: 'Animals', 33 | price: 50000, 34 | emote: ':dog:' 35 | }, { 36 | id: 2000, 37 | name: 'Asakaze', 38 | description: 'Completed the 16 June 1923, the Asakaze was a IJN Minekaze-class destroyer. She featured 4 Type 3 120 mm 45 caliber naval guns and 3x2 530mm torpedo tubes.\n\nShips can come in handy for a number of tasks, for example dealing with pirates', 39 | buyableOnce: true, 40 | family: 'Ships', 41 | data: { 42 | type: 'Destroyer', 43 | flagship: false 44 | }, 45 | price: 1e6, 46 | emote: ':ship:', 47 | image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Japanese_destroyer_Asakaze_around_1924.jpg/300px-Japanese_destroyer_Asakaze_around_1924.jpg' 48 | }, { 49 | id: 2001, 50 | name: 'Hiei', 51 | description: 'The Hiei, first commissioned the 4 August 1914, was a IJN Kongō-class fast-battleship. After her 1935 refit, she featured 4x2 356mm main battery turrets and a relatively strong armor.\n\nShips can come in handy for a number of tasks, for example dealing with pirates', 52 | buyableOnce: true, 53 | family: 'Ships', 54 | data: { 55 | type: 'Battleship', 56 | flagship: false 57 | }, 58 | price: 2e6, 59 | emote: ':ship:', 60 | image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/Japanese_Battleship_Hiei.jpg/220px-Japanese_Battleship_Hiei.jpg' 61 | }, { 62 | id: 2002, 63 | name: 'Yuudachi', 64 | description: 'Poi, i mean, Yūdachi, nowadays more commonly known as Yuudachi, was a IJN Shiratsuyu-class destroyer. Commissioned the 7 January 1937, She was part of the mightiest destroyers of her time, featuring a set of 2x4 610mm torpedo tubes. As of now, Yuudachi is also a Discord bot available [here](https://bots.discord.pw/bots/388799526103941121) that you should check out (totally not advertising hello yes)\n\nShips can come in handy for a number of tasks, for example dealing with pirates', 65 | buyableOnce: true, 66 | family: 'Ships', 67 | data: { 68 | type: 'Destroyer', 69 | flagship: false 70 | }, 71 | price: 1e6, 72 | emote: ':ship:', 73 | image: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8e/Yudachi_II.jpg/300px-Yudachi_II.jpg' 74 | }, { 75 | id: 3000, 76 | name: 'Love point', 77 | description: 'Gives an extra love point to use', 78 | buyableOnce: false, 79 | family: 'Perks', 80 | price: (context) => 1e7 * context.userEntry.cooldowns.loveCooldown.max, 81 | emote: ':heart:', 82 | // @ts-ignore 83 | run: (context) => context.userEntry.cooldowns.loveCooldown.max++ 84 | }]; 85 | 86 | module.exports = marketItems; -------------------------------------------------------------------------------- /utils/log.js: -------------------------------------------------------------------------------- 1 | //Draft-logs proof of concept by Aetheryx#2222 (284122164582416385) 2 | //Afaik this frame animation proof of concept 3 | 4 | /** 5 | * @typedef {Object} Draft 6 | * @property {Boolean} spinning 7 | * @property {string} text 8 | * @property {any} draft 9 | */ 10 | 11 | const chalk = require('chalk'); 12 | const sleep = require('./sleep'); 13 | require('draftlog').into(console); 14 | 15 | const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; 16 | /** 17 | * Provides some fancy colored logs for errors, warns and info, but also animated logs 18 | * @prop {Map} drafts A map of all the current draft-logs going on 19 | * @typedef {Log} Log 20 | */ 21 | class Log { 22 | constructor() { 23 | /** @type {Map} */ 24 | this.drafts = new Map(); 25 | } 26 | 27 | /** 28 | * Log to the console a fancy red error message 29 | * @param {string} err - The error to log 30 | * @param {Boolean} [returnString] - Optional, default is false: Whether the string should be returned instead of being logged 31 | * @returns {void|string} errror log in red color 32 | */ 33 | error(err, returnString) { 34 | const log = `[${chalk.default.red(Date().toString().split(' ').slice(1, 5).join(' ') + ' ERROR')}] ${err}`; 35 | if (returnString) { 36 | return log; 37 | } else { 38 | console.log(log); 39 | } 40 | } 41 | 42 | /** 43 | * Log to the console a fancy yellow warning message 44 | * @param {string} warning - The warning to log 45 | * @param {Boolean} [returnString] - Optional, default is false: Whether the string should be returned instead of being logged 46 | * @returns {void|string} warning log in yellow 47 | */ 48 | warn(warning, returnString) { 49 | const log = `[${chalk.default.yellow(Date().toString().split(' ').slice(1, 5).join(' ') + ' WARNING')}] ${warning}`; 50 | if (returnString) { 51 | return log; 52 | } else { 53 | console.log(log); 54 | } 55 | } 56 | 57 | /** 58 | * Log to the console a fancy yellow warning message 59 | * @param {string} info - The warning to log 60 | * @param {Boolean} [returnString] - Optional, default is false: Whether the string should be returned instead of being logged 61 | * @returns {string|void} info log in green 62 | */ 63 | info(info, returnString = false) { 64 | const log = `[${chalk.default.green(Date().toString().split(' ').slice(1, 5).join(' ') + ' INFO')}] ${info}`; 65 | if (returnString) { 66 | return log; 67 | } else { 68 | console.log(log); 69 | } 70 | } 71 | 72 | /** 73 | * Log an animated "loading" message 74 | * @param {String|Number} name - The name of the draft-log, this is needed to retrieve it later 75 | * @param {string} text - The text to be logged 76 | * @returns {Promise} TODO 77 | */ 78 | async draft(name, text) { 79 | //If the terminal cannot handle draft logs, make a simple log 80 | if (!process.stderr.isTTY) { 81 | return this.info(text); 82 | } 83 | this.drafts.set(name, { 84 | spinning: true, 85 | text, 86 | // @ts-ignore 87 | draft: console.draft(this.info(`${frames[0]} ${text}`, true)) 88 | }); 89 | for (let i = 0; this.drafts.get(name).spinning; i++) { 90 | await sleep(50); 91 | this.drafts.get(name).draft(this.info(`${frames[i % frames.length]} ${text}`, true)); 92 | } 93 | } 94 | 95 | /** 96 | * End an animated draft-log 97 | * @param {String|Number} name - The name of the draft-log to end 98 | * @param {string} text - Text to update the log with 99 | * @param {Boolean} [succeed=true] - Whether the operation succeed or not, will respectively result in a info or an error message 100 | * @returns {Promise} TODO 101 | */ 102 | async endDraft(name, text, succeed = true) { 103 | this.drafts.get(name).spinning = false; 104 | await sleep(50); 105 | this.drafts.get(name).draft(this[succeed ? 'info' : 'error'](`${succeed ? '✔' : '✖'} ${text}`, true)); 106 | this.drafts.delete(name); 107 | } 108 | } 109 | 110 | module.exports = new Log(); 111 | -------------------------------------------------------------------------------- /commands/moderation/clear.js: -------------------------------------------------------------------------------- 1 | const ModerationCommands = require("../../structures/CommandCategories/ModerationCommands"); 2 | 3 | class Clear extends ModerationCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: "clear", 8 | description: "Prune messages, the available filters are `-b`, (deletes only bot messages) `-c` (delete commands and their outputs) and `-u` (delete the specified user messages)\n\nSo for example `{prefix}clear 50 -bcu @Baguette` will clear all the bots messages, the commands and the messages from the user `Baguette` in the last 50 messages", 9 | usage: "{prefix}clear " 10 | }, 11 | conf: { 12 | requirePerms: ["manageMessages"], 13 | aliases: [ 14 | "clean", "prune" 15 | ], 16 | guildOnly: true 17 | } 18 | }); 19 | } 20 | 21 | /** @param {import("../../structures/Contexts/ModerationContext")} context */ 22 | 23 | async run(context) { 24 | const limit = context.args[0]; 25 | if (!limit || !context.client.utils.isWholeNumber(limit)) { 26 | return context.message.channel.createMessage(`:x: You didn't specify how many messages to delete`); 27 | } 28 | let filtered = []; 29 | const slice = (collection, count) => { 30 | const newColl = new this.client.Collection(); 31 | const colEntries = new this.client.Collection(collection).sort((a, b) => b.timestamp - a.timestamp).entries(); 32 | for (let i = 0; i < count; i++) { 33 | const value = colEntries.next().value; 34 | newColl.set(value[0], value[1]); 35 | } 36 | return newColl; 37 | }; 38 | //Don't fetch the messages if they're already cached, use the cached messages and take only the specified amount 39 | let fetchedMessages = context.message.channel.messages.size >= limit 40 | ? slice(context.message.channel.messages, limit) 41 | : await context.message.channel.getMessages(parseInt(limit)); 42 | //Filter messages older than 2 weeks 43 | fetchedMessages = Array.isArray(fetchedMessages) 44 | ? fetchedMessages.filter(m => m.timestamp > Date.now() - 1209600000) 45 | : fetchedMessages.filterArray(m => m.timestamp > Date.now() - 1209600000); 46 | for (const arg of context.args) { 47 | if (arg.startsWith("-")) { 48 | if (arg.toLowerCase().includes("b")) { 49 | filtered = filtered.concat(fetchedMessages.filter(m => m.author.bot)); 50 | } 51 | if (arg.toLowerCase().includes("u")) { 52 | const user = await this.getUserFromText({message: context.message, client: this.client, text: context.args.splice(2).join(" ")}); 53 | filtered = filtered.concat(fetchedMessages.filter(m => m.author.id === ( 54 | user 55 | ? user.id 56 | : context.message.author.id))); 57 | } 58 | if (arg.toLowerCase().includes("c")) { 59 | filtered = filtered.concat(fetchedMessages.filter(m => m.author.id === context.client.bot.user.id)); 60 | filtered = filtered.concat(fetchedMessages.filter(m => m.context.startsWith( 61 | context.guildEntry 62 | ? context.guildEntry.getPrefix 63 | : context.client.config.prefix) || m.context.startsWith(`<@${context.client.bot.user.id}>`) || m.context.startsWith(`<@!${context.client.bot.user.id}`))); 64 | } 65 | } 66 | } 67 | let uniqueMessages = filtered[0] 68 | ? [] 69 | : fetchedMessages.map(m => m.id); 70 | 71 | for (const m of filtered) { 72 | if (!uniqueMessages.find(msg => msg === m.id)) { 73 | uniqueMessages.push(m.id); 74 | } 75 | } 76 | if (uniqueMessages.length < 2) { 77 | context.message.channel.createMessage(":x: Not enough messages have been matched with the filter"); 78 | } 79 | await context.message.channel.deleteMessages(uniqueMessages); 80 | context.message.channel.createMessage(`:white_check_mark: Deleted **${uniqueMessages.length}** messages`).then(m => { 81 | setTimeout(() => { 82 | m.delete().catch(() => {}); 83 | }, 4000); 84 | }); 85 | } 86 | } 87 | 88 | module.exports = Clear; -------------------------------------------------------------------------------- /handlers/EconomyManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import("../main.js").Client} Client 3 | * @typedef {import("../structures/HandlersStructures/marketItems").MarketItem} MarketItem 4 | * @typedef {import("../structures/HandlersStructures/slotsEvents").SlotsEvent} SlotsEvent 5 | * @typedef {import("../structures/HandlersStructures/dailyEvents").DailyEvent} DailyEvent 6 | */ 7 | 8 | 9 | class EconomyManager { 10 | /** 11 | * Provides methods related to the economy, such as crediting, debiting or transferring coins 12 | * @param {Client} client - The client instance 13 | */ 14 | constructor(client) { 15 | /** @type {Client} The client instance */ 16 | this.client = client; 17 | /** @type {Array} The market items */ 18 | this.marketItems = require('../structures/HandlersStructures/marketItems'); 19 | /** @type {Array} The slots events */ 20 | this.slotsEvents = require('../structures/HandlersStructures/slotsEvents')(client, this); 21 | /** @type {Array} The slots events */ 22 | this.dailyEvents = require('../structures/HandlersStructures/dailyEvents')(client, this); 23 | } 24 | 25 | /** 26 | * Transfer coins from one account to another, taking into account the coins limit, so the coins that can't be given because the receiver has hit the limit will be given back 27 | * @param {object} params An object of parameters 28 | * @param {object} params.from Who is transferring their coins, aka who will be debited (this has to be the database entry) 29 | * @param {object} params.to Who is receiving the coins, aka who will be credited (this has to be the database entry) 30 | * @param {number} params.amount The amount of coins to transfer 31 | * @returns {Promise} A summary of the transaction 32 | */ 33 | async transfer(params) { 34 | const transactionSummary = { 35 | donor: { 36 | user: params.from.id, 37 | debited: (params.to.economy.coins + params.amount) > this.client.config.options.coinsLimit ? params.amount - ((params.to.economy.coins + params.amount) - this.client.config.options.coinsLimit) : params.amount 38 | }, 39 | receiver: { 40 | user: params.to.id, 41 | credited: (params.to.economy.coins + params.amount) > this.client.config.options.coinsLimit ? params.amount - ((params.to.economy.coins + params.amount) - this.client.config.options.coinsLimit) : params.amount 42 | } 43 | }; 44 | params.from.economy.coins = params.from.economy.coins - transactionSummary.donor.debited; 45 | params.to.economy.coins = params.to.economy.coins + transactionSummary.receiver.credited; 46 | const registeredTransaction = this._registerTransaction(transactionSummary, params.from, params.to); 47 | await Promise.all([this.client.handlers.DatabaseWrapper.set(registeredTransaction.donor, 'user'), this.client.handlers.DatabaseWrapper.set(registeredTransaction.receiver, 'user')]); 48 | return transactionSummary; 49 | } 50 | 51 | /** 52 | * 53 | * @param {object} transactionSummary The summary of the transaction 54 | * @param {object} donor The donor 55 | * @param {object} receiver The receiver 56 | * @returns {{donor, receiver}} Returns the donor and the receiver entries with the transaction registered 57 | * @private 58 | */ 59 | _registerTransaction(transactionSummary, donor, receiver) { 60 | donor.economy.transactions.unshift(this.client.structures.References.transactionData({ 61 | amount: -transactionSummary.donor.debited, 62 | from: transactionSummary.donor.user, 63 | to: transactionSummary.receiver.user, 64 | reason: 'transfer' 65 | })); 66 | donor.economy.transactions = donor.economy.transactions.slice(0, 10); 67 | receiver.economy.transactions.unshift(this.client.structures.References.transactionData({ 68 | amount: transactionSummary.receiver.credited, 69 | from: transactionSummary.donor.user, 70 | to: transactionSummary.receiver.user, 71 | reason: 'transfer' 72 | })); 73 | receiver.economy.transactions = receiver.economy.transactions.slice(0, 10); 74 | return { 75 | donor: donor, 76 | receiver: receiver 77 | }; 78 | } 79 | 80 | /** 81 | * Get a market item by its ID 82 | * @param {number} itemID - The ID of the item 83 | * @returns {object} The item 84 | */ 85 | getItem(itemID) { 86 | return this.marketItems.find(i => i.id === itemID); 87 | } 88 | } 89 | 90 | module.exports = EconomyManager; -------------------------------------------------------------------------------- /commands/admin/clientstats.js: -------------------------------------------------------------------------------- 1 | const AdminCommands = require('../../structures/CommandCategories/AdminCommands'); 2 | 3 | class ClientStats extends AdminCommands { 4 | constructor(client) { 5 | super(client, { 6 | help: { 7 | name: 'clientstats', 8 | description: 'Get detailed statistics about the bot', 9 | usage: '{prefix}stats' 10 | }, 11 | conf: { 12 | aliases: ["cs", "botstats"], 13 | } 14 | }); 15 | } 16 | /** @param {import("../../structures/Contexts/AdminContext")} context */ 17 | 18 | async run(context) { 19 | if (context.client.bot.uptime < 60000) { 20 | return context.message.channel.createMessage(`:x: Please wait another ${60000 - context.client.bot.uptime}ms`); 21 | } 22 | const normalizeMemory = (memory) => `${(memory / 1024 / 1024).toFixed(2)}MB`; 23 | const normalizeLoad = (load) => `${(load * 100).toFixed(2)}%`; 24 | await context.message.channel.createMessage({ 25 | embed: { 26 | title: ':gear: Client stats', 27 | fields: [{ 28 | name: 'Clusters', 29 | value: `Total: ${context.client.stats.clusters.length}\nActive: ${context.client.stats.clusters.length - context.client.stats.clusters.filter(c => c.guilds < 1).length}`, 30 | inline: true 31 | }, 32 | { 33 | name: 'RAM usage', 34 | value: `${context.client.stats.totalRam.toFixed(2)}MB`, 35 | inline: true 36 | }, 37 | { 38 | name: 'General stats', 39 | value: `Guilds: ${context.client.stats.guilds} | Cached users: ${context.client.stats.users} | Large guilds: ${context.client.stats.largeGuilds}` 40 | }, 41 | { 42 | name: 'Lavalink nodes', 43 | value: (() => { 44 | let nodesStatus = '```ini\n'; 45 | for (const node of context.client.config.options.music.nodes) { 46 | const lavalinkNode = context.client.bot.voiceConnections.nodes.get(node.host); 47 | nodesStatus += `[${node.location}]: ${lavalinkNode.connected ? '[Online]' : '[Offline]'}\n`; 48 | nodesStatus += `=> Used memory: [${normalizeMemory(lavalinkNode.stats.memory.used)}]\n`; 49 | nodesStatus += `=> Allocated memory: [${normalizeMemory(lavalinkNode.stats.memory.allocated)}]\n`; 50 | nodesStatus += `=> Free memory: [${normalizeMemory(lavalinkNode.stats.memory.free)}]\n`; 51 | nodesStatus += `=> Reservable memory: [${normalizeMemory(lavalinkNode.stats.memory.reservable)}]\n`; 52 | nodesStatus += `=> Cores: [${lavalinkNode.stats.cpu.cores}]\n`; 53 | nodesStatus += `=> System load: [${normalizeLoad(lavalinkNode.stats.cpu.systemLoad)}]\n`; 54 | nodesStatus += `=> Node load: [${normalizeLoad(lavalinkNode.stats.cpu.lavalinkLoad)}]\n`; 55 | nodesStatus += `=> Uptime: [${context.client.utils.timeConverter.toElapsedTime(lavalinkNode.stats.uptime, true)}]\n`; 56 | nodesStatus += `=> Players: [${lavalinkNode.stats.players}]\n`; 57 | nodesStatus += `=> Paused players: [${lavalinkNode.stats.players - lavalinkNode.stats.playingPlayers}]\n`; 58 | } 59 | return nodesStatus + '```'; 60 | })() 61 | } 62 | ], 63 | color: context.client.config.options.embedColor.generic 64 | } 65 | }); 66 | const clustersShardsStats = await context.client.handlers.IPCHandler.fetchShardsStats(); 67 | return context.message.channel.createMessage('```ini\n' + context.client.stats.clusters.map(c => { 68 | const cluster = clustersShardsStats.find(cl => cl.clusterID === c.cluster); 69 | let clusterStats = `Cluster [${c.cluster}]: [${c.shards}] shard(s) | [${c.guilds}] guild(s) | [${c.ram.toFixed(2)}]MB RAM used | Up for [${context.client.utils.timeConverter.toElapsedTime(c.uptime, true)}] | Music connections: [${cluster.data[0].musicConnections}]\n`; 70 | for (const shard of cluster.data) { 71 | clusterStats += `=> Shard [${shard.id}]: [${shard.guilds}] guild(s) | [${shard.status}] | ~[${shard.latency}]ms\n`; 72 | } 73 | return clusterStats; 74 | }).join('\n--\n') + '```'); 75 | } 76 | } 77 | 78 | module.exports = ClientStats; --------------------------------------------------------------------------------