├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── arguments ├── channelname.js ├── membername.js ├── rolename.js └── username.js ├── azure-pipelines.yml ├── commands ├── Configuration │ └── prefix.js ├── Fun │ ├── 8ball.js │ ├── adorableavatar.js │ ├── badge.js │ ├── banner.js │ ├── biggify.js │ ├── card.js │ ├── catfacts.js │ ├── choice.js │ ├── chucknorris.js │ ├── coinflip.js │ ├── compliment.js │ ├── d20.js │ ├── deobfuscate.js │ ├── dogfacts.js │ ├── faceapp.js │ ├── insult.js │ ├── markov.js │ ├── meme.js │ ├── obfuscate.js │ ├── randquote.js │ ├── shame.js │ ├── thanosquote.js │ ├── trumpquote.js │ ├── urban.js │ ├── waifu.js │ ├── wordcloud.js │ └── yomomma.js ├── General │ └── Chat Bot Info │ │ └── help.js ├── Misc │ ├── cat.js │ ├── dog.js │ ├── duck.js │ ├── echo.js │ ├── fml.js │ ├── fox.js │ └── lizard.js ├── Moderation │ ├── ban.js │ ├── check.js │ ├── kick.js │ ├── mute.js │ ├── prune.js │ ├── softban.js │ ├── unban.js │ ├── unmute.js │ └── voicekick.js ├── System │ ├── Admin │ │ ├── docs.js │ │ ├── eval.js │ │ ├── heapsnapshot.js │ │ └── reboot.js │ └── exec.js └── Tools │ ├── artist.js │ ├── avatar.js │ ├── crate.js │ ├── discordemoji.js │ ├── followage.js │ ├── hastebin.js │ ├── movie.js │ ├── pkgsize.js │ ├── price.js │ ├── randomreddit.js │ ├── remindme.js │ ├── role-info.js │ ├── server-info.js │ ├── song.js │ ├── subreddit.js │ ├── todo.js │ ├── topinvites.js │ ├── tvshow.js │ ├── twitch.js │ ├── user-info.js │ ├── wikipedia.js │ └── wolfram.js ├── events ├── rateLimit.js └── twitterStream.js ├── extendables ├── fetchImage.js ├── message.js └── sendLoading.js ├── finalizers └── deleteCommand.js ├── inhibitors └── requiredProviders.js ├── languages ├── de-DE.js ├── es-ES.js ├── fr-FR.js ├── it-IT.js ├── ja-JP.js ├── ro-RO.js ├── ru-RU.js ├── tr-TR.js └── zh-TW.js ├── monitors ├── everyone.js ├── filter.js ├── invitedetection.js └── nomention.js ├── package.json ├── providers ├── btf.js ├── cockroachdb.js ├── firestore.js ├── mongodb.js ├── mssql.js ├── mysql.js ├── nedb.js ├── neo4j.js ├── postgresql.js ├── rebirthdb.js ├── redis.js ├── rethinkdb.js ├── sqlite.js └── toml.js ├── routes ├── channel.js ├── channels.js ├── emoji.js ├── emojis.js ├── guild.js ├── guilds.js ├── member.js ├── members.js ├── piece.js ├── pieces.js ├── role.js ├── roles.js ├── user.js └── users.js ├── serializers ├── bigint.js └── messagepromise.js ├── tasks ├── cleanup.js ├── getSpotifyToken.js ├── jsonBackup.js ├── reloadOnChanges.js ├── reminder.js └── unmute.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "klasa" 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | 61 | package-lock\.json 62 | docs 63 | desktop.ini 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 dirigeants 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Klasa Pieces repository 2 | 3 | This repository contains the various *Pieces* submitted by users and collaborators. 4 | 5 | ## What are Pieces? 6 | 7 | *Pieces* are simply parts of code that can be downloaded and installed straight into your [Klasa](https://github.com/dirigeants/klasa) bot installation. 8 | 9 | Pieces can include: 10 | 11 | - **Commands**: Chat commands that generally respond with a message after taking some actions. 12 | - **Events**: Pieces that get executed when a Discord event triggers. 13 | - **Extendables**: Pieces that act passively, attaching new [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get), [setters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) or [methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions) or [static](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static) to the current Discord.js classes. They're executed at startup before any other piece. 14 | - **Finalizers**: Pieces that run on messages after a successful command. 15 | - **Inhibitors**: Inhibitors are pieces that run before a command is executed and may take action on the message received, and block a command from running in certain cases (thus *inhibit* a command). 16 | - **Monitors**: Monitors are pieces that can run on every message, whether or not it triggers a command. Useful for spam monitoring, swear filters, etc. 17 | - **Providers**: Support for a specific database type. By default a very small amount of DBs are supported, but you can extend the support by adding a provider for whatever database you choose, and configure it to point to your own database. 18 | - **Tasks**: Pieces that get executed on scheduled tasks. 19 | 20 | ## Submitting Pieces 21 | 22 | Check out the documentation: 23 | 24 | - **[Getting Started](https://klasa.js.org/#/docs/main/master/Getting%20Started/GettingStarted)** 25 | 26 | - **[Creating Commands](https://klasa.js.org/#/docs/main/master/Piece%20Basics/CreatingCommands)** 27 | - **[Creating Events](https://klasa.js.org/#/docs/main/master/Piece%20Basics/CreatingEvents)** 28 | - **[Creating Extendables](https://klasa.js.org/#/docs/main/master/Piece%20Basics/CreatingExtendables)** 29 | - **[Creating Finalizers](https://klasa.js.org/#/docs/main/master/Piece%20Basics/CreatingFinalizers)** 30 | - **[Creating Inhibitors](https://klasa.js.org/#/docs/main/master/Piece%20Basics/CreatingInhibitors)** 31 | - **[Creating Languages](https://klasa.js.org/#/docs/main/master/Piece%20Basics/CreatingLanguages)** 32 | - **[Creating Monitors](https://klasa.js.org/#/docs/main/master/Piece%20Basics/CreatingMonitors)** 33 | - **[Creating Providers](https://klasa.js.org/#/docs/main/master/Piece%20Basics/CreatingProviders)** 34 | - **[Creating Tasks](https://klasa.js.org/#/docs/main/master/Piece%20Basics/CreatingTasks)** 35 | 36 | To submit your own pieces for approval (quick steps): 37 | 38 | - Fork this repository 39 | - Create a new piece in the appropriate folder 40 | - Lint your code with [klasa-lint](https://github.com/dirigeants/klasa-lint) 41 | - Create a Pull Request to the repository 42 | - Be patient. Someone will approve/deny it as soon as they can 43 | 44 | We will automatically deny PRs that: 45 | 46 | - Have identical functionality to an existing *Piece* 47 | - Have code that breaks/does not catch errors/etc 48 | - Contain NSFW, NSFL contents or contents we deem to be unacceptable 49 | - Contain hacks/exploits/etc 50 | - Have code that might cause a bot to break the TOS or Ratelimits 51 | - Do not abide by our [Code of Conduct](https://github.com/dirigeants/CoC/blob/master/CODE_OF_CONDUCT.md) 52 | - Any reason **WE** feel is valid 53 | 54 | > WE RESERVE THE RIGHT TO REFUSE ANY CONTENTS FOR ANY REASON WHETHER YOU ACCEPT THEM OR NOT. 55 | -------------------------------------------------------------------------------- /arguments/channelname.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Argument, util: { regExpEsc } } = require('klasa'); 3 | const { Channel, Message } = require('discord.js'); 4 | 5 | const CHANNEL_REGEXP = Argument.regex.channel; 6 | 7 | function resolveChannel(query, guild) { 8 | if (query instanceof Channel) return guild.channels.has(query.id) ? query : null; 9 | if (query instanceof Message) return query.guild.id === guild.id ? query.channel : null; 10 | if (typeof query === 'string' && CHANNEL_REGEXP.test(query)) return guild.channels.get(CHANNEL_REGEXP.exec(query)[1]); 11 | return null; 12 | } 13 | 14 | module.exports = class extends Argument { 15 | 16 | async run(arg, possible, msg) { 17 | if (!msg.guild) return this.store.get('channel').run(arg, possible, msg); 18 | const resChannel = resolveChannel(arg, msg.guild); 19 | if (resChannel) return resChannel; 20 | 21 | const results = []; 22 | const reg = new RegExp(regExpEsc(arg), 'i'); 23 | for (const channel of msg.guild.channels.values()) { 24 | if (reg.test(channel.name)) results.push(channel); 25 | } 26 | 27 | let querySearch; 28 | if (results.length > 0) { 29 | const regWord = new RegExp(`\\b${regExpEsc(arg)}\\b`, 'i'); 30 | const filtered = results.filter(channel => regWord.test(channel.name)); 31 | querySearch = filtered.length > 0 ? filtered : results; 32 | } else { 33 | querySearch = results; 34 | } 35 | 36 | switch (querySearch.length) { 37 | case 0: throw `${possible.name} Must be a valid name, id or channel mention`; 38 | case 1: return querySearch[0]; 39 | default: throw `Found multiple matches: \`${querySearch.map(channel => channel.name).join('`, `')}\``; 40 | } 41 | } 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /arguments/membername.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Argument, util: { regExpEsc } } = require('klasa'); 3 | const { GuildMember, User } = require('discord.js'); 4 | 5 | const USER_REGEXP = Argument.regex.userOrMember; 6 | 7 | function resolveMember(query, guild) { 8 | if (query instanceof GuildMember) return query; 9 | if (query instanceof User) return guild.members.fetch(query.id); 10 | if (typeof query === 'string') { 11 | if (USER_REGEXP.test(query)) return guild.members.fetch(USER_REGEXP.exec(query)[1]).catch(() => null); 12 | if (/\w{1,32}#\d{4}/.test(query)) { 13 | const res = guild.members.find(member => member.user.tag.toLowerCase() === query.toLowerCase()); 14 | return res || null; 15 | } 16 | } 17 | return null; 18 | } 19 | 20 | module.exports = class extends Argument { 21 | 22 | async run(arg, possible, msg) { 23 | if (!msg.guild) throw 'This command can only be used inside a guild.'; 24 | const resUser = await resolveMember(arg, msg.guild); 25 | if (resUser) return resUser; 26 | 27 | const results = []; 28 | const reg = new RegExp(regExpEsc(arg), 'i'); 29 | for (const member of msg.guild.members.values()) { 30 | if (reg.test(member.user.username)) results.push(member); 31 | } 32 | 33 | let querySearch; 34 | if (results.length > 0) { 35 | const regWord = new RegExp(`\\b${regExpEsc(arg)}\\b`, 'i'); 36 | const filtered = results.filter(member => regWord.test(member.user.username)); 37 | querySearch = filtered.length > 0 ? filtered : results; 38 | } else { 39 | querySearch = results; 40 | } 41 | 42 | switch (querySearch.length) { 43 | case 0: throw `${possible.name} Must be a valid name, id or user mention`; 44 | case 1: return querySearch[0]; 45 | default: throw `Found multiple matches: \`${querySearch.map(member => member.user.tag).join('`, `')}\``; 46 | } 47 | } 48 | 49 | }; 50 | -------------------------------------------------------------------------------- /arguments/rolename.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Argument, util: { regExpEsc } } = require('klasa'); 3 | const { Role } = require('discord.js'); 4 | 5 | const ROLE_REGEXP = Argument.regex.role; 6 | 7 | function resolveRole(query, guild) { 8 | if (query instanceof Role) return guild.roles.has(query.id) ? query : null; 9 | if (typeof query === 'string' && ROLE_REGEXP.test(query)) return guild.roles.get(ROLE_REGEXP.exec(query)[1]); 10 | return null; 11 | } 12 | 13 | module.exports = class extends Argument { 14 | 15 | async run(arg, possible, msg) { 16 | if (!msg.guild) return this.store.get('role').run(arg, possible, msg); 17 | const resRole = resolveRole(arg, msg.guild); 18 | if (resRole) return resRole; 19 | 20 | const results = []; 21 | const reg = new RegExp(regExpEsc(arg), 'i'); 22 | for (const role of msg.guild.roles.values()) { if (reg.test(role.name)) results.push(role); } 23 | 24 | let querySearch; 25 | if (results.length > 0) { 26 | const regWord = new RegExp(`\\b${regExpEsc(arg)}\\b`, 'i'); 27 | const filtered = results.filter(role => regWord.test(role.name)); 28 | querySearch = filtered.length > 0 ? filtered : results; 29 | } else { 30 | querySearch = results; 31 | } 32 | 33 | switch (querySearch.length) { 34 | case 0: throw `${possible.name} Must be a valid name, id or role mention`; 35 | case 1: return querySearch[0]; 36 | default: 37 | if (querySearch[0].name.toLowerCase() === arg.toLowerCase()) return querySearch[0]; 38 | throw `Found multiple matches: \`${querySearch.map(role => role.name).join('`, `')}\``; 39 | } 40 | } 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /arguments/username.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Argument, util: { regExpEsc } } = require('klasa'); 3 | const { GuildMember, User } = require('discord.js'); 4 | 5 | const USER_REGEXP = Argument.regex.userOrMember; 6 | 7 | function resolveUser(query, guild) { 8 | if (query instanceof GuildMember) return query.user; 9 | if (query instanceof User) return query; 10 | if (typeof query === 'string') { 11 | if (USER_REGEXP.test(query)) return guild.client.users.fetch(USER_REGEXP.exec(query)[1]).catch(() => null); 12 | if (/\w{1,32}#\d{4}/.test(query)) { 13 | const res = guild.members.find(member => member.user.tag === query); 14 | return res ? res.user : null; 15 | } 16 | } 17 | return null; 18 | } 19 | 20 | module.exports = class extends Argument { 21 | 22 | async run(arg, possible, msg) { 23 | if (!msg.guild) return this.store.get('user').run(arg, possible, msg); 24 | const resUser = await resolveUser(arg, msg.guild); 25 | if (resUser) return resUser; 26 | 27 | const results = []; 28 | const reg = new RegExp(regExpEsc(arg), 'i'); 29 | for (const member of msg.guild.members.values()) { 30 | if (reg.test(member.user.username)) results.push(member.user); 31 | } 32 | 33 | let querySearch; 34 | if (results.length > 0) { 35 | const regWord = new RegExp(`\\b${regExpEsc(arg)}\\b`, 'i'); 36 | const filtered = results.filter(user => regWord.test(user.username)); 37 | querySearch = filtered.length > 0 ? filtered : results; 38 | } else { 39 | querySearch = results; 40 | } 41 | 42 | switch (querySearch.length) { 43 | case 0: throw `${possible.name} Must be a valid name, id or user mention`; 44 | case 1: return querySearch[0]; 45 | default: throw `Found multiple matches: \`${querySearch.map(user => user.tag).join('`, `')}\``; 46 | } 47 | } 48 | 49 | }; 50 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | repositories: 3 | - repository: templates 4 | type: github 5 | name: dirigeants/pipelines 6 | endpoint: dirigeants 7 | 8 | jobs: 9 | - template: lint.yml@templates 10 | -------------------------------------------------------------------------------- /commands/Configuration/prefix.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | aliases: ['setPrefix'], 9 | cooldown: 5, 10 | description: 'Change the command prefix the bot uses in your server.', 11 | permissionLevel: 0, 12 | runIn: ['text'], 13 | usage: '[reset|prefix:str{1,10}]' 14 | }); 15 | } 16 | 17 | async run(message, [prefix]) { 18 | if (!prefix) return message.send(`The prefix for this guild is \`${message.guild.settings.prefix}\``); 19 | if (!await message.hasAtLeastPermissionLevel(6)) throw message.language.get('INHIBITOR_PERMISSIONS'); 20 | if (prefix === 'reset') return this.reset(message); 21 | if (message.guild.settings.prefix === prefix) throw message.language.get('CONFIGURATION_EQUALS'); 22 | await message.guild.settings.update('prefix', prefix); 23 | return message.send(`The prefix for this guild has been set to \`${prefix}\``); 24 | } 25 | 26 | async reset(message) { 27 | await message.guild.settings.reset('prefix'); 28 | return message.send(`Switched back the guild's prefix back to \`${this.client.options.prefix}\`!`); 29 | } 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /commands/Fun/8ball.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | aliases: ['8', 'magic', '8ball', 'mirror'], 9 | description: 'Magic 8-Ball, does exactly what the toy does.', 10 | usage: '' 11 | }); 12 | } 13 | 14 | run(msg, [question]) { 15 | return msg.reply(question.endsWith('?') ? 16 | `🎱 ${answers[Math.floor(Math.random() * answers.length)]}` : 17 | "🎱 That doesn't look like a question, try again please."); 18 | } 19 | 20 | }; 21 | 22 | const answers = [ 23 | 'Maybe.', 24 | 'Certainly not.', 25 | 'I hope so.', 26 | 'Not in your wildest dreams.', 27 | 'There is a good chance.', 28 | 'Quite likely.', 29 | 'I think so.', 30 | 'I hope not.', 31 | 'I hope so.', 32 | 'Never!', 33 | 'Fuhgeddaboudit.', 34 | 'Ahaha! Really?!?', 35 | 'Pfft.', 36 | 'Sorry, bucko.', 37 | 'Hell, yes.', 38 | 'Hell to the no.', 39 | 'The future is bleak.', 40 | 'The future is uncertain.', 41 | 'I would rather not say.', 42 | 'Who cares?', 43 | 'Possibly.', 44 | 'Never, ever, ever.', 45 | 'There is a small chance.', 46 | 'Yes!' 47 | ]; 48 | -------------------------------------------------------------------------------- /commands/Fun/adorableavatar.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | description: 'Sends an avatar generated from adorable.io', 9 | usage: '[name:str]' 10 | }); 11 | } 12 | 13 | async run(msg, [name]) { 14 | return msg.send(`https://api.adorable.io/avatars/285/${ 15 | encodeURIComponent(name || msg.author.username || msg.id) 16 | }.png`); 17 | } 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /commands/Fun/badge.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | const { MessageEmbed } = require('discord.js'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | description: 'Get a themed badge from robohash.org', 9 | usage: '[user:mention] [set:integer{1,4}]', 10 | usageDelim: ' ' 11 | }); 12 | } 13 | 14 | async run(message, [user = message.author, set = 1]) { 15 | return message.sendEmbed(new MessageEmbed() 16 | .setImage(`https://robohash.org/${user.id}?set=set${set}`) 17 | .setColor('RANDOM') 18 | .setFooter(`Requested by ${message.author.tag}`, message.author.displayAvatarURL()) 19 | .setTimestamp()); 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /commands/Fun/banner.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const figletAsync = require('util').promisify(require('figlet')); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | description: 'Creates an ASCII banner from the string you supply.', 10 | usage: '' 11 | }); 12 | } 13 | 14 | async run(msg, [banner]) { 15 | return msg.sendCode('', await figletAsync(banner)); 16 | } 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /commands/Fun/biggify.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const jimp = require('jimp'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { description: 'Makes a big version of an image.' }); 9 | } 10 | 11 | async run(msg) { 12 | const { url } = await msg.channel.fetchImage(); 13 | const img = await jimp.read(url); 14 | img.resize(400, jimp.AUTO); 15 | const chunkHeight = img.bitmap.height / 4; 16 | for (let i = 0; i <= 3; i++) { 17 | const tempImg = img.clone().crop(0, chunkHeight * i, 400, chunkHeight); 18 | await new Promise((resolve, reject) => { 19 | tempImg.getBuffer('image/png', (err, buffer) => err ? 20 | reject(err) : 21 | resolve(msg.channel.sendFile(buffer, 'image.png'))); 22 | }); 23 | } 24 | } 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /commands/Fun/card.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const ranks = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']; 4 | const suits = ['♠️', '♦', '♥️', '♠️']; 5 | 6 | module.exports = class extends Command { 7 | 8 | constructor(...args) { 9 | super(...args, { 10 | description: 'Draws some random cards from a deck.', 11 | usage: '' 12 | }); 13 | } 14 | 15 | run(msg, [numCards]) { 16 | const lines = []; 17 | 18 | for (let i = 0; i < numCards; ++i) { 19 | lines.push(`**${ranks[Math.floor(Math.random() * ranks.length)]}**${suits[Math.floor(Math.random() * suits.length)]}`); 20 | } 21 | 22 | return msg.sendMessage(lines.join(', ')); 23 | } 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /commands/Fun/catfacts.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | aliases: ['catfact', 'kittenfact'], 10 | description: 'Let me tell you a misterious cat fact.' 11 | }); 12 | } 13 | 14 | async run(msg) { 15 | const fact = await fetch('https://catfact.ninja/fact') 16 | .then(response => response.json()) 17 | .then(body => body.fact); 18 | return msg.sendMessage(`📢 **Catfact:** *${fact}*`); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /commands/Fun/choice.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | aliases: ['choose', 'decide'], 9 | description: 'Makes a decision for you given some choices.', 10 | usage: ' [...]', 11 | usageDelim: '|' 12 | }); 13 | } 14 | 15 | run(msg, choices) { 16 | return msg.reply(choices.length === 1 ? 17 | 'You only gave me one choice, dummy.' : 18 | `I think you should go with "${choices[Math.floor(Math.random() * choices.length)]}"`); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /commands/Fun/chucknorris.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | aliases: ['chucknorrisjoke'], 10 | description: 'Chuck Norris has some good jokes.' 11 | }); 12 | } 13 | 14 | async run(msg) { 15 | const joke = await fetch('http://api.chucknorris.io/jokes/random') 16 | .then(response => response.json()) 17 | .then(body => body.value); 18 | return msg.sendMessage(`**😁 Chuck Norris Joke:** ${joke}`); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /commands/Fun/coinflip.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | aliases: ['coin'], 9 | description: 'Flips one or more coins', 10 | usage: '[coins:int]' 11 | }); 12 | } 13 | 14 | run(msg, [coins = 0]) { 15 | if (coins <= 0) return msg.sendMessage(`You flipped ${Math.random() > 0.5 ? 'Heads' : 'Tails'}.`); 16 | 17 | let heads = 0; 18 | let tails = 0; 19 | for (let i = 0; i < coins; i++) { 20 | if (Math.random() > 0.5) heads++; 21 | else tails++; 22 | } 23 | return msg.sendMessage(`You flipped ${coins} coins. ${heads} ${heads === 1 ? 'was' : 'were'} heads, and ${tails} ${tails === '1' ? 'was' : 'were'} tails.`); 24 | } 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /commands/Fun/compliment.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | description: 'Compliments a user.', 9 | usage: '[UserToCompliment:member]' 10 | }); 11 | } 12 | 13 | run(msg, [mentioned = msg.member]) { 14 | return msg.sendMessage(`${mentioned.user.tag}: ${compliments[Math.floor(Math.random() * compliments.length)]}`); 15 | } 16 | 17 | }; 18 | 19 | const compliments = [ 20 | 'Your smile is contagious.', 21 | 'You look great today.', 22 | "You're a smart cookie.", 23 | 'I bet you make babies smile.', 24 | 'You have impeccable manners.', 25 | 'I like your style.', 26 | 'You have the best laugh.', 27 | 'I appreciate you.', 28 | 'You are the most perfect you there is.', 29 | 'You are enough.', 30 | "You're strong.", 31 | 'Your perspective is refreshing.', 32 | "You're an awesome friend.", 33 | 'You light up the room.', 34 | 'You shine brighter than a shooting star.', 35 | 'You deserve a hug right now.', 36 | 'You should be proud of yourself.', 37 | "You're more helpful than you realize.", 38 | 'You have a great sense of humor.', 39 | "You've got all the right moves!", 40 | "Is that your picture next to 'charming' in the dictionary?", 41 | 'Your kindness is a balm to all who encounter it.', 42 | "You're all that and a super-size bag of chips.", 43 | "On a scale from 1 to 10, you're an 11.", 44 | 'You are brave.', 45 | "You're even more beautiful on the inside than you are on the outside.", 46 | 'You have the courage of your convictions.', 47 | 'Your eyes are breathtaking.', 48 | 'If cartoon bluebirds were real, a bunch of them would be sitting on your shoulders singing right now.', 49 | 'You are making a difference.', 50 | "You're like sunshine on a rainy day.", 51 | 'You bring out the best in other people.', 52 | 'Your ability to recall random factoids at just the right time is impressive.', 53 | "You're a great listener.", 54 | 'How is it that you always look great, even in sweatpants?', 55 | 'Everything would be better if more people were like you!', 56 | 'I bet you sweat glitter.', 57 | 'You were cool way before hipsters were cool.', 58 | 'That color is perfect on you.', 59 | 'Hanging out with you is always a blast.', 60 | 'You always know -- and say -- exactly what I need to hear when I need to hear it.', 61 | 'You smell really good.', 62 | "You may dance like no one's watching, but everyone's watching because you're an amazing dancer!", 63 | 'Being around you makes everything better!', 64 | "When you say, 'I meant to do that,' I totally believe you.", 65 | "When you're not afraid to be yourself is when you're most incredible.", 66 | "Colors seem brighter when you're around.", 67 | "You're more fun than a ball pit filled with candy. (And seriously, what could be more fun than that?)", 68 | "That thing you don't like about yourself is what makes you so interesting.", 69 | "You're wonderful.", 70 | 'You have cute elbows. For reals!', 71 | 'Jokes are funnier when you tell them.', 72 | "You're better than a triple-scoop ice cream cone. With sprinkles.", 73 | 'Your bellybutton is kind of adorable.', 74 | 'Your hair looks stunning.', 75 | "You're one of a kind!", 76 | "You're inspiring.", 77 | "If you were a box of crayons, you'd be the giant name-brand one with the built-in sharpener.", 78 | 'You should be thanked more often. So thank you!!', 79 | "Our community is better because you're in it.", 80 | "Someone is getting through something hard right now because you've got their back.", 81 | 'You have the best ideas.', 82 | 'You always know how to find that silver lining.', 83 | 'Everyone gets knocked down sometimes, but you always get back up and keep going.', 84 | "You're a candle in the darkness.", 85 | "You're a great example to others.", 86 | 'Being around you is like being on a happy little vacation.', 87 | 'You always know just what to say.', 88 | "You're always learning new things and trying to better yourself, which is awesome.", 89 | 'If someone based an Internet meme on you, it would have impeccable grammar.', 90 | 'You could survive a Zombie apocalypse.', 91 | "You're more fun than bubble wrap.", 92 | 'When you make a mistake, you fix it.', 93 | 'Who raised you? They deserve a medal for a job well done.', 94 | "You're great at figuring stuff out.", 95 | 'Your voice is magnificent.', 96 | 'The people you love are lucky to have you in their lives.', 97 | "You're like a breath of fresh air.", 98 | "You're gorgeous -- and that's the least interesting thing about you, too.", 99 | "You're so thoughtful.", 100 | 'Your creative potential seems limitless.', 101 | 'Your name suits you to a T.', 102 | "You're irresistible when you blush.", 103 | 'Actions speak louder than words, and yours tell an incredible story.', 104 | 'Somehow you make time stop and fly at the same time.', 105 | 'When you make up your mind about something, nothing stands in your way.', 106 | 'You seem to really know who you are.', 107 | 'Any team would be lucky to have you on it.', 108 | "In high school I bet you were voted 'most likely to keep being awesome.'", 109 | 'I bet you do the crossword puzzle in ink.', 110 | 'Babies and small animals probably love you.', 111 | "If you were a scented candle they'd call it Perfectly Imperfect (and it would smell like summer).", 112 | "You're someone's reason to smile.", 113 | "You're even better than a unicorn, because you're real.", 114 | 'How do you keep being so funny and making everyone laugh?', 115 | 'You have a good head on your shoulders.', 116 | 'Has anyone ever told you that you have great posture?', 117 | 'The way you treasure your loved ones is incredible.', 118 | "You're really something special.", 119 | "You're a gift to those around you.", 120 | 'You deserve better.' 121 | ]; 122 | -------------------------------------------------------------------------------- /commands/Fun/d20.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { description: 'Rolls a D20' }); 8 | } 9 | 10 | async run(msg) { 11 | return msg.reply(`Rolling a D20... 🎲 **${Math.ceil(Math.random() * 20)}**`); 12 | } 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /commands/Fun/deobfuscate.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { remove } = require('confusables'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | usage: '', 10 | description: 'Deobfuscate a string which has confusable unicode characters.' 11 | }); 12 | } 13 | 14 | async run(msg, [str]) { 15 | return msg.send(remove(str)); 16 | } 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /commands/Fun/dogfacts.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { description: 'Gives you a random dog fact.' }); 9 | } 10 | 11 | async run(msg) { 12 | const fact = await fetch(`http://dog-api.kinduff.com/api/facts?number=1`) 13 | .then(response => response.json()) 14 | .then(body => body.facts[0]); 15 | return msg.sendMessage(`📢 **Dogfact:** *${fact}*`); 16 | } 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /commands/Fun/faceapp.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { MessageAttachment } = require('discord.js'); 4 | const fetch = require('node-fetch'); 5 | const faceapp = require('faceapp'); 6 | 7 | module.exports = class extends Command { 8 | 9 | constructor(...args) { 10 | super(...args, { 11 | cooldown: 5, 12 | requiredPermissions: ['ATTACH_FILES'], 13 | description: 'Applies a faceapp filter to an image.', 14 | usage: '', 15 | usageDelim: ' ' 16 | }); 17 | } 18 | 19 | async run(msg, [filter]) { 20 | const [attachment] = msg.attachments.values(); 21 | if (!attachment || !attachment.height) throw 'Please upload an image.'; 22 | 23 | const image = await fetch(attachment.url) 24 | .then(response => response.buffer()) 25 | .catch(() => { 26 | throw 'I could not download the file. Can you try again with another image?'; 27 | }); 28 | 29 | const faceappImage = await faceapp 30 | .process(image, filter) 31 | .then(img => img) 32 | .catch(() => { 33 | throw "Error - Couldn't find a face in the image."; 34 | }); 35 | 36 | return msg.sendMessage(new MessageAttachment(faceappImage, `${Math.round(Math.random() * 10000)}.jpg`)); 37 | } 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /commands/Fun/insult.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const roll = type => type[Math.floor(Math.random() * type.length)]; 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | description: 'Insults who you mention.', 10 | usage: '', 11 | nsfw: true 12 | }); 13 | } 14 | 15 | run(msg, [user]) { 16 | return msg.sendMessage(`${user}, you know what? you're nothing but ${roll(start)} ${roll(middle)} ${roll(end)}.`); 17 | } 18 | 19 | }; 20 | 21 | const start = [ 22 | 'a lazy', 23 | 'a stupid', 24 | 'an insecure', 25 | 'an idiotic', 26 | 'a slimy', 27 | 'a slutty', 28 | 'a smelly', 29 | 'a pompous', 30 | 'a communist', 31 | 'a dicknose', 32 | 'a pie-eating', 33 | 'a racist', 34 | 'an elitist', 35 | 'a white trash', 36 | 'a drug-loving', 37 | 'a butterface', 38 | 'a tone deaf', 39 | 'a ugly', 40 | 'a creepy', 41 | 'an artless', 42 | 'a bawdy', 43 | 'a beslubbering', 44 | 'a bootless', 45 | 'a churlish', 46 | 'a cockered', 47 | 'a clouted', 48 | 'a craven', 49 | 'a currish', 50 | 'a dankish', 51 | 'a dissembling', 52 | 'a droning', 53 | 'an errant', 54 | 'a fawning', 55 | 'a fobbing', 56 | 'a frothy', 57 | 'a gleeking', 58 | 'a goatfish', 59 | 'a gorbellied', 60 | 'an impertinent', 61 | 'an infectious', 62 | 'a jarring', 63 | 'a loggerheaded', 64 | 'a lumpish', 65 | 'a mammering', 66 | 'a mangled', 67 | 'a mewling', 68 | 'a paunchy', 69 | 'a pribbling', 70 | 'a puking', 71 | 'a puny', 72 | 'a qualling', 73 | 'a rank', 74 | 'a reeky', 75 | 'a roguish', 76 | 'a ruttish', 77 | 'a saucy', 78 | 'a spleeny', 79 | 'a spongy', 80 | 'a surly', 81 | 'a tottering', 82 | 'an unmuzzled', 83 | 'a vain', 84 | 'a venomed', 85 | 'a villainous', 86 | 'a warped', 87 | 'a wayward', 88 | 'a weedy', 'a yeasty', 89 | 'a lilly-livered', 90 | 'a rotten', 91 | 'a stinky', 92 | 'a lame', 93 | 'a dim-witted', 94 | 'a funky', 95 | 'a crusty', 96 | 'a steamy', 97 | 'a drizzly', 98 | 'a grizzly', 99 | 'a squirty', 100 | 'an uptight', 101 | 'a hairy', 102 | 'a husky', 103 | 'an arrogant', 104 | 'a nippy', 105 | 'a chunky', 106 | 'a smelly', 107 | 'a drooling', 108 | 'a crusty', 109 | 'a decrepic', 110 | 'a stupid', 111 | 'a moronic', 112 | 'a greasy', 113 | 'a poxy', 114 | 'an ugly', 115 | 'a smelly', 116 | 'a putrid', 117 | 'a shitty', 118 | 'an assinine', 119 | 'a sickening' 120 | ]; 121 | 122 | const middle = [ 123 | 'douche', 124 | 'ass', 125 | 'turd', 126 | 'rectum', 127 | 'butt', 128 | 'cock', 129 | 'shit', 130 | 'crotch', 131 | 'bitch', 132 | 'turd', 133 | 'prick', 134 | 'slut', 135 | 'taint', 136 | 'fuck', 137 | 'dick', 138 | 'boner', 139 | 'shart', 140 | 'nut', 141 | 'sphincter', 142 | 'base-court', 143 | 'bat-fowling', 144 | 'beef-witted', 145 | 'beetle-headed', 146 | 'boil-brained', 147 | 'clapper-clawed', 148 | 'clay-brained', 149 | 'common-kissing', 150 | 'crook-pated', 151 | 'dismal-dreaming', 152 | 'dizzy-eyed', 153 | 'doghearted', 154 | 'dread-bolted', 155 | 'earth-vexing', 156 | 'elf-skinned', 157 | 'fat-kidneyed', 158 | 'fen-sucked', 159 | 'flap-mouthed', 160 | 'fly-bitten', 161 | 'folly-fallen', 162 | 'fool-born', 163 | 'full-gorged', 164 | 'guts-gripping', 165 | 'half-faced', 166 | 'hasty-witted', 167 | 'hedge-born', 168 | 'hell-hated', 169 | 'idle-headed', 170 | 'ill-breeding', 171 | 'ill-nurtured', 172 | 'knotty-pated', 173 | 'milk-livered', 174 | 'motly-minded', 175 | 'onion-eyed', 176 | 'plume-plucked', 177 | 'pottle-deep', 178 | 'pox-marked', 179 | 'reeling-ripe', 180 | 'rough-hewn', 181 | 'rude-growing', 182 | 'rump-red', 183 | 'shard-borne', 184 | 'sheep-biting', 185 | 'spur-galled', 186 | 'swag-bellied', 187 | 'tardy-gaited', 188 | 'tickle-brained', 189 | 'toad-spotted', 190 | 'unchin-snouted', 191 | 'weather-bitten', 192 | 'hiney', 193 | 'poop', 194 | 'toot', 195 | 'wedgie', 196 | 'stool', 197 | 'fudge', 198 | 'bum', 199 | 'potty', 200 | 'dookie', 201 | 'pudding', 202 | 'sphincter', 203 | 'booger', 204 | 'feces', 205 | 'snot', 206 | 'crust', 207 | 'badonk-a', 208 | 'crud', 209 | 'sludge', 210 | 'tool', 211 | 'shit-kicking', 212 | 'monkey-licking', 213 | 'butt-munching', 214 | 'crotch-sniffing', 215 | 'donkey-spanking', 216 | 'fashion-illiterate', 217 | 'worm-ridden', 218 | 'grub-fucking', 219 | 'lathered-up', 220 | 'pasty-waisted', 221 | 'snot-flicking', 222 | 'fart-eating']; 223 | 224 | const end = [ 225 | 'pilot', 226 | 'canoe', 227 | 'captain', 228 | 'pirate', 229 | 'hammer', 230 | 'knob', 231 | 'box', 232 | 'jockey', 233 | 'waffle', 234 | 'goblin', 235 | 'blossom', 236 | 'biscuit', 237 | 'clown', 238 | 'socket', 239 | 'monster', 240 | 'hound', 241 | 'dragon', 242 | 'balloon', 243 | 'apple-john', 244 | 'baggage', 245 | 'barnacle', 246 | 'bladder', 247 | 'boar-pig', 248 | 'bugbear', 249 | 'bum-bailey', 250 | 'canker-blossom', 251 | 'clack-dish', 252 | 'clotpole', 253 | 'coxcomb', 254 | 'codpiece', 255 | 'death-token', 256 | 'dewberry', 257 | 'flap-dragon', 258 | 'flax-wench', 259 | 'flirt-gill', 260 | 'foot-licker', 261 | 'fustilarian', 262 | 'giglet', 263 | 'gudgeon', 264 | 'haggard', 265 | 'harpy', 266 | 'hedge-pig', 267 | 'horn-beast', 268 | 'hugger-mugger', 269 | 'joithead', 270 | 'lewdster', 271 | 'lout', 272 | 'maggot-pie', 273 | 'malt-worm', 274 | 'mammet', 275 | 'measle', 276 | 'minnow', 277 | 'miscreant', 278 | 'moldwarp', 279 | 'mumble-news', 280 | 'nut-hook', 281 | 'pigeon-egg', 282 | 'pignut', 283 | 'puttock', 284 | 'pumpion', 285 | 'ratsbane', 286 | 'scut', 287 | 'skinsmate', 288 | 'strumpet', 289 | 'varlot', 290 | 'vassal', 291 | 'whey-face', 292 | 'wagtail', 293 | 'squeegee', 294 | 'turtle', 295 | 'cabbage', 296 | 'bomb', 297 | 'sniffer', 298 | 'binkie', 299 | 'stump', 300 | 'nugget', 301 | 'whistle', 302 | 'twig', 303 | 'knuckle', 304 | 'burger', 305 | 'hotdog', 306 | 'loaf', 307 | 'freckle', 308 | 'soldier', 309 | 'kernal', 310 | 'shingle', 311 | 'warrior', 312 | 'hemorrhoid', 313 | 'fuckface', 314 | 'asshole', 315 | 'scumbucket', 316 | 'toerag', 317 | 'hackwack', 318 | 'imbecile', 319 | 'stunodigan', 320 | 'maggot', 321 | 'hipster', 322 | 'gargabe', 323 | 'jerkstore']; 324 | -------------------------------------------------------------------------------- /commands/Fun/markov.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const MarkovChain = require('markovchain'); 4 | const messageLimitHundreds = 1; 5 | 6 | module.exports = class extends Command { 7 | 8 | constructor(...args) { 9 | super(...args, { 10 | description: 'Generate a markov chain from the text chat.', 11 | requiredPermissions: ['READ_MESSAGE_HISTORY'] 12 | }); 13 | } 14 | 15 | async run(msg) { 16 | let messageBank = await msg.channel.messages.fetch({ limit: 100 }); 17 | for (let i = 1; i < messageLimitHundreds; i++) { 18 | messageBank = messageBank.concat(await msg.channel.messages.fetch({ limit: 100, before: messageBank.last().id })); 19 | } 20 | 21 | const quotes = new MarkovChain(messageBank.map(message => message.content).join(' ')); 22 | const chain = quotes.start(this.useUpperCase).end(20).process(); 23 | return msg.sendMessage(chain.substring(0, 1999)); 24 | } 25 | 26 | useUpperCase(wordList) { 27 | const tmpList = Object.keys(wordList).filter((word) => word[0] >= 'A' && word[0] <= 'Z'); 28 | return tmpList[Math.floor(Math.random() * tmpList.length)]; 29 | } 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /commands/Fun/meme.js: -------------------------------------------------------------------------------- 1 | const { MessageEmbed } = require('discord.js'); 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { description: 'Shows a meme image from reddit.' }); 9 | this._subreddits = [ 10 | 'memes', 11 | 'DeepFriedMemes', 12 | 'bonehurtingjuice', 13 | 'surrealmemes', 14 | 'dankmemes', 15 | 'meirl', 16 | 'me_irl', 17 | 'funny' 18 | ]; 19 | } 20 | 21 | async run(msg) { 22 | const data = await fetch(`https://imgur.com/r/${this._subreddits[Math.floor(Math.random() * this._subreddits.length)]}/hot.json`) 23 | .then(response => response.json()) 24 | .then(body => body.data); 25 | const selected = data[Math.floor(Math.random() * data.length)]; 26 | return msg.send(new MessageEmbed().setImage(`http://imgur.com/${selected.hash}${selected.ext.replace(/\?.*/, '')}`)); 27 | } 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /commands/Fun/obfuscate.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { obfuscate } = require('confusables'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | usage: '', 10 | description: 'Modify a string to have confusing letters in it.' 11 | }); 12 | } 13 | 14 | async run(msg, [str]) { 15 | const variations = []; 16 | 17 | for (let i = 0; i < 5; i++) { 18 | variations.push(obfuscate(str)); 19 | } 20 | 21 | return msg.send(variations.join('\n')); 22 | } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /commands/Fun/randquote.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | const messageLimitHundreds = 1; 5 | 6 | module.exports = class extends Command { 7 | 8 | constructor(...args) { 9 | super(...args, { 10 | description: 'Returns a random message from someone in the channel.', 11 | requiredPermissions: ['READ_MESSAGE_HISTORY', 'EMBED_LINKS'] 12 | }); 13 | } 14 | 15 | async run(msg) { 16 | let messageBank = await msg.channel.messages.fetch({ limit: 100 }); 17 | for (let i = 1; i < messageLimitHundreds; i++) { 18 | messageBank = messageBank.concat(await msg.channel.messages.fetch({ limit: 100, before: messageBank.last().id })); 19 | } 20 | 21 | const message = messageBank 22 | .filter(ms => !ms.author.bot && ms.content.replace(/[\W0-9]*/g, '').length >= 20) 23 | .random(); 24 | 25 | if (!message) throw 'Could not find a quote'; 26 | 27 | return msg.sendEmbed(new MessageEmbed() 28 | .setDescription(message.content) 29 | .setAuthor(message.author.username, message.author.displayAvatarURL())); 30 | } 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /commands/Fun/shame.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | description: 'Rings a bell on the server shaming the mentioned person.', 9 | usage: '' 10 | }); 11 | } 12 | 13 | run(msg, [user]) { 14 | return msg.sendMessage(`🔔 SHAME 🔔 ${user} 🔔 SHAME 🔔`); 15 | } 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /commands/Fun/thanosquote.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | 3 | module.exports = class extends Command { 4 | 5 | constructor(...args) { 6 | super(...args, { description: 'Gives a random quote from Thanos.' }); 7 | } 8 | 9 | async run(msg) { 10 | return msg.send(quotes[Math.floor(Math.random() * quotes.length)]); 11 | } 12 | 13 | }; 14 | 15 | const quotes = [ 16 | 'The end is near.', 17 | 'You\'re strong. But I could snap my fingers, and you\'d all cease to exist.', 18 | 'Fun isn\'t something one considers when balancing the universe. But this… does put a smile on my face.', 19 | 'Stark… you have my respect. I hope the people of Earth will remember you.', 20 | 'When I\'m done, half of humanity will still exist. Perfectly balanced, as all things should be. I hope they remember you.', 21 | 'You should have gone for the head.', 22 | 'I know what it\'s like to lose. To feel so desperately that you\'re right, yet to fail nonetheless. Dread it. Run from it.' + 23 | ' Destiny still arrives. Or should I say, I have.', 24 | 'Going to bed hungry. Scrounging for scraps. Your planet was on the brink of collapse. I was the one who stopped that.' + 25 | ' You know what\'s happened since then? The children born have known nothing but full bellies and clear skies. It\'s a paradise.', 26 | 'I ignored my destiny once, I can not do that again. Even for you. I\'m sorry little one.', 27 | 'With all six stones, I can simply snap my fingers, they would all cease to exist. I call that mercy.', 28 | 'The hardest choices require the strongest wills.', 29 | 'A soul for a soul.', 30 | 'Balanced, as all things should be.', 31 | 'Fun isn\'t something one considers when balancing the universe. But this… does put a smile on my face.', 32 | 'I know what it\'s like to lose. To feel so desperately that you\'re right, yet to fail nonetheless. Dread it. Run from it. Destiny still arrives. Or should I say, I have.', 33 | 'You could not live with your own failure, and where did that bring you? Back to me.', 34 | 'I am... inevitable.', 35 | 'I don\'t even know who you are.', 36 | 'I used the stones to destroy the stones. It nearly killed me, but the work is done. It always will be. I\'m inevitable.', 37 | 'You\'re not the only one cursed with knowledge.', 38 | 'Reality is often disappointing.', 39 | 'A small price to pay for salvation.' 40 | ]; 41 | -------------------------------------------------------------------------------- /commands/Fun/trumpquote.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { description: 'Returns a random Donald Trump quote.' }); 9 | } 10 | 11 | async run(msg) { 12 | const quote = await fetch('https://api.tronalddump.io/random/quote') 13 | .then(response => response.json()) 14 | .then(body => body.value) 15 | .catch(() => { throw 'There was an error. Please try again.'; }); 16 | return msg.sendMessage(quote); 17 | } 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /commands/Fun/urban.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, util: { toTitleCase } } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | const fetch = require('node-fetch'); 5 | 6 | const ZWS = '\u200B'; 7 | 8 | module.exports = class extends Command { 9 | 10 | constructor(...args) { 11 | super(...args, { 12 | aliases: ['ud', 'urbandictionary'], 13 | requiredPermissions: ['EMBED_LINKS'], 14 | description: 'Searches the Urban Dictionary library for a definition to the search term.', 15 | usage: ' [page:integer{0,10}]', 16 | usageDelim: ', ', 17 | nsfw: true 18 | }); 19 | } 20 | 21 | async run(msg, [query, ind = 1]) { 22 | const index = ind - 1; 23 | if (index < 0) { 24 | throw 'The number cannot be zero or negative.'; 25 | } 26 | 27 | const response = await fetch(`http://api.urbandictionary.com/v0/define?term=${encodeURIComponent(query)}`); 28 | const { list } = await response.json(); 29 | 30 | const result = list[index]; 31 | if (typeof result === 'undefined') { 32 | throw index === 0 ? 33 | 'I could not find this entry in UrbanDictionary' : 34 | 'I could not find this page in UrbanDictionary, try a lower page index'; 35 | } 36 | 37 | const definition = this.content(result.definition, result.permalink); 38 | return msg.sendEmbed(new MessageEmbed() 39 | .setTitle(`Word: ${toTitleCase(query)}`) 40 | .setURL(result.permalink) 41 | .setColor(msg.color) 42 | .setThumbnail('http://i.imgur.com/CcIZZsa.png') 43 | .setDescription([ 44 | `→ \`Definition\` :: ${ind}/${list.length}\n${definition}`, 45 | `→ \`Example\` :: ${this.cutText(result.example, 750)}`, 46 | `→ \`Author\` :: ${result.author}` 47 | ]) 48 | .addField(ZWS, `\\👍 ${result.thumbs_up}`, true) 49 | .addField(ZWS, `\\👎 ${result.thumbs_down}`, true) 50 | .setFooter('© Urban Dictionary')); 51 | } 52 | 53 | content(definition, permalink) { 54 | if (definition.length < 750) return definition; 55 | return `${this.cutText(definition, 750)}... [continue reading](${permalink})`; 56 | } 57 | 58 | cutText(str, length) { 59 | if (str.length < length) return str; 60 | const cut = this.splitText(str, length - 3); 61 | if (cut.length < length - 3) return `${cut}...`; 62 | return `${cut.slice(0, length - 3)}...`; 63 | } 64 | 65 | splitText(str, length, char = ' ') { 66 | // eslint-disable-next-line id-length 67 | const x = str.substring(0, length).lastIndexOf(char); 68 | const pos = x === -1 ? length : x; 69 | return str.substring(0, pos); 70 | } 71 | 72 | }; 73 | -------------------------------------------------------------------------------- /commands/Fun/waifu.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | 3 | module.exports = class extends Command { 4 | 5 | constructor(...args) { 6 | super(...args, { description: 'Sends a randomly generated Waifu from thiswaifudoesnotexist.net' }); 7 | } 8 | 9 | async run(msg) { 10 | return msg.sendMessage(`https://www.thiswaifudoesnotexist.net/example-${Math.floor(Math.random() * 100000)}.jpg`); 11 | } 12 | 13 | }; 14 | -------------------------------------------------------------------------------- /commands/Fun/wordcloud.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { MessageAttachment } = require('discord.js'); 4 | const cloud = require('d3-cloud'); 5 | const Canvas = require('canvas'); 6 | const messageLimitHundreds = 1; 7 | 8 | // Blame node-canvas for this dirty workaround. Canvas.createCanvas for 2.x, new canvas.Canvas for 1.x 9 | const createCanvas = typeof Canvas.createCanvas === 'function' ? 10 | (...args) => Canvas.createCanvas(...args) : 11 | (...args) => new Canvas(...args); 12 | 13 | module.exports = class extends Command { 14 | 15 | constructor(...args) { 16 | super(...args, { 17 | description: 'Generate a wordcloud from the messages in a chat.', 18 | requiredPermissions: ['ATTACH_FILES', 'READ_MESSAGE_HISTORY'] 19 | }); 20 | } 21 | 22 | async run(msg) { 23 | const finalImage = createCanvas(2000, 2000); 24 | const ctx = finalImage.getContext('2d'); 25 | const wordBank = {}; 26 | 27 | let messageBank = await msg.channel.messages.fetch({ limit: 100 }); 28 | for (let i = 1; i < messageLimitHundreds; i++) { 29 | messageBank = messageBank.concat(await msg.channel.messages.fetch({ limit: 100, before: messageBank.last().id })); 30 | } 31 | 32 | for (const message of messageBank.values()) { 33 | if (message.content.length <= 2) continue; 34 | message.content.split('.').join(' ').split(' ').forEach(word => { 35 | const cleanWord = word.replace(/\W+/g, '').substring(0, 20); 36 | if (!wordBank[cleanWord]) wordBank[cleanWord] = 0; 37 | wordBank[cleanWord]++; 38 | }); 39 | } 40 | 41 | const wordList = Object.keys(wordBank).filter(word => wordBank[word] > 3 && word.length > 4) 42 | .map(word => ({ text: word, size: 10 * wordBank[word] })); 43 | 44 | ctx.fillStyle = 'black'; 45 | ctx.fillRect(0, 0, 2000, 2000); 46 | ctx.translate(1000, 1000); 47 | const end = (words) => { 48 | for (let i = 0; i < words.length; i++) { 49 | const word = words[i]; 50 | const rotation = word.rotate; 51 | ctx.fillStyle = `#${Math.floor(Math.random() * 16777215).toString(16)}`; 52 | ctx.font = `${(word.size * 0.8) + 3}px Arial`; 53 | ctx.rotate(rotation); 54 | ctx.fillText(word.text, word.x, word.y); 55 | ctx.rotate(-rotation); 56 | } 57 | const buffer = finalImage.toBuffer(); 58 | return msg.sendMessage(new MessageAttachment(buffer, 'image.jpg')); 59 | }; 60 | 61 | 62 | cloud().size([1950, 1950]) 63 | .canvas(() => createCanvas(1, 1)) 64 | .words(wordList) 65 | .padding(1) 66 | .rotate(() => 0) 67 | .font('Arial') 68 | .text((word) => word.text) 69 | .fontSize((word) => word.size) 70 | .on('end', end) 71 | .start(); 72 | } 73 | 74 | }; 75 | -------------------------------------------------------------------------------- /commands/Fun/yomomma.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | aliases: ['yomama'], 10 | description: 'Yo momma is so fat, yo.' 11 | }); 12 | } 13 | 14 | async run(msg) { 15 | const joke = await fetch('http://api.yomomma.info') 16 | .then(response => response.json()) 17 | .then(body => body.joke); 18 | return msg.sendMessage(`📢 **Yomomma joke:** *${joke}*`); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /commands/General/Chat Bot Info/help.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, RichDisplay, util: { isFunction } } = require('klasa'); 3 | const { MessageEmbed, Permissions } = require('discord.js'); 4 | 5 | const PERMISSIONS_RICHDISPLAY = new Permissions([Permissions.FLAGS.MANAGE_MESSAGES, Permissions.FLAGS.ADD_REACTIONS]); 6 | const time = 1000 * 60 * 3; 7 | 8 | module.exports = class extends Command { 9 | 10 | constructor(...args) { 11 | super(...args, { 12 | aliases: ['commands', 'cmd', 'cmds'], 13 | guarded: true, 14 | description: (language) => language.get('COMMAND_HELP_DESCRIPTION'), 15 | usage: '(Command:command)' 16 | }); 17 | 18 | this.createCustomResolver('command', (arg, possible, message) => { 19 | if (!arg || arg === '') return undefined; 20 | return this.client.arguments.get('command').run(arg, possible, message); 21 | }); 22 | 23 | // Cache the handlers 24 | this.handlers = new Map(); 25 | } 26 | 27 | async run(message, [command]) { 28 | if (command) { 29 | return message.sendMessage([ 30 | `= ${command.name} = `, 31 | isFunction(command.description) ? command.description(message.language) : command.description, 32 | message.language.get('COMMAND_HELP_USAGE', command.usage.fullUsage(message)), 33 | message.language.get('COMMAND_HELP_EXTENDED'), 34 | isFunction(command.extendedHelp) ? command.extendedHelp(message.language) : command.extendedHelp 35 | ], { code: 'asciidoc' }); 36 | } 37 | 38 | if (!('all' in message.flags) && message.guild && message.channel.permissionsFor(this.client.user).has(PERMISSIONS_RICHDISPLAY)) { 39 | // Finish the previous handler 40 | const previousHandler = this.handlers.get(message.author.id); 41 | if (previousHandler) previousHandler.stop(); 42 | 43 | const handler = await (await this.buildDisplay(message)).run(await message.send('Loading Commands...'), { 44 | filter: (reaction, user) => user.id === message.author.id, 45 | time 46 | }); 47 | handler.on('end', () => this.handlers.delete(message.author.id)); 48 | this.handlers.set(message.author.id, handler); 49 | return handler; 50 | } 51 | 52 | return message.author.send(await this.buildHelp(message), { split: { char: '\n' } }) 53 | .then(() => { if (message.channel.type !== 'dm') message.sendMessage(message.language.get('COMMAND_HELP_DM')); }) 54 | .catch(() => { if (message.channel.type !== 'dm') message.sendMessage(message.language.get('COMMAND_HELP_NODM')); }); 55 | } 56 | 57 | async buildHelp(message) { 58 | const commands = await this._fetchCommands(message); 59 | const { prefix } = message.guildSettings; 60 | 61 | const helpMessage = []; 62 | for (const [category, list] of commands) { 63 | helpMessage.push(`**${category} Commands**:\n`, list.map(this.formatCommand.bind(this, message, prefix, false)).join('\n'), ''); 64 | } 65 | 66 | return helpMessage.join('\n'); 67 | } 68 | 69 | async buildDisplay(message) { 70 | const commands = await this._fetchCommands(message); 71 | const { prefix } = message.guildSettings; 72 | const display = new RichDisplay(); 73 | const color = message.member.displayColor; 74 | for (const [category, list] of commands) { 75 | display.addPage(new MessageEmbed() 76 | .setTitle(`${category} Commands`) 77 | .setColor(color) 78 | .setDescription(list.map(this.formatCommand.bind(this, message, prefix, true)).join('\n')) 79 | ); 80 | } 81 | 82 | return display; 83 | } 84 | 85 | formatCommand(message, prefix, richDisplay, command) { 86 | const description = isFunction(command.description) ? command.description(message.language) : command.description; 87 | return richDisplay ? `• ${prefix}${command.name} → ${description}` : `• **${prefix}${command.name}** → ${description}`; 88 | } 89 | 90 | async _fetchCommands(message) { 91 | const run = this.client.inhibitors.run.bind(this.client.inhibitors, message); 92 | const commands = new Map(); 93 | await Promise.all(this.client.commands.map((command) => run(command, true) 94 | .then(() => { 95 | const category = commands.get(command.category); 96 | if (category) category.push(command); 97 | else commands.set(command.category, [command]); 98 | }).catch(() => { 99 | // noop 100 | }) 101 | )); 102 | 103 | return commands; 104 | } 105 | 106 | }; 107 | -------------------------------------------------------------------------------- /commands/Misc/cat.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | aliases: ['randomcat', 'meow'], 10 | description: 'Grabs a random cat image from random.cat.' 11 | }); 12 | } 13 | 14 | async run(msg) { 15 | const file = await fetch('http://aws.random.cat/meow') 16 | .then(response => response.json()) 17 | .then(body => body.file); 18 | return msg.channel.sendFile(file, `cat.${file.slice(file.lastIndexOf('.'), file.length)}`); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /commands/Misc/dog.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | aliases: ['randomdog', 'woof'], 10 | description: 'Grabs a random dog image from random.dog.', 11 | extendedHelp: 'This command grabs a random dog from "https://dog.ceo/api/breeds/image/random".' 12 | }); 13 | } 14 | 15 | async run(msg) { 16 | const url = await fetch('https://dog.ceo/api/breeds/image/random') 17 | .then(response => response.json()) 18 | .then(body => body.message); 19 | return msg.channel.sendFile(url); 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /commands/Misc/duck.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | const fetch = require('node-fetch'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | aliases: ['randomduck', 'ducc'], 9 | description: 'Grabs a random duck image from random-d.uk.', 10 | extendedHelp: 'This command grabs a random duck from "https://random-d.uk/api/v1/random".' 11 | }); 12 | } 13 | 14 | async run(msg) { 15 | const url = await fetch('https://random-d.uk/api/v1/random') 16 | .then(response => response.json()) 17 | .then(body => body.url); 18 | return msg.channel.sendFile(url); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /commands/Misc/echo.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | permissionLevel: 6, 9 | runIn: ['text'], 10 | description: 'Send a message to a channel through the bot.', 11 | usage: '[channel:channel] ', 12 | usageDelim: ' ' 13 | }); 14 | } 15 | 16 | async run(msg, [channel = msg.channel, message]) { 17 | if (channel.guild !== msg.guild) throw 'You can\'t echo in other servers!'; 18 | if (!channel.postable) throw 'The selected channel is not postable.'; 19 | return channel.send(message); 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /commands/Misc/fml.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | const fetch = require('node-fetch'); 5 | const HTMLParser = require('fast-html-parser'); 6 | 7 | module.exports = class extends Command { 8 | 9 | constructor(...args) { 10 | super(...args, { description: 'Gets a random FML story.' }); 11 | } 12 | 13 | async run(msg) { 14 | const root = await fetch('http://www.fmylife.com/random') 15 | .then(result => result.text()) 16 | .then(HTMLParser.parse); 17 | const article = root.querySelector('.block a'); 18 | const downdoot = root.querySelector('.vote-down'); 19 | const updoot = root.querySelector('.vote-up'); 20 | 21 | if (article.childNodes[0].text.length < 5) { 22 | return msg.sendMessage('Today, something went wrong, so you will have to try again in a few moments. FML again.'); 23 | } 24 | 25 | return msg.sendEmbed(new MessageEmbed() 26 | .setTitle(`Requested by ${msg.author.tag}`) 27 | .setAuthor('FML Stories') 28 | .setColor(msg.member.displayColor) 29 | .setTimestamp() 30 | .setDescription(`_${article.childNodes[0].text}\n\n_`) 31 | .addField('I agree, your life sucks', updoot.childNodes[0].text, true) 32 | .addField('You deserved it:', downdoot.childNodes[0].text, true)); 33 | } 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /commands/Misc/fox.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | const fetch = require('node-fetch'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | aliases: ['randomfox'], 9 | description: 'Grabs a random fox image from randomfox.ca', 10 | extendedHelp: 'This command grabs a random fox from "https://randomfox.ca/floof/".' 11 | }); 12 | } 13 | 14 | async run(msg) { 15 | const url = await fetch('https://randomfox.ca/floof/') 16 | .then(response => response.json()) 17 | .then(body => body.image); 18 | return msg.channel.sendFile(url); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /commands/Misc/lizard.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | const fetch = require('node-fetch'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | aliases: ['randomlizard'], 9 | description: 'Grabs a random lizard image from nekos.life.', 10 | extendedHelp: 'This command grabs a random lizard from "https://nekos.life/api/v2/img/lizard".' 11 | }); 12 | } 13 | 14 | async run(msg) { 15 | const url = await fetch('https://nekos.life/api/v2/img/lizard') 16 | .then(response => response.json()) 17 | .then(body => body.url); 18 | return msg.channel.sendFile(url); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /commands/Moderation/ban.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | permissionLevel: 6, 9 | requiredPermissions: ['BAN_MEMBERS'], 10 | runIn: ['text'], 11 | description: 'Bans a mentioned user. Currently does not require reason (no mod-log).', 12 | usage: ' [reason:...string]', 13 | usageDelim: ' ' 14 | }); 15 | } 16 | 17 | async run(msg, [user, reason]) { 18 | if (user.id === msg.author.id) throw 'Why would you ban yourself?'; 19 | if (user.id === this.client.user.id) throw 'Have I done something wrong?'; 20 | 21 | const member = await msg.guild.members.fetch(user).catch(() => null); 22 | if (member) { 23 | if (member.roles.highest.position >= msg.member.roles.highest.position) throw 'You cannot ban this user.'; 24 | if (!member.bannable) throw 'I cannot ban this user.'; 25 | } 26 | 27 | const options = {}; 28 | if (reason) options.reason = reason; 29 | 30 | await msg.guild.members.ban(user, options); 31 | return msg.sendMessage(`${user.tag} got banned.${reason ? ` With reason of: ${reason}` : ''}`); 32 | } 33 | 34 | }; 35 | -------------------------------------------------------------------------------- /commands/Moderation/check.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, util } = require('klasa'); 3 | 4 | // Add to your schema definition: 5 | // KlasaClient.defaultGuildSchema.add('minAccAge', 'integer', { default: 1800000 }); 6 | 7 | module.exports = class extends Command { 8 | 9 | constructor(...args) { 10 | super(...args, { 11 | permissionLevel: 6, 12 | runIn: ['text'], 13 | requiredSettings: ['minAccAge'], 14 | description: 'Checks the guild for any user accounts younger than the minimum account age.' 15 | }); 16 | } 17 | 18 | async run(msg) { 19 | const accAge = msg.guild.settings.minAccAge; 20 | const mtime = msg.createdTimestamp; 21 | 22 | const users = []; 23 | for (const member of msg.guild.members.values()) { 24 | if ((mtime - member.user.createdTimestamp) >= accAge) continue; 25 | users.push(`${member.user.tag}, Created:${((mtime - member.user.createdTimestamp) / 1000 / 60).toFixed(0)} min(s) ago`); 26 | } 27 | 28 | return msg.sendMessage(users.length > 0 ? 29 | `The following users are less than the Minimum Account Age:${util.codeBlock('', users.join('\n'))}` : 30 | 'No users less than Minimum Account Age were found in this server.'); 31 | } 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /commands/Moderation/kick.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | permissionLevel: 6, 9 | requiredPermissions: ['KICK_MEMBERS'], 10 | runIn: ['text'], 11 | description: 'Kicks a mentioned user. Currently does not require reason (no mod-log).', 12 | usage: ' [reason:...string]', 13 | usageDelim: ' ' 14 | }); 15 | } 16 | 17 | async run(msg, [member, reason]) { 18 | if (member.id === msg.author.id) throw 'Why would you kick yourself?'; 19 | if (member.id === this.client.user.id) throw 'Have I done something wrong?'; 20 | 21 | if (member.roles.highest.position >= msg.member.roles.highest.position) throw 'You cannot kick this user.'; 22 | if (!member.kickable) throw 'I cannot kick this user.'; 23 | 24 | await member.kick(reason); 25 | return msg.sendMessage(`${member.user.tag} got kicked.${reason ? ` With reason of: ${reason}` : ''}`); 26 | } 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /commands/Moderation/mute.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, Duration } = require('klasa'); 3 | 4 | /* 5 | 6 | To use this correctly, you will also need the unmute task located in 7 | /tasks/unmute.js 8 | 9 | */ 10 | 11 | // Add to your schema definition: 12 | // KlasaClient.defaultGuildSchema.add('roles', schema => schema 13 | // .add('muted', 'role')); 14 | 15 | module.exports = class extends Command { 16 | 17 | constructor(...args) { 18 | super(...args, { 19 | permissionLevel: 6, 20 | requiredPermissions: ['MANAGE_ROLES'], 21 | runIn: ['text'], 22 | description: 'Mutes a mentioned member.', 23 | usage: '[when:time] [reason:...string]', 24 | usageDelim: ' ' 25 | }); 26 | } 27 | 28 | async run(msg, [when, member, reason]) { 29 | if (member.id === msg.author.id) throw 'Why would you mute yourself?'; 30 | if (member.id === this.client.user.id) throw 'Have I done something wrong?'; 31 | 32 | if (member.roles.highest.position >= msg.member.roles.highest.position) throw 'You cannot mute this user.'; 33 | 34 | if (member.roles.has(msg.guild.settings.roles.muted)) throw 'The member is already muted.'; 35 | await member.roles.add(msg.guild.settings.roles.muted); 36 | 37 | if (when) { 38 | await this.client.schedule.create('unmute', when, { 39 | data: { 40 | guild: msg.guild.id, 41 | user: member.id 42 | } 43 | }); 44 | return msg.sendMessage(`${member.user.tag} got temporarily muted for ${Duration.toNow(when)}.${reason ? ` With reason of: ${reason}` : ''}`); 45 | } 46 | 47 | return msg.sendMessage(`${member.user.tag} got muted.${reason ? ` With reason of: ${reason}` : ''}`); 48 | } 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /commands/Moderation/prune.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | permissionLevel: 6, 9 | requiredPermissions: ['MANAGE_MESSAGES'], 10 | runIn: ['text'], 11 | description: 'Prunes a certain amount of messages w/o filter.', 12 | usage: '[limit:integer] [link|invite|bots|you|me|upload|user:user]', 13 | usageDelim: ' ' 14 | }); 15 | } 16 | 17 | async run(msg, [limit = 50, filter = null]) { 18 | let messages = await msg.channel.messages.fetch({ limit: 100 }); 19 | if (filter) { 20 | const user = typeof filter !== 'string' ? filter : null; 21 | const type = typeof filter === 'string' ? filter : 'user'; 22 | messages = messages.filter(this.getFilter(msg, type, user)); 23 | } 24 | messages = messages.array().slice(0, limit); 25 | await msg.channel.bulkDelete(messages); 26 | return msg.sendMessage(`Successfully deleted ${messages.length} messages from ${limit}.`); 27 | } 28 | 29 | getFilter(msg, filter, user) { 30 | switch (filter) { 31 | case 'link': return mes => /https?:\/\/[^ /.]+\.[^ /.]+/.test(mes.content); 32 | case 'invite': return mes => /(https?:\/\/)?(www\.)?(discord\.(gg|li|me|io)|discordapp\.com\/invite)\/.+/.test(mes.content); 33 | case 'bots': return mes => mes.author.bot; 34 | case 'you': return mes => mes.author.id === this.client.user.id; 35 | case 'me': return mes => mes.author.id === msg.author.id; 36 | case 'upload': return mes => mes.attachments.size > 0; 37 | case 'user': return mes => mes.author.id === user.id; 38 | default: return () => true; 39 | } 40 | } 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /commands/Moderation/softban.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | permissionLevel: 6, 9 | requiredPermissions: ['BAN_MEMBERS'], 10 | runIn: ['text'], 11 | description: 'Softbans a mentioned user. Currently does not require reason (no mod-log).', 12 | usage: ' [days:int{1,7}] [reason:...string]', 13 | usageDelim: ' ' 14 | }); 15 | } 16 | 17 | async run(msg, [user, days = 1, reason]) { 18 | if (user.id === msg.author.id) throw 'Why would you ban yourself?'; 19 | if (user.id === this.client.user.id) throw 'Have I done something wrong?'; 20 | 21 | const member = await msg.guild.members.fetch(user).catch(() => null); 22 | if (member) { 23 | if (member.roles.highest.position >= msg.member.roles.highest.position) throw 'You cannot ban this user.'; 24 | if (!member.bannable) throw 'I cannot ban this user.'; 25 | } 26 | 27 | const options = { days }; 28 | if (reason) options.reason = reason; 29 | 30 | await msg.guild.members.ban(user, options); 31 | await msg.guild.members.unban(user, 'Softban released.'); 32 | return msg.sendMessage(`${member.user.tag} got softbanned.${reason ? ` With reason of: ${reason}` : ''}`); 33 | } 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /commands/Moderation/unban.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | permissionLevel: 6, 9 | requiredPermissions: ['BAN_MEMBERS'], 10 | runIn: ['text'], 11 | description: 'Unbans a user.', 12 | usage: ' [reason:...string]', 13 | usageDelim: ' ' 14 | }); 15 | } 16 | 17 | async run(msg, [user, reason]) { 18 | const bans = await msg.guild.fetchBans(); 19 | if (bans.has(user.id)) { 20 | await msg.guild.members.unban(user, reason); 21 | return msg.sendMessage(`${user.tag} was unbanned.${reason ? ` With reason of: ${reason}` : ''}`); 22 | } 23 | 24 | throw `${user.tag} was never banned. How could I unban them?`; 25 | } 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /commands/Moderation/unmute.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | permissionLevel: 6, 9 | requiredPermissions: ['MANAGE_ROLES'], 10 | runIn: ['text'], 11 | description: 'Unmutes a mentioned user.', 12 | usage: ' [reason:...string]', 13 | usageDelim: ' ' 14 | }); 15 | } 16 | 17 | async run(msg, [member, reason]) { 18 | if (member.roles.highest.position >= msg.member.roles.highest.position) throw 'You cannot unmute this user.'; 19 | if (!member.roles.has(msg.guild.settings.roles.muted)) throw 'This user is not muted.'; 20 | 21 | await member.roles.remove(msg.guild.settings.roles.muted); 22 | 23 | return msg.sendMessage(`${member.user.tag} was unmuted.${reason ? ` With reason of: ${reason}` : ''}`); 24 | } 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /commands/Moderation/voicekick.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | 3 | module.exports = class extends Command { 4 | 5 | constructor(...args) { 6 | super(...args, { 7 | enabled: true, 8 | runIn: ['text'], 9 | requiredPermissions: ['MOVE_MEMBERS'], 10 | permissionLevel: 6, 11 | description: 'Disconnects a member from a voice channel.', 12 | usage: ' [reason:...string]', 13 | usageDelim: ' ' 14 | }); 15 | } 16 | 17 | async run(message, [member, reason]) { 18 | if (member.id === message.author.id) throw 'Why would you voice kick yourself?'; 19 | if (member.id === this.client.user.id) throw 'Have I done something wrong?'; 20 | if (!member.voice.channelID) throw 'That member is not in a voice channel.'; 21 | if (member.roles.highest.position >= message.member.roles.highest.position) throw 'You cannot voice kick this user.'; 22 | 23 | await member.voice.setChannel(null, reason); 24 | return message.send(`Voice kicked **${member.user.tag}**.${reason ? `With reason of: ${reason}` : ''}`); 25 | } 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /commands/System/Admin/docs.js: -------------------------------------------------------------------------------- 1 | const { Command, Stopwatch } = require('klasa'); 2 | const { createHash } = require('crypto'); 3 | 4 | // The permssion levels of commands must be *lower* than this number to be 5 | // shown in the docs. 6 | const permissionLevel = 9; 7 | 8 | module.exports = class extends Command { 9 | 10 | constructor(...args) { 11 | super(...args, { 12 | guarded: true, 13 | description: 'Generates documentation in different formats.', 14 | permissionLevel: 10, 15 | usage: '', 16 | subcommands: true 17 | }); 18 | } 19 | 20 | init() { 21 | this.username = this.client.user.username; 22 | this.avatar = (size) => this.client.user.avatarURL({ format: 'png', size: size }); 23 | this.prefix = this.client.options.prefix; 24 | this.invite = this.client.invite; 25 | } 26 | 27 | finish(data, stopwatch, format) { 28 | const buffer = Buffer.from(data); 29 | const hash = createHash('sha1').update(data).digest('base64').substr(0, 7); 30 | const duration = `Generated in ${stopwatch.stop()}`; 31 | return this.msg.channel.sendFile(buffer, `commands_${hash}.${format}`, duration); 32 | } 33 | 34 | async buildCommands(type, msg, normalize = false) { 35 | this.msg = msg; 36 | const stopwatch = new Stopwatch(); 37 | let categories; 38 | const commands = normalize ? [] : {}; 39 | if (normalize) categories = []; 40 | const commandNames = Array.from(this.client.commands.keys()); 41 | await Promise.all( 42 | this.client.commands 43 | .filter(cmd => !cmd.permissionLevel || (cmd.permissionLevel && 44 | cmd.permissionLevel < permissionLevel)) 45 | .map(cmd => { 46 | if (normalize) { 47 | if (!categories.includes(cmd.category)) categories.push(cmd.category); 48 | return commands.push(cmd); 49 | } 50 | 51 | if (!commands.hasOwnProperty(cmd.category)) commands[cmd.category] = {}; 52 | if (!commands[cmd.category].hasOwnProperty(cmd.subCategory)) { 53 | commands[cmd.category][cmd.subCategory] = []; 54 | } 55 | const description = typeof cmd.description === 'function' ? 56 | cmd.description(msg.language) : cmd.description || 'No description.'; 57 | 58 | return commands[cmd.category][cmd.subCategory] 59 | .push({ name: cmd.name, aliases: cmd.aliases, description }); 60 | }) 61 | ); 62 | if (!normalize) categories = Object.keys(commands); 63 | const longest = commandNames.reduce((long, str) => Math.max(long, str.length), 0); 64 | return { commands, categories, longest, stopwatch }; 65 | } 66 | 67 | async markdown(msg) { 68 | const { commands, categories, stopwatch } = await this.buildCommands('markdown', msg); 69 | const markdown = []; 70 | 71 | markdown.push(`# ![${this.username}](${this.avatar(32)}) ${this.username}`); 72 | markdown.push(`[Invite Link](${this.invite})`); 73 | markdown.push(`# Commands\n`); 74 | 75 | for (let cat = 0; cat < categories.length; cat++) { 76 | const categoryName = categories[cat]; 77 | const subCategories = Object.keys(commands[categories[cat]]); 78 | if (commands[categories[cat]].General) markdown.push(`\n## ${categoryName} / ${commands[categories[cat]].General.length} Commands`); 79 | 80 | for (let subCat = 0; subCat < subCategories.length; subCat++) { 81 | if (subCategories.length > 1) markdown.push(`\n### ${subCategories[subCat]}`); 82 | markdown.push(`| Command Name | Aliases | Description |`); 83 | markdown.push(`|--------------|----------|--------------------------|`); 84 | markdown.push( 85 | `${commands[categories[cat]][subCategories[subCat]] 86 | .map(cmd => `| ${this.prefix + cmd.name} | ${cmd.aliases.join(', ')} | ${cmd.description} |`) 87 | .join('\n')}` 88 | ); 89 | } 90 | } 91 | 92 | this.finish(markdown.join('\n'), stopwatch, 'md'); 93 | } 94 | 95 | async html(msg) { 96 | const { commands, categories, stopwatch } = await this.buildCommands('html', msg); 97 | const esc = this.escapeHtml; 98 | 99 | let html = ` 100 | ${this.username} 101 | 102 | `; 106 | for (let cat = 0; cat < categories.length; cat++) { 107 | html += `
`; 108 | const category = commands[categories[cat]].General; 109 | if (category) { 110 | html += `

${esc(categories[cat])} / ${category.length} Commands

`; 111 | } 112 | 113 | const subCategories = Object.keys(commands[categories[cat]]); 114 | 115 | for (let subCat = 0; subCat < subCategories.length; subCat++) { 116 | html += `
`; 117 | if (subCategories.length > 1) html += `

${esc(subCategories[subCat])}

`; 118 | html += ``; 119 | html += ` 120 | 121 | `; 122 | html += ``; 123 | html += `${commands[categories[cat]][subCategories[subCat]] 124 | .map(cmd => `` + 125 | `` + 126 | ``) 127 | .join('\n')}` 128 | ; 129 | html += `
CommandAliasesDescription
${esc(this.prefix + cmd.name)}${esc(cmd.aliases.join(', '))}${esc(cmd.description)}
`; 130 | } 131 | html += `
`; 132 | } 133 | html += ''; 134 | this.finish(html, stopwatch, 'html'); 135 | } 136 | 137 | async plaintext(msg) { 138 | const { commands, categories, longest, stopwatch } = await this.buildCommands('plaintext', msg); 139 | const plaintext = []; 140 | 141 | plaintext.push(`${this.username}\n`); 142 | plaintext.push(`Invite Link: ${this.invite}\n`); 143 | plaintext.push(`Commands\n`); 144 | 145 | for (let cat = 0; cat < categories.length; cat++) { 146 | const categoryName = categories[cat]; 147 | const subCategories = Object.keys(commands[categories[cat]]); 148 | if (commands[categories[cat]].General) plaintext.push(`${categoryName} / ${commands[categories[cat]].General.length} Commands\n`); 149 | 150 | for (let subCat = 0; subCat < subCategories.length; subCat++) { 151 | if (subCategories.length > 1) plaintext.push(`${subCategories[subCat]}\n`); 152 | plaintext.push( 153 | `${commands[categories[cat]][subCategories[subCat]].map(cmd => `${this.prefix + cmd.name.padEnd(longest)}: ${cmd.description}`).join('\n')}` 154 | ); 155 | plaintext.push('\n'); 156 | } 157 | } 158 | 159 | this.finish(plaintext.join('\n'), stopwatch, 'txt'); 160 | } 161 | 162 | async json(msg) { 163 | const { commands, categories, stopwatch } = await this.buildCommands('json', msg, true); 164 | 165 | const meta = { 166 | username: this.username, 167 | avatar: this.avatar(128), 168 | prefix: this.prefix, 169 | invite: this.invite, 170 | categories 171 | }; 172 | 173 | this.finish(JSON.stringify({ commands, meta }), stopwatch, 'json'); 174 | } 175 | 176 | escapeHtml(text) { 177 | const map = { 178 | '&': '&', 179 | '<': '<', 180 | '>': '>', 181 | '"': '"', 182 | "'": ''' 183 | }; 184 | 185 | return text.replace(/[&<>"']/g, (char) => map[char]); 186 | } 187 | 188 | }; 189 | -------------------------------------------------------------------------------- /commands/System/Admin/eval.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, Stopwatch, Type, util } = require('klasa'); 3 | const { inspect } = require('util'); 4 | const fetch = require('node-fetch'); 5 | 6 | module.exports = class extends Command { 7 | 8 | constructor(...args) { 9 | super(...args, { 10 | aliases: ['ev'], 11 | description: (language) => language.get('COMMAND_EVAL_DESCRIPTION'), 12 | extendedHelp: (language) => language.get('COMMAND_EVAL_EXTENDED'), 13 | guarded: true, 14 | permissionLevel: 10, 15 | usage: '' 16 | }); 17 | 18 | this.timeout = 30000; 19 | } 20 | 21 | async run(msg, [code]) { 22 | const flagTime = 'no-timeout' in msg.flags ? 'wait' in msg.flags ? Number(msg.flags.wait) : this.timeout : Infinity; 23 | const language = msg.flags.lang || msg.flags.language || (msg.flags.json ? 'json' : 'js'); 24 | const { success, result, time, type } = await this.timedEval(msg, code, flagTime); 25 | 26 | if (msg.flags.silent) { 27 | if (!success && result && result.stack) this.client.emit('error', result.stack); 28 | return null; 29 | } 30 | 31 | const footer = util.codeBlock('ts', type); 32 | const sendAs = msg.flags.output || msg.flags['output-to'] || (msg.flags.log ? 'log' : null); 33 | return this.handleMessage(msg, { sendAs, hastebinUnavailable: false, url: null }, { success, result, time, footer, language }); 34 | } 35 | 36 | async handleMessage(msg, options, { success, result, time, footer, language }) { 37 | switch (options.sendAs) { 38 | case 'file': { 39 | if (msg.channel.attachable) return msg.channel.sendFile(Buffer.from(result), 'output.txt', msg.language.get('COMMAND_EVAL_OUTPUT_FILE', time, footer)); 40 | await this.getTypeOutput(msg, options); 41 | return this.handleMessage(msg, options, { success, result, time, footer, language }); 42 | } 43 | case 'haste': 44 | case 'hastebin': { 45 | if (!options.url) options.url = await this.getHaste(result, language).catch(() => null); 46 | if (options.url) return msg.sendMessage(msg.language.get('COMMAND_EVAL_OUTPUT_HASTEBIN', time, options.url, footer)); 47 | options.hastebinUnavailable = true; 48 | await this.getTypeOutput(msg, options); 49 | return this.handleMessage(msg, options, { success, result, time, footer, language }); 50 | } 51 | case 'console': 52 | case 'log': { 53 | this.client.emit('log', result); 54 | return msg.sendMessage(msg.language.get('COMMAND_EVAL_OUTPUT_CONSOLE', time, footer)); 55 | } 56 | case 'none': 57 | return null; 58 | default: { 59 | if (result.length > 2000) { 60 | await this.getTypeOutput(msg, options); 61 | return this.handleMessage(msg, options, { success, result, time, footer, language }); 62 | } 63 | return msg.sendMessage(msg.language.get(success ? 'COMMAND_EVAL_OUTPUT' : 'COMMAND_EVAL_ERROR', 64 | time, util.codeBlock(language, result), footer)); 65 | } 66 | } 67 | } 68 | 69 | async getTypeOutput(msg, options) { 70 | const _options = ['log']; 71 | if (msg.channel.attachable) _options.push('file'); 72 | if (!options.hastebinUnavailable) _options.push('hastebin'); 73 | let _choice; 74 | do { 75 | _choice = await msg.prompt(`Choose one of the following options: ${_options.join(', ')}`).catch(() => ({ content: 'none' })); 76 | } while (!['file', 'haste', 'hastebin', 'console', 'log', 'default', 'none', null].includes(_choice.content)); 77 | options.sendAs = _choice.content; 78 | } 79 | 80 | timedEval(msg, code, flagTime) { 81 | if (flagTime === Infinity || flagTime === 0) return this.eval(msg, code); 82 | return Promise.race([ 83 | util.sleep(flagTime).then(() => ({ 84 | success: false, 85 | result: msg.language.get('COMMAND_EVAL_TIMEOUT', flagTime / 1000), 86 | time: '⏱ ...', 87 | type: 'EvalTimeoutError' 88 | })), 89 | this.eval(msg, code) 90 | ]); 91 | } 92 | 93 | // Eval the input 94 | async eval(msg, code) { 95 | const stopwatch = new Stopwatch(); 96 | let success, syncTime, asyncTime, result; 97 | let thenable = false; 98 | let type; 99 | try { 100 | if (msg.flags.async) code = `(async () => {\n${code}\n})();`; 101 | result = eval(code); 102 | syncTime = stopwatch.toString(); 103 | type = new Type(result); 104 | if (util.isThenable(result)) { 105 | thenable = true; 106 | stopwatch.restart(); 107 | result = await result; 108 | asyncTime = stopwatch.toString(); 109 | } 110 | success = true; 111 | } catch (error) { 112 | if (!syncTime) syncTime = stopwatch.toString(); 113 | if (thenable && !asyncTime) asyncTime = stopwatch.toString(); 114 | if (!type) type = new Type(error); 115 | result = error; 116 | success = false; 117 | } 118 | 119 | stopwatch.stop(); 120 | if (typeof result !== 'string') { 121 | result = result instanceof Error ? result.stack : msg.flags.json ? JSON.stringify(result, null, 4) : inspect(result, { 122 | depth: msg.flags.depth ? parseInt(msg.flags.depth) || 0 : 0, 123 | showHidden: Boolean(msg.flags.showHidden) 124 | }); 125 | } 126 | return { success, type, time: this.formatTime(syncTime, asyncTime), result: util.clean(result) }; 127 | } 128 | 129 | formatTime(syncTime, asyncTime) { 130 | return asyncTime ? `⏱ ${asyncTime}<${syncTime}>` : `⏱ ${syncTime}`; 131 | } 132 | 133 | async getHaste(evalResult, language) { 134 | const key = await fetch('https://hastebin.com/documents', { method: 'POST', body: evalResult }) 135 | .then(response => response.json()) 136 | .then(body => body.key); 137 | return `https://hastebin.com/${key}.${language}`; 138 | } 139 | 140 | }; 141 | 142 | /** 143 | The eval command evaluates code as-in, any error thrown from it will be handled. 144 | It also uses the flags feature. Write --silent, --depth=number or --async to customize the output. 145 | The --wait flag changes the time the eval will run. Defaults to 10 seconds. Accepts time in milliseconds. 146 | The --output and --output-to flag accept either 'file', 'log', 'haste' or 'hastebin'. 147 | The --delete flag makes the command delete the message that executed the message after evaluation. 148 | The --silent flag will make it output nothing. 149 | The --depth flag accepts a number, for example, --depth=2, to customize util.inspect's depth. 150 | The --async flag will wrap the code into an async function where you can enjoy the use of await, however, if you want to return something, you will need the return keyword 151 | The --showHidden flag will enable the showHidden option in util.inspect. 152 | The --lang and --language flags allow different syntax highlight for the output. 153 | The --json flag converts the output to json 154 | The --no-timeout flag disables the timeout 155 | If the output is too large, it'll send the output as a file, or in the console if the bot does not have the ATTACH_FILES permission. 156 | */ 157 | 158 | /** 159 | COMMAND_EVAL_TIMEOUT: (seconds) => `TIMEOUT: Took longer than ${seconds} seconds.`, 160 | COMMAND_EVAL_ERROR: (time, output, type) => `**Error**:${output}\n**Type**:${type}\n${time}`, 161 | COMMAND_EVAL_OUTPUT: (time, output, type) => `**Output**:${output}\n**Type**:${type}\n${time}`, 162 | COMMAND_EVAL_OUTPUT_CONSOLE: (time, type) => `Sent the result to console.\n**Type**:${type}\n${time}`, 163 | COMMAND_EVAL_OUTPUT_FILE: (time, type) => `Sent the result as a file.\n**Type**:${type}\n${time}`, 164 | COMMAND_EVAL_OUTPUT_HASTEBIN: (time, url, type) => `Sent the result to hastebin: ${url}\n**Type**:${type}\n${time}\n` 165 | */ 166 | -------------------------------------------------------------------------------- /commands/System/Admin/heapsnapshot.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { join } = require('path'); 4 | const writeSnapshot = require('util') 5 | .promisify(require('heapdump').writeSnapshot); 6 | 7 | module.exports = class extends Command { 8 | 9 | constructor(...args) { 10 | super(...args, { 11 | permissionLevel: 10, 12 | guarded: true, 13 | description: 'Creates a heapdump for finding memory leaks.', 14 | extendedHelp: [ 15 | 'The heapsnapshot command is very useful for bots that have memory issues, it uses the heapdump library', 16 | 'which freezes the entire process for a moment to analize all elements from the process\' HEAP, NEVER share', 17 | 'heapsnapshot files with anybody, as everything your bot holds is included in that file.\n\nTo open heapsnapshot', 18 | 'files, open Google Chrome, open Developer Tools, go to the tab Memory, and in Profiles, click on the buttom "load".', 19 | 'Finally, open the profile and you will be given a table of all objects in your process, have fun!\n\nP.S:', 20 | 'heapsnapshot files are as big as the amount of RAM you use, in big bots, the snapshots can freeze the bot', 21 | 'much longer and the files can be much heavier.' 22 | ].join(' ') 23 | }); 24 | } 25 | 26 | async run(msg) { 27 | await msg.sendMessage('Capturing HEAP Snapshot. This may take a while...'); 28 | 29 | // Capture the snapshot (this freezes the entire VM) 30 | const path = join(process.cwd(), `${Date.now()}.heapsnapshot`); 31 | await writeSnapshot(path); 32 | 33 | return msg.sendMessage(`Captured in \`${path}\`, check! Remember, do NOT share this with anybody, it may contain a lot of sensitive data.`); 34 | } 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /commands/System/Admin/reboot.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | 3 | /* 4 | * In your main file, put: 5 | * Client.defaultClientSchema.add('restart', folder => folder 6 | * .add('message', 'messagepromise') 7 | * .add('timestamp', 'bigint', { min: 0 })); 8 | * 9 | * Uses the bigint and messagepromise serializers from https://github.com/dirigeants/klasa-pieces/tree/master/serializers 10 | * 11 | * If BigInt doesn't exist, update Node.js. 12 | */ 13 | 14 | const { Command } = require('klasa'); 15 | 16 | const DIGITS_TO_UNITS = new Map([ 17 | [9, 's'], 18 | [6, 'ms'], 19 | [3, 'μs'] 20 | ]); 21 | 22 | const rebootKeys = ['message', 'timestamp'].map(key => `restart.${key}`); 23 | 24 | module.exports = class extends Command { 25 | 26 | constructor(...args) { 27 | super(...args, { 28 | permissionLevel: 10, 29 | guarded: true, 30 | description: language => language.get('COMMAND_REBOOT_DESCRIPTION') 31 | }); 32 | } 33 | 34 | async run(msg) { 35 | await Promise.all([ 36 | ...this.client.providers.map(provider => provider.shutdown()), 37 | this.client.settings.update({ 38 | restart: { 39 | message: await msg.sendLocale('COMMAND_REBOOT'), 40 | timestamp: process.hrtime.bigint() 41 | } 42 | }).then(result => result.errors.length && this.client.emit('error', result.errors.join('\n'))) 43 | ]); 44 | process.exit(); 45 | } 46 | 47 | async init() { 48 | // "message" needs to be awaited 49 | const [message, timestamp] = await Promise.all(rebootKeys.map(key => this._resolveSetting(key))); 50 | await this.client.settings.reset(rebootKeys); 51 | 52 | if (message) message.send(`✅ Successfully rebooted. (Took: ${timestamp && this.constructor.getFriendlyDuration(timestamp)})`); 53 | else this.client.emit('info', 'No restart channel'); 54 | } 55 | 56 | _resolveSetting(path) { 57 | const { settings, languages: { default: language } } = this.client; 58 | 59 | const route = typeof path === 'string' ? path.split('.') : path; 60 | const piece = settings.gateway.schema.get(route); 61 | 62 | let objOrData = settings; 63 | for (const key of route) objOrData = objOrData[key]; 64 | 65 | try { 66 | return piece.serializer.deserialize(objOrData, piece, language); 67 | } catch (err) { 68 | return undefined; 69 | } 70 | } 71 | 72 | static bigAbs(bigint) { 73 | return bigint < 0 ? -bigint : bigint; 74 | } 75 | 76 | static getFriendlyDuration(from, to = process.hrtime.bigint()) { 77 | const time = this.bigAbs(to - from).toString(); 78 | let shift, suffix; 79 | 80 | const digits = time.length; 81 | for (const [d, suf] of DIGITS_TO_UNITS) { 82 | if (digits > d) { 83 | shift = -d; 84 | suffix = suf; 85 | break; 86 | } 87 | } 88 | 89 | const whole = time.slice(0, shift); 90 | const fractional = `${time.slice(shift, shift + 1)}${this._roundDigit(time.slice(shift + 1, shift + 3))}`; 91 | return `${whole}.${fractional}${suffix}`; 92 | } 93 | 94 | static _roundDigit([digit, otherDigit]) { 95 | return Number(digit) + (otherDigit >= 5); 96 | } 97 | 98 | }; 99 | -------------------------------------------------------------------------------- /commands/System/exec.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, util: { exec, codeBlock } } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | aliases: ['execute'], 9 | description: 'Execute commands in the terminal, use with EXTREME CAUTION.', 10 | guarded: true, 11 | permissionLevel: 10, 12 | usage: '', 13 | extendedHelp: 'Times out in 60 seconds by default. This can be changed with --timeout=TIME_IN_MILLISECONDS' 14 | }); 15 | } 16 | 17 | async run(msg, [input]) { 18 | await msg.sendMessage('Executing your command...'); 19 | 20 | const result = await exec(input, { timeout: 'timeout' in msg.flags ? Number(msg.flags.timeout) : 60000 }) 21 | .catch(error => ({ stdout: null, stderr: error })); 22 | const output = result.stdout ? `**\`OUTPUT\`**${codeBlock('prolog', result.stdout)}` : ''; 23 | const outerr = result.stderr ? `**\`ERROR\`**${codeBlock('prolog', result.stderr)}` : ''; 24 | 25 | return msg.sendMessage([output, outerr].join('\n') || 'Done. There was no output to stdout or stderr.'); 26 | } 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /commands/Tools/artist.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | /* 3 | * Requires getSpotifyToken task 4 | * https://github.com/dirigeants/klasa-pieces/blob/master/tasks/getSpotifyToken.js 5 | * 6 | */ 7 | const { Command } = require('klasa'); 8 | const fetch = require('node-fetch'); 9 | 10 | module.exports = class extends Command { 11 | 12 | constructor(...args) { 13 | super(...args, { 14 | cooldown: 10, 15 | description: 'Searches Spotify for an artist, and returns an embed.', 16 | usage: '' 17 | }); 18 | } 19 | 20 | async run(msg, [query]) { 21 | if (!this.client._spotifyToken) return this.client.emit('wtf', 'Spotify Token is undefined.'); 22 | 23 | const artist = await fetch(`https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=artist&limit=1`, 24 | { 25 | headers: { 26 | Accept: 'application/json', 27 | 'Content-Type': 'application/json', 28 | Authorization: `Bearer ${this.client._spotifyToken}` 29 | } 30 | }) 31 | .then(response => response.json()) 32 | .then(response => response.artists.items[0]) 33 | .catch(() => { throw 'There was an error. Please try again later.'; }); 34 | 35 | if (artist) return msg.sendMessage(artist.external_urls.spotify); 36 | throw "Couldn't find any artists with that name."; 37 | } 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /commands/Tools/avatar.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | const { MessageEmbed } = require('discord.js'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | description: 'Shows a user\'s avatar', 9 | usage: '[user:user]' 10 | }); 11 | } 12 | 13 | async run(msg, [user = msg.author]) { 14 | const avatar = user.displayAvatarURL({ size: 512 }); 15 | 16 | return msg.sendEmbed(new MessageEmbed() 17 | .setAuthor(user.username, avatar) 18 | .setImage(avatar)); 19 | } 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /commands/Tools/crate.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | const fetch = require('node-fetch'); 3 | const { MessageEmbed } = require('discord.js'); 4 | 5 | const suffixes = ['Bytes', 'KB', 'MB']; 6 | const getBytes = (bytes) => { 7 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 8 | return (!bytes && '0 Bytes') || `${(bytes / Math.pow(1024, i)).toFixed(2)} ${suffixes[i]}`; 9 | }; 10 | 11 | module.exports = class extends Command { 12 | 13 | constructor(...args) { 14 | super(...args, { 15 | description: 'Shows the install/publish size of a cargo crate.', 16 | usage: '' 17 | }); 18 | } 19 | 20 | async run(msg, [name]) { 21 | const { crate, versions: [latest] } = await fetch(`https://crates.io/api/v1/crates/${encodeURIComponent(name)}`) 22 | .then(response => response.json()) 23 | .catch(() => { 24 | throw 'There was an unexpected error. Try again later.'; 25 | }); 26 | 27 | if (!crate) throw 'That crate doesn\'t exist.'; 28 | 29 | const embed = new MessageEmbed() 30 | .setColor(15051318) 31 | .setThumbnail('https://doc.rust-lang.org/cargo/images/Cargo-Logo-Small.png') 32 | .setTitle(name) 33 | .setURL(`https://crates.io/crates/${name}`) 34 | .setDescription(`${crate.description} 35 | 36 | [Documentation](${crate.documentation}) - [Repository](${crate.repository}) 37 | `) 38 | .addField('Total Downloads', crate.downloads.toLocaleString(), true) 39 | .addField('Categories', crate.categories.join(', '), true) 40 | .addField('Keywords', crate.keywords.join(', '), true) 41 | .addField('Latest Version', ` 42 | **Number:** ${latest.num} 43 | **Size:** ${getBytes(latest.crate_size)} 44 | **Downloads:** ${latest.downloads.toLocaleString()} 45 | **License:** ${latest.license} 46 | `, true); 47 | 48 | return msg.sendEmbed(embed); 49 | } 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /commands/Tools/discordemoji.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | const fetch = require('node-fetch'); 3 | 4 | const API_URL = 'https://discordemoji.com/api/'; 5 | 6 | module.exports = class extends Command { 7 | 8 | constructor(...args) { 9 | super(...args, { 10 | aliases: ['de'], 11 | description: 'Searches discordemoji.com for an emoji.', 12 | usage: ' [count:int{1,100}]', 13 | usageDelim: ',' 14 | }); 15 | this.emojis = null; 16 | } 17 | 18 | async run(msg, [query, count = 4]) { 19 | const matches = this.emojis.filter(({ nsfw, title }) => { 20 | // Don't post NSFW emoji in a SFW channel 21 | if (!msg.channel.nsfw && nsfw) return false; 22 | return title.toUpperCase().includes(query.toUpperCase()); 23 | }); 24 | 25 | if (matches.length === 0) return msg.send('No results.'); 26 | 27 | return msg.send( 28 | matches 29 | .sort(() => Math.random() - 0.5) 30 | .slice(0, count) 31 | .map(emj => emj.image) 32 | .join(' ')); 33 | } 34 | 35 | async init() { 36 | // Fetch the emojis and categories from the API 37 | const [emojis, cats] = await Promise.all( 38 | [API_URL, `${API_URL}?request=categories`].map(url => fetch(url).then(res => res.json())) 39 | ); 40 | 41 | // Change the emojis' properties to be more useful 42 | this.emojis = emojis.map(emj => ({ 43 | ...emj, 44 | category: cats[emj.category], 45 | nsfw: cats[emj.category] === 'NSFW', 46 | description: emj.description.includes('View more') ? '' : emj.description 47 | })); 48 | } 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /commands/Tools/followage.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | const fetch = require('node-fetch'); 5 | 6 | /** 7 | * https://dev.twitch.tv/docs/v5/guides/authentication/ 8 | */ 9 | const query = new URLSearchParams([['client_id', 'CLIENT_ID_HERE']]); 10 | 11 | module.exports = class extends Command { 12 | 13 | constructor(...args) { 14 | super(...args, { 15 | description: 'Shows the followage of a given user from a given twitch channel.', 16 | usage: ' ', 17 | usageDelim: ' ' 18 | }); 19 | } 20 | 21 | async run(msg, [twitchName, channelName]) { 22 | const url = new URL(`https://api.twitch.tv/kraken/users/${encodeURIComponent(twitchName)}/follows/channels/${channelName}`); 23 | url.search = query; 24 | 25 | const body = await fetch(url) 26 | .then(response => response.json()) 27 | .catch(() => { throw `${twitchName} isn't following ${channelName}, or it is banned, or doesn't exist at all.`; }); 28 | const days = this.differenceDays(new Date(body.created_at), new Date()); 29 | return msg.sendEmbed(new MessageEmbed() 30 | .setColor(6570406) 31 | .setAuthor(`${twitchName} has been following ${channelName} for ${days} days.`, body.channel.logo)); 32 | } 33 | 34 | differenceDays(first, second) { 35 | return (second - first) / (1000 * 60 * 60 * 24); 36 | } 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /commands/Tools/hastebin.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | aliases: ['hb'], 10 | description: 'Upload code or text to hastebin.', 11 | usage: '' 12 | }); 13 | } 14 | 15 | async run(msg, [code]) { 16 | const key = await fetch('https://hastebin.com/documents', { method: 'POST', body: code }) 17 | .then(response => response.json()) 18 | .then(body => body.key); 19 | return msg.sendMessage(`https://hastebin.com/${key}`); 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /commands/Tools/movie.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | const fetch = require('node-fetch'); 5 | // Create a TMDB account on https://www.themoviedb.org/ (if you haven't yet) and go to https://www.themoviedb.org/settings/api to get your API key. 6 | const tmdbAPIkey = 'API_KEY_HERE'; 7 | 8 | module.exports = class extends Command { 9 | 10 | constructor(...args) { 11 | super(...args, { 12 | aliases: ['movies', 'film', 'films'], 13 | description: 'Finds a movie on TMDB.org', 14 | extendedHelp: 'e.g. `s.movie infinity war, 2`', 15 | usage: ' [Page:number]', 16 | usageDelim: ', ' 17 | }); 18 | } 19 | 20 | async run(msg, [query, page = 1]) { 21 | const url = new URL('https://api.themoviedb.org/3/search/movie'); 22 | url.search = new URLSearchParams([['api_key', tmdbAPIkey], ['query', query]]); 23 | 24 | const body = await fetch(url) 25 | .then(response => response.json()) 26 | .catch(() => { throw `I couldn't find a movie with title **${query}** in page ${page}.`; }); 27 | 28 | const movie = body.results[page - 1]; 29 | if (!movie) throw `I couldn't find a movie with title **${query}** in page ${page}.`; 30 | 31 | const embed = new MessageEmbed() 32 | .setImage(`https://image.tmdb.org/t/p/original${movie.poster_path}`) 33 | .setTitle(`${movie.title} (${page} out of ${body.results.length} results)`) 34 | .setDescription(movie.overview) 35 | .setFooter(`${this.client.user.username} uses the TMDb API but is not endorsed or certified by TMDb.`, 36 | 'https://www.themoviedb.org/assets/1/v4/logos/408x161-powered-by-rectangle-green-bb4301c10ddc749b4e79463811a68afebeae66ef43d17bcfd8ff0e60ded7ce99.png'); 37 | if (movie.title !== movie.original_title) embed.addField('Original Title', movie.original_title, true); 38 | embed 39 | .addField('Vote Count', movie.vote_count, true) 40 | .addField('Vote Average', movie.vote_average, true) 41 | .addField('Popularity', movie.popularity, true) 42 | .addField('Adult Content', movie.adult ? 'Yep' : 'Nope', true) 43 | .addField('Release Date', movie.release_date); 44 | 45 | return msg.sendEmbed(embed); 46 | } 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /commands/Tools/pkgsize.js: -------------------------------------------------------------------------------- 1 | const { Command } = require('klasa'); 2 | const fetch = require('node-fetch'); 3 | 4 | const suffixes = ['Bytes', 'KB', 'MB', 'GB']; 5 | const getBytes = (bytes) => { 6 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 7 | return (!bytes && '0 Bytes') || `${(bytes / Math.pow(1024, i)).toFixed(2)} ${suffixes[i]}`; 8 | }; 9 | 10 | module.exports = class extends Command { 11 | 12 | constructor(...args) { 13 | super(...args, { 14 | description: 'Shows the install/publish size of a npm package.', 15 | usage: '' 16 | }); 17 | } 18 | 19 | async run(msg, [name]) { 20 | const { publishSize, installSize } = await fetch(`https://packagephobia.now.sh/api.json?p=${encodeURIComponent(name)}`) 21 | .then(response => response.json()) 22 | .catch(() => { 23 | throw 'There was an unexpected error. Try again later.'; 24 | }); 25 | 26 | if (!publishSize && !installSize) throw 'That package doesn\'t exist.'; 27 | 28 | return msg.send(` 29 | 30 | 31 | **Publish Size:** ${getBytes(publishSize)} 32 | **Install Size:** ${getBytes(installSize)} 33 | `); 34 | } 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /commands/Tools/price.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | description: 'Compares the value of a currency (crypto, fiat) with another.', 10 | usage: ' [amount:int{1}]', 11 | usageDelim: ' ' 12 | }); 13 | } 14 | 15 | async run(msg, [coin, currency, amount = 1]) { 16 | const c1 = coin.toUpperCase(); 17 | const c2 = currency.toUpperCase(); 18 | 19 | const body = await fetch(`https://min-api.cryptocompare.com/data/price?fsym=${c1}&tsyms=${c2}`) 20 | .then(response => response.json()) 21 | .catch(() => { throw 'There was an error, please make sure you specified an appropriate coin and currency.'; }); 22 | if (!body[c2]) return msg.sendMessage('There was an error, please make sure you specified an appropriate coin and currency.'); 23 | return msg.sendMessage(`Current price of ${amount} ${c1} is ${(body[c2] * amount).toLocaleString()} ${c2}`); 24 | } 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /commands/Tools/randomreddit.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | description: 'Returns a random reddit post on a given subreddit.', 10 | usage: '' 11 | }); 12 | this.errorMessage = `There was an error. Reddit may be down, or the subreddit doesnt exist.`; 13 | } 14 | 15 | async run(msg, [subreddit]) { 16 | const data = await fetch(`https://www.reddit.com/r/${subreddit}/random.json`) 17 | .then(response => response.json()) 18 | .then(body => { 19 | if (body.error) throw this.errorMessage; 20 | return body[0].data.children[0].data; 21 | }) 22 | .catch(() => { throw this.errorMessage; }); 23 | 24 | if (data.over_18 && !msg.channel.nsfw) { 25 | throw 'I cant post a NSFW image in this channel unless you mark it as NSFW!'; 26 | } 27 | 28 | return msg.sendMessage(data.url); 29 | } 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /commands/Tools/remindme.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | /* 5 | 6 | To use this correctly, you will also need the reminder task located in 7 | /tasks/reminder.js 8 | 9 | */ 10 | 11 | module.exports = class extends Command { 12 | 13 | constructor(...args) { 14 | super(...args, { 15 | description: 'creates a reminder', 16 | usage: ' ', 17 | usageDelim: ', ' 18 | }); 19 | } 20 | 21 | async run(msg, [when, text]) { 22 | const reminder = await this.client.schedule.create('reminder', when, { 23 | data: { 24 | channel: msg.channel.id, 25 | user: msg.author.id, 26 | text 27 | } 28 | }); 29 | return msg.sendMessage(`Ok, I created you a reminder with the id: \`${reminder.id}\``); 30 | } 31 | 32 | }; 33 | -------------------------------------------------------------------------------- /commands/Tools/role-info.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, Timestamp } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | runIn: ['text'], 9 | description: 'Get information on a role with an id or a mention.', 10 | usage: '' 11 | }); 12 | this.perms = { 13 | ADMINISTRATOR: 'Administrator', 14 | VIEW_AUDIT_LOG: 'View Audit Log', 15 | MANAGE_GUILD: 'Manage Server', 16 | MANAGE_ROLES: 'Manage Roles', 17 | MANAGE_CHANNELS: 'Manage Channels', 18 | KICK_MEMBERS: 'Kick Members', 19 | BAN_MEMBERS: 'Ban Members', 20 | CREATE_INSTANT_INVITE: 'Create Instant Invite', 21 | CHANGE_NICKNAME: 'Change Nickname', 22 | MANAGE_NICKNAMES: 'Manage Nicknames', 23 | MANAGE_EMOJIS: 'Manage Emojis', 24 | MANAGE_WEBHOOKS: 'Manage Webhooks', 25 | VIEW_CHANNEL: 'Read Text Channels and See Voice Channels', 26 | SEND_MESSAGES: 'Send Messages', 27 | SEND_TTS_MESSAGES: 'Send TTS Messages', 28 | MANAGE_MESSAGES: 'Manage Messages', 29 | EMBED_LINKS: 'Embed Links', 30 | ATTACH_FILES: 'Attach Files', 31 | READ_MESSAGE_HISTORY: 'Read Message History', 32 | MENTION_EVERYONE: 'Mention Everyone', 33 | USE_EXTERNAL_EMOJIS: 'Use External Emojis', 34 | ADD_REACTIONS: 'Add Reactions', 35 | CONNECT: 'Connect', 36 | SPEAK: 'Speak', 37 | MUTE_MEMBERS: 'Mute Members', 38 | DEAFEN_MEMBERS: 'Deafen Members', 39 | MOVE_MEMBERS: 'Move Members', 40 | USE_VAD: 'Use Voice Activity' 41 | }; 42 | this.timestamp = new Timestamp('dddd, MMMM d YYYY'); 43 | } 44 | 45 | run(msg, [role]) { 46 | const allPermissions = Object.entries(role.permissions.serialize()).filter(perm => perm[1]).map(([perm]) => this.perms[perm]).join(', '); 47 | 48 | return msg.sendEmbed(new MessageEmbed() 49 | .setColor(role.hexColor || 0xFFFFFF) 50 | .addField('❯ Name', role.name, true) 51 | .addField('❯ ID', role.id, true) 52 | .addField('❯ Color', role.hexColor || 'None', true) 53 | .addField('❯ Creation Date', this.timestamp.display(role.createdAt), true) 54 | .addField('❯ Hoisted', role.hoist ? 'Yes' : 'No', true) 55 | .addField('❯ Mentionable', role.mentionable ? 'Yes' : 'No', true) 56 | .addField('❯ Permissions', allPermissions || 'None')); 57 | } 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /commands/Tools/server-info.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, Timestamp } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | runIn: ['text'], 10 | aliases: ['guild'], 11 | description: 'Get information on the current server.' 12 | }); 13 | this.verificationLevels = [ 14 | 'None', 15 | 'Low', 16 | 'Medium', 17 | '(╯°□°)╯︵ ┻━┻', 18 | '┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻' 19 | ]; 20 | 21 | this.filterLevels = [ 22 | 'Off', 23 | 'No Role', 24 | 'Everyone' 25 | ]; 26 | this.timestamp = new Timestamp('d MMMM YYYY'); 27 | } 28 | 29 | run(msg) { 30 | return msg.sendEmbed(new MessageEmbed() 31 | .setColor(0x00AE86) 32 | .setThumbnail(msg.guild.iconURL()) 33 | .addField('❯ Name', msg.guild.name, true) 34 | .addField('❯ ID', msg.guild.id, true) 35 | .addField('❯ Creation Date', this.timestamp.display(msg.guild.createdAt), true) 36 | .addField('❯ Region', msg.guild.region, true) 37 | .addField('❯ Explicit Filter', this.filterLevels[msg.guild.explicitContentFilter], true) 38 | .addField('❯ Verification Level', this.verificationLevels[msg.guild.verificationLevel], true) 39 | .addField('❯ Owner', msg.guild.owner ? msg.guild.owner.user.tag : 'None', true) 40 | .addField('❯ Members', msg.guild.memberCount, true)); 41 | } 42 | 43 | }; 44 | -------------------------------------------------------------------------------- /commands/Tools/song.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | /* 3 | * Requires getSpotifyToken task 4 | * https://github.com/dirigeants/klasa-pieces/blob/master/tasks/getSpotifyToken.js 5 | * 6 | */ 7 | const { Command } = require('klasa'); 8 | const fetch = require('node-fetch'); 9 | 10 | module.exports = class extends Command { 11 | 12 | constructor(...args) { 13 | super(...args, { 14 | cooldown: 10, 15 | description: 'Searches Spotify for a song, and returns an embeddable link.', 16 | usage: '' 17 | }); 18 | } 19 | 20 | async run(msg, [query]) { 21 | if (!this.client._spotifyToken) return this.client.emit('wtf', 'Spotify Token is undefined.'); 22 | 23 | const song = await fetch(`https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=1`, 24 | { 25 | headers: { 26 | Accept: 'application/json', 27 | 'Content-Type': 'application/json', 28 | Authorization: `Bearer ${this.client._spotifyToken}` 29 | } 30 | }) 31 | .then(response => response.json()) 32 | .then(response => response.tracks.items[0]) 33 | .catch(() => { throw 'There was an error. Please try again later.'; }); 34 | 35 | if (song) return msg.sendMessage(song.external_urls.spotify); 36 | throw "Couldn't find any songs with that name."; 37 | } 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /commands/Tools/subreddit.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | const { MessageEmbed } = require('discord.js'); 5 | 6 | module.exports = class extends Command { 7 | 8 | constructor(...args) { 9 | super(...args, { 10 | aliases: ['sub'], 11 | description: 'Returns information on a subreddit.', 12 | usage: '' 13 | }); 14 | this.errorMessage = `There was an error. Reddit may be down, or the subreddit doesnt exist.`; 15 | } 16 | 17 | async run(msg, [subredditName]) { 18 | const subreddit = await fetch(`https://www.reddit.com/r/${subredditName}/about.json`) 19 | .then(response => response.json()) 20 | .then(body => { 21 | if (body.kind === 't5') return body.data; 22 | throw `That subreddit doesn't exist.`; 23 | }) 24 | .catch(() => { throw this.errorMessage; }); 25 | 26 | return msg.sendEmbed(new MessageEmbed() 27 | .setTitle(subreddit.title) 28 | .setDescription(subreddit.public_description) 29 | .setURL(`https://www.reddit.com/r/${subredditName}/`) 30 | .setColor(6570404) 31 | .setThumbnail(subreddit.icon_img) 32 | .setImage(subreddit.banner_img) 33 | .addField('Subscribers', subreddit.subscribers.toLocaleString(), true) 34 | .addField('Users Active', subreddit.accounts_active.toLocaleString(), true)); 35 | } 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /commands/Tools/todo.js: -------------------------------------------------------------------------------- 1 | /* 2 | This piece requires a `TODOs` key to work, add in your main file: 3 | Client.defaultUserSchema 4 | .add('TODOs', 'any', { array: true }); 5 | */ 6 | 7 | const { Command } = require('klasa'); 8 | 9 | module.exports = class extends Command { 10 | 11 | constructor(...args) { 12 | super(...args, { 13 | runIn: ['dm'], 14 | description: 'add|remove|list user\'s TODOs through DM', 15 | extendedHelp: 'No extended help available.', 16 | usage: ' (TODO:string) [content:...string]', 17 | usageDelim: ' ', 18 | subcommands: true 19 | }); 20 | this.createCustomResolver('string', (arg, possible, message, [action]) => { 21 | if (action === 'list') return arg; 22 | return this.client.arguments.get('string').run(arg, possible, message); 23 | }); 24 | } 25 | 26 | async add(message, [TODO, content]) { 27 | await message.author.settings.update('TODOs', [...message.author.settings.TODOs, [TODO, content]], { action: 'overwrite' }); 28 | return message.send(`Added \`${TODO}\` TODO`); 29 | } 30 | 31 | async remove(message, [TODO]) { 32 | const filtered = message.author.settings.TODOs.filter(([name]) => name !== TODO); 33 | await message.author.settings.update('TODOs', filtered, { action: 'overwrite' }); 34 | return message.send(`Removed \`${TODO}\` TODO`); 35 | } 36 | 37 | list(message) { 38 | return message.send(`List of TODOs for this user: \`${message.author.settings.TODOs.join('`, `')}\``); 39 | } 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /commands/Tools/topinvites.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | 4 | module.exports = class extends Command { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | runIn: ['text'], 9 | aliases: ['ti'], 10 | requiredPermissions: ['MANAGE_GUILD'], 11 | description: 'Shows the top invites in a server.' 12 | }); 13 | } 14 | 15 | async run(msg) { 16 | const invites = await msg.guild.fetchInvites(); 17 | const topTen = invites.filter(inv => inv.uses > 0).sort((a, b) => b.uses - a.uses).first(10); 18 | if (topTen.length === 0) throw 'There are no invites, or none of them have been used!'; 19 | return msg.sendMessage( 20 | topTen.map(inv => `**${inv.inviter.username}**'s invite **${inv.code}** has **${inv.uses.toLocaleString()}** uses.`) 21 | ); 22 | } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /commands/Tools/tvshow.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | const fetch = require('node-fetch'); 5 | // Create a TMDB account on https://www.themoviedb.org/ (if you haven't yet) and go to https://www.themoviedb.org/settings/api to get your API key. 6 | const tmdbAPIkey = 'API_KEY_HERE'; 7 | 8 | module.exports = class extends Command { 9 | 10 | constructor(...args) { 11 | super(...args, { 12 | aliases: ['tvshows', 'tv', 'tvseries'], 13 | description: 'Finds a TV show on TMDB.org', 14 | extendedHelp: 'e.g. `s.tvshow universe, 2`', 15 | usage: ' [Page:number]', 16 | usageDelim: ',' 17 | }); 18 | } 19 | 20 | async run(msg, [query, page = 1]) { 21 | const url = new URL('https://api.themoviedb.org/3/search/tv'); 22 | url.search = new URLSearchParams([['api_key', tmdbAPIkey], ['query', query]]); 23 | 24 | const body = await fetch(url) 25 | .then(response => response.json()); 26 | const show = body.results[page - 1]; 27 | if (!show) throw `I couldn't find a TV show with title **${query}** in page ${page}.`; 28 | 29 | const embed = new MessageEmbed() 30 | .setColor('RANDOM') 31 | .setImage(`https://image.tmdb.org/t/p/original${show.poster_path}`) 32 | .setTitle(`${show.name} (${page} out of ${body.results.length} results)`) 33 | .setDescription(show.overview) 34 | .setFooter(`${this.client.user.username} uses the TMDb API but is not endorsed or certified by TMDb.`, 35 | 'https://www.themoviedb.org/assets/1/v4/logos/408x161-powered-by-rectangle-green-bb4301c10ddc749b4e79463811a68afebeae66ef43d17bcfd8ff0e60ded7ce99.png'); 36 | if (show.title !== show.original_name) embed.addField('Original Title', show.original_name, true); 37 | embed 38 | .addField('Vote Count', show.vote_count, true) 39 | .addField('Vote Average', show.vote_average, true) 40 | .addField('Popularity', show.popularity, true) 41 | .addField('First Air Date', show.first_air_date); 42 | 43 | return msg.sendEmbed(embed); 44 | } 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /commands/Tools/twitch.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, Timestamp } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | const fetch = require('node-fetch'); 5 | 6 | /** 7 | * https://dev.twitch.tv/docs/v5/guides/authentication/ 8 | */ 9 | const query = new URLSearchParams([['client_id', 'CLIENT_ID_HERE']]); 10 | 11 | module.exports = class extends Command { 12 | 13 | constructor(...args) { 14 | super(...args, { 15 | description: 'Returns information on a Twitch.tv Account', 16 | usage: '' 17 | }); 18 | this.timestamp = new Timestamp('DD-MM-YYYY'); 19 | } 20 | 21 | async run(msg, [twitchName]) { 22 | const url = new URL(`https://api.twitch.tv/kraken/channels/${encodeURIComponent(twitchName)}`); 23 | url.search = query; 24 | 25 | const body = await fetch(url) 26 | .then(response => response.json()) 27 | .catch(() => { throw 'Unable to find account. Did you spell it correctly?'; }); 28 | 29 | const creationDate = this.timestamp.display(body.created_at); 30 | const embed = new MessageEmbed() 31 | .setColor(6570406) 32 | .setThumbnail(body.logo) 33 | .setAuthor(body.display_name, 'https://i.imgur.com/OQwQ8z0.jpg', body.url) 34 | .addField('Account ID', body._id, true) 35 | .addField('Followers', body.followers, true) 36 | .addField('Created On', creationDate, true) 37 | .addField('Channel Views', body.views, true); 38 | 39 | return msg.sendEmbed(embed); 40 | } 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /commands/Tools/user-info.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command, Timestamp } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | 5 | module.exports = class extends Command { 6 | 7 | constructor(...args) { 8 | super(...args, { 9 | description: 'Get information on a mentioned user.', 10 | usage: '[Member:member]' 11 | }); 12 | this.statuses = { 13 | online: '💚 Online', 14 | idle: '💛 Idle', 15 | dnd: '❤ Do Not Disturb', 16 | offline: '💔 Offline' 17 | }; 18 | this.timestamp = new Timestamp('d MMMM YYYY'); 19 | } 20 | 21 | run(msg, [member = msg.member]) { 22 | return msg.sendEmbed(new MessageEmbed() 23 | .setColor(member.displayHexColor || 0xFFFFFF) 24 | .setThumbnail(member.user.displayAvatarURL()) 25 | .addField('❯ Name', member.user.tag, true) 26 | .addField('❯ ID', member.id, true) 27 | .addField('❯ Discord Join Date', this.timestamp.display(member.user.createdAt), true) 28 | .addField('❯ Server Join Date', this.timestamp.display(member.joinedTimestamp), true) 29 | .addField('❯ Status', this.statuses[member.presence.status], true) 30 | .addField('❯ Playing', member.presence.activity ? member.presence.activity.name : 'N/A', true) 31 | .addField('❯ Highest Role', member.roles.size > 1 ? member.roles.highest.name : 'None', true) 32 | .addField('❯ Hoist Role', member.roles.hoist ? member.roles.hoist.name : 'None', true)); 33 | } 34 | 35 | }; 36 | -------------------------------------------------------------------------------- /commands/Tools/wikipedia.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const { MessageEmbed } = require('discord.js'); 4 | const fetch = require('node-fetch'); 5 | 6 | module.exports = class extends Command { 7 | 8 | constructor(...args) { 9 | super(...args, { 10 | aliases: ['wiki'], 11 | description: 'Finds a Wikipedia Article by title.', 12 | usage: '' 13 | }); 14 | } 15 | 16 | async run(msg, [query]) { 17 | const article = await fetch(`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`) 18 | .then(response => response.json()) 19 | .catch(() => { throw "I couldn't find a wikipedia article with that title!"; }); 20 | 21 | const embed = new MessageEmbed() 22 | .setColor(4886754) 23 | .setThumbnail((article.thumbnail && article.thumbnail.source) || 'https://i.imgur.com/fnhlGh5.png') 24 | .setURL(article.content_urls.desktop.page) 25 | .setTitle(article.title) 26 | .setDescription(article.extract); 27 | 28 | return msg.sendEmbed(embed); 29 | } 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /commands/Tools/wolfram.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Command } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | /** 6 | * https://account.wolfram.com/auth/create 7 | */ 8 | const wolframAppID = 'CLIENT_ID_HERE'; 9 | 10 | module.exports = class extends Command { 11 | 12 | constructor(...args) { 13 | super(...args, { 14 | description: 'Query Wolfram Alpha with any mathematical question.', 15 | usage: '' 16 | }); 17 | } 18 | 19 | async run(msg, [query]) { 20 | const url = new URL('http://api.wolframalpha.com/v2/query'); 21 | url.search = new URLSearchParams([ 22 | ['input', query], 23 | ['primary', true], 24 | ['appid', wolframAppID], 25 | ['output', 'json'] 26 | ]); 27 | 28 | const pods = await fetch(url) 29 | .then(response => response.json()) 30 | .then(body => body.queryresult.pods) 31 | .catch(() => { throw 'There was an error. Please try again.'; }); 32 | 33 | if (!pods || pods.error) throw "Couldn't find an answer to that question!"; 34 | 35 | return msg.sendMessage([ 36 | `**Input Interpretation:** ${pods[0].subpods[0].plaintext}`, 37 | `**Result:** ${pods[1].subpods[0].plaintext.substring(0, 1500)}` 38 | ].join('\n')); 39 | } 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /events/rateLimit.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Event, Colors } = require('klasa'); 3 | 4 | /** 5 | * KlasaConsoleConfig#useColor must be 6 | * enabled in order to use this piece. 7 | */ 8 | 9 | const HEADER = new Colors({ text: 'red' }).format('[RATELIMIT]'); 10 | 11 | module.exports = class extends Event { 12 | 13 | run({ timeout, limit, method, route }) { 14 | this.client.emit('verbose', [ 15 | HEADER, 16 | `Timeout: ${timeout}ms`, 17 | `Limit: ${limit} requests`, 18 | `Method: ${method.toUpperCase()}`, 19 | `Route: ${route}` 20 | ].join('\n')); 21 | } 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /events/twitterStream.js: -------------------------------------------------------------------------------- 1 | const { Event } = require('klasa'); 2 | const { MessageEmbed } = require('discord.js'); 3 | const he = require('he'); 4 | const Twit = require('twit'); 5 | 6 | // The channel ID you want the tweets to be sent to 7 | const tweetChannel = ''; 8 | 9 | // The ID's of the accounts you want to stream tweets from - http://gettwitterid.com/ 10 | const twitterAccounts = ['']; 11 | 12 | // https://developer.twitter.com/en/apply/user 13 | /* eslint-disable camelcase */ 14 | const twitter = new Twit({ 15 | consumer_key: '', 16 | consumer_secret: '', 17 | access_token: '', 18 | access_token_secret: '' 19 | }); 20 | /* eslint-enable camelcase */ 21 | 22 | module.exports = class extends Event { 23 | 24 | constructor(...args) { 25 | super(...args, { once: true, event: 'klasaReady' }); 26 | } 27 | 28 | run() { 29 | const stream = twitter.stream('statuses/filter', { follow: twitterAccounts }); 30 | 31 | stream.on('tweet', this.handleTweet.bind(this)); 32 | } 33 | 34 | handleTweet(tweet) { 35 | // Skip tweets that are retweets, replies, etc 36 | if ( 37 | tweet.retweeted || 38 | tweet.retweeted_status || 39 | tweet.in_reply_to_status_id || 40 | tweet.in_reply_to_user_id || 41 | tweet.delete 42 | ) { 43 | return; 44 | } 45 | 46 | const _tweet = tweet.extended_tweet ? tweet.extended_tweet : tweet; 47 | 48 | const formattedTweet = { 49 | text: he.decode(tweet.extended_tweet ? tweet.extended_tweet.full_text : tweet.text), 50 | url: `https://twitter.com/${tweet.user.screen_name}/status/${tweet.id_str}`, 51 | name: he.decode(tweet.user.name), 52 | avatar: tweet.user.profile_image_url_https, 53 | image: (_tweet.entities.media && _tweet.entities.media[0].media_url_https) || null 54 | }; 55 | 56 | this.sendTweet(formattedTweet); 57 | } 58 | 59 | sendTweet({ text, url, name, avatar, image }) { 60 | const embed = new MessageEmbed() 61 | .setDescription(`\n ${text}`) 62 | .setColor(1942002) 63 | .setThumbnail(avatar) 64 | .setAuthor(name) 65 | .setImage(image); 66 | 67 | this.client.channels.get(tweetChannel).send(url, { embed }) 68 | .catch((err) => this.client.emit('wtf', err)); 69 | } 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /extendables/fetchImage.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Extendable } = require('klasa'); 3 | const { DMChannel, TextChannel } = require('discord.js'); 4 | 5 | module.exports = class extends Extendable { 6 | 7 | constructor(...args) { 8 | super(...args, { appliesTo: [DMChannel, TextChannel] }); 9 | } 10 | 11 | async fetchImage() { 12 | const messageBank = await this.messages.fetch({ limit: 20 }); 13 | 14 | for (const message of messageBank.values()) { 15 | const fetchedAttachment = message.attachments.first(); 16 | if (fetchedAttachment && fetchedAttachment.height) return fetchedAttachment; 17 | } 18 | 19 | throw `Couldn't find an image.`; 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /extendables/message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Extendable } = require('klasa'); 3 | const { Message } = require('discord.js'); 4 | 5 | module.exports = class extends Extendable { 6 | 7 | constructor(...args) { 8 | super(...args, { appliesTo: [Message] }); 9 | } 10 | 11 | async ask(content, options) { 12 | const message = await this.sendMessage(content, options); 13 | if (this.channel.permissionsFor(this.guild.me).has('ADD_REACTIONS')) return awaitReaction(this, message); 14 | return awaitMessage(this); 15 | } 16 | 17 | async awaitReply(question, time = 60000, embed) { 18 | await (embed ? this.send(question, { embed }) : this.send(question)); 19 | return this.channel.awaitMessages(message => message.author.id === this.author.id, 20 | { max: 1, time, errors: ['time'] }) 21 | .then(messages => messages.first().content) 22 | .catch(() => false); 23 | } 24 | 25 | async unreact(emojiID) { 26 | const reaction = this.reactions.get(emojiID); 27 | return reaction ? reaction.users.remove(this.client.user) : null; 28 | } 29 | 30 | }; 31 | 32 | const awaitReaction = async (msg, message) => { 33 | await message.react('🇾'); 34 | await message.react('🇳'); 35 | const data = await message.awaitReactions(reaction => reaction.users.has(msg.author.id), { time: 20000, max: 1 }); 36 | if (data.firstKey() === '🇾') return true; 37 | throw null; 38 | }; 39 | 40 | const awaitMessage = async (message) => { 41 | const messages = await message.channel.awaitMessages(mes => mes.author === message.author, { time: 20000, max: 1 }); 42 | if (messages.size === 0) throw null; 43 | const responseMessage = await messages.first(); 44 | if (responseMessage.content.toLowerCase() === 'yes') return true; 45 | throw null; 46 | }; 47 | -------------------------------------------------------------------------------- /extendables/sendLoading.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Extendable } = require('klasa'); 3 | const { Message } = require('discord.js'); 4 | 5 | module.exports = class extends Extendable { 6 | 7 | constructor(...args) { 8 | super(...args, { appliesTo: [Message] }); 9 | } 10 | 11 | /** 12 | * @param {Function} cb The callback function to call in the middle 13 | * @param {Object} [options] Extra options 14 | * @param {string} [options.loadingText='Just a moment.'] Text to send before the callback 15 | * @returns {Promise} Resolves to the return of cb 16 | */ 17 | async sendLoading(cb, { loadingText = 'Just a moment.' } = {}) { 18 | const loadingMsg = await this.send(loadingText); 19 | const oldContent = loadingMsg.content; 20 | // eslint-disable-next-line callback-return 21 | const response = await cb(loadingMsg); 22 | // If the message was edited in cb, we don't wanna delete it 23 | if (!(response && response.id === loadingMsg.id) && oldContent === loadingMsg.content) await loadingMsg.delete(); 24 | return response; 25 | } 26 | 27 | /** 28 | * @param {(KlasaMessage|TextChannel)} channel The channel you intend to post in 29 | * @param {Function} cb The callback function to call in the middle 30 | * @param {Object} [options] Extra options 31 | * @param {string} [options.loadingText='Just a moment.'] Text to send to this.channel before the callback 32 | * @param {string} [options.doneText='Sent the image 👌'] Text to send to this.channel after the callback 33 | * @returns {Promise<[KlasaMessage, KlasaMessage]>} Resolves to a confirmation message in this.channel and the return of cb 34 | */ 35 | async sendLoadingFor(channel, cb, { 36 | loadingText = 'Just a moment.', 37 | doneText = 'Sent the image 👌' 38 | } = {}) { 39 | await this.send(loadingText); 40 | // eslint-disable-next-line callback-return 41 | const response = await cb(channel); 42 | return [await this.send(doneText), response]; 43 | } 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /finalizers/deleteCommand.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Finalizer } = require('klasa'); 3 | 4 | // Add to your schema definition: 5 | // KlasaClient.defaultGuildSchema.add('deleteCommand', 'boolean', { default: false }); 6 | 7 | module.exports = class extends Finalizer { 8 | 9 | async run(msg) { 10 | if (msg.guild && msg.guild.settings.deleteCommand && msg.deletable) await msg.delete(); 11 | } 12 | 13 | }; 14 | -------------------------------------------------------------------------------- /inhibitors/requiredProviders.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Inhibitor } = require('klasa'); 3 | 4 | module.exports = class extends Inhibitor { 5 | 6 | async run(msg, cmd) { 7 | if (!cmd.requiredProviders || !cmd.requiredProviders.length) return false; 8 | const providers = cmd.requiredProviders.filter(provider => !this.client.providers.has(provider)); 9 | if (!providers.length) throw `The client is missing the **${providers.join(', ')}** provider${providers.length > 1 ? 's' : ''} and cannot run.`; 10 | return false; 11 | } 12 | 13 | }; 14 | -------------------------------------------------------------------------------- /languages/ro-RO.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Language, util } = require('klasa'); 3 | 4 | module.exports = class extends Language { 5 | 6 | constructor(...args) { 7 | super(...args); 8 | this.language = { 9 | DEFAULT: (key) => `${key} nu a fost încă localizată în ro-RO.`, 10 | DEFAULT_LANGUAGE: 'Limba implicită', 11 | SETTING_GATEWAY_EXPECTS_GUILD: 'Paramentrul așteaptă un obiect de tip Guild sau un Guild.', 12 | SETTING_GATEWAY_VALUE_FOR_KEY_NOEXT: (data, key) => `Valorarea ${data} pentru cheia ${key} nu există.`, 13 | SETTING_GATEWAY_VALUE_FOR_KEY_ALREXT: (data, key) => `Valorarea ${data} pentru cheia ${key} deja există.`, 14 | SETTING_GATEWAY_SPECIFY_VALUE: 'Trebuie să specifici valoarea adăugată sau un filtru.', 15 | SETTING_GATEWAY_KEY_NOT_ARRAY: (key) => `Cheia ${key} nu este un Array.`, 16 | SETTING_GATEWAY_KEY_NOEXT: (key) => `Cheia ${key} nu există în schema de date curentă.`, 17 | SETTING_GATEWAY_INVALID_TYPE: 'Parametrul tip trebuie să fie add sau remove.', 18 | RESOLVER_INVALID_PIECE: (name, piece) => `${name} trebuie să fie un nume de ${piece} valid.`, 19 | RESOLVER_INVALID_MESSAGE: (name) => `${name} trebuie să fie un id de mesaj valid.`, 20 | RESOLVER_INVALID_USER: (name) => `${name} trebuie să fie un id de utilizator valid sau o mențiune.`, 21 | RESOLVER_INVALID_MEMBER: (name) => `${name} trebuie să fie un id de utilizator valid sau o mențiune.`, 22 | RESOLVER_INVALID_CHANNEL: (name) => `${name} trebuie să fie un id sau tag de canal valid.`, 23 | RESOLVER_INVALID_GUILD: (name) => `${name} trebuie să fie un id de guild valid.`, 24 | RESOLVER_INVALID_ROLE: (name) => `${name} trebuie să fie un id sau o mențiune de rol validă.`, 25 | RESOLVER_INVALID_LITERAL: (name) => `Opțiunea ta nu se potrivește cu singura posibilitate: ${name}`, 26 | RESOLVER_INVALID_BOOL: (name) => `${name} trebuie să fie true sau false.`, 27 | RESOLVER_INVALID_INT: (name) => `${name} trebuie să fie un integer.`, 28 | RESOLVER_INVALID_FLOAT: (name) => `${name} trebuie să fie un număr valid.`, 29 | RESOLVER_INVALID_URL: (name) => `${name} trebuie să fie un url valid.`, 30 | RESOLVER_STRING_SUFFIX: ' caractere', 31 | RESOLVER_MINMAX_EXACTLY: (name, min, suffix) => `${name} trebuie să fie exact ${min}${suffix}.`, 32 | RESOLVER_MINMAX_BOTH: (name, min, max, suffix) => `${name} trebuie să fie între ${min} și ${max}${suffix}.`, 33 | RESOLVER_MINMAX_MIN: (name, min, suffix) => `${name} trebuie să fie mai mare de ${min}${suffix}.`, 34 | RESOLVER_MINMAX_MAX: (name, max, suffix) => `${name} trebuie să fie mai mic de ${max}${suffix}.`, 35 | COMMANDMESSAGE_MISSING: 'Lipsește unul sau mai multe argumente necesare după sfârșitul inputului.', 36 | COMMANDMESSAGE_MISSING_REQUIRED: (name) => `${name} este un argument necesar.`, 37 | COMMANDMESSAGE_MISSING_OPTIONALS: (possibles) => `Lipsește o opțiune necesară: (${possibles})`, 38 | COMMANDMESSAGE_NOMATCH: (possibles) => `Opțiunea ta nu se potrivește cu una din posibilități: (${possibles})`, 39 | MONITOR_COMMAND_HANDLER_REPROMPT: (tag, error, time) => `${tag} | **${error}** | Ai **${time}** să raspunzi cu un argument valid. Scrie **"ABORT"** sa anulezi execuția.`, 40 | MONITOR_COMMAND_HANDLER_ABORTED: 'Anulat', 41 | INHIBITOR_COOLDOWN: (remaining) => `Ai folosit această comandă recent. Poți folosi comanda în ${remaining} de secunde.`, 42 | INHIBITOR_DISABLED: 'Această comandă este la moment oprită', 43 | INHIBITOR_MISSING_BOT_PERMS: (missing) => `Permisinuni insuficiente, lipsește: **${missing}**`, 44 | INHIBITOR_PERMISSIONS: 'Nu ai permisiunea să folosești această comandă', 45 | INHIBITOR_REQUIRED_SETTINGS: (settings) => `Guildul lipsește set${settings.length > 1 ? 'ările' : 'area'} **${settings.join(', ')}** și nu poate rula.`, 46 | INHIBITOR_RUNIN: (types) => `Această comandă este valabilă doar in canalele de tipul ${types}`, 47 | INHIBITOR_RUNIN_NONE: (name) => `Comanda ${name} nu este configurată să ruleze în orice canal.`, 48 | COMMAND_UNLOAD: (type, name) => `✅ Descărcat ${type}: ${name}`, 49 | COMMAND_TRANSFER_ERROR: '❌ Acest fișier a fost transferat deja sau nu este existent.', 50 | COMMAND_TRANSFER_SUCCESS: (type, name) => `✅ Transferat cu succes ${type}: ${name}`, 51 | COMMAND_TRANSFER_FAILED: (type, name) => `Transferul ${type}: ${name} la Client a eșuat. Verificați consola.`, 52 | COMMAND_RELOAD: (type, name) => `✅ Reîncărcat ${type}: ${name}`, 53 | COMMAND_RELOAD_ALL: (type) => `✅ Reîncărcat toate ${type}.`, 54 | COMMAND_REBOOT: 'Se restartează...', 55 | COMMAND_PING: 'Ping?', 56 | COMMAND_PINGPONG: (diff, ping) => `Pong! (Pingul dus-întors a durat: ${diff}ms. Bătaia de inimă: ${ping}ms.)`, 57 | COMMAND_INVITE_SELFBOT: 'De ce ți-ar trebui un link de invite pentru un selfbot...', 58 | COMMAND_INVITE: (client) => [ 59 | `Pentru a adăuga ${client.user.username} în guildul tău de Discord:`, 60 | `<${client.invite}>`, 61 | util.codeBlock('', [ 62 | 'Linkul deasupra este generat cu permisiunile minime necesare pentru a folosit toate comenzile existente.', 63 | 'Eu știu ca nu toate permisiunile sunt pentru orice server așa că nu vă fie frică să scoateți unele permisiuni.', 64 | 'Dacă încerci să folosești o comandă care necesită mai multe permisiuni botul te va notifica.' 65 | ].join(' ')), 66 | 'Generați un raport pe dacă găsiți orice bug.' 67 | ], 68 | COMMAND_INFO: [ 69 | "Klasa este un framework 'plug-and-play' construit deasupra librăriei Discord.js.", 70 | 'Majoritatea codului este modularizat, ceea ce permite dezvoltatorilor să editeze Klasa după necesitățile lor.', 71 | '', 72 | 'Unele din caracteristici Klasa sunt:', 73 | '• 🐇💨 Lansare rapidă cu ajutorul la ES2017 (`async`/`await`)', 74 | '• 🎚🎛 Setări pentru fiecare guild în parte care pot fi extinse în propriul tău cod', 75 | '• 💬 Un sistem de comenzi care poate fi customizat, cu rezolvare a argumentelor automată, și cu abilitatea de încărcare / reîncărcare a comenzilor.', 76 | '• 👀 "Monitoare", care pot verifica orice mesaj creat sau editat, și care pot acționa asupra lor (filtru de înjurături, protecție împotriva spam-ului, etc.).', 77 | '• ⛔ "Inhibitoare", care pot preveni rularea comenzilor prin parametrii setați (pentru permisiuni, lista neagră, etc.).', 78 | '• 🗄 "Providere", care simplifică folosirea oricărui database dorești.', 79 | '• ✅ "Finalizere", care rulează după ce o comandă a avut succes (pentru logare, colecționare de statistici, curățare de răspunsuri, etc.).', 80 | '• ➕ "Extendabile", care adaugă pasiv metode, getteri sau setteri, sau chiar proprietăți statice către clase care există deja in Discord.js sau Klasa.', 81 | '• 🌐 "Limbi", care permit localizarea răspunsurilor botului tău.', 82 | '• ⏲ "Taskuri", care pot fi programate să ruleze în viitor, opțional repetându-se.', 83 | '', 84 | 'Noi aspirăm ca acest framework să fie 100% customizabil încât să poată satisface orice audiență. Noi facem updateuri des și bugfixuri când este posibil.', 85 | 'Dacă ești interesat în noi, vizitează-ne la https://klasa.js.org' 86 | ], 87 | COMMAND_HELP_DM: '📥 | Comenzile au fost trimise în DMurile tale.', 88 | COMMAND_HELP_NODM: '❌ | Ai DMurile dezactivate, nu am să-ți trimit comenzile în DM.', 89 | COMMAND_ENABLE: (type, name) => `+ Activat cu succes ${type}: ${name}`, 90 | COMMAND_DISABLE: (type, name) => `+ Dezactivat cu succes ${type}: ${name}`, 91 | COMMAND_DISABLE_WARN: 'Probabil nu ai vrea să dezactivezi aceasta, deoarece nu vei putea să rulezi orice comandă să o activezi din nou', 92 | COMMAND_CONF_NOKEY: 'Trebuie să introduci o cheie', 93 | COMMAND_CONF_NOVALUE: 'Trebuie să introduci o valoare', 94 | COMMAND_CONF_ADDED: (value, key) => `Am adăugat cu succes valoarea \`${value}\` la cheia: **${key}**`, 95 | COMMAND_CONF_UPDATED: (key, response) => `Am updatat cu succes cheia **${key}**: \`${response}\``, 96 | COMMAND_CONF_KEY_NOT_ARRAY: 'Această cheie nu este de tip array. Folosește acțiunea \'reset\'.', 97 | COMMAND_CONF_REMOVE: (value, key) => `Am șters cu succes valoarea \`${value}\` la cheia: **${key}**`, 98 | COMMAND_CONF_GET_NOEXT: (key) => `Cheia **${key}** nu există.`, 99 | COMMAND_CONF_GET: (key, value) => `Valoarea pentru cheia **${key}** este: \`${value}\``, 100 | COMMAND_CONF_RESET: (key, response) => `Cheia **${key}** a fost resetată la: \`${response}\`` 101 | }; 102 | } 103 | 104 | }; 105 | -------------------------------------------------------------------------------- /languages/tr-TR.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Language, util } = require('klasa'); 3 | 4 | module.exports = class extends Language { 5 | 6 | constructor(...args) { 7 | super(...args); 8 | this.language = { 9 | DEFAULT: (key) => `${key} henüz tr-TR için çevirilmemiş.`, 10 | DEFAULT_LANGUAGE: 'Varsayılan Dil', 11 | SETTING_GATEWAY_EXPECTS_GUILD: 'Parametre için bir Guild veya Guild Objesi vermeniz gerekiyor.', 12 | SETTING_GATEWAY_VALUE_FOR_KEY_NOEXT: (data, key) => `${data} için ${key} anahtarı yok..`, 13 | SETTING_GATEWAY_VALUE_FOR_KEY_ALREXT: (data, key) => `${data} zaten ${key} içinde var.`, 14 | SETTING_GATEWAY_SPECIFY_VALUE: 'Filtre veya değer vermeniz gerekiyor.', 15 | SETTING_GATEWAY_KEY_NOT_ARRAY: (key) => `${key} Array değil.`, 16 | SETTING_GATEWAY_KEY_NOEXT: (key) => `${key} veri şemasında bulunamadı.`, 17 | SETTING_GATEWAY_INVALID_TYPE: 'parametrenin add veya remove olması gerekiyor.', 18 | RESOLVER_INVALID_PIECE: (name, piece) => `${name} herhangi bir ${piece} olması gerekiyor.`, 19 | RESOLVER_INVALID_MESSAGE: (name) => `${name} geçerli bir mesaj idsi olması gerekiyor`, 20 | RESOLVER_INVALID_USER: (name) => `${name} geçerli bir mention veya kullanıcı idsi olması gerekiyor.`, 21 | RESOLVER_INVALID_MEMBER: (name) => `${name} geçerli bir mention veya kullanıcı idsi olması gerekiyor.`, 22 | RESOLVER_INVALID_CHANNEL: (name) => `${name} geçerli bir kanal etiketi veya kanal idsi olması gerekiyor.`, 23 | RESOLVER_INVALID_GUILD: (name) => `${name} geçerli bir guild idsi olması gerekiyor`, 24 | RESOLVER_INVALID_ROLE: (name) => `${name} geçerli bir rol idsi veya mentionu olması gerekiyor.`, 25 | RESOLVER_INVALID_LITERAL: (name) => `${name}: Olması beklenen bir değer değil`, 26 | RESOLVER_INVALID_BOOL: (name) => `${name} true yada false olması gerekiyor`, 27 | RESOLVER_INVALID_INT: (name) => `${name} bir tamsayı olması gerekiyor.`, 28 | RESOLVER_INVALID_FLOAT: (name) => `${name} bir sayı olması gerekiyor`, 29 | RESOLVER_INVALID_REGEX_MATCH: (name, pattern) => `${name} \`${pattern}\` regex paterni ile eşleşmiyor.`, 30 | RESOLVER_INVALID_URL: (name) => `${name} geçerli bir url olması gerekiyor.`, 31 | RESOLVER_STRING_SUFFIX: ' karakterler', 32 | RESOLVER_MINMAX_EXACTLY: (name, min, suffix) => `${name} tamamen ${min}${suffix} şeklinde olması gerekiyor.`, 33 | RESOLVER_MINMAX_BOTH: (name, min, max, suffix) => `${name}: ${min} ve ${max}${suffix} arasında olması gerekiyor.`, 34 | RESOLVER_MINMAX_MIN: (name, min, suffix) => `${name}: ${min}${suffix}'den-'dan büyük olması gerekiyor.`, 35 | RESOLVER_MINMAX_MAX: (name, max, suffix) => `${name}: ${max}${suffix}'den-'dan küçük olması gerekiyor.`, 36 | COMMANDMESSAGE_MISSING: 'Bir veya birden fazla argüman eksik.', 37 | COMMANDMESSAGE_MISSING_REQUIRED: (name) => `${name} zorunlu bir argüman.`, 38 | COMMANDMESSAGE_MISSING_OPTIONALS: (possibles) => `Zorunlu argümanlardan birisi eksik: (${possibles})`, 39 | COMMANDMESSAGE_NOMATCH: (possibles) => `Seçiminiz olası değerlerden biri değil: (${possibles})`, 40 | MONITOR_COMMAND_HANDLER_REPROMPT: (tag, error, time) => `${tag} | **${error}** | Mesaja geçerli bir argüman ile yanıt vermek için **${time}** saniyeniz var . iptal etmek için **"ABORT"** yazın.`, // eslint-disable-line max-len 41 | MONITOR_COMMAND_HANDLER_ABORTED: 'İptal edildi.', 42 | INHIBITOR_COOLDOWN: (remaining) => `Komutu daha yeni kullanıdınız. ${remaining} saniye içerisinde tekrar deneyin.`, 43 | INHIBITOR_DISABLED: 'Bu komut şu anlık kapatılmış.', 44 | INHIBITOR_MISSING_BOT_PERMS: (missing) => `Gerekli **${missing}** yetkisine sahip değilim.`, 45 | INHIBITOR_PERMISSIONS: 'Bu komutu kullanmaya yetkin yok.', 46 | INHIBITOR_REQUIRED_SETTINGS: (settings) => `Bu sunucu **${settings.join(', ')}** ayar${settings.length > 1 ? 'larına' : ''} sahip değil. Bu yüzden bu komut çalıştırılamaz.`, 47 | INHIBITOR_RUNIN: (types) => `Bu komut sadece ${types} kanallarında kullanılabilir.`, 48 | INHIBITOR_RUNIN_NONE: (name) => `${name} komut herhangi bir kanalda kullanılmak için ayarlanmamış.`, 49 | COMMAND_UNLOAD: (type, name) => `✅ Geçici olarak hafızadan silindi ${type}: ${name}`, 50 | COMMAND_TRANSFER_ERROR: '❌ Bu dosya çoktan transfer edildi yada hiç varolmadı.', 51 | COMMAND_TRANSFER_SUCCESS: (type, name) => `✅ Başarıyla transfer edildi ${type}: ${name}`, 52 | COMMAND_TRANSFER_FAILED: (type, name) => `${type}: ${name} transferi başarısız. Lütfen Konsola bakın.`, 53 | COMMAND_RELOAD: (type, name) => `✅ Yeniden yüklendi ${type}: ${name}`, 54 | COMMAND_RELOAD_ALL: (type) => `✅ ${type} tümü yeniden yüklendi.`, 55 | COMMAND_REBOOT: 'Yeniden başlatılıyor...', 56 | COMMAND_PING: 'Ping?', 57 | COMMAND_PINGPONG: (diff, ping) => `Pong! (Dolaşım: ${diff}ms. Kaynak: ${ping}ms.)`, 58 | COMMAND_INVITE_SELFBOT: 'Neden selfbot için invite linkine ihtiyaç duyasın ki!?!?', 59 | COMMAND_INVITE: (client) => [ 60 | `${client.user.username} guildinize eklemek için:`, 61 | `<${client.invite}>`, 62 | util.codeBlock('', [ 63 | 'Yukarıdaki bağlantı tüm komutları kullanmak için istediği minimum yetkiyle oluşturulmuştur.', 64 | 'Tüm yetkilerin her sunucu için doğru olmadığını biliyorum, bu yüzden yetkilerden herhangi birini almaktan sakınma.', 65 | 'Eğer botun herhangi bir komutu gerçekleştirmeye yetkisi yoksa sizi bilgilendirecektir.' 66 | ].join(' ')), 67 | 'Eğer herhangi bir hata ile karşılaşırsanız . adresine bildirin.' 68 | ], 69 | COMMAND_INFO: [ 70 | "Klasa bir 'tak-çalıştır' kütüphanesidir ve Discord.js kitaplığı ile yazılmıştır.", 71 | 'Kodun çoğu modüler buda geliştiricilere Klasa\'yı istedikleri gibi değiştirmelerine izin verir.', 72 | '', 73 | 'Klasa\'nın bazı özellikleri:', 74 | '• 🐇💨 ES2017 desteği ile hızlı yüklenme zamanları (`async`/`await`)', 75 | '• 🎚🎛 Sunucu başına ayarlar özelliği ve bu özellik yazdığınız kodla genişletilebilir.', 76 | '• 💬 Değiştirilebilir komut sistemi otomatik kullanım belirleme özelliği ile yeniden yüklemesi kolay ve indirilebilir modüller.', 77 | '• 👀 Mesajlara tıpkı bir event gibi (Küfür filtresi, Spam koruması, vb.) bakabilen "Monitörler" ', 78 | '• ⛔ Komutları belirlenen parametrelere göre kullanımlarını engelleyen (Yetkiler, Karaliste, vb.) "İnhibitörler"', 79 | '• 🗄 Dış veritabanlarına bağlanmak için kullanılabilen "Sağlayıcılar"', 80 | '• ✅ Başarılı bir komuttan sonra çalışabilen "Sonlayıcılar"', 81 | '• ➕ Pasif çalışan "Esneticiler". Discord.js sınfılarından özellik veya method eklemek için kullanılır.', 82 | '• 🌐 Botunuzu yerelleştirmek için kullanabileceğiniz "Diller".', 83 | '• ⏲ Zamanlayabileceğiniz ve istediğiniz zaman tekrar ettirebileceğiniz "Görevler".', 84 | '', 85 | 'Herkese hitap eden ve %100 düzenlenebilir bir framework yaptığımızı umuyoruz. Sıklıkla güncelliyoruz ve bugları kapatmaya çalışıyoruz.', 86 | 'Eğer ilgilendiyseniz bize https://klasa.js.org adresinden ulaşabilirsiniz' 87 | ], 88 | COMMAND_HELP_DM: '📥 | Kullanabileceğiniz komutların listesi DM olarak gönderildi.', 89 | COMMAND_HELP_NODM: '❌ | DM alımınız bu sunucu için kapalı, bu yüzden DM olarak gönderemedim.', 90 | COMMAND_ENABLE: (type, name) => `+ ${type}: ${name} başarıyla aktifleştirildi.`, 91 | COMMAND_DISABLE: (type, name) => `+ ${type}: ${name} başarıyla kapatıldı`, 92 | COMMAND_DISABLE_WARN: 'Bunu kapattığın hiç bir komutu açamayacağın için kapatmak istemezsin.', 93 | COMMAND_CONF_NOKEY: 'Bir anahtar sağlaman gerekiyor.', 94 | COMMAND_CONF_NOVALUE: 'Bir değer sağlaman gerekiyor.', 95 | COMMAND_CONF_ADDED: (value, key) => `\`${value}\` başarıyla **${key}** anahtarına eklendi.`, 96 | COMMAND_CONF_UPDATED: (key, response) => `**${key}** başarıyla \`${response}\` ile güncellendi`, 97 | COMMAND_CONF_KEY_NOT_ARRAY: 'Bu anahtar array tipinde değil. \'reset\' kullanın.', 98 | COMMAND_CONF_REMOVE: (value, key) => `\`${value}\` başarıyla **${key}** anahtarından silindi.`, 99 | COMMAND_CONF_GET_NOEXT: (key) => `**${key}** anahtarı hiç varolmamış gibi.`, 100 | COMMAND_CONF_GET: (key, value) => `**${key}** anahtarı için şu anki değer: \`${value}\``, 101 | COMMAND_CONF_RESET: (key, response) => `**${key}** anahtarı \`${response}\` şeklinde sıfırlandı.` 102 | }; 103 | } 104 | 105 | }; 106 | -------------------------------------------------------------------------------- /monitors/everyone.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Monitor } = require('klasa'); 3 | const { Permissions: { FLAGS } } = require('discord.js'); 4 | 5 | // Add to your schema definition: 6 | // KlasaClient.defaultGuildSchema.add('roles', schema => schema 7 | // .add('everyone', 'role')); 8 | 9 | module.exports = class extends Monitor { 10 | 11 | constructor(...args) { 12 | super(...args, { ignoreOthers: false }); 13 | } 14 | 15 | async run(msg) { 16 | if (!msg.member || 17 | !msg.guild.settings.roles.everyone || 18 | !msg.mentions.roles.size || 19 | msg.guild.me.roles.highest.position <= msg.member.roles.highest.position || 20 | !msg.guild.me.permissions.has(FLAGS.MANAGE_ROLES) 21 | ) return; 22 | 23 | const everyone = msg.guild.roles.get(msg.guild.settings.roles.everyone); 24 | if (!everyone) { 25 | await msg.guild.settings.reset('roles.everyone'); 26 | return; 27 | } 28 | if (!msg.mentions.roles.has(everyone.id)) return; 29 | 30 | await msg.member.roles.add(everyone); 31 | await msg.reply([ 32 | 'Welcome to the everyone role.', 33 | 'Whenever someone mentions it, they get the role and everyone who has it is mentioned.', 34 | '', 35 | 'By the way leaving and rejoining to remove the role from yourself **WILL** result in a stricter punishment.' 36 | ].join('\n')); 37 | } 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /monitors/filter.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Monitor } = require('klasa'); 3 | const { remove } = require('confusables'); 4 | 5 | /* 6 | * 7 | * Requires "filteredWords" guild setting. 8 | * Client.defaultGuildSchema.add('filteredWords', 'string', { default: [], array: true }) 9 | * 10 | * This is a simple moderation filter setup. You may 11 | * also want to: mute the person, log it to a logs 12 | * channel in the guild, message the person telling 13 | * them they said a prohibited word, and perform different 14 | * checks and actions based on your requirements or needs. 15 | * You may also exempt bots, staff, or certain channels 16 | * from the filter. 17 | * 18 | * For the sake of simplicity, all these extra actions 19 | * aren't done, as each bot will almost definitely want 20 | * to do things differently. 21 | */ 22 | 23 | module.exports = class extends Monitor { 24 | 25 | constructor(...args) { 26 | super(...args, { 27 | ignoreOthers: false, 28 | ignoreBots: false, 29 | ignoreEdits: false 30 | }); 31 | } 32 | 33 | async run(msg) { 34 | if (!msg.guild) return; 35 | 36 | const { content } = msg; 37 | if (!content || !content.length) return; 38 | const cleanContent = this.sanitize(content); 39 | 40 | const filteredWords = msg.guild.settings.get('filteredWords'); 41 | if (!filteredWords || !filteredWords.length) return; 42 | 43 | // If they said a filtered word, this variable will be equal to that word. 44 | const hitTheFilter = filteredWords.find(word => cleanContent.includes(this.sanitize(word))); 45 | if (!hitTheFilter) return; 46 | 47 | msg.delete(); 48 | } 49 | 50 | sanitize(str) { 51 | return remove(str).toUpperCase(); 52 | } 53 | 54 | }; 55 | -------------------------------------------------------------------------------- /monitors/invitedetection.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Monitor } = require('klasa'); 3 | 4 | // Add to your schema definition: 5 | // KlasaClient.defaultGuildSchema.add('antiinvite', 'boolean', { default: false }); 6 | 7 | module.exports = class extends Monitor { 8 | 9 | constructor(...args) { 10 | super(...args, { 11 | name: 'invitedetection', 12 | enabled: true, 13 | ignoreSelf: true, 14 | ignoreOthers: false 15 | }); 16 | } 17 | 18 | async run(msg) { 19 | if (!msg.guild || !msg.guild.settings.antiinvite) return null; 20 | if (await msg.hasAtLeastPermissionLevel(6)) return null; 21 | if (!/(https?:\/\/)?(www\.)?(discord\.(gg|li|me|io)|discordapp\.com\/invite)\/.+/.test(msg.content)) return null; 22 | return msg.delete() 23 | .catch(err => this.client.emit('log', err, 'error')); 24 | } 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /monitors/nomention.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Monitor } = require('klasa'); 3 | 4 | module.exports = class extends Monitor { 5 | 6 | constructor(...args) { 7 | super(...args, { 8 | name: 'nomention', 9 | enabled: true, 10 | ignoreSelf: true 11 | }); 12 | } 13 | 14 | run(msg) { 15 | if (msg.channel.type !== 'text') return; 16 | const user = `${msg.author.tag} (${msg.author.id})`; 17 | const channel = `#${msg.channel.name} (${msg.channel.id}) from ${msg.guild.name}`; 18 | 19 | if (msg.mentions.everyone) this.client.emit('log', `${user} mentioned everyone in ${channel}`); 20 | else if (msg.mentions.users.has(this.client.user.id)) this.client.emit('log', `${user} mentioned you in ${channel}`); 21 | } 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klasa-pieces", 3 | "description": "This repository contains the various *Pieces* submitted by users and collaborators.", 4 | "scripts": { 5 | "test:lint": "eslint .", 6 | "lint": "npm run test:lint -- --fix" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/dirigeants/klasa-pieces.git" 11 | }, 12 | "license": "MIT", 13 | "homepage": "https://github.com/dirigeants/klasa-pieces#readme", 14 | "devDependencies": { 15 | "eslint": "^4.18.2", 16 | "eslint-config-klasa": "github:dirigeants/klasa-lint" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /providers/btf.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Provider, util } = require('klasa'); 3 | const { serialize, deserialize } = require('binarytf'); 4 | const { resolve } = require('path'); 5 | const fsn = require('fs-nextra'); 6 | 7 | module.exports = class extends Provider { 8 | 9 | constructor(...args) { 10 | super(...args); 11 | 12 | const baseDirectory = resolve(this.client.userBaseDirectory, 'bwd', 'provider', 'btf'); 13 | const defaults = util.mergeDefault({ baseDirectory }, this.client.options.providers.btf); 14 | 15 | this.baseDirectory = defaults.baseDirectory; 16 | } 17 | 18 | async init() { 19 | await fsn.ensureDir(this.baseDirectory).catch(err => this.client.emit('error', err)); 20 | } 21 | 22 | /* Table methods */ 23 | hasTable(table) { 24 | return fsn.pathExists(resolve(this.baseDirectory, table)); 25 | } 26 | 27 | createTable(table) { 28 | return fsn.mkdir(resolve(this.baseDirectory, table)); 29 | } 30 | 31 | deleteTable(table) { 32 | return this.hasTable(table) 33 | .then(exists => exists ? fsn.emptyDir(resolve(this.baseDirectory, table)) 34 | .then(() => fsn.remove(resolve(this.baseDirectory, table))) : null); 35 | } 36 | 37 | /* Document methods */ 38 | 39 | async getAll(table, entries) { 40 | if (!Array.isArray(entries) || !entries.length) entries = await this.getKeys(table); 41 | if (entries.length < 5000) { 42 | return Promise.all(entries.map(this.get.bind(this, table))); 43 | } 44 | 45 | const chunks = util.chunk(entries, 5000); 46 | const output = []; 47 | for (const chunk of chunks) output.push(...await Promise.all(chunk.map(this.get.bind(this, table)))); 48 | return output; 49 | } 50 | 51 | async getKeys(table) { 52 | const dir = resolve(this.baseDirectory, table); 53 | const filenames = await fsn.readdir(dir); 54 | const files = []; 55 | for (const filename of filenames) { 56 | if (filename.endsWith('.btf')) files.push(filename.slice(0, filename.length - 4)); 57 | } 58 | return files; 59 | } 60 | 61 | get(table, id) { 62 | return fsn.readFile(resolve(this.baseDirectory, table, `${id}.btf`)) 63 | .then(deserialize) 64 | .catch(() => null); 65 | } 66 | 67 | has(table, id) { 68 | return fsn.pathExists(resolve(this.baseDirectory, table, `${id}.btf`)); 69 | } 70 | 71 | getRandom(table) { 72 | return this.getKeys(table).then(data => data.length ? this.get(table, data[Math.floor(Math.random() * data.length)]) : null); 73 | } 74 | 75 | create(table, id, data = {}) { 76 | return fsn.outputFileAtomic(resolve(this.baseDirectory, table, `${id}.btf`), serialize({ id, ...this.parseUpdateInput(data) })); 77 | } 78 | 79 | async update(table, id, data) { 80 | const existent = await this.get(table, id); 81 | return fsn.outputFileAtomic(resolve(this.baseDirectory, table, `${id}.btf`), serialize(util.mergeObjects(existent || { id }, this.parseUpdateInput(data)))); 82 | } 83 | 84 | replace(table, id, data) { 85 | return fsn.outputFileAtomic(resolve(this.baseDirectory, table, `${id}.btf`), serialize({ id, ...this.parseUpdateInput(data) })); 86 | } 87 | 88 | delete(table, id) { 89 | return fsn.unlink(resolve(this.baseDirectory, table, `${id}.btf`)); 90 | } 91 | 92 | }; 93 | -------------------------------------------------------------------------------- /providers/firestore.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Provider } = require('klasa'); 3 | const firebase = require('firebase-admin'); 4 | 5 | /* 6 | 1. Go to https://firebase.google.com/ 7 | 2. Login/Signup 8 | 3. Go to Console 9 | 4. Create new project 10 | 5. Go to the database section 11 | 6. Select Firestore. NOTE- Don't select Real time database 12 | 7. Then in the LHS of the page, finda settings icon next to Project Overview, select Project settings. 13 | 8. Go to service accounts 14 | 9. Click Generate new private key, which will download a json file. 15 | 10. Copy the databaseURL from the same page. 16 | 11. Import the json, where ever you are initializing the client. 17 | 12. Pass this to the constructor, providers: { default: 'firestore', firestore: { credentials: variable_name_for_json, databaseURL: 'databaseURL from the service account page.'}} 18 | 13. Download the `firebase-admin` module. 19 | */ 20 | 21 | module.exports = class extends Provider { 22 | 23 | constructor(...args) { 24 | super(...args); 25 | this.db = null; 26 | } 27 | async init() { 28 | await firebase.initializeApp({ 29 | credential: firebase.credential.cert(this.client.options.providers.firestore.credentials), 30 | databaseURL: this.client.options.providers.firestore.databaseURL 31 | }); 32 | 33 | this.db = firebase.firestore(); 34 | } 35 | 36 | hasTable(table) { 37 | return this.db.collection(table).get().then(col => Boolean(col.size)); 38 | } 39 | 40 | createTable(table) { 41 | return this.db.collection(table); 42 | } 43 | 44 | getKeys(table) { 45 | return this.db.collection(table).get().then(snaps => snaps.docs.map(snap => snap.id)); 46 | } 47 | 48 | get(table, id) { 49 | return this.db.collection(table).doc(id).get().then(snap => this.packData(snap.data(), snap.id)); 50 | } 51 | 52 | has(table, id) { 53 | return this.db.collection(table).doc(id).get().then(data => data.exists); 54 | } 55 | 56 | create(table, id, doc = {}) { 57 | return this.db.collection(table).doc(id).set(this.parseUpdateInput(doc)); 58 | } 59 | 60 | update(table, id, doc) { 61 | return this.db.collection(table).doc(id).update(this.parseUpdateInput(doc)); 62 | } 63 | 64 | delete(table, id) { 65 | return this.db.collection(table).doc(id).delete(); 66 | } 67 | 68 | replace(...args) { 69 | return this.create(...args); 70 | } 71 | 72 | async getAll(table, filter = []) { 73 | const data = await this.db.collection(table).get() 74 | .then(snaps => snaps.docs.map(snap => this.packData(snap.data(), snap.id))); 75 | 76 | return filter.length ? data.filter(nodes => filter.includes(nodes.id)) : data; 77 | } 78 | 79 | packData(data, id) { 80 | return { 81 | ...data, 82 | id 83 | }; 84 | } 85 | 86 | }; 87 | -------------------------------------------------------------------------------- /providers/mongodb.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Provider, util: { mergeDefault, mergeObjects, isObject } } = require('klasa'); 3 | const { MongoClient: Mongo } = require('mongodb'); 4 | 5 | module.exports = class extends Provider { 6 | 7 | constructor(...args) { 8 | super(...args, { description: 'Allows use of MongoDB functionality throughout Klasa' }); 9 | this.db = null; 10 | } 11 | 12 | async init() { 13 | const connection = mergeDefault({ 14 | host: 'localhost', 15 | port: 27017, 16 | db: 'klasa', 17 | options: {} 18 | }, this.client.options.providers.mongodb); 19 | 20 | // If full connection string is provided, use that, otherwise fall back to individual parameters 21 | const connectionString = this.client.options.providers.mongodb.connectionString || `mongodb://${connection.user}:${connection.password}@${connection.host}:${connection.port}/${connection.db}`; 22 | 23 | const mongoClient = await Mongo.connect( 24 | connectionString, 25 | mergeObjects(connection.options, { useNewUrlParser: true, useUnifiedTopology: true }) 26 | ); 27 | 28 | this.db = mongoClient.db(connection.db); 29 | } 30 | 31 | /* Table methods */ 32 | 33 | get exec() { 34 | return this.db; 35 | } 36 | 37 | hasTable(table) { 38 | return this.db.listCollections().toArray().then(collections => collections.some(col => col.name === table)); 39 | } 40 | 41 | createTable(table) { 42 | return this.db.createCollection(table); 43 | } 44 | 45 | deleteTable(table) { 46 | return this.db.dropCollection(table); 47 | } 48 | 49 | /* Document methods */ 50 | 51 | getAll(table, filter = []) { 52 | if (filter.length) return this.db.collection(table).find({ id: { $in: filter } }, { _id: 0 }).toArray(); 53 | return this.db.collection(table).find({}, { _id: 0 }).toArray(); 54 | } 55 | 56 | getKeys(table) { 57 | return this.db.collection(table).find({}, { id: 1, _id: 0 }).toArray(); 58 | } 59 | 60 | get(table, id) { 61 | return this.db.collection(table).findOne(resolveQuery(id)); 62 | } 63 | 64 | has(table, id) { 65 | return this.get(table, id).then(Boolean); 66 | } 67 | 68 | getRandom(table) { 69 | return this.db.collection(table).aggregate({ $sample: { size: 1 } }); 70 | } 71 | 72 | create(table, id, doc = {}) { 73 | return this.db.collection(table).insertOne(mergeObjects(this.parseUpdateInput(doc), resolveQuery(id))); 74 | } 75 | 76 | delete(table, id) { 77 | return this.db.collection(table).deleteOne(resolveQuery(id)); 78 | } 79 | 80 | update(table, id, doc) { 81 | return this.db.collection(table).updateOne(resolveQuery(id), { $set: isObject(doc) ? flatten(doc) : parseEngineInput(doc) }); 82 | } 83 | 84 | replace(table, id, doc) { 85 | return this.db.collection(table).replaceOne(resolveQuery(id), this.parseUpdateInput(doc)); 86 | } 87 | 88 | }; 89 | 90 | const resolveQuery = query => isObject(query) ? query : { id: query }; 91 | 92 | function flatten(obj, path = '') { 93 | let output = {}; 94 | for (const [key, value] of Object.entries(obj)) { 95 | if (isObject(value)) output = Object.assign(output, flatten(value, path ? `${path}.${key}` : key)); 96 | else output[path ? `${path}.${key}` : key] = value; 97 | } 98 | return output; 99 | } 100 | 101 | function parseEngineInput(updated) { 102 | return Object.assign({}, ...updated.map(entry => ({ [entry.data[0]]: entry.data[1] }))); 103 | } 104 | -------------------------------------------------------------------------------- /providers/mssql.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { SQLProvider, QueryBuilder, Timestamp, Type, util: { mergeDefault } } = require('klasa'); 3 | const mssql = require('mssql'); 4 | 5 | /** 6 | * ##################################### 7 | * # UNTESTED # 8 | * # THIS PROVIDER MAY OR MAY NOT WORK # 9 | * ##################################### 10 | */ 11 | 12 | const TIMEPARSERS = { 13 | DATE: new Timestamp('YYYY-MM-DD'), 14 | DATETIME: new Timestamp('YYYY-MM-DD hh:mm:ss') 15 | }; 16 | 17 | module.exports = class extends SQLProvider { 18 | 19 | constructor(...args) { 20 | super(...args); 21 | this.qb = new QueryBuilder({ 22 | integer: ({ max }) => max >= 2 ** 32 ? 'BIGINT' : 'INTEGER', 23 | float: 'REAL', 24 | date: { type: 'DATETIME', resolver: (input) => TIMEPARSERS.DATETIME.display(input) }, 25 | time: { type: 'DATETIME', resolver: (input) => TIMEPARSERS.DATETIME.display(input) }, 26 | timestamp: { type: 'TIMESTAMP', resolver: (input) => TIMEPARSERS.DATE.display(input) }, 27 | array: type => type, 28 | arrayResolver: (values) => `'${sanitizeString(JSON.stringify(values))}'`, 29 | formatDatatype: (name, datatype, def = null) => `\`${name}\` ${datatype}${def !== null ? ` NOT NULL DEFAULT ${def}` : ''}` 30 | }); 31 | this.pool = null; 32 | } 33 | 34 | async init() { 35 | const connection = mergeDefault({ 36 | host: 'localhost', 37 | db: 'klasa', 38 | user: 'database-user', 39 | password: 'database-password', 40 | options: { encrypt: false } 41 | }, this.client.options.providers.mssql); 42 | this.pool = new mssql.ConnectionPool({ 43 | user: connection.user, 44 | password: connection.password, 45 | server: connection.host, 46 | database: connection.database, 47 | // If you're on Windows Azure, you will need this enabled: 48 | options: connection.options 49 | }); 50 | 51 | await this.pool.connect(); 52 | } 53 | 54 | async shutdown() { 55 | await this.pool.close(); 56 | } 57 | 58 | /** 59 | * @returns {Promise} 60 | */ 61 | getTables() { 62 | return this.run('SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES;'); 63 | } 64 | 65 | /* Table methods */ 66 | 67 | hasTable(table) { 68 | return this.run(`IF ( EXISTS ( 69 | SELECT * 70 | FROM INFORMATION_SCHEMA.TABLES 71 | WHERE TABLE_NAME = @0 72 | ) )`, [table]); 73 | } 74 | 75 | createTable(table, rows) { 76 | if (rows) return this.run(`CREATE TABLE @0 ( ${rows.map(([k, v]) => `${k} ${v}`).join(', ')} );`, [table]); 77 | 78 | const gateway = this.client.gateways[table]; 79 | if (!gateway) throw new Error(`There is no gateway defined with the name ${table} nor an array of rows with datatypes have been given. Expected any of either.`); 80 | 81 | const schemaValues = [...gateway.schema.values(true)]; 82 | return this.run(` 83 | CREATE TABLE @0 ( 84 | id VARCHAR(${gateway.idLength || 18}) PRIMARY KEY NOT NULL UNIQUE${schemaValues.length ? `, ${schemaValues.map(this.qb.parse.bind(this.qb)).join(', ')}` : ''} 85 | )`, [table] 86 | ); 87 | } 88 | 89 | deleteTable(table) { 90 | return this.run('IF OBJECT_ID(@0, \'U\') IS NOT NULL DROP TABLE @0;', [table]); 91 | } 92 | 93 | countRows(table) { 94 | return this.run(`SELECT COUNT(*) FROM @0;`, [table]); 95 | } 96 | 97 | /* Row methods */ 98 | 99 | getAll(table, entries = []) { 100 | if (entries.length) { 101 | return this.run(`SELECT * FROM @0 WHERE id IN (@1);`, [table, `'${entries.join("', '")}'`]) 102 | .then(results => results.map(output => this.parseEntry(table, output))); 103 | } 104 | return this.run(`SELECT * FROM @0;`, [table]) 105 | .then(results => results.map(output => this.parseEntry(table, output))); 106 | } 107 | 108 | getKeys(table) { 109 | return this.run(`SELECT id FROM @0;`, [table]); 110 | } 111 | 112 | get(table, key, value) { 113 | // If a key is given (id), swap it and search by id - value 114 | if (typeof value === 'undefined') { 115 | value = key; 116 | key = 'id'; 117 | } 118 | return this.run('SELECT TOP 1 * FROM @0 WHERE @1 = @2;', [table, key, value]) 119 | .then(result => this.parseEntry(table, result)); 120 | } 121 | 122 | has(table, id) { 123 | return this.run('IF ( EXISTS ( SELECT TOP 1 * FROM @0 WHERE id = @1 ) )', [table, id]); 124 | } 125 | 126 | getRandom(table) { 127 | return this.run('SELECT TOP 1 * FROM @0 ORDER BY NEWID();', [table]) 128 | .then(result => this.parseEntry(table, result)); 129 | } 130 | 131 | create(table, id, data) { 132 | const [keys, values] = this.parseUpdateInput(data, false); 133 | 134 | // Push the id to the inserts. 135 | if (!keys.includes('id')) { 136 | keys.push('id'); 137 | values.push(id); 138 | } 139 | return this.run(`INSERT INTO ${sanitizeKeyName(table)} 140 | (${keys.map(sanitizeKeyName).join(', ')}) 141 | VALUES (${Array.from({ length: keys.length }, (__, i) => `@${i}`).join(', ')});`, values); 142 | } 143 | 144 | update(table, id, data) { 145 | const [keys, values] = this.parseUpdateInput(data, false); 146 | return this.run(` 147 | UPDATE ${sanitizeKeyName(table)} 148 | SET ${keys.map((key, i) => `${sanitizeKeyName(key)} = @${i}`)} 149 | WHERE id = ${sanitizeString(id)};`, values); 150 | } 151 | 152 | replace(...args) { 153 | return this.update(...args); 154 | } 155 | 156 | delete(table, id) { 157 | return this.run(` 158 | DELETE * 159 | FROM @0 160 | WHERE id = @1;`, [table, id]); 161 | } 162 | 163 | addColumn(table, piece) { 164 | return this.run(piece.type !== 'Folder' ? 165 | `ALTER TABLE ${sanitizeKeyName(table)} ADD ${this.qb.parse(piece)};` : 166 | `ALTER TABLE ${sanitizeKeyName(table)} ${[...piece.values(true)].map(subpiece => `ADD ${this.qb.parse(subpiece)}`).join(', ')}`); 167 | } 168 | 169 | removeColumn(table, key) { 170 | if (typeof key === 'string') return this.run(`ALTER TABLE @0 DROP COLUMN @1;`, [table, key]); 171 | if (Array.isArray(key)) return this.run(`ALTER TABLE @0 DROP ${key.map(sanitizeKeyName).join(', ')};`, [table]); 172 | throw new TypeError('Invalid usage of MSSQL#removeColumn. Expected a string or string[].'); 173 | } 174 | 175 | updateColumn(table, piece) { 176 | const [column, ...datatype] = this.qb.parse(piece).split(' '); 177 | return this.run(` 178 | ALTER TABLE @0 179 | ALTER COLUMN @1 @2;`, [table, column, datatype]); 180 | } 181 | 182 | getColumns(table) { 183 | return this.run(` 184 | SELECT Field_Name 185 | FROM INFORMATION_SCHEMA.COLUMNS 186 | WHERE TABLE_NAME = @0; 187 | `, [table]).then(result => result.map(row => row.Field_Name)); 188 | } 189 | 190 | run(sql, inputs, outputs) { 191 | if (inputs.length > 0) { 192 | const request = new mssql.Request(); 193 | for (let i = 0; i < inputs.length; i++) request.input(String(i), inputs[i]); 194 | for (let i = 0; i < outputs.length; i++) request.input(outputs[i]); 195 | return request.query(sql) 196 | .then(result => Promise.resolve(result)) 197 | .catch(error => Promise.reject(error)); 198 | } 199 | return new mssql.Request().query(sql) 200 | .then(result => Promise.resolve(result)) 201 | .catch(error => Promise.reject(error)); 202 | } 203 | 204 | }; 205 | 206 | /** 207 | * @param {string} value The string to sanitize 208 | * @returns {string} 209 | * @private 210 | */ 211 | function sanitizeString(value) { 212 | return `'${value.replace(/'/g, "''")}'`; 213 | } 214 | 215 | /** 216 | * @param {string} value The string to sanitize as a key 217 | * @returns {string} 218 | * @private 219 | */ 220 | function sanitizeKeyName(value) { 221 | if (typeof value !== 'string') throw new TypeError(`%MSSQL.sanitizeString expects a string, got: ${new Type(value)}`); 222 | if (/`/.test(value)) throw new TypeError(`Invalid input (${value}).`); 223 | return value; 224 | } 225 | -------------------------------------------------------------------------------- /providers/nedb.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Provider, util: { mergeObjects, isObject } } = require('klasa'); 3 | const { Collection } = require('discord.js'); 4 | const { resolve } = require('path'); 5 | const fs = require('fs-nextra'); 6 | 7 | const Datastore = require('nedb-core'); 8 | require('tsubaki').promisifyAll(Datastore.prototype); 9 | 10 | module.exports = class extends Provider { 11 | 12 | constructor(...args) { 13 | super(...args); 14 | this.baseDir = resolve(this.client.userBaseDirectory, 'bwd', 'provider', 'nedb'); 15 | this.dataStores = new Collection(); 16 | } 17 | 18 | init() { 19 | return fs.ensureDir(this.baseDir); 20 | } 21 | 22 | /* Table methods */ 23 | 24 | hasTable(table) { 25 | return this.dataStores.has(table); 26 | } 27 | 28 | createTable(table, persistent) { 29 | if (this.dataStores.has(table)) return null; 30 | const db = new Datastore(persistent ? { filename: resolve(this.baseDir, `${table}.db`), autoload: true } : {}); 31 | this.dataStores.set(table, db); 32 | return db; 33 | } 34 | 35 | async deleteTable(table) { 36 | if (this.dataStores.has(table)) { 37 | await this.deleteAll(table); 38 | this.dataStores.delete(table); 39 | return true; 40 | } 41 | return false; 42 | } 43 | 44 | /* Document methods */ 45 | 46 | async getAll(table, filter = []) { 47 | let entries; 48 | if (filter.length) entries = await this.dataStores.get(table).findAsync({ id: { $in: filter } }); 49 | else entries = await this.dataStores.get(table).findAsync({}); 50 | for (const entry of entries) delete entry._id; 51 | return entries; 52 | } 53 | 54 | async get(table, query) { 55 | const data = await this.dataStores.get(table).findOneAsync(resolveQuery(query)); 56 | if (data) { 57 | delete data._id; 58 | return data; 59 | } 60 | 61 | return null; 62 | } 63 | 64 | has(table, query) { 65 | return this.get(table, query).then(Boolean); 66 | } 67 | 68 | create(table, query, doc) { 69 | return this.dataStores.get(table).insertAsync(mergeObjects(this.parseUpdateInput(doc), resolveQuery(query))); 70 | } 71 | 72 | async update(table, query, doc) { 73 | const res = await this.get(table, query); 74 | return this.replace(table, query, mergeObjects(res, this.parseUpdateInput(doc))); 75 | } 76 | 77 | async replace(table, query, doc) { 78 | await this.dataStores.get(table).updateAsync(resolveQuery(query), this.parseUpdateInput(doc)); 79 | await this.dataStores.get(table).persistence.compactDatafile(); 80 | return true; 81 | } 82 | 83 | delete(table, query, all = false) { 84 | return this.dataStores.get(table).removeAsync(resolveQuery(query), { multi: all }); 85 | } 86 | 87 | deleteAll(table) { 88 | return this.delete(table, {}, true); 89 | } 90 | 91 | count(table, query = {}) { 92 | return this.dataStores.get(table).countAsync(resolveQuery(query)); 93 | } 94 | 95 | }; 96 | 97 | const resolveQuery = query => isObject(query) ? query : { id: query }; 98 | -------------------------------------------------------------------------------- /providers/neo4j.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Provider, util: { mergeDefault, isObject, mergeObjects } } = require('klasa'); 3 | const { v1: neo4j } = require('neo4j-driver'); 4 | 5 | module.exports = class extends Provider { 6 | 7 | constructor(...args) { 8 | super(...args); 9 | this.db = null; 10 | } 11 | 12 | init() { 13 | const { host, username, password } = mergeDefault({ 14 | host: 'bolt://localhost', 15 | username: 'neo4j', 16 | password: 'neo4j' 17 | }, this.client.options.providers.neo4j); 18 | 19 | const driver = neo4j.driver(host, neo4j.auth.basic(username, password)); 20 | this.db = driver.session(); 21 | } 22 | 23 | hasTable(table) { 24 | return this.db.run(`MATCH (n:${table}) RETURN n`).then(data => Boolean(data.records.length)); 25 | } 26 | 27 | createTable(table) { 28 | return this.db.run(`CREATE (n:${table}) RETURN n`).then(data => data.records.map(node => node._fields[0].identity.low)[0]); 29 | } 30 | 31 | deleteTable(table) { 32 | return this.db.run(`MATCH (n:${table}) DELETE n`); 33 | } 34 | 35 | getKeys(table) { 36 | return this.db.run(`MATCH (n:${table}) RETURN n`).then(data => data.records.map(node => node._fields[0].identity.low)); 37 | } 38 | 39 | get(table, id) { 40 | return this.db.run(`MATCH (n:${table}) WHERE n.id = ${typeof id === 'string' ? `'${id}'` : id} RETURN n`) 41 | .then(data => data.records.map(node => node._fields[0].properties)[0]); 42 | } 43 | 44 | has(table, id) { 45 | return this.db.run(`MATCH (n:${table} {id: {id} }) RETURN n`, { id }).then(data => Boolean(data.records.length)); 46 | } 47 | 48 | create(table, id, doc = {}) { 49 | return this.db.run(`CREATE (n:${table} ${JSON.stringify({ id, ...this.parseUpdateInput(doc) }).replace(/"([^"]+)":/g, '$1:')}) RETURN n`); 50 | } 51 | 52 | update(table, id, doc) { 53 | const object = this.constructFlatObject(this.parseUpdateInput(doc)); 54 | return this.db.run(`MATCH (n:${table} {id : {id} }) SET ${object} RETURN n`, { id }); 55 | } 56 | 57 | delete(table, id) { 58 | return this.db.run(`MATCH (n:${table} {id: {id} }) DELETE n`, { id }); 59 | } 60 | 61 | updatebyID(table, id, doc) { 62 | const object = this.constructFlatObject(this.parseUpdateInput(doc)); 63 | return this.db.run(`MATCH (n:${table}) WHERE ID(n) = ${id} SET ${object} RETURN n`); 64 | } 65 | 66 | constructFlatObject(object) { 67 | const string = []; 68 | Object.entries(object).map(([key, value]) => { 69 | if (isObject(object)) value = JSON.stringify(value).replace(/"([^"]+)":/g, '$1:'); 70 | return string.push(`n.${key} = ${typeof string === 'string' ? `'${value}'` : value}`); 71 | }); 72 | return string.join(', '); 73 | } 74 | 75 | async getAll(table, filter = []) { 76 | return await this.db.run(`MATCH (n:${table}) ${filter.length ? `WHERE n.id IN ${filter}` : ''}RETURN n`) 77 | .then(data => data.records.map(node => node._fields[0].properties)); 78 | } 79 | 80 | async replace(table, id, doc) { 81 | const { data } = await this.get(table, id); 82 | const object = this.constructFlatObject(mergeObjects(data, this.parseUpdateInput(doc))); 83 | return this.db.run(`MATCH (n:${table} {id : {id} }) SET ${object} RETURN n`, { id }); 84 | } 85 | 86 | async remove(table, id, path) { 87 | const [entry] = this.db.run(`MATCH (n:${table} {id: {id} }) RETURN n`, { id }).then(data => data.records.map(node => node._fields[0].properties)); 88 | Object.keys(path).map(key => delete entry[key]); 89 | await this.updatebyID(table, id, entry); 90 | } 91 | 92 | }; 93 | -------------------------------------------------------------------------------- /providers/rebirthdb.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Provider, util: { mergeDefault, chunk } } = require('klasa'); 3 | const { r } = require('rebirthdbts'); // eslint-disable-line id-length 4 | 5 | module.exports = class extends Provider { 6 | 7 | constructor(...args) { 8 | super(...args); 9 | this.db = r; 10 | this.pool = null; 11 | } 12 | 13 | async init() { 14 | this.pool = await r.connectPool(mergeDefault({ 15 | db: 'test', 16 | silent: false 17 | }, this.client.options.providers.rebirthdb)); 18 | } 19 | 20 | get exec() { 21 | return this.db; 22 | } 23 | 24 | async ping() { 25 | const now = Date.now(); 26 | return (await this.db.now().run()).getTime() - now; 27 | } 28 | 29 | /* Table methods */ 30 | 31 | hasTable(table) { 32 | return this.db.tableList().contains(table).run(); 33 | } 34 | 35 | createTable(table) { 36 | return this.db.tableCreate(table).run(); 37 | } 38 | 39 | deleteTable(table) { 40 | return this.db.tableDrop(table).run(); 41 | } 42 | 43 | sync(table) { 44 | return this.db.table(table).sync().run(); 45 | } 46 | 47 | /* Document methods */ 48 | 49 | async getAll(table, entries = []) { 50 | if (entries.length) { 51 | const chunks = chunk(entries, 50000); 52 | const output = []; 53 | for (const myChunk of chunks) output.push(...await this.db.table(table).getAll(...myChunk).run()); 54 | return output; 55 | } 56 | return this.db.table(table).run(); 57 | } 58 | 59 | async getKeys(table, entries = []) { 60 | if (entries.length) { 61 | const chunks = chunk(entries, 50000); 62 | const output = []; 63 | for (const myChunk of chunks) output.push(...await this.db.table(table).getAll(...myChunk)('id').run()); 64 | return output; 65 | } 66 | return this.db.table(table)('id').run(); 67 | } 68 | 69 | get(table, id) { 70 | return this.db.table(table).get(id).run(); 71 | } 72 | 73 | has(table, id) { 74 | return this.db.table(table).get(id).ne(null).run(); 75 | } 76 | 77 | getRandom(table) { 78 | return this.db.table(table).sample(1).run(); 79 | } 80 | 81 | create(table, id, value = {}) { 82 | return this.db.table(table).insert({ ...this.parseUpdateInput(value), id }).run(); 83 | } 84 | 85 | update(table, id, value = {}) { 86 | return this.db.table(table).get(id).update(this.parseUpdateInput(value)).run(); 87 | } 88 | 89 | replace(table, id, value = {}) { 90 | return this.db.table(table).get(id).replace({ ...this.parseUpdateInput(value), id }).run(); 91 | } 92 | 93 | delete(table, id) { 94 | return this.db.table(table).get(id).delete().run(); 95 | } 96 | 97 | }; 98 | -------------------------------------------------------------------------------- /providers/redis.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Provider, util } = require('klasa'); 3 | const { Client } = require('redis-nextra'); 4 | 5 | module.exports = class extends Provider { 6 | 7 | constructor(...args) { 8 | super(...args); 9 | 10 | const { hosts, options } = util.mergeDefault(this.client.providers.redis, { 11 | hosts: ['127.0.0.1:6379'], 12 | options: {} 13 | }); 14 | 15 | this.db = new Client(hosts, options); 16 | 17 | this.db.on('ready', () => this.client.emit('debug', 'Redis initialized.')) 18 | .on('serverReconnect', server => this.client.emit('warn', `Redis server: ${server.host.string} is reconnecting.`)) 19 | .on('error', err => this.client.emit('error', err)); 20 | } 21 | 22 | hasTable(table) { 23 | return this.db.tables.has(table); 24 | } 25 | 26 | createTable(table) { 27 | return this.db.createTable(table); 28 | } 29 | 30 | deleteTable(table) { 31 | return this.db.deleteTable(table); 32 | } 33 | 34 | getAll(table) { 35 | return this.db.table(table).valuesJson('*'); 36 | } 37 | 38 | get(table, id) { 39 | return this.db.table(table).getJson(id); 40 | } 41 | 42 | has(table, id) { 43 | return this.db.table(table).has(id); 44 | } 45 | 46 | getRandom(table) { 47 | return this.db.table(table).srandmember(); 48 | } 49 | 50 | create(table, id, data) { 51 | return this.db.table(table).setJson(id, data); 52 | } 53 | 54 | async update(table, id, data) { 55 | const existent = await this.get(table, id); 56 | return this.create(table, id, util.mergeObjects(existent || { id }, this.parseUpdateInput(data))); 57 | } 58 | 59 | replace(...args) { 60 | return this.set(...args); 61 | } 62 | 63 | delete(table, id) { 64 | return this.db.table(table).del(id); 65 | } 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /providers/rethinkdb.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Provider, util: { mergeDefault, chunk } } = require('klasa'); 3 | const { r } = require('rethinkdb-ts'); // eslint-disable-line id-length 4 | 5 | module.exports = class extends Provider { 6 | 7 | constructor(...args) { 8 | super(...args); 9 | 10 | this.db = r; 11 | this.pool = null; 12 | } 13 | 14 | async init() { 15 | const options = mergeDefault({ 16 | db: 'test', 17 | silent: false 18 | }, this.client.options.providers.rethinkdb); 19 | 20 | this.pool = await r.connectPool(options); 21 | await this.db.branch(this.db.dbList().contains(options.db), null, this.db.dbCreate(options.db)).run(); 22 | } 23 | 24 | async ping() { 25 | const now = Date.now(); 26 | return (await this.db.now().run()).getTime() - now; 27 | } 28 | 29 | shutdown() { 30 | return this.pool.drain(); 31 | } 32 | 33 | /* Table methods */ 34 | 35 | hasTable(table) { 36 | return this.db.tableList().contains(table).run(); 37 | } 38 | 39 | createTable(table) { 40 | return this.db.tableCreate(table).run(); 41 | } 42 | 43 | deleteTable(table) { 44 | return this.db.tableDrop(table).run(); 45 | } 46 | 47 | sync(table) { 48 | return this.db.table(table).sync().run(); 49 | } 50 | 51 | /* Document methods */ 52 | 53 | async getAll(table, entries = []) { 54 | if (entries.length) { 55 | const chunks = chunk(entries, 50000); 56 | const output = []; 57 | for (const myChunk of chunks) output.push(...await this.db.table(table).getAll(...myChunk).run()); 58 | return output; 59 | } 60 | return this.db.table(table).run(); 61 | } 62 | 63 | async getKeys(table, entries = []) { 64 | if (entries.length) { 65 | const chunks = chunk(entries, 50000); 66 | const output = []; 67 | for (const myChunk of chunks) output.push(...await this.db.table(table).getAll(...myChunk)('id').run()); 68 | return output; 69 | } 70 | return this.db.table(table)('id').run(); 71 | } 72 | 73 | get(table, id) { 74 | return this.db.table(table).get(id).run(); 75 | } 76 | 77 | has(table, id) { 78 | return this.db.table(table).get(id).ne(null).run(); 79 | } 80 | 81 | getRandom(table) { 82 | return this.db.table(table).sample(1).run(); 83 | } 84 | 85 | create(table, id, value = {}) { 86 | return this.db.table(table).insert({ ...this.parseUpdateInput(value), id }).run(); 87 | } 88 | 89 | update(table, id, value) { 90 | return this.db.table(table).get(id).update(this.parseUpdateInput(value)).run(); 91 | } 92 | 93 | replace(table, id, value) { 94 | return this.db.table(table).get(id).replace({ ...this.parseUpdateInput(value), id }).run(); 95 | } 96 | 97 | delete(table, id) { 98 | return this.db.table(table).get(id).delete().run(); 99 | } 100 | 101 | }; 102 | -------------------------------------------------------------------------------- /providers/sqlite.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { SQLProvider, QueryBuilder, Type, Timestamp, util: { chunk } } = require('klasa'); 3 | const { resolve } = require('path'); 4 | const db = require('sqlite'); 5 | const fs = require('fs-nextra'); 6 | 7 | const valueList = amount => new Array(amount).fill('?').join(', '); 8 | 9 | const TIMEPARSERS = { 10 | DATE: new Timestamp('YYYY-MM-DD'), 11 | DATETIME: new Timestamp('YYYY-MM-DD hh:mm:ss') 12 | }; 13 | 14 | module.exports = class extends SQLProvider { 15 | 16 | constructor(...args) { 17 | super(...args); 18 | this.baseDir = resolve(this.client.userBaseDirectory, 'bwd', 'provider', 'sqlite'); 19 | this.qb = new QueryBuilder({ 20 | null: 'NULL', 21 | integer: ({ max }) => max >= 2 ** 32 ? 'BIGINT' : 'INTEGER', 22 | float: 'DOUBLE PRECISION', 23 | boolean: { type: 'TINYINT', resolver: (input) => input ? '1' : '0' }, 24 | date: { type: 'DATETIME', resolver: (input) => TIMEPARSERS.DATETIME.display(input) }, 25 | time: { type: 'DATETIME', resolver: (input) => TIMEPARSERS.DATETIME.display(input) }, 26 | timestamp: { type: 'TIMESTAMP', resolver: (input) => TIMEPARSERS.DATE.display(input) } 27 | }); 28 | } 29 | 30 | async init() { 31 | await fs.ensureDir(this.baseDir); 32 | await fs.ensureFile(resolve(this.baseDir, 'db.sqlite')); 33 | return db.open(resolve(this.baseDir, 'db.sqlite')); 34 | } 35 | 36 | /* Table methods */ 37 | 38 | hasTable(table) { 39 | return this.runGet(`SELECT name FROM sqlite_master WHERE type='table' AND name=${sanitizeKeyName(table)};`) 40 | .then(Boolean); 41 | } 42 | 43 | createTable(table, rows) { 44 | if (rows) return this.run(`CREATE TABLE ${sanitizeKeyName(table)} (${rows.map(([k, v]) => `${sanitizeKeyName(k)} ${v}`).join(', ')});`); 45 | const gateway = this.client.gateways[table]; 46 | if (!gateway) throw new Error(`There is no gateway defined with the name ${table} nor an array of rows with datatypes have been given. Expected any of either.`); 47 | 48 | const schemaValues = [...gateway.schema.values(true)]; 49 | return this.run(` 50 | CREATE TABLE ${sanitizeKeyName(table)} ( 51 | id VARCHAR(${gateway.idLength || 18}) PRIMARY KEY NOT NULL UNIQUE${schemaValues.length ? `, ${schemaValues.map(this.qb.parse.bind(this.qb)).join(', ')}` : ''} 52 | );` 53 | ); 54 | } 55 | 56 | deleteTable(table) { 57 | return this.run(`DROP TABLE ${sanitizeKeyName(table)};`); 58 | } 59 | 60 | /* Document methods */ 61 | 62 | async getAll(table, entries = []) { 63 | let output = []; 64 | if (entries.length) for (const myChunk of chunk(entries, 999)) output.push(...await this.runAll(`SELECT * FROM ${sanitizeKeyName(table)} WHERE id IN ( ${valueList(myChunk.length)} );`, myChunk)); 65 | else output = await this.runAll(`SELECT * FROM ${sanitizeKeyName(table)};`); 66 | return output.map(entry => this.parseEntry(table, entry)); 67 | } 68 | 69 | get(table, key, value = null) { 70 | return this.runGet(value === null ? 71 | `SELECT * FROM ${sanitizeKeyName(table)} WHERE id = ?;` : 72 | `SELECT * FROM ${sanitizeKeyName(table)} WHERE ${sanitizeKeyName(key)} = ?;`, [value ? transformValue(value) : key]) 73 | .then(entry => this.parseEntry(table, entry)) 74 | .catch(() => null); 75 | } 76 | 77 | has(table, key) { 78 | return this.runGet(`SELECT id FROM ${sanitizeKeyName(table)} WHERE id = ?;`, [key]) 79 | .then(() => true) 80 | .catch(() => false); 81 | } 82 | 83 | getRandom(table) { 84 | return this.runGet(`SELECT * FROM ${sanitizeKeyName(table)} ORDER BY RANDOM() LIMIT 1;`) 85 | .then(entry => this.parseEntry(table, entry)) 86 | .catch(() => null); 87 | } 88 | 89 | create(table, id, data) { 90 | const [keys, values] = this.parseUpdateInput(data, false); 91 | 92 | // Push the id to the inserts. 93 | if (!keys.includes('id')) { 94 | keys.push('id'); 95 | values.push(id); 96 | } 97 | return this.run(`INSERT INTO ${sanitizeKeyName(table)} ( ${keys.map(sanitizeKeyName).join(', ')} ) VALUES ( ${valueList(values.length)} );`, values.map(transformValue)); 98 | } 99 | 100 | update(table, id, data) { 101 | const [keys, values] = this.parseUpdateInput(data, false); 102 | return this.run(` 103 | UPDATE ${sanitizeKeyName(table)} 104 | SET ${keys.map(key => `${sanitizeKeyName(key)} = ?`)} 105 | WHERE id = ?;`, [...values.map(transformValue), id]); 106 | } 107 | 108 | replace(...args) { 109 | return this.update(...args); 110 | } 111 | 112 | delete(table, row) { 113 | return this.run(`DELETE FROM ${sanitizeKeyName(table)} WHERE id = ?;`, [row]); 114 | } 115 | 116 | addColumn(table, piece) { 117 | return this.exec(`ALTER TABLE ${sanitizeKeyName(table)} ADD ${sanitizeKeyName(piece.path)} ${piece.type};`); 118 | } 119 | 120 | async removeColumn(table, schemaPiece) { 121 | const gateway = this.client.gateways[table]; 122 | if (!gateway) throw new Error(`There is no gateway defined with the name ${table}.`); 123 | 124 | const sanitizedTable = sanitizeKeyName(table), 125 | sanitizedCloneTable = sanitizeKeyName(`${table}_temp`); 126 | 127 | const allPieces = [...gateway.schema.values(true)]; 128 | const index = allPieces.findIndex(piece => schemaPiece.path === piece.path); 129 | if (index === -1) throw new Error(`There is no key ${schemaPiece.key} defined in the current schema for ${table}.`); 130 | 131 | const filteredPieces = allPieces.slice(); 132 | filteredPieces.splice(index, 1); 133 | 134 | const filteredPiecesNames = filteredPieces.map(piece => sanitizeKeyName(piece.path)).join(', '); 135 | 136 | await this.createTable(sanitizedCloneTable, filteredPieces.map(this.qb.parse.bind(this.qb))); 137 | await this.exec([ 138 | `INSERT INTO ${sanitizedCloneTable} (${filteredPiecesNames})`, 139 | ` SELECT ${filteredPiecesNames}`, 140 | ` FROM ${sanitizedTable};` 141 | ].join('\n')); 142 | await this.exec(`DROP TABLE ${sanitizedTable};`); 143 | await this.exec(`ALTER TABLE ${sanitizedCloneTable} RENAME TO ${sanitizedTable};`); 144 | return true; 145 | } 146 | 147 | async updateColumn(table, schemaPiece) { 148 | const gateway = this.client.gateways[table]; 149 | if (!gateway) throw new Error(`There is no gateway defined with the name ${table}.`); 150 | 151 | const sanitizedTable = sanitizeKeyName(table), 152 | sanitizedCloneTable = sanitizeKeyName(`${table}_temp`); 153 | 154 | const allPieces = [...gateway.schema.values(true)]; 155 | const index = allPieces.findIndex(piece => schemaPiece.path === piece.path); 156 | if (index === -1) throw new Error(`There is no key ${schemaPiece.key} defined in the current schema for ${table}.`); 157 | 158 | const allPiecesNames = allPieces.map(piece => sanitizeKeyName(piece.path)).join(', '); 159 | const parsedDatatypes = allPieces.map(this.qb.parse.bind(this.qb)); 160 | parsedDatatypes[index] = `${sanitizeKeyName(schemaPiece.key)} ${schemaPiece.type}`; 161 | 162 | await this.createTable(sanitizedCloneTable, parsedDatatypes); 163 | await this.exec([ 164 | `INSERT INTO ${sanitizedCloneTable} (${allPiecesNames})`, 165 | ` SELECT ${allPiecesNames}`, 166 | ` FROM ${sanitizedTable};` 167 | ].join('\n')); 168 | await this.exec(`DROP TABLE ${sanitizedTable};`); 169 | await this.exec(`ALTER TABLE ${sanitizedCloneTable} RENAME TO ${sanitizedTable};`); 170 | return true; 171 | } 172 | 173 | getColumns(table) { 174 | return this.runAll(`PRAGMA table_info(${sanitizeKeyName(table)});`) 175 | .then(result => result.map(row => row.name)); 176 | } 177 | 178 | // Get a row from an arbitrary SQL query. 179 | runGet(...sql) { 180 | return db.get(...sql); 181 | } 182 | 183 | // Get all rows from an arbitrary SQL query. 184 | runAll(...sql) { 185 | return db.all(...sql); 186 | } 187 | 188 | // Run arbitrary SQL query. 189 | run(...sql) { 190 | return db.run(...sql); 191 | } 192 | 193 | // Execute arbitrary SQL query. 194 | exec(...sql) { 195 | return db.exec(...sql); 196 | } 197 | 198 | }; 199 | 200 | /** 201 | * @param {string} value The string to sanitize as a key 202 | * @returns {string} 203 | * @private 204 | */ 205 | function sanitizeKeyName(value) { 206 | if (typeof value !== 'string') throw new TypeError(`[SANITIZE_NAME] Expected a string, got: ${new Type(value)}`); 207 | if (/`|"/.test(value)) throw new TypeError(`Invalid input (${value}).`); 208 | if (value.charAt(0) === '"' && value.charAt(value.length - 1) === '"') return value; 209 | return `"${value}"`; 210 | } 211 | 212 | function transformValue(value) { 213 | switch (typeof value) { 214 | case 'boolean': 215 | case 'number': return value; 216 | case 'object': return value === null ? value : JSON.stringify(value); 217 | default: return String(value); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /providers/toml.js: -------------------------------------------------------------------------------- 1 | const TOML = require('@iarna/toml'); 2 | const fs = require('fs-nextra'); 3 | const { Provider, util } = require('klasa'); 4 | const { resolve } = require('path'); 5 | 6 | module.exports = class extends Provider { 7 | 8 | constructor(...args) { 9 | super(...args); 10 | 11 | const baseDirectory = resolve(this.client.userBaseDirectory, 'bwd', 'provider', 'toml'); 12 | const defaults = util.mergeDefault({ baseDirectory }, this.client.options.providers.toml); 13 | this.baseDirectory = defaults.baseDirectory; 14 | } 15 | 16 | async init() { 17 | await fs.ensureDir(this.baseDirectory).catch(err => this.client.emit('error', err)); 18 | } 19 | 20 | hasTable(table) { 21 | return fs.pathExists(resolve(this.baseDirectory, table)); 22 | } 23 | 24 | createTable(table) { 25 | return fs.mkdir(resolve(this.baseDirectory, table)); 26 | } 27 | 28 | deleteTable(table) { 29 | return this.hasTable(table) 30 | .then(exists => exists ? fs.emptyDir(resolve(this.baseDirectory, table)).then(() => fs.remove(resolve(this.baseDirectory, table))) : null); 31 | } 32 | 33 | async getAll(table, entries) { 34 | if (!Array.isArray(entries) || !entries.length) entries = await this.getKeys(table); 35 | if (entries.length < 5000) { 36 | return Promise.all(entries.map(this.get.bind(this, table))); 37 | } 38 | 39 | const chunks = util.chunk(entries, 5000); 40 | const output = []; 41 | for (const chunk of chunks) output.push(...await Promise.all(chunk.map(this.get.bind(this, table)))); 42 | return output; 43 | } 44 | 45 | async getKeys(table) { 46 | const dir = resolve(this.baseDirectory, table); 47 | const filenames = await fs.readdir(dir); 48 | const files = []; 49 | for (const filename of filenames) { 50 | if (filename.endsWith('.toml')) files.push(filename.slice(0, filename.length - 5)); 51 | } 52 | return files; 53 | } 54 | 55 | get(table, id) { 56 | return fs.readFile(resolve(this.baseDirectory, table, `${id}.toml`)) 57 | .then(buffer => TOML.parse(buffer.toString('utf8').replace(/^\uFEFF/, ''))).catch(() => null); 58 | } 59 | 60 | has(table, id) { 61 | return fs.pathExists(resolve(this.baseDirectory, table, `${id}.toml`)); 62 | } 63 | 64 | getRandom(table) { 65 | return this.getKeys(table).then(data => this.get(table, data[Math.floor(Math.random() * data.length)])); 66 | } 67 | 68 | create(table, id, data = {}) { 69 | return fs.outputFileAtomic(resolve(this.baseDirectory, table, `${id}.toml`), TOML.stringify({ id, ...this.parseUpdateInput(data) })); 70 | } 71 | 72 | async update(table, id, data) { 73 | const existent = await this.get(table, id); 74 | return fs.outputFileAtomic(resolve(this.baseDirectory, table, `${id}.toml`), TOML.stringify(util.mergeObjects(existent || { id }, this.parseUpdateInput(data)))); 75 | } 76 | 77 | replace(table, id, data) { 78 | return fs.outputFileAtomic(resolve(this.baseDirectory, table, `${id}.toml`, TOML.stringify({ id, ...this.parseUpdateInput(data) }))); 79 | } 80 | 81 | delete(table, id) { 82 | return fs.unlink(resolve(this.baseDirectory, table, `${id}.toml`)); 83 | } 84 | 85 | }; 86 | -------------------------------------------------------------------------------- /routes/channel.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds/:guildID/channels/:channelID' }); 8 | } 9 | 10 | get(request, response) { 11 | const { guildID, channelID } = request.params; 12 | const guild = this.client.guilds.get(guildID); 13 | if (!guild) return response.end('{}'); 14 | const channel = guild.channels.get(channelID); 15 | if (!channel) return response.end('{}'); 16 | return response.end(JSON.stringify(channel)); 17 | } 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /routes/channels.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds/:guildID/channels' }); 8 | } 9 | 10 | get(request, response) { 11 | const { guildID } = request.params; 12 | const guild = this.client.guilds.get(guildID); 13 | if (!guild) return response.end('[]'); 14 | return response.end(JSON.stringify(guild.channels.keyArray())); 15 | } 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /routes/emoji.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds/:guildID/emojis/:emojiID' }); 8 | } 9 | 10 | get(request, response) { 11 | const { guildID, emojiID } = request.params; 12 | const guild = this.client.guilds.get(guildID); 13 | if (!guild) return this.notFound(response); 14 | const emoji = guild.emojis.get(emojiID); 15 | if (!emoji) return this.notFound(response); 16 | return response.end(JSON.stringify(emoji)); 17 | } 18 | 19 | notFound(response) { 20 | response.writeHead(404); 21 | return response.end('{}'); 22 | } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /routes/emojis.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds/:guildID/emojis' }); 8 | } 9 | 10 | get(request, response) { 11 | const { guildID } = request.params; 12 | const guild = this.client.guilds.get(guildID); 13 | if (!guild) return this.notFound(response); 14 | return response.end(JSON.stringify(guild.emojis.keyArray())); 15 | } 16 | 17 | notFound(response) { 18 | response.writeHead(404); 19 | return response.end('[]'); 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /routes/guild.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds/:guildID' }); 8 | } 9 | 10 | get(request, response) { 11 | const { guildID } = request.params; 12 | const guild = this.client.guilds.get(guildID); 13 | if (!guild) return response.end('{}'); 14 | return response.end(JSON.stringify(guild)); 15 | } 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /routes/guilds.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds' }); 8 | } 9 | 10 | get(request, response) { 11 | return response.end(JSON.stringify(this.client.guilds.keyArray())); 12 | } 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /routes/member.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds/:guildID/members/:memberID' }); 8 | } 9 | 10 | get(request, response) { 11 | const { guildID, memberID } = request.params; 12 | const guild = this.client.guilds.get(guildID); 13 | if (!guild) return response.end('{}'); 14 | const member = guild.members.get(memberID); 15 | if (!member) return response.end('{}'); 16 | return response.end(JSON.stringify(member)); 17 | } 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /routes/members.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds/:guildID/members' }); 8 | } 9 | 10 | get(request, response) { 11 | const { guildID } = request.params; 12 | const guild = this.client.guilds.get(guildID); 13 | if (!guild) return response.end('[]'); 14 | return response.end(JSON.stringify(guild.members.keyArray())); 15 | } 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /routes/piece.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'pieces/:type/:name' }); 8 | } 9 | 10 | get(request, response) { 11 | const { type, name } = request.params; 12 | const store = this.client.pieceStores.get(type); 13 | if (!store) response.end('[]'); 14 | if (name === 'all') return response.end(JSON.stringify(store.array())); 15 | const piece = store.get(name); 16 | if (!piece) return response.end('{}'); 17 | return response.end(JSON.stringify(piece)); 18 | } 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /routes/pieces.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'pieces/:type' }); 8 | } 9 | 10 | get(request, response) { 11 | const { type } = request.params; 12 | const store = this.client.pieceStores.get(type); 13 | if (!store) return response.end('[]'); 14 | return response.end(JSON.stringify(store.keyArray())); 15 | } 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /routes/role.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds/:guildID/roles/:roleID' }); 8 | } 9 | 10 | get(request, response) { 11 | const { guildID, roleID } = request.params; 12 | const guild = this.client.guilds.get(guildID); 13 | if (!guild) return response.end('{}'); 14 | const role = guild.roles.get(roleID); 15 | if (!role) return response.end('{}'); 16 | return response.end(JSON.stringify(role)); 17 | } 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /routes/roles.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'guilds/:guildID/roles' }); 8 | } 9 | 10 | get(request, response) { 11 | const { guildID } = request.params; 12 | const guild = this.client.guilds.get(guildID); 13 | if (!guild) return response.end('[]'); 14 | return response.end(JSON.stringify(guild.roles.keyArray())); 15 | } 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /routes/user.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'users/:userID' }); 8 | } 9 | 10 | get(request, response) { 11 | const { userID } = request.params; 12 | const user = this.client.users.get(userID); 13 | if (!user) return response.end('{}'); 14 | return response.end(JSON.stringify(user)); 15 | } 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Route } = require('klasa-dashboard-hooks'); 3 | 4 | module.exports = class extends Route { 5 | 6 | constructor(...args) { 7 | super(...args, { route: 'users' }); 8 | } 9 | 10 | get(request, response) { 11 | return response.end(JSON.stringify(this.client.users.keyArray())); 12 | } 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /serializers/bigint.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | /* global BigInt:false */ 3 | 4 | const { Serializer } = require('klasa'); 5 | 6 | module.exports = class extends Serializer { 7 | 8 | deserialize(data, piece, language) { 9 | if (data instanceof BigInt) return data; 10 | try { 11 | // eslint-disable-next-line new-cap 12 | return BigInt(data); 13 | } catch (err) { 14 | throw language.get('RESOLVER_INVALID_INT', piece.key); 15 | } 16 | } 17 | 18 | serialize(data) { 19 | return data.toString(); 20 | } 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /serializers/messagepromise.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Message } = require('discord.js'); 3 | const { Serializer } = require('klasa'); 4 | 5 | module.exports = class extends Serializer { 6 | 7 | deserialize(data, piece, language, guild) { 8 | if (data instanceof Message) return data; 9 | if (typeof data !== 'string') throw this.constructor.error(language, piece.key); 10 | const [channelID, messageID] = data.split('/', 2); 11 | if (!(channelID && messageID)) throw this.constructor.error(language, piece.key); 12 | 13 | const channel = this.client.serializers.get('channel').deserialize(channelID, 14 | { key: piece.key, type: 'textchannel' }, language, guild); 15 | const messagePromise = this.constructor.regex.snowflake.test(messageID) ? channel.messages.fetch(messageID) : null; 16 | if (messagePromise) return messagePromise; 17 | // Yes, the split is supposed to be text, not code 18 | throw language.get('RESOLVER_INVALID_MESSAGE', `${piece.key}.split('/')[1]`); 19 | } 20 | 21 | serialize(data) { 22 | return `${data.channel.id}/${data.id}`; 23 | } 24 | 25 | stringify(data, channel) { 26 | // channel might be a message, I sure as heck don't know 27 | return ((channel.messages || channel.channel.messages).get(data) || { content: (data && data.content) || data }).content; 28 | } 29 | 30 | static error(language, name) { 31 | // Yes, the split is supposed to be text, not code 32 | return [ 33 | language.get('RESOLVER_INVALID_CHANNEL', `${name}.split('/')[0]`), 34 | language.get('RESOLVER_INVALID_MESSAGE', `${name}.split('/')[1]`) 35 | ].join(' '); 36 | } 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /tasks/cleanup.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Task, Colors } = require('klasa'); 3 | const { SnowflakeUtil } = require('discord.js'); 4 | 5 | // THRESHOLD equals to 30 minutes in milliseconds: 6 | // - 1000 milliseconds = 1 second 7 | // - 60 seconds = 1 minute 8 | // - 30 minutes 9 | const THRESHOLD = 1000 * 60 * 30; 10 | 11 | module.exports = class MemorySweeper extends Task { 12 | 13 | constructor(...args) { 14 | super(...args); 15 | 16 | // The colors to stylise the console's logs 17 | this.colors = { 18 | red: new Colors({ text: 'lightred' }), 19 | yellow: new Colors({ text: 'lightyellow' }), 20 | green: new Colors({ text: 'green' }) 21 | }; 22 | 23 | // The header with the console colors 24 | this.header = new Colors({ text: 'lightblue' }).format('[CACHE CLEANUP]'); 25 | } 26 | 27 | async run() { 28 | const OLD_SNOWFLAKE = SnowflakeUtil.generate(Date.now() - THRESHOLD); 29 | let presences = 0, guildMembers = 0, voiceStates = 0, emojis = 0, lastMessages = 0, users = 0; 30 | 31 | // Per-Guild sweeper 32 | for (const guild of this.client.guilds.values()) { 33 | // Clear presences 34 | presences += guild.presences.size; 35 | guild.presences.clear(); 36 | 37 | // Clear members that haven't send a message in the last 30 minutes 38 | const { me } = guild; 39 | for (const [id, member] of guild.members) { 40 | if (member === me) continue; 41 | if (member.voice.channelID) continue; 42 | if (member.lastMessageID && member.lastMessageID > OLD_SNOWFLAKE) continue; 43 | guildMembers++; 44 | voiceStates++; 45 | guild.voiceStates.delete(id); 46 | guild.members.delete(id); 47 | } 48 | 49 | // Clear emojis 50 | emojis += guild.emojis.size; 51 | guild.emojis.clear(); 52 | } 53 | 54 | // Per-Channel sweeper 55 | for (const channel of this.client.channels.values()) { 56 | if (!channel.lastMessageID) continue; 57 | channel.lastMessageID = null; 58 | lastMessages++; 59 | } 60 | 61 | // Per-User sweeper 62 | for (const user of this.client.users.values()) { 63 | if (user.lastMessageID && user.lastMessageID > OLD_SNOWFLAKE) continue; 64 | this.client.users.delete(user.id); 65 | users++; 66 | } 67 | 68 | // Emit a log 69 | this.client.emit('verbose', 70 | `${this.header} ${ 71 | this.setColor(presences)} [Presence]s | ${ 72 | this.setColor(guildMembers)} [GuildMember]s | ${ 73 | this.setColor(voiceStates)} [VoiceState]s | ${ 74 | this.setColor(users)} [User]s | ${ 75 | this.setColor(emojis)} [Emoji]s | ${ 76 | this.setColor(lastMessages)} [Last Message]s.`); 77 | } 78 | 79 | /** 80 | * Set a colour depending on the amount: 81 | * > 1000 : Light Red colour 82 | * > 100 : Light Yellow colour 83 | * < 100 : Green colour 84 | * @since 3.0.0 85 | * @param {number} number The number to colourise 86 | * @returns {string} 87 | */ 88 | setColor(number) { 89 | const text = String(number).padStart(5, ' '); 90 | // Light Red color 91 | if (number > 1000) return this.colors.red.format(text); 92 | // Light Yellow color 93 | if (number > 100) return this.colors.yellow.format(text); 94 | // Green color 95 | return this.colors.green.format(text); 96 | } 97 | 98 | }; 99 | -------------------------------------------------------------------------------- /tasks/getSpotifyToken.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Task } = require('klasa'); 3 | const fetch = require('node-fetch'); 4 | 5 | // Login to https://developer.spotify.com/dashboard/applications 6 | // Create an app & get your clientID/secret, place them below 7 | // Run every 1 hour to refresh: this.client.schedule.create("getSpotifyToken", "0 * * * *"); 8 | const clientID = ''; 9 | const clientSecret = ''; 10 | 11 | const authorization = Buffer.from(`${clientID}:${clientSecret}`).toString('base64'); 12 | const options = { 13 | method: 'POST', 14 | body: 'grant_type=client_credentials', 15 | headers: { 16 | Authorization: `Basic ${authorization}`, 17 | 'Content-Type': 'application/x-www-form-urlencoded' 18 | } 19 | }; 20 | 21 | module.exports = class extends Task { 22 | 23 | async run() { 24 | this._getToken(); 25 | } 26 | 27 | async init() { 28 | this._getToken(); 29 | } 30 | 31 | async _getToken() { 32 | try { 33 | this.client._spotifyToken = await fetch(`https://accounts.spotify.com/api/token`, options) 34 | .then(response => response.json()) 35 | .then(response => response.access_token); 36 | } catch (error) { 37 | this.client.emit('wtf', error); 38 | } 39 | } 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /tasks/jsonBackup.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Task, Timestamp } = require('klasa'); 3 | const { targz, ensureDir } = require('fs-nextra'); 4 | const { resolve, dirname } = require('path'); 5 | 6 | module.exports = class extends Task { 7 | 8 | constructor(...args) { 9 | super(...args); 10 | this.timestamp = new Timestamp('YYYY-MM-DD[T]HHmmss'); 11 | } 12 | 13 | get provider() { 14 | return this.client.providers.get('json'); 15 | } 16 | 17 | async run(data = { folder: './' }) { 18 | if (!('folder' in data)) data = { folder: './' }; 19 | const file = resolve(data.folder, `json-backup-${this.timestamp}.tar.gz`); 20 | 21 | await ensureDir(dirname(file)); 22 | await targz(file, this.provider.baseDirectory); 23 | } 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /tasks/reloadOnChanges.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Task, Stopwatch } = require('klasa'); 3 | const { watch } = require('chokidar'); 4 | const { extname, basename, sep } = require('path'); 5 | 6 | const nodeModules = `${sep}node_modules${sep}`; 7 | 8 | /* 9 | * When any piece file is changed in your bot (e.g. when you `git pull`, or just edit a file), 10 | * this piece will automatically reload that piece so the change is instantly updated into 11 | * your bot. Test this piece on a test bot before using it in production. 12 | */ 13 | 14 | module.exports = class extends Task { 15 | 16 | async run(name, _path, piece) { 17 | const timer = new Stopwatch(); 18 | 19 | for (const module of Object.keys(require.cache)) { 20 | if (!module.includes(nodeModules) && extname(module) !== '.node') { 21 | delete require.cache[module]; 22 | } 23 | } 24 | 25 | let log; 26 | const reload = this.client.commands.get('reload'); 27 | if (piece) { 28 | await reload.run({ sendLocale: () => null, sendMessage: () => null }, [piece]); 29 | log = `Reloaded it in ${timer}`; 30 | } else { 31 | await reload.everything({ sendLocale: () => null, sendMessage: () => null }); 32 | log = `Reloaded everything in ${timer}.`; 33 | } 34 | 35 | timer.stop(); 36 | this.client.emit('log', `${name} was updated. ${log}`); 37 | } 38 | 39 | async init() { 40 | if (this.client._fileChangeWatcher) return; 41 | 42 | this.client._fileChangeWatcher = watch(process.cwd(), { 43 | ignored: [ 44 | '**/node_modules/**/*', 45 | '**/bwd/provider/**/*' 46 | ], 47 | persistent: true, 48 | ignoreInitial: true, 49 | cwd: process.cwd() 50 | }); 51 | 52 | const reloadStore = async (_path) => { 53 | const store = _path.split(sep) 54 | .find(dir => this.client.pieceStores.has(dir)); 55 | 56 | const name = basename(_path); 57 | 58 | if (!store) { 59 | if (this._running) return; 60 | this._running = true; 61 | await this.run(name, _path); 62 | this._running = false; 63 | return; 64 | } 65 | 66 | const piece = this.client.pieceStores.get(store) 67 | .get(name.replace(extname(name), '')); 68 | 69 | this.run(name, _path, piece); 70 | }; 71 | 72 | for (const event of ['add', 'change', 'unlink']) { 73 | this.client._fileChangeWatcher.on(event, reloadStore); 74 | } 75 | } 76 | 77 | }; 78 | -------------------------------------------------------------------------------- /tasks/reminder.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Task } = require('klasa'); 3 | 4 | /* 5 | 6 | This is to be used with the remindme command located in 7 | /commands/Tools/remindme.js 8 | 9 | */ 10 | 11 | module.exports = class extends Task { 12 | 13 | async run({ channel, user, text }) { 14 | const _channel = this.client.channels.get(channel); 15 | if (_channel) await _channel.send(`<@${user}> You wanted me to remind you: ${text}`); 16 | } 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /tasks/unmute.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017-2019 dirigeants. All rights reserved. MIT license. 2 | const { Task } = require('klasa'); 3 | 4 | /* 5 | 6 | This is to be used with the mute command located in 7 | /commands/Moderation/mute.js 8 | 9 | */ 10 | 11 | module.exports = class extends Task { 12 | 13 | async run({ guild, user }) { 14 | const _guild = this.client.guilds.get(guild); 15 | if (!_guild) return; 16 | const member = await _guild.members.fetch(user).catch(() => null); 17 | if (!member) return; 18 | await member.roles.remove(_guild.settings.roles.muted); 19 | } 20 | 21 | }; 22 | --------------------------------------------------------------------------------