├── .gitignore ├── LICENSE ├── README.md ├── TRANSLATING.md ├── bot.js ├── commands ├── apis │ ├── 8ball.js │ ├── cat.js │ ├── dog.js │ ├── math.js │ ├── mchistory.js │ ├── mcserver.js │ ├── mcskin.js │ ├── strawpoll.js │ ├── ud.js │ ├── webshot.js │ └── xkcd.js ├── developer │ ├── broadcasteval.js │ ├── eval.js │ ├── exec.js │ ├── hardblacklist.js │ ├── id.js │ ├── load.js │ ├── pull.js │ ├── reject.js │ ├── reload.js │ ├── reply.js │ ├── restart.js │ ├── send.js │ └── stats.js ├── eggs │ ├── bots.js │ ├── daniel.js │ ├── dude.js │ ├── eli.js │ ├── elog.js │ ├── fiona.js │ ├── fuckthis.js │ ├── garfield.js │ ├── gymno.js │ ├── india.js │ ├── isaac.js │ ├── jay.js │ ├── max.js │ ├── melon.js │ ├── nik.js │ ├── nintenbot.js │ ├── panda.js │ ├── rice.js │ ├── roles.js │ ├── ryan.js │ ├── techno.js │ └── wap.js ├── fun │ ├── dice.js │ ├── endpoll.js │ ├── hug.js │ ├── humongoji.js │ ├── l.js │ ├── lenny.js │ ├── meme.js │ ├── owoify.js │ ├── poll.js │ ├── reverse.js │ ├── spacify.js │ ├── spongebob.js │ └── stopwatch.js ├── help.js ├── i18n │ ├── guildlanguage.js │ ├── langinfo.js │ ├── language.js │ └── languages.js ├── info.js ├── invite.js ├── levels │ ├── leaderboard.js │ ├── levels.js │ ├── xp.js │ └── xpinfo.js ├── management │ ├── blacklist.js │ ├── levelmessages.js │ ├── prefix.js │ └── unblacklist.js ├── misc │ ├── mp3.js │ └── suggest.js └── music │ ├── forceskip.js │ ├── like.js │ ├── liked.js │ ├── likedlb.js │ ├── loop.js │ ├── move.js │ ├── nowplaying.js │ ├── npnotify.js │ ├── pause.js │ ├── play.js │ ├── playnext.js │ ├── queue.js │ ├── remove.js │ ├── resume.js │ ├── shuffle.js │ ├── skip.js │ ├── stop.js │ └── unlike.js ├── events ├── guildCreate.js ├── messageCreate.js ├── messageReactionAdd.js ├── ready.js └── voiceStateUpdate.js ├── functions ├── askQuestion.js ├── commandLoader.js ├── dbots.js ├── defaultChannel.js ├── eventLoader.js ├── findMember.js └── permLevel.js ├── index.js ├── locales ├── en-US English, US.json ├── nl Nederlands.json ├── pl Polski.json └── ro Română.json ├── package-lock.json ├── package.json └── struct ├── ArthurClient.js ├── Music.js ├── Util.js ├── i18n.js ├── ipc.js ├── soundcloud.js └── xp.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .DS_Store 4 | commands/developer/test.js 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 - 2022 Nikolas Brandt 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 | ## Arthur 2 | 3 | [Support Server](https://discord.gg/HW7KYG9) . [TOS](http://bit.ly/ArthurTOS) . [Invite](https://discordapp.com/oauth2/authorize?client_id=329085343800229889&permissions=34683526&scope=bot) . [Trello](https://trello.com/b/wt7ptHpC/arthur) 4 | [![Discord Bots](https://discordbots.org/api/widget/status/329085343800229889.svg)](https://discordbots.org/bot/329085343800229889) [![Discord Bots](https://discordbots.org/api/widget/servers/329085343800229889.svg)](https://discordbots.org/bot/329085343800229889) [![Discord Bots](https://discordbots.org/api/widget/upvotes/329085343800229889.svg)](https://discordbots.org/bot/329085343800229889) 5 | 6 | --- 7 | 8 | Arthur - Another one of those bots that does things. It has an active developer, at least for now. He'll probably respond to anything you ask him within a short amount of time. 9 | 10 | Arthur does have some hot features that other bots don't, but not that many. To list a few, `mp3` which downloads and converts YouTube videos to mp3, and `webshot` which takes a full-length shot of a website. And I think the leveling system is pretty damn nice, but who knows. More features are always being added, just wait a while because the developer is very slow to add things. 11 | 12 | I guess the overall point of this is that Arthur is like a nice multipurpose bot. Yeah. 13 | 14 | Eh, we're going back to using an MIT license, I feel weird having contributors but still being under copyright. To get this bot up and running, you'll probably need to add quite a lot of files to a `../media` directory, and since I'm not *really* trying to decentralize this bot, that challenge is up to you. 15 | 16 | PRs, issues, complaints, whatever, all welcome. You can also complain in the support server above. 17 | 18 | Nik Brandt 19 | -------------------------------------------------------------------------------- /TRANSLATING.md: -------------------------------------------------------------------------------- 1 | # Translating Arthur 2 | 3 | First off, thank you for having an interest in translating Arthur into your language! Your time means a lot to me. You'll get a translator role in the Discord server and I'm open to other suggestions for rewards, within reason and within the scope of Arthur. 4 | 5 | I have plans to make the translation process easier with a website, but I realize that it may be a while until that happens and I might as well have proper documentation of the current translation process. **Please read this whole document if you would like to translate Arthur.** It isn't too long (especially in comparison to the actual translation :eyes:), and I'd like to ensure that the translation process is as smooth as possible, both for you and me. 6 | 7 | Thanks again! 8 | \- Nik Brandt 9 | 10 | ## Setup 11 | 12 | Please follow steps 1 and 2. I'll happily setup a file for you, though (meaning you can stop at step 2) - just ask! I'd say that the method from steps 3 onward is slightly more complicated if you don't know how JSON works, as you'll have to type in text as you go. You can alternatively copy the en-US file (see step 2) and then rename it as detailed in step 3.i and just change the authors/flag/translations to fit you (steps 4 and 5). 13 | 14 | 1. Get a good code editor. You could do this all in TextEdit or Notepad, but it'll be harder and more time consuming. I highly recommend [Visual Studio Code](https://code.visualstudio.com/Download), but any editor that supports JSON syntax highlighting and code indentation will do. A big plus if it'll catch JSON errors for you. 15 | 16 | 2. Open up Arthur's [en-US locale file](https://github.com/nikbrandt/Arthur/blob/master/locales/en-US%20English%2C%20US.json) for reference. If you'd like to view it locally instead of in your browser, click the "Raw" button and then right click -> `Save as...`. 17 | 18 | 3. Make a JSON file for your language and open it in your editor. 19 | 20 | i. Name the file in the format ` .json`. For example, the US English file is named `en-US English, US.json`, with the locale code `en-US` and the language name `English, US`. Note that `US` was added on to the locale code and language name; if your translation is specific to a country, add that (in two letters) to the code and (in full) to the language name (e.g. `es-ES Español, España`, more examples of just the codes [here](https://www.ge.com/digital/documentation/predix-services/c_custom_locale_support.html)). Please use the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language (first column). The language/country should be in your language, not English. Ask me if you have any questions about this step (or any step!). 21 | 22 | ii. Copy and paste the following into the document: 23 | ```JSON 24 | { 25 | "meta": { 26 | "authors": [ 27 | "YOUR NAME HERE" 28 | ], 29 | "flag": "SEE INSTRUCTIONS", 30 | "translations": { 31 | 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | 4. Replace the `SEE INSTRUCTIONS` with the flag of the country your language is primarily spoken on. This is the flag displayed in Discord when listing languages; it's from a list of Discord emojis of countries. In Discord, go to the emoji picker, scroll to the very bottom, and find the name of your countries flag. Replace `SEE INSTRUCTIONS` with the part after the `flag_` (For example, the US is `:flag_us:`, so `us` is used). 38 | 39 | 5. Start translating! Replace `YOUR NAME HERE` with your name, however you want it to be displayed. Add any other names if you're collaborating. Start with translations of each language, by locale code, in the meta. All *values* need to be translated; that is, the parts after the colon (`:`). 40 | 41 | ## General Instructions 42 | 43 | - The above method of setting up means you'll have to retype or copy/paste in all the keys of the file. For example, starting with the `translations`, you'd have to type in the same `"cs": ` as in the en-US file, and then put your translation in quotes. The advantage to this is that anything you don't copy over will simply fall back to its English translation. However, if you know you are going to translate everything, or you would like to translate sections of the file at a time (e.g. a few commands at a time), it may be easier to copy and paste parts of the en-US file (or the whole thing) in, and then change the English translations into those of your language. 44 | 45 | - Every value string should be translated. If you don't want to translate parts of the file, that's fine, but please don't leave the English translation in it. 46 | 47 | - When filling out the `"meta"` section, keep the following in mind: 48 | - Please do override the English versions of commands. Arthur will try to find the command in your language, but will fall back to the English version if it can't. 49 | - Not all aliases need to be translated if they don't all make sense. You can also add more if it is logical, necessary, or a good play on words/joke (that still makes sense to be an alias). 50 | - Please keep the square brackets (`[]`) and angle brackets (`<>`) in the `usage` section (but translate the text inside of them). 51 | 52 | - Understand how Discord markdown works: `*asterisks*` make *italics*, `**double asterisks**` make **bold**, `***triple asterisks***` do ***both***, `__double underlines__` make underlined text and can be combined with any of the previous formatting. Please keep formatting where appropriate. 53 | - Also note that bots have access to masked links in certain scenarios, taking on the format `[text](link)`, where the `text` part becomes a clickable link. Please translate the `text`, but not the `link`. 54 | 55 | - Arthur has variables within the text that start with `$` or `@@`. Please leave these as is and translate surrounding text. 56 | - For example, a string might say `"Page $page of $total pages."` You can rearrange the `$page` and `$total` variables in context (translating "Page", "of", and "pages", in a way that makes sense), but they must stay named `$page` and `$total` for Arthur to know where to insert those variables in the text. The same goes for variables that start with `@@`. 57 | 58 | - When a response is an array (that is, it is surrounded by opening and closing square brackets, `[]`, and it has multiple strings in it), that means that there are multiple possible responses. Arthur will choose one of them randomly each time they're needed. Translate as many of these as you can, and feel free to add more if the meaning is still conveyed well. 59 | 60 | - All developer commands need not have their meta translated, as they are only used by me. They start with `eval` and end with `stats`; you can simply not include any of them in the translation file. 61 | 62 | - The "time" section towards the bottom of the file is used for formatting dates. If the country of your language formats dates/times in a different format (M/D/Y vs. D/M/Y, 24 hour time, etc.), feel free to change it around. See [the moment.js docs](https://momentjs.com/docs/#/displaying/) for all formatting types you can use. 63 | 64 | - Try to maintain the sass of Arthur as best you can. Obviously if things can't be maintained in translation, though, it's fine. I guess. :) 65 | 66 | ## Tips and Tricks 67 | 68 | - Try to reference the en-US file that I had you open in the setup if you're confused about JSON syntax. 69 | - To see a working example translation, look at the [Dutch locale file](https://github.com/nikbrandt/Arthur/blob/master/locales/nl%20Nederlands.json). It's no longer complete (as I've added many commands/translation features since it was completed), but it is still relevant. 70 | 71 | - The en-US file is huge. Take your time, you can translate in steps. If you only feel like translating part of it, that's also fine; a partial translation is better than nothing, and I'll gladly add it to Arthur. 72 | 73 | - Save often! I've already seen two people lose a fair bit of progress because of computer restarts/accidentally closing windows/etc. If your editor doesn't auto save, consider saving after every command or some other often interval to avoid this frustration. 74 | 75 | - Please ask questions about anything if you're confused! Join the [support server](https://discord.gg/2SDdyF7) if you haven't already to ask questions, or if you prefer message me at Gymno#4741. 76 | 77 | ## Finished translating? 78 | 79 | If you know how to use GitHub, feel free to submit a PR with your locale file added to the locales folder. Otherwise, send it to me over Discord and I'll add it to Arthur after confirming it works and looking it over. Thank you so much! 80 | -------------------------------------------------------------------------------- /bot.js: -------------------------------------------------------------------------------- 1 | const ArthurClient = require('./struct/ArthurClient'); 2 | const { errorLog } = require('./functions/eventLoader'); 3 | const { statusUpdate } = require('./functions/eventLoader'); 4 | 5 | global.__basedir = __dirname; 6 | 7 | let sqlQueue = new Map(); 8 | let sqlErrorQueue = new Map(); 9 | let sqlCount = 0; 10 | 11 | const client = new ArthurClient({ 12 | allowedMentions: { 13 | parse: [ 'roles', 'users' ], 14 | repliedUser: false 15 | }, 16 | messageCacheMaxSize: 10, 17 | intents: [ 18 | 'GUILDS', 19 | 'GUILD_VOICE_STATES', 20 | 'GUILD_MESSAGE_REACTIONS', 21 | 'GUILD_MESSAGES', 22 | 'DIRECT_MESSAGES' 23 | ], 24 | partials: [ 25 | 'CHANNEL' 26 | ] 27 | }); 28 | 29 | 30 | global.sql = { 31 | get: (query, args) => { 32 | return sqlPromise('get', query, args); 33 | }, 34 | all: (query, args) => { 35 | return sqlPromise('all', query, args); 36 | }, 37 | run: (query, args) => { 38 | return sqlPromise('run', query, args); 39 | } 40 | }; 41 | 42 | function sqlPromise(type, query, args) { 43 | return new Promise((resolve, reject) => { 44 | let id = sqlCount++; 45 | 46 | sqlQueue.set(id, resolve); 47 | sqlErrorQueue.set(id, reject); 48 | 49 | client.shard.send({ 50 | action: 'sql', 51 | type: type, 52 | query: query, 53 | args: args, 54 | id: id 55 | }).catch(err => { 56 | sqlQueue.delete(id); 57 | sqlErrorQueue.delete(id); 58 | reject(err); 59 | }); 60 | }); 61 | } 62 | 63 | client.init().catch(errorLog.simple); 64 | 65 | const clientEval = (function(script) { 66 | return new Promise(async (resolve, reject) => { 67 | let res; 68 | 69 | try { 70 | res = await eval(script); 71 | } catch (e) { 72 | return reject(e); 73 | } 74 | 75 | resolve(res); 76 | }) 77 | }).bind(client); 78 | 79 | process.on('message', message => { 80 | if (!client.shardQueue || !client.shardErrorQueue || !client.shardQueue.get || typeof client.shardQueue.get !== 'function') return setTimeout(() => { 81 | message.retries = message.retries ? message.retries + 1 : 1; 82 | if (message.retries > 200) return; 83 | process.emit('message', message); 84 | }, 100); // retry later if client not instantiated yet 85 | 86 | switch (message.action) { 87 | case 'uptime': { 88 | client.shard.uptimeStart = message.uptime; 89 | client.shard.id = message.id; 90 | errorLog.shardID = message.id; 91 | 92 | statusUpdate({ 93 | title: `Shard ${message.id} started`, 94 | timestamp: new Date().toISOString(), 95 | color: 0x00c140 96 | }); 97 | 98 | break; 99 | } 100 | 101 | case 'sql': { 102 | let { error, result, id } = message; 103 | 104 | if (!error) { 105 | if (sqlQueue.has(id)) sqlQueue.get(id)(result); 106 | } else { 107 | if (sqlErrorQueue.has(id)) sqlErrorQueue.get(id)(error); 108 | } 109 | 110 | break; 111 | } 112 | 113 | case 'stopwatch': { 114 | if (client.shardQueue.has(message.id)) client.shardQueue.get(message.id)(message); 115 | client.shardQueue.delete(message.id); 116 | 117 | break; 118 | } 119 | 120 | case 'eval': { 121 | let { script, id } = message; 122 | 123 | clientEval(script).then(result => { 124 | client.shard.send({ 125 | action: 'eval', 126 | result: result, 127 | id: id 128 | }).catch(errorLog.simple); 129 | }).catch(err => { 130 | client.shard.send({ 131 | action: 'eval', 132 | error: err.toString(), 133 | id: id 134 | }).catch(errorLog.simple); 135 | }); 136 | 137 | break; 138 | } 139 | 140 | case 'broadcastEval': { 141 | let { error, result, id } = message; 142 | 143 | if (error) client.shardErrorQueue.get(id)(error); 144 | else client.shardQueue.get(id)(result); 145 | 146 | client.shardQueue.delete(id); 147 | client.shardErrorQueue.delete(id); 148 | 149 | break; 150 | } 151 | 152 | case 'stats': { 153 | if (client.shardQueue.has(message.id)) client.shardQueue.get(message.id)(message.value); 154 | client.shardQueue.delete(message.id); 155 | 156 | break; 157 | } 158 | } 159 | }); 160 | -------------------------------------------------------------------------------- /commands/apis/8ball.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | message.channel.send(':8ball: ' + message.__('answer')); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 1, 8 | category: 'apis' 9 | }; -------------------------------------------------------------------------------- /commands/apis/cat.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const { toJson } = require('xml2json'); 3 | 4 | exports.run = (message) => { 5 | request('http://thecatapi.com/api/images/get?format=xml', (err, response, body) => { 6 | if (err) return message.channel.send(message.__('error')); 7 | let json; 8 | 9 | try { 10 | json = JSON.parse(toJson(body)); 11 | } catch (e) { 12 | return message.channel.send(message.__('error')) 13 | } 14 | 15 | if (!json || !json.response || !json.response.data) return message.channel.send(message.__('error')); 16 | 17 | message.channel.send({embeds: [{ 18 | description: message.__('embed_description', { url: json.response.data.images.image.source_url }), 19 | image: { 20 | url: json.response.data.images.image.url 21 | }, 22 | color: 0x00c140 23 | }]}) 24 | }); 25 | }; 26 | 27 | exports.config = { 28 | enabled: true, 29 | permLevel: 1, 30 | perms: ['EMBED_LINKS'], 31 | category: 'apis' 32 | }; 33 | -------------------------------------------------------------------------------- /commands/apis/dog.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | 3 | exports.run = message => { 4 | request('http://dog.ceo/api/breeds/image/random', (err, response, body) => { 5 | if (err) return message.channel.send(message.__('error')); 6 | let json; 7 | try { 8 | json = JSON.parse(body); 9 | } catch (e) { 10 | return message.channel.send(message.__('error')); 11 | } 12 | 13 | if (!json) return message.channel.send(message.__('error')); 14 | 15 | message.channel.send({embeds: [{ 16 | description: message.__('embed_description', { url: json.message }), 17 | image: { 18 | url: json.message 19 | }, 20 | color: 0x00c140 21 | }]}) 22 | }); 23 | }; 24 | 25 | exports.config = { 26 | enabled: true, 27 | permLevel: 1, 28 | perms: ['EMBED_LINKS'], 29 | category: 'apis' 30 | }; 31 | -------------------------------------------------------------------------------- /commands/apis/math.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | 3 | exports.run = (message, args, suffix) => { 4 | if (!args[0]) return message.channel.send(message.__('no_args')); 5 | if (suffix.length > 256) return message.channel.send(message.__('long_equation')); 6 | 7 | request(`http://api.mathjs.org/v1/?expr=${encodeURIComponent(suffix)}&precision=3`, (err, response, body) => { 8 | if (err) return message.channel.send(message.__('error')); 9 | if (body.toLowerCase().startsWith('error')) { 10 | message.channel.send({embeds: [{ 11 | title: suffix, 12 | description: `\`\`\`js\n${body}\n\`\`\``, 13 | color: 0xff3d3d 14 | }]}); 15 | } else { 16 | message.channel.send({embeds: [{ 17 | title: suffix, 18 | description: `\`\`\`js\n${body}\n\`\`\``, 19 | color: 0x70ff3d 20 | }]}); 21 | } 22 | }); 23 | }; 24 | 25 | exports.config = { 26 | enabled: true, 27 | permLevel: 1, 28 | cooldown: 2000, 29 | perms: ['EMBED_LINKS'], 30 | category: 'apis' 31 | }; -------------------------------------------------------------------------------- /commands/apis/mchistory.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise'); 2 | const moment = require('moment'); 3 | 4 | async function getUUID (name) { 5 | let json = await request( { uri: `https://api.mojang.com/users/profiles/minecraft/${name}?at=${moment().format('x')}`, json: true } ); 6 | if (!json || json.error) return undefined; 7 | else return json.id; 8 | } 9 | 10 | function reverse (array) { 11 | let reversed = []; 12 | array.forEach(i => { 13 | reversed.unshift(i); 14 | }); 15 | return reversed; 16 | } 17 | 18 | exports.run = async (message, args) => { 19 | if (!args[0]) return message.channel.send(message.__('no_args')); 20 | 21 | let uuid; 22 | let page; 23 | 24 | if (args[0].length === 36) args[0] = args[0].replace(/-/g, ''); 25 | if (args[0].length === 32) uuid = args[0]; 26 | else if (args[0].length <= 16) uuid = await getUUID(args[0]); 27 | else return message.channel.send(message.__('long_name')); 28 | 29 | if (!uuid) return message.channel.send(message.__('invalid_name')); 30 | 31 | if (!args[1]) page = 1; 32 | else { 33 | page = parseInt(args[1]); 34 | if (!page) return message.channel.send(message.__('invalid_page')); 35 | if (page < 0) return message.channel.send(message.__('negative_page')); 36 | } 37 | 38 | let body = await request(`https://api.mojang.com/user/profiles/${uuid}/names`); 39 | 40 | if (!body) return message.channel.send(message.__('no_body')); 41 | 42 | let json = JSON.parse(body); 43 | let rever = reverse(json); 44 | let nameArray = []; 45 | let dateArray = []; 46 | 47 | let maxPage = Math.ceil(json.length / 10); 48 | if (page > maxPage) return message.channel.send(message.__('page_too_high', { page })); 49 | 50 | let index = json.length - (page * 10 - 10); 51 | let reversed = rever.slice(page * 10 - 10, page * 10); 52 | 53 | let locale = i18n.getLocaleCode(message); 54 | if (locale === 'en-US') locale = 'en'; 55 | 56 | for (let object of reversed) { 57 | if (index !== json.length && index >= 1) { 58 | nameArray.push(`${index}. ${object.name} \u200b`); 59 | if (object.changedToAt) dateArray.push(moment(object.changedToAt).locale(locale).utc().format(i18n.get('time.moment.precise', message))); 60 | else dateArray.push(''); 61 | } 62 | index--; 63 | } 64 | 65 | if (!nameArray.length) nameArray = [ message.__('no_name_changes') ]; 66 | 67 | message.channel.send({embeds: [{ 68 | author: { 69 | name: message.__('embed_title', { name: rever[0].name }), 70 | icon_url: `https://visage.surgeplay.com/face/256/${uuid}.png`, 71 | url: `https://namemc.com/profile/${uuid}` 72 | }, 73 | color: 0x00AA00, 74 | fields: [ 75 | { 76 | name: message.__('name'), 77 | value: '\u200b' + nameArray.join('\n'), 78 | inline: true 79 | }, 80 | { 81 | name: message.__('date_changed'), 82 | value: '\u200b' + dateArray.join('\n'), 83 | inline: true 84 | } 85 | ], 86 | footer: { 87 | text: message.__('embed_footer', { page, maxPage, name: json[0].name }) 88 | } 89 | }]}); 90 | }; 91 | 92 | exports.config = { 93 | enabled: true, 94 | permLevel: 1, 95 | perms: ['EMBED_LINKS'], 96 | category: 'apis' 97 | }; -------------------------------------------------------------------------------- /commands/apis/mcserver.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const { MessageEmbed, MessageAttachment, Message } = require('discord.js'); 3 | const moment = require('moment'); 4 | const captureWebsite = require('capture-website'); 5 | 6 | const dataRegex = /^data:image\/([a-z]+);base64,/; 7 | const { errorLog } = require('../../functions/eventLoader'); 8 | 9 | const customCSS = ` 10 | @font-face { font-family: 'Minecraftia'; src: url('../media/fonts/Minecraft-Regular.ttf') format('truetype'); font-weight: normal; font-style: normal; } 11 | @font-face { font-family: 'Minecraftia'; src: url('../media/fonts/Minecraft-Italic.ttf') format('truetype'); font-weight: normal; font-style: italic; } 12 | @font-face { font-family: 'Minecraftia'; src: url('../media/fonts/Minecraft-Bold.ttf') format('truetype'); font-weight: bold; font-style: normal; } 13 | @font-face { font-family: 'Minecraftia'; src: url('../media/fonts/Minecraft-BoldItalic.ttf') format('truetype'); font-weight: bold; font-style: italic; } 14 | * { font-size: 30px; font-family: Minecraftia, Minecraft, sans-serif } 15 | :not(span) { color: #AAAAAA } 16 | `; 17 | 18 | function failed (messageOptions, msg, client, index) { 19 | msg.edit(messageOptions); 20 | client.processing.splice(index, 1); 21 | } 22 | 23 | exports.run = async (message, args, s, client) => { 24 | const failedEmbed = { 25 | title: message.__('failed.title'), 26 | description: message.__('failed.description'), 27 | color: 0xff9335, 28 | footer: { 29 | text: message.__('failed.footer') 30 | } 31 | }; 32 | 33 | if (!args[0]) return message.channel.send(message.__('no_args')); 34 | let index = client.processing.length; 35 | client.processing.push(moment().format('h:mm:ss A') + ' - MC Server/Webshot'); 36 | let msg = await message.channel.send(message.__('processing')); 37 | 38 | request(`https://api.mcsrvstat.us/2/${args[0]}`, async (err, response, body) => { 39 | if (err || !body) return failed({embed: failedEmbed}, msg, client, index); 40 | 41 | try { 42 | body = JSON.parse(body); 43 | } catch (e) { 44 | return failed({ embed: failedEmbed }, msg, client, index); 45 | } 46 | 47 | if (!body.online) return failed({embed: failedEmbed}, msg, client, index); 48 | 49 | let fileLocation = `../media/temp/${message.id}.png`; 50 | let html = body.motd.html.join('
').replace(/ {2}/g, '  '); 51 | 52 | let captureError = false; 53 | 54 | captureWebsite.file(html, fileLocation, { inputType: 'html', width: 700, height: 85, styles: [ customCSS ], defaultBackground: false }).catch(error => { 55 | captureError = error; 56 | }).then(() => { 57 | let iconBase64 = body.icon; 58 | let iconFilename; 59 | let files = []; 60 | 61 | if (iconBase64) { 62 | iconFilename = 'icon.' + iconBase64.match(dataRegex)[1]; 63 | iconBase64 = iconBase64.replace(dataRegex, ''); 64 | let iconBuffer = Buffer.from(iconBase64, 'base64'); 65 | files.push(new MessageAttachment(iconBuffer, iconFilename)); 66 | } 67 | 68 | const embed = new MessageEmbed() 69 | .setAuthor({ 70 | name: message.__('embed.title', { hostname: args[0].indexOf(':') > -1 ? args[0].substring(0, args[0].indexOf(':')) : args[0] }), 71 | iconURL: (iconFilename ? `attachment://${iconFilename}` : undefined) 72 | }) 73 | .setFooter({ text: message.__('embed.footer', { port: body.port, protocol: body.protocol }) }) 74 | .addFields( [ 75 | { name: message.__('embed.players'), value: `${body.players.online}/${body.players.max}`, inline: true }, 76 | { name: message.__('embed.version'), value: body.version.replace(/§./g, ''), inline: true } 77 | ] ) 78 | .setColor(0x00c140); 79 | 80 | if (captureError) { 81 | errorLog('mcserver command failed while getting webshot', captureError); 82 | embed.setDescription('```\n' + body.motd.clean.join('\n') + '```'); 83 | } else { 84 | files.push(new MessageAttachment(fileLocation, 'motd.png' )); 85 | 86 | embed.setImage(`attachment://motd.png`); 87 | } 88 | 89 | message.channel.send({ embeds: [ embed ], files }).then(() => { 90 | msg.delete().catch(() => {}); 91 | }).then(() => { 92 | client.processing.splice(index, 1); 93 | }); 94 | }) 95 | }); 96 | }; 97 | 98 | exports.config = { 99 | enabled: true, 100 | permLevel: 1, 101 | perms: [ 'EMBED_LINKS', 'ATTACH_FILES' ], 102 | category: 'apis' 103 | }; 104 | -------------------------------------------------------------------------------- /commands/apis/mcskin.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise'); 2 | const moment = require('moment'); 3 | const randos = ['f84c6a790a4e45e0879bcd49ebd4c4e2', '879d4fb880a84b80bc474d0f85964a47', '61699b2ed3274a019f1e0ea8c3f06bc6', '853c80ef3c3749fdaa49938b674adae6']; 4 | 5 | async function getUUID (name) { 6 | let json = await request( { uri: `https://api.mojang.com/users/profiles/minecraft/${name}?at=${moment().format('x')}`, json: true } ); 7 | if (!json || json.error) return undefined; 8 | else return json.id; 9 | } 10 | 11 | exports.run = async (message, args) => { 12 | let fake = false; 13 | let uuid; 14 | 15 | if (!args[0]) { 16 | fake = true; 17 | uuid = randos[Math.floor(Math.random() * randos.length)]; 18 | } 19 | else if (args[0].length === 36) args[0] = args[0].replace(/-/g, ''); 20 | else if (args[0].length === 32) uuid = args[0]; 21 | else if (args[0].length <= 16) uuid = await getUUID(args[0]); 22 | else return message.channel.send(i18n.get('commands.mchistory.long_name', message)); 23 | 24 | if (!uuid) { 25 | fake = true; 26 | uuid = randos[Math.floor(Math.random() * randos.length)]; 27 | } 28 | 29 | message.channel.send({embeds: [{ 30 | title: message.__('embed.title', { user: fake ? message.__('some_rando') : args[0] }), 31 | url: `https://namemc.com/profile/${uuid}`, 32 | footer: { 33 | text: message.__('embed.footer') 34 | }, 35 | image: { 36 | url: `https://visage.surgeplay.com/full/512/${uuid}.png` 37 | }, 38 | color: 0x00c140 39 | }]}); 40 | }; 41 | 42 | exports.config = { 43 | enabled: true, 44 | permLevel: 1, 45 | perms: ['EMBED_LINKS'], 46 | category: 'apis' 47 | }; 48 | -------------------------------------------------------------------------------- /commands/apis/strawpoll.js: -------------------------------------------------------------------------------- 1 | const { askWithCondition } = require('../../functions/askQuestion'); 2 | const requestPromise = require('request-promise'); 3 | 4 | const quoteRegex = /^"([^"]+)"/; 5 | 6 | const dupeCondition = text => { 7 | return [ 'normal', 'permissive', 'disabled' ].includes(text.toLowerCase()); 8 | }; 9 | 10 | let lastPoll = { 11 | start: null, 12 | last: null, 13 | uses: 0 14 | }; 15 | 16 | exports.run = async (message, args, suffix) => { 17 | const footer = { 18 | text: message.__('default_footer') 19 | }; 20 | 21 | const isYesNo = text => { 22 | let regex = new RegExp(`^(${i18n.get('booleans.yesno.abbreviations.yes', message)}(${i18n.get('booleans.yesno.abbreviations.yes_end', message)})?|${i18n.get('booleans.yesno.abbreviations.no', message)}(${i18n.get('booleans.yesno.abbreviations.no_end', message)})?)$`, 'i'); 23 | return regex.test(text); 24 | }; // ` 25 | const parseYesNo = text => { 26 | const lowercased = text.toLowerCase(); 27 | return lowercased.includes(i18n.get('booleans.yesno.abbreviations.yes', message)); 28 | }; 29 | 30 | const multiEmbed = { 31 | title: message.__('multiple_votes'), 32 | description: message.__('multiple_votes_description'), 33 | color: 0x007c29, 34 | footer 35 | }; 36 | 37 | const dupeEmbed = { 38 | title: message.__('duplication_checking'), 39 | description: message.__('duplication_checking_description'), 40 | color: 0x007c29, 41 | footer 42 | }; 43 | 44 | const captchaEmbed = { 45 | title: 'Captcha', 46 | description: message.__('captcha_description'), 47 | color: 0x007c29, 48 | footer 49 | }; 50 | 51 | if (!args[0]) return message.channel.send(message.__('no_title')); 52 | if (!args[1]) return message.channel.send(message.__('no_options')); 53 | 54 | if (!lastPoll.start) lastPoll.start = Date.now(); 55 | if (!lastPoll.last) lastPoll.last = Date.now(); 56 | if (lastPoll.last - lastPoll.start > 3600000) { 57 | lastPoll.start = Date.now(); 58 | lastPoll.last = Date.now(); 59 | lastPoll.uses = 1; 60 | } 61 | if (lastPoll.uses >= 100) return message.channel.send(message.__('too_many_polls')); 62 | 63 | let title; 64 | let options; 65 | let advanced = false; 66 | let multi = false; 67 | let dupcheck = 'normal'; 68 | let captcha = false; 69 | let editMessage = undefined; 70 | 71 | if (quoteRegex.test(suffix)) { 72 | let matchedString = suffix.match(quoteRegex); 73 | title = matchedString[1]; 74 | suffix = suffix.slice(matchedString[0].length); 75 | if (suffix.startsWith(' ')) suffix = suffix.slice(1); 76 | } else { 77 | title = args[0]; 78 | suffix = suffix.slice(args[0].length + 1); 79 | } 80 | 81 | if (suffix.includes(`--${message.__('advanced')}`) || suffix.includes(`--${message.__('advanced_abbreviation')}`)) { 82 | advanced = true; 83 | suffix = suffix.replace(new RegExp(` *--${message.__('advanced_abbreviation')}(${message.__('advanced_abbreviation_ending')})? *`, 'g'), '');// / *--adv(anced)? */g, ''); 84 | } 85 | 86 | options = suffix.split('|'); 87 | if (options.length < 2) return message.channel.send(message.__('not_enough_options')); 88 | if (options.length > 30) return message.channel.send(message.__('too_many_options')); 89 | options.forEach((op, i) => { 90 | options[i] = op.replace(/^ *(.*) *$/, '$1'); 91 | }); 92 | 93 | if (advanced) { 94 | try { 95 | let multiObject = await askWithCondition(message.channel, multiEmbed, message.author.id, undefined, 1, undefined, isYesNo, 15000); 96 | multi = parseYesNo(multiObject.response); 97 | let dupeObject = await askWithCondition(message.channel, dupeEmbed, message.author.id, multiObject.message, 1, undefined, dupeCondition, 15000); 98 | dupcheck = dupeObject.response.toLowerCase(); 99 | let captchaObject = await askWithCondition(message.channel, captchaEmbed, message.author.id, dupeObject.message, 1, undefined, isYesNo, 15000); 100 | captcha = parseYesNo(captchaObject.response); 101 | editMessage = captchaObject.message; 102 | } catch (e) { 103 | return e; 104 | } 105 | } 106 | 107 | let response; 108 | try { 109 | response = await requestPromise({ 110 | method: 'POST', 111 | uri: 'https://www.strawpoll.me/api/v2/polls', 112 | headers: { 113 | 'User-Agent': 'Arthur Discord Bot (https://github.com/nikbrandt/Arthur)' 114 | }, 115 | body: { title, options, multi, dupcheck, captcha }, 116 | json: true 117 | }); 118 | } catch (err) { 119 | err = err.stack ? err.stack.split('\n')[0] : err; 120 | return editMessage 121 | ? editMessage.edit({ content: message.__('error', { err }), embeds: [{}] }).catch(() => {}) 122 | : message.channel.send(message.__('error', { err })); 123 | } 124 | 125 | let embed = { 126 | title: message.__('poll_created'), 127 | description: message.__('it_can_be_accessed_here', { id: response.id }), 128 | color: 0x00c140 129 | }; 130 | 131 | editMessage 132 | ? editMessage.edit({ content: '', embeds: [ embed ] }) 133 | : message.channel.send({ embeds: [ embed ] }); 134 | }; 135 | 136 | exports.config = { 137 | enabled: true, 138 | permLevel: 1, 139 | category: 'apis' 140 | }; 141 | -------------------------------------------------------------------------------- /commands/apis/ud.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const requestPromise = require('request-promise'); 3 | 4 | const linkRegex = /\[(.+?)]/g; 5 | const inlineRegex = /\[([^[]*?)]?(\([^[]*?( ?"?[^[]*?)?)?([^)])$/; 6 | 7 | async function asyncForEach(array, callback) { 8 | for (let index = 0; index < array.length; index++) { 9 | await callback(array[index], index, array) 10 | } 11 | } 12 | 13 | async function getDefinition (term) { 14 | let body; 15 | try { 16 | body = await requestPromise(`http://api.urbandictionary.com/v0/define?term=${encodeURIComponent(term)}`); 17 | body = JSON.parse(body); 18 | } catch (e) { 19 | return 'No definition found.'; 20 | } 21 | 22 | if (!body || body.result_type === 'no_results' || !body.list || !body.list[0] || !body.list[0].definition) return 'No definition found.'; 23 | return sliceIfTooBig(body.list[0].definition.replace(linkRegex, '$1'), 100, true); 24 | } 25 | 26 | function sliceIfTooBig(string, num, elipsis, beautify) { 27 | if (string.length > num) { 28 | string = string.slice(0, -(string.length - num)); 29 | if (beautify) { 30 | if (inlineRegex.test(string)) string = string.replace(inlineRegex, '$1'); 31 | } 32 | if (elipsis) string = string + '...'; 33 | } 34 | return string; 35 | } 36 | 37 | exports.run = async (message, a, suffix) => { 38 | if (message.guild && !message.channel.nsfw) return message.channel.send(message.__('nsfw')); 39 | request(`http://api.urbandictionary.com/v0/${suffix ? `define?term=${encodeURIComponent(suffix)}` : 'random'}`, async (err, resp, body) => { 40 | if (err) return message.channel.send(message.__('not_connected')); 41 | 42 | let hotBod; 43 | try { 44 | hotBod = JSON.parse(body); 45 | } catch (e) { 46 | return message.channel.send(message.__('no_results')); 47 | } 48 | if (!hotBod || hotBod.result_type === 'no_results') return message.channel.send(message.__('no_results')); 49 | const theChosenOne = hotBod.list[0]; 50 | 51 | if (!theChosenOne) return message.channel.send(message.__('no_results')); 52 | 53 | let definition = theChosenOne.definition; 54 | let defMatches = definition.match(linkRegex); 55 | if (defMatches) await asyncForEach(defMatches, async match => { 56 | let subDefinition = await getDefinition(encodeURIComponent(match.slice(1).slice(0, -1))); 57 | definition = definition.replace(match, `${match}(https://www.urbandictionary.com/define.php?term=${encodeURIComponent(match.slice(1).slice(0, -1))} "${subDefinition}")`); 58 | }); 59 | 60 | let example = theChosenOne.example; 61 | let exampleMatches = example.match(linkRegex); 62 | if (exampleMatches) await asyncForEach(exampleMatches, async match => { 63 | let subDefinition = await getDefinition(encodeURIComponent(match.slice(1).slice(0, -1))); 64 | example = example.replace(match, `${match}(https://www.urbandictionary.com/define.php?term=${encodeURIComponent(match.slice(1).slice(0, -1))} "${subDefinition}")`); 65 | }); 66 | 67 | message.channel.send({embeds: [{ 68 | color: 0x0095d1, 69 | title: theChosenOne.word, 70 | url: theChosenOne.permalink, 71 | fields: [ 72 | { 73 | name: message.__('definition'), 74 | value: sliceIfTooBig(definition, 1021, true, true) 75 | }, 76 | { 77 | name: message.__('example'), 78 | value: '*' + sliceIfTooBig(example, 1019, true, true) + '*' 79 | } 80 | ], 81 | footer: { 82 | text: message.__('footer', { author: theChosenOne.author, thumbsup: theChosenOne.thumbs_up, thumbsdown: theChosenOne.thumbs_down }) 83 | } 84 | }]}).catch(e => { console.log(e); message.channel.send(message.__('catch')) }); 85 | }); 86 | }; 87 | 88 | exports.config = { 89 | enabled: true, 90 | permLevel: 1, 91 | category: 'apis' 92 | }; 93 | -------------------------------------------------------------------------------- /commands/apis/webshot.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const captureWebsite = require('capture-website'); 3 | const moment = require('moment'); 4 | const fs = require('fs'); 5 | 6 | let nonobad = [ 'data:', 'file://', 'doom3.zoy.org', 'goatse', 'porn', 'redtube', 'sex', 'rule34', 'amateur', 'cuckold', 'creampie', 'cum', 'jiz', 'milf', 'orgasm', 'orgy', 'threesome', 'ass', 'tit', 'dick', 'penis', 'despacito', 'pussy', 'fuck', 'finger', 'bang', 'hentai', 'yaoi', 'virgin', 'handjob', 'blowjob', 'xxx', 'milf' ]; 7 | 8 | exports.run = async (message, args, s, client) => { 9 | if (!args[0]) return message.channel.send(message.__('no_args')); 10 | if (nonobad.some(i => args[0].toLowerCase().includes(i)) && message.author.id !== client.ownerId) return message.channel.send(message.__('blacklisted_website')); 11 | let index = client.processing.length; 12 | client.processing.push(moment().format('h:mm:ss A') + ' - Webshot'); 13 | let date = Date.now(); 14 | let sent = false; 15 | 16 | let options = { 17 | width: 1920, 18 | height: 1080, 19 | scaleFactor: 1, 20 | fullPage: true, 21 | cookies: [], 22 | timeout: 30, 23 | modules: [ 24 | `document.body.innerHTML = document.body.innerHTML.replace(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/g, ';)')` 25 | ] 26 | }; 27 | 28 | let msg; 29 | let del = false; 30 | let cancel = false; 31 | 32 | message.channel.send(message.__('loading')).then(m => { 33 | if (del) m.delete(); 34 | else msg = m; 35 | }); 36 | 37 | setTimeout(function () { 38 | if (!sent) { 39 | message.channel.send(message.__('timed_out')); 40 | cancel = true; 41 | client.processing.splice(index, 1); 42 | } 43 | }, 35000); // if webshot not complete in 35 seconds, cancel operation. 44 | 45 | let err = false; 46 | 47 | if (!args[0].startsWith('http')) args[0] = 'https://' + args[0]; 48 | 49 | captureWebsite.file(args[0], `../media/temp/${date}-${message.author.id}.png`, options).catch(error => { 50 | err = error; 51 | }).then(() => { 52 | if (cancel) return; 53 | 54 | if (err) { 55 | if (err.toString().includes('value 1')) message.channel.send(message.__('invalid_url')); 56 | else if (err.toString().includes('timeout setting')) message.channel.send(message.__('timed_out')); 57 | else if ([ 'ERR_NAME_NOT_RESOLVED', 'ERR_NAME_RESOLUTION_FAILED' ].some(e => err.toString().includes(e))) message.channel.send(message.__('dns_failed')); 58 | else if (err.toString().includes('ERR_CERT_COMMON_NAME_INVALID')) message.channel.send(message.__('invalid_certificate')); 59 | else { 60 | client.errorLog('Unknown webshot error', err); 61 | message.channel.send(message.__('unknown_error', { err: err.toString() })).catch(() => { 62 | message.channel.send(message.__('unknown_error', { err: 'Please join Arthur\'s support server for further help.' })).catch(() => {}); 63 | }); 64 | } 65 | client.processing.splice(index, 1); 66 | sent = true; 67 | return; 68 | } 69 | 70 | message.channel.send({embeds: [ new Discord.MessageEmbed() 71 | .setTitle(message.__('embed.title', { url: args[0].length > 245 ? args[0].slice(0, -(args[0].length - 245)) : args[0] })) 72 | .setDescription(message.__('embed.description', { url: args[0].startsWith('https://') || args[0].startsWith('http://') ? args[0] : 'https://' + args[0] })) 73 | .setImage(`attachment://${date}-${message.author.id}.png`) 74 | .setFooter({ text: message.__('embed.footer', { name: message.author.tag }) }) 75 | .setColor(0x00c140) 76 | ], files: [ new Discord.MessageAttachment(`../media/temp/${date}-${message.author.id}.png`, `${date}-${message.author.id}.png`) ] }).then(() => { 77 | sent = true; 78 | if (msg) msg.delete(); 79 | else del = true; 80 | fs.unlinkSync(`../media/temp/${date}-${message.author.id}.png`); 81 | client.processing.splice(index, 1); 82 | }); 83 | }); 84 | }; 85 | 86 | exports.config = { 87 | enabled: true, 88 | permLevel: 1, 89 | perms: ['ATTACH_FILES'], 90 | cooldown: 10000, 91 | category: 'other' 92 | }; 93 | -------------------------------------------------------------------------------- /commands/apis/xkcd.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise'); 2 | const moment = require('moment'); 3 | 4 | function send (message, json) { 5 | let locale = i18n.getLocaleCode(message); 6 | if (locale === 'en-US') locale = 'en'; 7 | 8 | let timeString = moment(`${json.month}-${json.day}-${json.year}`, 'M-D-YYYY').locale(locale).format(i18n.get('time.moment.date_only_nice', message)); 9 | 10 | message.channel.send({embeds: [{ 11 | title: json.title, 12 | url: `https://xkcd.com/${json.num}`, 13 | description: json.alt, 14 | image: { 15 | url: json.img 16 | }, 17 | footer: { 18 | text: `#${json.num} | ${timeString}` 19 | }, 20 | color: 0xffffff 21 | }]}); 22 | } 23 | 24 | exports.run = async (message, args) => { 25 | let latest = await request( { uri: 'https://xkcd.com/info.0.json', json: true } ); 26 | let comic; 27 | 28 | if (args[0] === message.__('latest')) comic = latest.num; 29 | else if (!args[0] || args[0] === message.__('random')) comic = Math.ceil(Math.random() * latest.num); 30 | else { 31 | let parsed = parseInt(args[0]); 32 | if (!parsed) return message.channel.send(i18n.get('parsing.invalid_number', message)); 33 | if (parsed < 0) return message.channel.send(message.__('negative_number')); 34 | if (parsed > latest.num) return message.channel.send(message.__('comic_not_created')); 35 | 36 | comic = parsed; 37 | } 38 | 39 | if (comic === latest.num) send(message, latest); 40 | else send(message, await request( { uri: `https://xkcd.com/${comic}/info.0.json`, json: true } )) 41 | }; 42 | 43 | exports.config = { 44 | enabled: true, 45 | permLevel: 1, 46 | perms: ['EMBED_LINKS'], 47 | category: 'apis' 48 | }; -------------------------------------------------------------------------------- /commands/developer/broadcasteval.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | 3 | function errorMessage (silent, channel, error, message) { 4 | if (silent) return; 5 | let errorContent = `**Input above.**\n\n❗Error:\n\`\`\`js\n${error}\n\`\`\``; 6 | 7 | if (message) message.edit(errorContent); 8 | else channel.send(errorContent); 9 | } 10 | 11 | async function successMessage(silent, channel, text, message) { 12 | if (silent) return 'no'; 13 | let sliceAmount = 0; 14 | if (text.length > 1800) sliceAmount = text.length - 1800; 15 | text = text.replace(/`/g, '\\`'); 16 | 17 | let successContent = `**Input above.** \n\n🎉 Success\n\`\`\`js\n${sliceAmount ? text.slice(0, -sliceAmount) : text}\n\`\`\`${sliceAmount ? `\n*Trimmed ${sliceAmount.toString().length > 20 ? 'a lot of' : sliceAmount.toString()} characters*` : ''}`; 18 | 19 | if (message) message.edit(successContent); 20 | else return await channel.send(successContent); 21 | } 22 | 23 | exports.run = async (message, args, suffix, client) => { 24 | if (!client.config.owners.includes(message.author.id)) return; 25 | if (!suffix) return message.channel.send('I need something to eval!'); 26 | 27 | let evaled; 28 | let response; 29 | let msg; 30 | let silent = false; 31 | 32 | if (suffix.toLowerCase().includes('-s') || suffix.toLowerCase().includes('--silent')) { 33 | suffix = suffix.replace(/ *--?s(ilent)? */i, ''); 34 | silent = true; 35 | } 36 | 37 | function awaitMsg (callback, i) { 38 | if (!i) i = 0; 39 | if (i > 20) return; 40 | 41 | if (msg) { 42 | if (msg !== 'no') callback(); 43 | } else setTimeout(() => { 44 | awaitMsg(callback, i++) 45 | }, 1000); 46 | } 47 | 48 | client.broadcastEval(suffix.replace(/(\n)?```(js)?(\n)?/g, '')).then(res => { 49 | awaitMsg(() => { successMessage(silent, null, 'Promise \n' + util.inspect(res), msg).catch(() => {}) }); 50 | }).catch(err => { 51 | awaitMsg(() => { errorMessage(silent, null, err.toString(), msg) }); 52 | }); 53 | 54 | msg = await successMessage(silent, message.channel, 'Promise '); 55 | }; 56 | 57 | exports.config = { 58 | enabled: true, 59 | permLevel: 10, 60 | category: 'developer' 61 | }; -------------------------------------------------------------------------------- /commands/developer/eval.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | 3 | function errorMessage (silent, channel, error, message) { 4 | if (silent) return; 5 | let errorContent = `**Input above.**\n\n❗Error:\n\`\`\`js\n${error}\n\`\`\``; 6 | 7 | if (message) message.edit(errorContent); 8 | else channel.send(errorContent); 9 | } 10 | 11 | async function successMessage(silent, channel, text, message) { 12 | if (silent) return 'no'; 13 | let sliceAmount = 0; 14 | if (text.length > 1800) sliceAmount = text.length - 1800; 15 | text = text.replace(/`/g, '\\`'); 16 | 17 | let successContent = `**Input above.** \n\n🎉 Success\n\`\`\`js\n${sliceAmount ? text.slice(0, -sliceAmount) : text}\n\`\`\`${sliceAmount ? `\n*Trimmed ${sliceAmount.toString().length > 20 ? 'a lot of' : sliceAmount.toString()} characters*` : ''}`; 18 | 19 | if (message) message.edit(successContent); 20 | else return await channel.send(successContent); 21 | } 22 | 23 | exports.run = async (message, args, suffix, client) => { 24 | if (!client.config.owners.includes(message.author.id)) return; 25 | if (!suffix) return message.channel.send('I need something to eval!'); 26 | 27 | let evaled; 28 | let response; 29 | let msg; 30 | let silent = false; 31 | 32 | if (suffix.toLowerCase().includes('-s') || suffix.toLowerCase().includes('--silent')) { 33 | suffix = suffix.replace(/ *--?s(ilent)? */i, ''); 34 | silent = true; 35 | } 36 | 37 | function awaitMsg (callback, i) { 38 | if (!i) i = 0; 39 | if (i > 20) return; 40 | 41 | if (msg) { 42 | if (msg !== 'no') callback(); 43 | } else setTimeout(() => { 44 | awaitMsg(callback, i++) 45 | }, 1000); 46 | } 47 | 48 | try { 49 | evaled = eval(suffix.replace(/(\n)?```(js)?(\n)?/g, '')); 50 | } catch (err) { 51 | return errorMessage(silent, message.channel, err.toString()); 52 | } 53 | 54 | if (evaled && typeof evaled.then === 'function' && typeof evaled.catch === 'function') { 55 | response = 'Promise '; 56 | 57 | evaled.then(res => { 58 | awaitMsg(() => { successMessage(silent, null, 'Promise \n' + util.inspect(res), msg).catch(() => {}) }); 59 | }); 60 | 61 | evaled.catch(err => { 62 | awaitMsg(() => { errorMessage(silent, null, err.toString(), msg) }); 63 | }); 64 | } else response = util.inspect(evaled); 65 | 66 | msg = await successMessage(silent, message.channel, response); 67 | }; 68 | 69 | exports.config = { 70 | enabled: true, 71 | permLevel: 10, 72 | category: 'developer' 73 | }; 74 | -------------------------------------------------------------------------------- /commands/developer/exec.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const { exec } = require('child_process'); 3 | 4 | function errorMessage (channel, error) { 5 | let errorContent = `**Input above.**\n\n❗Error:\n\`\`\`shell\n${error}\n\`\`\``; 6 | channel.send(errorContent); 7 | } 8 | 9 | function successMessage(channel, text) { 10 | let sliceAmount = 0; 11 | if (text.length > 1800) sliceAmount = text.length - 1800; 12 | text = text.replace(/`/g, '\\`'); 13 | 14 | let successContent = `**Input above.** \n\n🎉 Success\n\`\`\`js\n${sliceAmount ? text.slice(0, -sliceAmount) : text}\n\`\`\`${sliceAmount ? `\n*Trimmed ${sliceAmount.toString().length > 20 ? 'a lot of' : sliceAmount.toString()} characters*` : ''}`; 15 | 16 | channel.send(successContent); 17 | } 18 | 19 | exports.run = async (message, args, suffix, client) => { 20 | if (!client.config.owners.includes(message.author.id)) return; 21 | if (!suffix) return message.channel.send('I need something to eval!'); 22 | 23 | exec(suffix.replace(/(\n)?```(js|shell|bash)?(\n)?/g, ''), (error, stdout, stderr) => { 24 | if (error || stderr) 25 | return errorMessage(message.channel, error ? error.message : stderr); 26 | 27 | successMessage(message.channel, stdout); 28 | }); 29 | }; 30 | 31 | exports.config = { 32 | enabled: true, 33 | permLevel: 10, 34 | category: 'developer' 35 | }; 36 | -------------------------------------------------------------------------------- /commands/developer/hardblacklist.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args, suffix, client) => { 2 | if (!args[0]) return message.channel.send('I uh.. I need an ID'); 3 | if (args[0] === message.author.id) return message.channel.send('no, bad'); 4 | 5 | let user; 6 | try { 7 | user = await client.users.fetch(args[0]) || (await client.broadcastEval(`this.users.fetch('${args[0]}')`)).filter(item => !!item)[0]; 8 | } catch (e) { 9 | user = undefined; 10 | } 11 | 12 | let guild = client.guilds.cache.get(args[0]) || (await client.broadcastEval(`this.guilds.cache.get('${args[0]}')`)).filter(item => !!item)[0]; 13 | 14 | if (!user && !guild) return message.channel.send('Could not find user or guild by that ID. rip'); 15 | 16 | let deleteEntry = false; 17 | let result = await sql.get('SELECT * FROM hardBlacklist WHERE id = ?', [ args[0] ]); 18 | if (result) deleteEntry = true; 19 | 20 | if (user) { 21 | deleteEntry ? await sql.run('DELETE FROM hardBlacklist WHERE id = ?', [ args[0] ]) : await sql.run('INSERT INTO hardBlacklist (id, type) VALUES (?, ?)', [ args[0], 'user' ]); 22 | message.channel.send(deleteEntry ? `Removed \`${user.tag}\` from blacklist. But did they really learn their lesson?` : `Added \`${user.tag}\` to blacklist. *May they never bother you again*.`); 23 | } 24 | 25 | if (guild) { 26 | deleteEntry ? await sql.run('DELETE FROM hardBlacklist WHERE id = ?', [ args[0] ]) : await sql.run('INSERT INTO hardBlacklist (id, type) VALUES (?, ?)', [ args[0], 'guild' ]); 27 | message.channel.send(deleteEntry ? `Guild \`${guild.name}\` removed from blacklist. Anti-uf.` : `Guild \`${guild.name}\` added to blacklist. ***Uf***.`); 28 | } 29 | 30 | client.hardBlacklist = undefined; 31 | }; 32 | 33 | exports.config = { 34 | enabled: true, 35 | permLevel: 9, 36 | category: 'developer' 37 | }; 38 | -------------------------------------------------------------------------------- /commands/developer/id.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args, suffix, client, permLevel) => { 2 | if (!args[0]) return message.channel.send('How am I going to get the ID of no one? Ey?'); 3 | 4 | let user; 5 | if (permLevel === 10) user = client.users.cache.find(u => u.tag === suffix) 6 | || client.users.cache.find(u => u.tag.toLowerCase().includes(suffix.toLowerCase())) 7 | || (await client.broadcastEval(`this.users.cache.find(u => u.tag.toLowerCase().includes("${suffix.toLowerCase().replace(/"/g, '')}"))`)).filter(item => !!item)[0]; 8 | else user = message.guild.members.cache.find(m => m.user.tag.toLowerCase() === suffix.toLowerCase()) || message.guild.members.cache.find(m => m.user.tag.toLowerCase().includes(suffix.toLowerCase())); 9 | 10 | if (!user) return message.channel.send(message.__('user_does_not_exist')); 11 | 12 | message.channel.send({embed: { 13 | title: user.id, 14 | footer: { 15 | text: message.__('id_of', { name: user.username }) 16 | } 17 | }}); 18 | }; 19 | 20 | exports.config = { 21 | enabled: true, 22 | permLevel: 1, 23 | perms: [ 'EMBED_LINKS' ], 24 | category: 'developer' 25 | }; -------------------------------------------------------------------------------- /commands/developer/load.js: -------------------------------------------------------------------------------- 1 | const load = require('../../functions/commandLoader'); 2 | 3 | exports.run = async (message, a, s, client) => { 4 | let msg = await message.channel.send('Reloading..'); 5 | console.log('Reloading all commands via `load` command.'); 6 | let ret = await load(client, true); 7 | msg.edit(`Done. Loaded ${ret[0]} commands in ${ret[1]} ms.`); 8 | }; 9 | 10 | exports.config = { 11 | enabled: true, 12 | permLevel: 10, 13 | category: 'developer' 14 | }; -------------------------------------------------------------------------------- /commands/developer/pull.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | exports.run = (message, args, suffix, client) => { 4 | let kys = false; 5 | if (args[0] === '-k' || args[0] === '-kys') kys = true; 6 | 7 | exec('git pull', (error, stdout) => { 8 | if (error) return message.channel.send(`Received error: \`\`\`shell\n${error.stack}\`\`\``); 9 | 10 | if (stdout.includes('up to date.')) return message.channel.send('Already up to date.'); 11 | if (stdout.includes('changed, ')) { 12 | message.channel.send(`Pulled successfully <:discreetkappa:474445143974608898> ${kys ? '- Killing self now.' : ''}`); 13 | if (kys) client.commands.get('restart').run(message, args, suffix, client); 14 | return; 15 | } 16 | 17 | message.channel.send(`Unexpected output: \`\`\`shell\n${stdout}\n\`\`\``) 18 | }); 19 | }; 20 | 21 | exports.config = { 22 | enabled: true, 23 | permLevel: 10, 24 | category: 'developer' 25 | }; 26 | -------------------------------------------------------------------------------- /commands/developer/reject.js: -------------------------------------------------------------------------------- 1 | const { trello } = require('../misc/suggest'); 2 | 3 | exports.run = async (message, args, suffix) => { 4 | if (message.channel.id !== message.client.config.trello.channel) return message.channel.send('Wrong channel.'); 5 | if (!args[0]) return message.channel.send('Please speciify which suggestion message to reject.'); 6 | 7 | let suggestionMsg = await message.channel.messages.fetch(args[0]).catch(() => {}); 8 | if (!suggestionMsg) return message.channel.send('Could not fetch message.'); 9 | if (suggestionMsg.author.id !== message.client.user.id || !suggestionMsg.embeds[0] || !suggestionMsg.embeds[0].footer || !suggestionMsg.embeds[0].footer.text || !suggestionMsg.embeds[0].footer.text.startsWith('Suggested by')) 10 | return message.channel.send('Message is not a suggestion message.'); 11 | 12 | let cardURLSplit = suggestionMsg.embeds[0].url.split('/'); 13 | let cardID = cardURLSplit[cardURLSplit.length - 1]; 14 | let splitText = suggestionMsg.embeds[0].footer.text.split(' | '); 15 | let suggesterID = splitText[splitText.length - 1]; 16 | 17 | trello.updateCard(cardID, 'closed', true).catch(err => { 18 | message.channel.send('Card update failed, error logged'); 19 | message.client.errorLog('Error updating trello card for reject command', err); 20 | }).then(() => { 21 | message.noDelete = true; 22 | 23 | let rejectionMessage = `Your suggestion of "${suggestionMsg.embeds[0].title}" has been rejected. `; 24 | if (args[1]) rejectionMessage += `Provided reason: ${suffix.slice(args[0].length + 1)}`; 25 | else rejectionMessage += 'No reason was provided.'; 26 | rejectionMessage += '\nIf you\'d like to discuss this further, feel free to reply to this message.'; 27 | 28 | suggestionMsg.embeds[0].color = 0xf56942; 29 | suggestionMsg.embeds[0].title = 'Rejected: ' + suggestionMsg.embeds[0].title; 30 | suggestionMsg.edit({ embeds: [ suggestionMsg.embeds[0] ] }).catch(() => {}); 31 | 32 | message.client.commands.get('send').run(message, [ suggesterID, 'aa'], suggesterID + ' ' + rejectionMessage, message.client).then(() => { 33 | message.channel.send('Suggestion rejected.'); 34 | }).catch(() => { 35 | message.channel.send('Message send failed.'); 36 | }); 37 | }); 38 | }; 39 | 40 | exports.config = { 41 | enabled: true, 42 | permLevel: 9, 43 | category: 'developer' 44 | }; 45 | -------------------------------------------------------------------------------- /commands/developer/reload.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | let folders = ['']; 3 | 4 | exports.run = (message, args, suffix, client) => { 5 | if (!args[0]) return message.channel.send('I can\'t reload nothing <:thonk:281211813494915083>'); 6 | let cmd = args[0]; 7 | 8 | let isAlias = false; 9 | let isNotCommand = false; 10 | let comm = client.commands.get(cmd); 11 | if (!comm) { 12 | comm = client.commands.get(client.aliases.get(cmd)); 13 | if (comm) isAlias = true; 14 | } 15 | if (!comm) { 16 | if (fs.existsSync(`${__basedir}/${suffix}`)) isNotCommand = true; 17 | else return message.channel.send(`\`${cmd}\` is not a command nor is \`${suffix}\` a file. (try using ./)`); 18 | } 19 | 20 | if (!isNotCommand) { 21 | fs.readdirSync(`${__dirname}/../`).filter(f => fs.statSync(`${__dirname}/../${f}`).isDirectory()).forEach(d => folders.push(d)); 22 | 23 | let command; 24 | try { 25 | folders.forEach(fold => { 26 | try { 27 | delete require.cache[require.resolve(`${__dirname}/../${fold ? `${fold}/` : ''}${isAlias ? client.aliases.get(args[0]) : args[0]}.js`)]; 28 | command = require(`${__dirname}/../${fold ? `${fold}/` : ''}${isAlias ? client.aliases.get(args[0]) : args[0]}.js`); 29 | } catch (e) {} 30 | }); 31 | } catch (err) { 32 | return message.channel.send(`Error with require cache: \n${err}`); 33 | } 34 | 35 | client.commands.delete(cmd); 36 | client.commands.set(cmd, command); 37 | client.aliases.forEach((cm, alias) => { 38 | if (cm === cmd) client.aliases.delete(alias); 39 | }); 40 | // noinspection JSUnusedAssignment 41 | command.config.aliases.forEach(alias => { 42 | client.aliases.set(alias, cmd); 43 | }); 44 | 45 | message.channel.send(`The ${suffix.includes('/') ? '*' + suffix.split('/').slice(0, -1) + '*/**' + suffix.split('/')[suffix.split('/').length - 1].charAt(0) + suffix.split('/')[suffix.split('/').length - 1].slice(1) : '**' + suffix}** command has been reloaded.`); 46 | } else { 47 | try { 48 | delete require.cache[require.resolve(`${__basedir}/${suffix}`)]; 49 | } catch (e) { 50 | return message.channel.send(`Could not reload - \`${e.name}\``); 51 | } 52 | 53 | message.channel.send(`Successfully reloaded \`${suffix}\``); 54 | } 55 | }; 56 | 57 | exports.config = { 58 | enabled: true, 59 | permLevel: 10, 60 | category: 'developer' 61 | }; 62 | -------------------------------------------------------------------------------- /commands/developer/reply.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args, suffix, client) => { 2 | if (!args[0] && !message.attachments.size) return message.channel.send('yes, i\'ll send nothing. bravo.'); 3 | 4 | let user = client.shard.id === 0 ? client.lastMessage : (await client.broadcastEval('this.lastMessage'))[0]; 5 | if (!user) return message.channel.send('ive restarted since the last person sent a message, ffs'); 6 | message.delete().catch(() => {}); 7 | 8 | client.commands.get('send').run(message, [ user.id, suffix ], '.'.repeat(user.id.toString().length + 1) + suffix, client); 9 | }; 10 | 11 | exports.config = { 12 | enabled: true, 13 | permLevel: 9, 14 | category: 'developer' 15 | }; 16 | -------------------------------------------------------------------------------- /commands/developer/restart.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | exports.run = async (message, args, suffix, client) => { 4 | let force = false; 5 | let manager = false; 6 | let check = false; 7 | 8 | if (args.includes('-f')) force = true; 9 | if (args.includes('-m')) manager = true; 10 | if (args.includes('-c')) check = true; 11 | 12 | if (check && (force || manager)) return message.channel.send('Incompatible arguments: -c and any other'); 13 | 14 | let voice = 0; // (await client.broadcastEval('this.voice.connections.size')).reduce((prev, count) => prev + count, 0); 15 | let processing = (await client.broadcastEval('this.processing')).reduce((prev, processing) => { 16 | processing.forEach(item => { 17 | prev.push(item); 18 | }); 19 | 20 | return prev; 21 | }, []); 22 | 23 | if ((voice || processing.length) && !force) { 24 | const reasonString = `${voice ? `I've got ${voice} guild${voice !== 1 ? 's' : ''} listening to music through me..` : ''} ${processing.length ? `${voice ? '\n*and*' : ''} I've got the following things processing:\n${processing.map(p => `\`${p}\``).join('\n')}` : ''}`; 25 | if (check) return message.channel.send(reasonString); 26 | else return message.channel.send(`I'm not gonna restart. ${reasonString}\n*Bypass with -f*`); 27 | } 28 | 29 | if (check) return message.channel.send('All good to restart.'); 30 | 31 | const crashPath = require('path').join(__basedir, '..', 'media', 'temp', 'crash.txt'); 32 | if (fs.existsSync(crashPath)) fs.unlink(crashPath, err => { 33 | if (err) client.errorLog('Could not delete previous crash.txt file', err); 34 | }); 35 | 36 | if (manager) message.channel.send('Restarting shard manager/bot.').then(() => client.shard.send({ action: 'restart' })); 37 | else message.channel.send('Restarting all shards.').then(() => client.shard.respawnAll()); 38 | }; 39 | 40 | exports.config = { 41 | enabled: true, 42 | permLevel: 10, 43 | category: 'developer' 44 | }; 45 | -------------------------------------------------------------------------------- /commands/developer/send.js: -------------------------------------------------------------------------------- 1 | const MESSAGE_CHANNEL_ID = '304441662724243457'; 2 | 3 | exports.run = async (message, args, suffix, client) => { 4 | if (!args[0]) return message.channel.send('Mhm. I\'ll send it to no one.'); 5 | if (!args[1] && !message.attachments.size) return message.channel.send('Right. And what, exactly, are you sending?'); 6 | if (!message.noDelete) message.delete().catch(() => {}); 7 | 8 | let name; 9 | 10 | let channel = client.users.cache.get(Object.keys(client.recentMessages)[Object.values(client.recentMessages).indexOf(args[0])]) 11 | ||(await client.broadcastEval(`this.users.cache.get(Object.keys(this.recentMessages)[Object.values(this.recentMessages).indexOf('${args[0]}')])`)).filter(item => !!item)[0]; 12 | 13 | if (!channel) channel = client.users.cache.get(args[0]) 14 | || (await client.broadcastEval(`this.users.cache.get('${args[0]}')`)).filter(item => !!item)[0]; 15 | 16 | if (!channel) { 17 | channel = client.channels.cache.get(args[0]) 18 | || (await client.broadcastEval(`this.channels.cache.get('${args[0]}')`)).filter(item => !!item)[0]; 19 | 20 | if (!channel) return message.channel.send('That\'s not a valid ID, sorry.'); 21 | name = `${channel.name} in ${channel.guild.name}` 22 | } else name = channel.tag; 23 | 24 | let text = suffix.slice(args[0].length + 1); 25 | 26 | if (channel.client) { 27 | channel.send(suffix.slice(args[0].length + 1), { files: message.attachments.size ? [...message.attachments.values()].map(f => f.url) : [] }).then(() => { 28 | if (message.channel.id === MESSAGE_CHANNEL_ID) successMessage(client, message, name, text); 29 | }).catch(e => { 30 | if (message.channel.id === MESSAGE_CHANNEL_ID) failureMessage(client, name, text); 31 | }); 32 | } else { 33 | let success = (await client.broadcastEval(`new Promise(resolve => { 34 | let channel = this.${name.includes('#') ? 'users' : 'channels'}.cache.get('${channel.id}'); 35 | if (!channel) return resolve(null); 36 | channel.send("${suffix.slice(args[0].length + 1).replace(/"/g, '').replace(/\\/g, '\\\\')}").then(() => { 37 | resolve(true); 38 | }).catch(() => { 39 | resolve(false); 40 | }); 41 | });`)).filter(item => item !== null)[0]; 42 | 43 | if (message.channel.id !== MESSAGE_CHANNEL_ID) return; 44 | 45 | if (success === true) successMessage(client, message, name, text); 46 | else failureMessage(client, name, text); 47 | } 48 | }; 49 | 50 | function successMessage(client, message, name, text) { 51 | finalMessage(client, { 52 | embeds: [{ 53 | title: `Message to ${name}`, 54 | description: text, 55 | color: 0x00c140 56 | }], 57 | files: message.attachments.size ? [...message.attachments.values()].map(f => f.url) : [] 58 | }); 59 | } 60 | 61 | function failureMessage(client, name, text) { 62 | finalMessage(client, { 63 | embeds: [{ 64 | title: `Message to ${name} failed to send`, 65 | description: text, 66 | color: 0xff0000 67 | }] 68 | }) 69 | } 70 | 71 | function finalMessage(client, messageOptions) { 72 | if (client.channels.cache.has(MESSAGE_CHANNEL_ID)) return client.channels.cache.get(MESSAGE_CHANNEL_ID).send(messageOptions).catch(() => {}); 73 | 74 | client.broadcastEval(`if (!this.channels.cache.has('${MESSAGE_CHANNEL_ID}') return; 75 | this.channels.cache.get('${MESSAGE_CHANNEL_ID}').send(${JSON.stringify(messageOptions)}).catch(() => {})`).catch(() => {}); 76 | } 77 | 78 | exports.config = { 79 | enabled: true, 80 | permLevel: 9, 81 | category: 'developer' 82 | }; 83 | -------------------------------------------------------------------------------- /commands/developer/stats.js: -------------------------------------------------------------------------------- 1 | const { createCanvas, registerFont } = require('canvas'); 2 | const moment = require('moment'); 3 | const os = require('os'); 4 | 5 | registerFont('../media/fonts/Roboto-Light.ttf', { family: 'RobotoLight' }); 6 | registerFont('../media/fonts/Roboto-Medium.ttf', { family: 'RobotoMedium' }); 7 | 8 | function sort (array) { 9 | array.sort((a, b) => { 10 | return a - b; 11 | }); 12 | 13 | return array; 14 | } 15 | 16 | function sortKeys (object) { 17 | let keys = Object.keys(object); 18 | keys.sort((a, b) => { 19 | return object[a] - object[b] 20 | }); 21 | 22 | return keys; 23 | } 24 | 25 | function reverse (array) { 26 | let reversed = []; 27 | 28 | array.forEach(i => { 29 | reversed.unshift(i); 30 | }); 31 | 32 | return reversed; 33 | } 34 | 35 | function requestStats(type, id, client, arg) { 36 | return new Promise(resolve => { 37 | let obj = { 38 | action: 'getStats', 39 | type: type, 40 | id: id 41 | }; 42 | 43 | if (arg) obj.arg = arg; 44 | 45 | client.shardQueue.set(id, resolve); 46 | client.shard.send(obj).catch(() => { 47 | resolve(null); 48 | }); 49 | }); 50 | } 51 | 52 | exports.run = async (message, args, suffix, client) => { 53 | let object; 54 | 55 | if (args[0] === 'weekly') { 56 | object = await requestStats('weekly', message.id, client, moment().format('W/YYYY')); 57 | if (!object) return message.channel.send('Not enough commands have been used, somehow'); 58 | } else if (args[0] === 'daily') { 59 | object = await requestStats('daily', message.id, client, moment().format('M/D/YYYY')); 60 | if (!object) return message.channel.send('Not enough commands have been used, apparently'); 61 | } else { 62 | let stats = await requestStats('commands', message.id, client); 63 | 64 | let keys = Object.keys(stats); 65 | let values = Object.values(stats); 66 | let temp = {}; 67 | 68 | for (let i = 0; i < keys.length; i++) { 69 | temp[keys[i]] = values[i].uses; 70 | } 71 | 72 | object = temp; 73 | } 74 | 75 | let commandsArray = reverse(sortKeys(object)); 76 | let usesArray = reverse(sort(Object.values(object))); 77 | 78 | let barMaxHeight = 275; 79 | 80 | const canvas = createCanvas(1050, 450), 81 | ctx = canvas.getContext('2d'); 82 | 83 | let barWidth = Math.floor(700 / commandsArray.length); 84 | if (barWidth > 35) barWidth = 35; 85 | let curWidth = 15; 86 | let heightMult = barMaxHeight / usesArray[0]; 87 | let accent = '#00c140'; 88 | 89 | ctx.fillStyle = accent; // add fonts, set color 90 | ctx.font = `${barWidth}px RobotoLight`; 91 | 92 | for (let i = 0; i < commandsArray.length; i++) { // generate each bar of graph w/ command name 93 | let height = Math.floor(usesArray[i] * heightMult); 94 | ctx.fillRect(curWidth, canvas.height - height, barWidth, height); 95 | 96 | let numTop = ctx.measureText(usesArray[i]).width + 6 > height; 97 | 98 | ctx.save(); 99 | ctx.translate(curWidth + barWidth - 6, canvas.height - height - 3); // + ctx.measureText(commandsArray[i]).width 100 | ctx.rotate(-Math.PI / 2.5); 101 | ctx.fillStyle = '#fff'; 102 | let string = commandsArray[i]; 103 | if (numTop) string += ` - ${usesArray[i]}`; 104 | ctx.fillText(string, 0, 0); 105 | ctx.restore(); 106 | 107 | if (!numTop) { 108 | ctx.save(); 109 | ctx.translate(curWidth + barWidth - 3, canvas.height - 4); 110 | ctx.rotate(-Math.PI / 2); 111 | ctx.fillStyle = '#fff'; 112 | ctx.fillText(usesArray[i], 0, 0); 113 | ctx.restore(); 114 | } 115 | 116 | curWidth += barWidth + 4; 117 | } 118 | 119 | let guilds = (await client.broadcastEval('this.guilds.cache.size')).reduce((prev, cur) => prev + cur, 0).toString(); 120 | let users = (await client.broadcastEval('this.users.cache.size')).reduce((prev, cur) => prev + cur, 0).toString(); 121 | 122 | curWidth = 100; // show guild/user amounts 123 | ctx.font = '50px RobotoMedium'; 124 | ctx.fillText(guilds, curWidth, 70); 125 | curWidth += ctx.measureText(guilds).width; 126 | ctx.font = '50px RobotoLight'; 127 | ctx.fillText(' guilds ', curWidth, 70); 128 | curWidth += ctx.measureText(' guilds ').width; 129 | ctx.font = '50px RobotoMedium'; 130 | ctx.fillText(users, curWidth, 70); 131 | curWidth += ctx.measureText(users).width; 132 | ctx.font = '50px RobotoLight'; 133 | ctx.fillText(' users', curWidth, 70); 134 | 135 | let text; 136 | switch(args[0]) { 137 | case 'daily': 138 | text = moment().format('MMM Do YYYY'); 139 | break; 140 | case 'weekly': 141 | text = moment().format('wo [week of] YYYY'); 142 | break; 143 | default: 144 | text = 'All time'; 145 | break; 146 | } 147 | curWidth = 800 - ctx.measureText(text).width; 148 | ctx.font = '40px RobotoLight'; 149 | ctx.fillText(text, curWidth, 150); 150 | 151 | let lastEnd = -1.57; // RAM pie chart 152 | let mem = (await client.broadcastEval('process.memoryUsage().rss * 1.0e-6')).reduce((prev, cur) => prev + cur, 0); 153 | let total = 3000; 154 | let data = [mem, total - mem]; 155 | let colors = ['#fff', accent]; 156 | 157 | for (let i = 0; i < data.length; i++) { 158 | ctx.fillStyle = colors[i]; 159 | ctx.beginPath(); 160 | ctx.moveTo(925, 75); 161 | ctx.arc(925, 75, 70, lastEnd, lastEnd + (Math.PI * 2 * (data[i] / total)), false); 162 | ctx.lineTo(925, 75); 163 | ctx.fill(); 164 | lastEnd += Math.PI * 2 * (data[i] / total); 165 | } 166 | 167 | lastEnd = -1.57; // CPU pie chart 168 | let cpu = os.loadavg()[1] || 0; 169 | data = [cpu, 100 - cpu]; 170 | total = 100; 171 | 172 | for (let i = 0; i < 2; i++) { 173 | ctx.fillStyle = colors[i]; 174 | ctx.beginPath(); 175 | ctx.moveTo(925, 375); 176 | ctx.arc(925, 375, 70, lastEnd, lastEnd + (Math.PI * 2 * (data[i] / total)), false); 177 | ctx.lineTo(925, 375); 178 | ctx.fill(); 179 | lastEnd += Math.PI * 2 * (data[i] / total); 180 | } 181 | 182 | ctx.fillStyle = accent; // RAM and CPU amounts 183 | ctx.font = '50px RobotoMedium'; 184 | ctx.textAlign = 'center'; 185 | ctx.fillText(`${mem.toFixed(1)} MB`, 925, 195); 186 | ctx.fillText(`${cpu.toFixed(2)}%`, 925, 290); 187 | 188 | ctx.textBaseline = 'middle'; // RAM and CPU labels 189 | ctx.font = '50px RobotoLight'; 190 | ctx.globalCompositeOperation = 'xor'; 191 | ctx.beginPath(); 192 | ctx.fillText('RAM', 925, 75); 193 | ctx.fillText('CPU', 925, 375); 194 | ctx.fill(); 195 | 196 | message.channel.send({ files: [{ attachment: canvas.toBuffer(), name: 'stats.png' }] }); 197 | }; 198 | 199 | exports.config = { 200 | enabled: true, 201 | permLevel: 2, 202 | category: 'developer' 203 | }; 204 | -------------------------------------------------------------------------------- /commands/eggs/bots.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | message.channel.send({files: ['http://i.imgur.com/gyqGdK5.jpg']}); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 1, 8 | perms: ['ATTACH_FILES'], 9 | category: 'eggs' 10 | }; 11 | 12 | exports.meta = { 13 | command: 'bots', 14 | name: 'bots', 15 | description: 'An easter egg based off bots', 16 | help: 'An easter egg based off bots' 17 | }; -------------------------------------------------------------------------------- /commands/eggs/daniel.js: -------------------------------------------------------------------------------- 1 | const { getSubredditMeme } = require('../fun/meme'); 2 | 3 | exports.run = message => { 4 | getSubredditMeme('ANormalDayInRussia').then(meme => { 5 | message.channel.send({ embeds: [{ 6 | author: { 7 | icon_url: 'https://cdn.discordapp.com/emojis/585296471880892461.png?v=1', 8 | name: meme.title 9 | }, 10 | image: { 11 | url: `https://i.imgur.com/${meme.hash}${meme.ext}` 12 | }, 13 | color: 0xff0019 14 | }]}); 15 | }).catch(() => { 16 | message.channel.send('didn\'t work, sorry. :('); 17 | }) 18 | }; 19 | 20 | exports.config = { 21 | enabled: true, 22 | permLevel: 1, 23 | aliases: [ 'russia', 'russiaman', 'vodka', 'bear' ], 24 | perms: [ 'ATTACH_FILES', 'EMBED_LINKS' ], 25 | category: 'eggs' 26 | }; 27 | 28 | exports.meta = { 29 | command: 'daniel', 30 | name: 'Daniel', 31 | description: 'Just for you, Daniel.', 32 | help: 'Just for you, Daniel.' 33 | }; -------------------------------------------------------------------------------- /commands/eggs/dude.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | message.channel.send({files: ['https://cdn.discordapp.com/attachments/304429067892031490/585329733210734592/20190603_132205.jpg']}); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 1, 8 | perms: ['ATTACH_FILES'], 9 | category: 'eggs' 10 | }; 11 | 12 | exports.meta = { 13 | command: 'dude', 14 | name: 'dude', 15 | description: 'As per request of Tudor, dude.', 16 | help: 'As per request of Tudor, dude.' 17 | }; -------------------------------------------------------------------------------- /commands/eggs/eli.js: -------------------------------------------------------------------------------- 1 | exports.run = message => { 2 | message.channel.send({ files: [ 'https://i.imgur.com/NssVxHZ.png' ]}); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 1, 8 | aliases: [ 'elijah' ], 9 | perms: [ 'ATTACH_FILES' ], 10 | category: 'eggs' 11 | }; 12 | 13 | exports.meta = { 14 | command: 'eli', 15 | name: 'Eli', 16 | description: 'Eli\'s lovely easter egg photo.', 17 | help: 'Eli\'s lovely easter egg photo.' 18 | }; -------------------------------------------------------------------------------- /commands/eggs/elog.js: -------------------------------------------------------------------------------- 1 | const LINKS = [ 'https://youtu.be/p5rQHoaQpTw', 'https://youtu.be/w9uWPBDHEKE', 'https://youtu.be/Fl-rYuI1-0w', 'https://youtu.be/qgDrpWWxuto', 'https://youtu.be/9vMLTcftlyI', 'https://youtu.be/zYh-nANS444' ]; 2 | 3 | exports.run = (message, args, suffix, client, permLevel, prefix, ipc) => { 4 | message.__ = (string, variables) => { 5 | return i18n.get('commands.play.' + string, message, variables); 6 | }; 7 | 8 | const song = LINKS[Math.floor(Math.random() * LINKS.length)]; 9 | 10 | client.commands.get('play').run(message, [ song ], song, client, permLevel, prefix, ipc).catch(client.errorLog.simple); 11 | }; 12 | 13 | exports.config = { 14 | enabled: 'true', 15 | permLevel: 2, 16 | perms: [ 'EMBED_LINKS', 'SPEAK', 'CONNECT' ], 17 | category: 'eggs' 18 | }; 19 | 20 | exports.meta = { 21 | command: 'elog', 22 | name: 'E Log', 23 | description: '"What\'s another way to say hello?"', 24 | help: '"What\'s another way to say hello?"' 25 | }; -------------------------------------------------------------------------------- /commands/eggs/fiona.js: -------------------------------------------------------------------------------- 1 | const morseEncode = { 2 | "0": "-----", 3 | "1": ".----", 4 | "2": "..---", 5 | "3": "...--", 6 | "4": "....-", 7 | "5": ".....", 8 | "6": "-....", 9 | "7": "--...", 10 | "8": "---..", 11 | "9": "----.", 12 | "a": ".-", 13 | "b": "-...", 14 | "c": "-.-.", 15 | "d": "-..", 16 | "e": ".", 17 | "f": "..-.", 18 | "g": "--.", 19 | "h": "....", 20 | "i": "..", 21 | "j": ".---", 22 | "k": "-.-", 23 | "l": ".-..", 24 | "m": "--", 25 | "n": "-.", 26 | "o": "---", 27 | "p": ".--.", 28 | "q": "--.-", 29 | "r": ".-.", 30 | "s": "...", 31 | "t": "-", 32 | "u": "..-", 33 | "v": "...-", 34 | "w": ".--", 35 | "x": "-..-", 36 | "y": "-.--", 37 | "z": "--..", 38 | ".": ".-.-.-", 39 | ",": "--..--", 40 | "?": "..--..", 41 | "!": "-.-.--", 42 | "-": "-....-", 43 | "/": "-..-.", 44 | "@": ".--.-.", 45 | "(": "-.--.", 46 | ")": "-.--.-", 47 | " ": "_", 48 | "": " " 49 | }; // thanks to (https://gist.github.com/mohayonao/094c71af14fe4791c5dd)! 50 | 51 | const waluigiEncode = { 52 | '.': 'wa', 53 | '-': 'waaa', 54 | ' ': 'woo', 55 | '_': 'wah' 56 | }; 57 | 58 | function reverse (object) { 59 | let keys = Object.keys(object); 60 | let vals = Object.values(object); 61 | 62 | let out = {}; 63 | 64 | for (let i = 0; i < keys.length; i++) { 65 | out[vals[i]] = keys[i]; 66 | } 67 | 68 | return out; 69 | } 70 | 71 | const morseDecode = reverse(morseEncode); 72 | const waluigiDecode = reverse(waluigiEncode); 73 | 74 | exports.run = (message, args) => { 75 | if (!args[0] || !args[1]) return message.channel.send('waaa wa woo waaa waaa waaa woo wa waaa wa waaa wa waaa'); 76 | 77 | let suffix = args.slice(1).join(' ').toLowerCase(); 78 | 79 | if (args[0] === 'e' || args[0] === 'encode') { 80 | suffix = Array.from(suffix); 81 | let out = ''; 82 | 83 | for (let i = 0; i < suffix.length; i++) { 84 | if (morseEncode[suffix[i]]) { 85 | let morse = Array.from(morseEncode[suffix[i]]); 86 | 87 | for (let j = 0; j < morse.length; j++) { 88 | out += waluigiEncode[morse[j]] + ' '; 89 | } 90 | 91 | if (i + 1 < suffix.length && suffix[i + 1] !== ' ' && suffix[i] !== ' ') out += 'woo ' 92 | } else out += '? ' 93 | } 94 | 95 | if (!out || out.length >= 2000) return message.channel.send(':('); 96 | 97 | message.channel.send(out); 98 | } else if (args[0] === 'd' || args[0] === 'decode') { 99 | suffix = suffix.split(' wah '); 100 | let out = ''; 101 | 102 | suffix.forEach(arr => { 103 | arr = arr.split('woo').filter(item => !!item); 104 | 105 | arr.forEach(a => { 106 | a = a.split(' ').filter(item => !!item); 107 | let temp = ''; 108 | 109 | a.forEach(text => { 110 | if (waluigiDecode[text]) temp += waluigiDecode[text]; 111 | else temp += '?' 112 | }); 113 | 114 | if (morseDecode[temp]) out += morseDecode[temp]; 115 | else out += '?'; 116 | }); 117 | 118 | out += ' '; 119 | }); 120 | 121 | if (!out || out.length >= 2000) return message.channel.send(':('); 122 | 123 | message.channel.send(out); 124 | } else message.channel.send('waaa wa woo waaa waaa waaa woo wa waaa wa waaa wa waaa'); 125 | }; 126 | 127 | exports.config = { 128 | enabled: true, 129 | permLevel: 1, 130 | aliases: [], 131 | perms: [ 'EMBED_LINKS' ], 132 | category: 'eggs' 133 | }; 134 | 135 | exports.meta = { 136 | command: 'fiona', 137 | name: 'Fiona', 138 | description: 'We speak in waah', 139 | help: 'waaa woo waaa waaa waaa woo wa waaa waaa wa wah wa wa wa woo wa woo waaa wa waaa wa woo wa waaa wa woo wa woo waaa' 140 | }; -------------------------------------------------------------------------------- /commands/eggs/fuckthis.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | message.channel.send({files: ['http://i.imgur.com/IC84G1h.gif']}); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 2, 8 | aliases: ['ft'], 9 | perms: ['ATTACH_FILES'], 10 | category: 'eggs' 11 | }; 12 | 13 | exports.meta = { 14 | command: 'fuckthis', 15 | name: 'fuckthis', 16 | description: 'An easter egg based off fuckthis', 17 | help: 'An easter egg based off fuckthis' 18 | }; -------------------------------------------------------------------------------- /commands/eggs/garfield.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | message.channel.send({files: ['https://i.imgur.com/PhY66kU.png']}); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 1, 8 | perms: ['ATTACH_FILES'], 9 | category: 'eggs' 10 | }; 11 | 12 | exports.meta = { 13 | command: 'garfield', 14 | name: 'garfield', 15 | description: 'An easter egg based off garfield', 16 | help: 'An easter egg based off garfield' 17 | }; 18 | -------------------------------------------------------------------------------- /commands/eggs/gymno.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | message.channel.send({files: ['http://i.imgur.com/IJCXt7X.jpg']}); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 2, 8 | aliases: ['jim'], 9 | perms: ['ATTACH_FILES'], 10 | category: 'eggs' 11 | }; 12 | 13 | exports.meta = { 14 | command: 'gymno', 15 | name: 'gymno', 16 | description: 'An easter egg based off gymno', 17 | help: 'An easter egg based off gymno' 18 | }; -------------------------------------------------------------------------------- /commands/eggs/india.js: -------------------------------------------------------------------------------- 1 | const needle = require('needle'); 2 | 3 | exports.run = async message => { 4 | let resp = await needle('get', 'https://icanhazdadjoke.com/', { headers: { Accept: 'text/plain' } }).catch(() => { 5 | message.channel.send('Dad jokes failed. rip.'); 6 | }); 7 | 8 | if (resp) message.channel.send(resp.body); 9 | }; 10 | 11 | exports.config = { 12 | enabled: true, 13 | permLevel: 1, 14 | aliases: [ 'ndya', 'wampler' ], 15 | perms: [ 'ATTACH_FILES' ], 16 | category: 'eggs' 17 | }; 18 | 19 | exports.meta = { 20 | command: 'india', 21 | name: 'India', 22 | description: 'Dad jokes ftw', 23 | help: 'Dad jokes ftw' 24 | }; 25 | -------------------------------------------------------------------------------- /commands/eggs/isaac.js: -------------------------------------------------------------------------------- 1 | const { createCanvas, loadImage } = require('canvas'); 2 | 3 | exports.run = async (message, args, suffix, client) => { 4 | const canvas = createCanvas(2048, 2048); 5 | const ctx = canvas.getContext('2d'); 6 | 7 | let userImage; 8 | let obj = client.findMember(message, suffix); 9 | if (obj) userImage = obj.user; 10 | else userImage = message.author; 11 | 12 | userImage = await loadImage(userImage.displayAvatarURL({ format: 'png', size: 2048 })); 13 | 14 | ctx.drawImage(userImage, 0, 0, 2048, 2048); 15 | 16 | ctx.rotate(-Math.PI / 4); 17 | ctx.fillStyle = '#f00'; 18 | ctx.font = '1065px RobotoMedium'; 19 | ctx.fillText('Yikes', -1229, 2048); 20 | 21 | message.channel.send({ files: [{ attachment: canvas.toBuffer(), name: 'yikes.png' }] }); 22 | }; 23 | 24 | exports.config = { 25 | enabled: true, 26 | permLevel: 1, 27 | perms: [ 'ATTACH_FILES' ], 28 | aliases: [ 'yikes' ], 29 | category: 'eggs' 30 | }; 31 | 32 | exports.meta = { 33 | command: 'isaac', 34 | name: 'Isaac', 35 | description: 'Yikes.', 36 | usage: '[user]', 37 | help: 'Yikes.' 38 | }; 39 | -------------------------------------------------------------------------------- /commands/eggs/jay.js: -------------------------------------------------------------------------------- 1 | exports.run = message => message.channel.send('he\'s a blue jay or something idk'); 2 | 3 | exports.config = { 4 | enabled: true, 5 | permLevel: 1, 6 | category: 'eggs' 7 | }; 8 | 9 | exports.meta = { 10 | command: 'jay', 11 | name: 'Jay', 12 | description: 'it\'s jay or something', 13 | help: 'just jay' 14 | }; -------------------------------------------------------------------------------- /commands/eggs/max.js: -------------------------------------------------------------------------------- 1 | const LINKS = [ 'https://youtu.be/O5zuXg4NyEU' ]; 2 | 3 | exports.run = (message, args, suffix, client, permLevel, prefix, ipc) => { 4 | message.__ = (string, variables) => { 5 | return i18n.get('commands.play.' + string, message, variables); 6 | }; 7 | 8 | const song = LINKS[Math.floor(Math.random() * LINKS.length)]; 9 | 10 | client.commands.get('play').run(message, [ song ], song, client, permLevel, prefix, ipc).catch(client.errorLog.simple); 11 | }; 12 | 13 | exports.config = { 14 | enabled: 'true', 15 | permLevel: 2, 16 | perms: [ 'EMBED_LINKS', 'SPEAK', 'CONNECT' ], 17 | category: 'eggs' 18 | }; 19 | 20 | exports.meta = { 21 | command: 'max', 22 | name: 'mr. pinecone', 23 | description: 'just ease off you\'re killin\' me', 24 | help: 'ease off, baby, just ease off you\'re killin\' me' 25 | }; -------------------------------------------------------------------------------- /commands/eggs/melon.js: -------------------------------------------------------------------------------- 1 | exports.run = message => { 2 | if (message.author.melon === true) return message.author.melon = false; 3 | if (!message.author.melon) return message.author.melon = true; 4 | }; 5 | 6 | exports.config = { 7 | enabled: true, 8 | permLevel: 2, 9 | category: 'eggs' 10 | }; 11 | 12 | exports.meta = { 13 | command: 'melon', 14 | name: 'melon', 15 | description: 'A special easter egg for melon.', 16 | help: 'Quite the different easter egg.' 17 | }; -------------------------------------------------------------------------------- /commands/eggs/nintenbot.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | const links = [ 3 | 'http://i.imgur.com/qKlnz5n.jpg', 4 | 'http://i.imgur.com/EWNKZfA.jpg', 5 | 'http://i.imgur.com/hqHdmel.jpg', 6 | 'http://i.imgur.com/HOSmQe1.jpg' 7 | ]; 8 | 9 | message.channel.send({files: [links[Math.floor(Math.random() * (links.length))]]}); 10 | }; 11 | 12 | exports.config = { 13 | enabled: true, 14 | permLevel: 2, 15 | aliases: ['ninten'], 16 | perms: ['ATTACH_FILES'], 17 | category: 'eggs' 18 | }; 19 | 20 | exports.meta = { 21 | command: 'nintenbot', 22 | name: 'NintenBot', 23 | description: 'An easter egg based off NintenBot', 24 | help: 'An easter egg based off NintenBot' 25 | }; -------------------------------------------------------------------------------- /commands/eggs/panda.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | message.channel.send({files: ['../media/images/magicpanda.jpg']}); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 1, 8 | perms: ['ATTACH_FILES'], 9 | category: 'eggs' 10 | }; 11 | 12 | exports.meta = { 13 | command: 'panda', 14 | name: 'panda', 15 | description: 'An easter egg for Lumine', 16 | help: 'An easter egg for Lumine' 17 | }; -------------------------------------------------------------------------------- /commands/eggs/rice.js: -------------------------------------------------------------------------------- 1 | const grainyRice = [ 2 | 'https://image.shutterstock.com/image-photo/rice-black-cup-on-background-600w-1692247006.jpg', 3 | 'https://image.shutterstock.com/image-photo/white-rice-bowl-600w-550853263.jpg', 4 | 'https://image.shutterstock.com/image-photo/white-rice-thai-jasmine-wooden-600w-535218229.jpg', 5 | 'https://image.shutterstock.com/image-photo/rice-wooden-bowl-isolated-on-600w-1105317755.jpg', 6 | 'https://image.shutterstock.com/image-photo/long-basmati-rice-600w-1095028934.jpg', 7 | 'https://image.shutterstock.com/image-photo/uncooked-rice-bowl-600w-1693528666.jpg', 8 | 'https://image.shutterstock.com/image-photo/streamed-sticky-rice-ceramic-cup-600w-1608821710.jpg', 9 | 'https://image.shutterstock.com/image-photo/bowl-tasty-cooked-rice-parsley-600w-1657394374.jpg', 10 | 'https://image.shutterstock.com/image-photo/thailand-rice-wooden-bowl-600w-567229531.jpg', 11 | 'https://image.shutterstock.com/image-photo/cooked-white-rice-thai-jasmine-600w-688370464.jpg', 12 | 'https://image.shutterstock.com/image-vector/two-handfuls-white-parboiled-red-600w-758406544.jpg', 13 | 'https://image.shutterstock.com/image-photo/white-rice-bowl-600w-560830615.jpg', 14 | 'https://image.shutterstock.com/image-photo/fried-rice-chicken-prepared-served-600w-677985067.jpg', 15 | 'https://image.shutterstock.com/image-photo/jasmine-rice-brown-red-riceblack-600w-661479826.jpg', 16 | 'https://image.shutterstock.com/image-photo/fried-rice-plate-on-table-600w-783015019.jpg', 17 | 'https://image.shutterstock.com/image-photo/veg-schezwan-fried-rice-black-600w-1517080016.jpg', 18 | 'https://image.shutterstock.com/image-photo/rice-bowl-on-white-background-600w-710865547.jpg', 19 | 'https://image.shutterstock.com/image-photo/rice-field-600w-175193915.jpg', 20 | 'https://image.shutterstock.com/image-photo/vietnam-farmer-bearing-seedlings-rice-600w-1127931986.jpg', 21 | 'https://image.shutterstock.com/image-photo/appetizing-healthy-rice-vegetables-white-600w-279069605.jpg', 22 | 'https://image.shutterstock.com/image-photo/bowl-rice-vegetables-isolated-on-600w-751234129.jpg', 23 | 'https://image.shutterstock.com/image-photo/raw-red-rice-wooden-bowl-600w-1470669227.jpg', 24 | 'https://media.istockphoto.com/photos/cooked-rice-picture-id491090528', 25 | 'https://media.istockphoto.com/photos/rice-picture-id916000818', 26 | 'https://media.istockphoto.com/photos/raw-white-rice-in-brown-bowl-and-and-ear-of-rice-or-unmilled-rice-on-picture-id974779604', 27 | 'https://media.istockphoto.com/photos/rice-picture-id916000818', 28 | 'https://media.istockphoto.com/photos/rice-picture-id671580286', 29 | 'https://media.istockphoto.com/photos/cauliflower-rice-with-basil-in-bowl-closeup-horizontal-top-view-picture-id622914636', 30 | 'https://media.istockphoto.com/photos/woman-cooking-rice-in-saucepan-on-stove-picture-id1147294766', 31 | 'https://media.istockphoto.com/photos/closeup-of-a-spatula-over-a-pan-of-rice-picture-id494494922', 32 | 'https://media.istockphoto.com/photos/uncooked-white-longgrain-rice-background-picture-id1069180776', 33 | 'https://media.istockphoto.com/photos/white-rice-in-burlap-sack-bag-isolated-on-white-background-picture-id827465154', 34 | 'https://media.istockphoto.com/photos/top-view-of-raw-jasmine-rice-in-a-bowl-and-wooden-spoon-on-dark-picture-id1076982398', 35 | 'https://media.istockphoto.com/photos/rice-in-transparent-plastic-bag-isolated-on-white-background-picture-id1011356342', 36 | 'https://media.istockphoto.com/photos/hyderabadi-chicken-biryani-with-cucumber-raita-on-rustic-table-picture-id923387432', 37 | 'https://media.istockphoto.com/photos/rice-fields-on-terraced-of-mu-cang-chai-yenbai-rice-fields-prepare-picture-id694050758', 38 | 'https://media.istockphoto.com/photos/indian-food-madras-beef-with-basmati-rice-horizontal-top-view-picture-id623426322', 39 | 'https://media.istockphoto.com/photos/cauliflower-rice-in-a-bowl-top-view-overhead-copy-space-picture-id827951624', 40 | 'https://media.istockphoto.com/photos/woman-cooking-rice-in-saucepan-on-stove-picture-id843260772', 41 | 'https://media.istockphoto.com/photos/cauliflower-rice-picture-id623175642', 42 | 'https://media.istockphoto.com/photos/lentils-and-rice-with-crispy-onions-and-parsley-picture-id529139494', 43 | 'https://media.istockphoto.com/photos/rice-cooked-with-vegetables-and-olive-oil-in-a-frying-pan-picture-id868047170', 44 | 'https://media.istockphoto.com/photos/arroz-de-carreteiro-picture-id962396116', 45 | 'https://media.istockphoto.com/photos/rinse-the-rice-picture-id468861012', 46 | 'https://media.istockphoto.com/photos/red-yeast-rice-picture-id698793546', 47 | 'https://media.istockphoto.com/photos/varieties-of-grains-seeds-and-beans-picture-id458218433', 48 | 'https://media.istockphoto.com/photos/mixed-boiled-rice-with-chilli-and-basil-dietary-menu-picture-id622192690', 49 | 'https://media.istockphoto.com/photos/rows-of-rice-picture-id172794933', 50 | 'https://media.istockphoto.com/photos/white-basmati-rice-scattered-picture-id174749280' 51 | ]; 52 | 53 | const humanyRice = [ 54 | 'https://i.imgur.com/btnY10d.jpg', 55 | 'https://i.imgur.com/ZBe06eO.jpg', 56 | 'https://i.imgur.com/F9oohhC.jpg', 57 | 'https://i.imgur.com/fc4mtGQ.jpg', 58 | 'https://i.imgur.com/T3MnMRx.jpg', 59 | 'https://i.imgur.com/Gd7Spia.jpg', 60 | 'https://i.imgur.com/4B386M0.jpg', 61 | 'https://i.imgur.com/fGMpnVa.jpg', 62 | 'https://i.imgur.com/SPFfNKP.jpg', 63 | 'https://i.imgur.com/9zYmTjf.jpg', 64 | 'https://i.imgur.com/wz5PmqB.jpg', 65 | 'https://i.imgur.com/bj5ONe3.jpg', 66 | 'https://i.imgur.com/uPlqYS0.jpg', 67 | 'https://i.imgur.com/hOWx7dt.jpg', 68 | 'https://i.imgur.com/UxZpVja.jpg', 69 | 'https://i.imgur.com/bz48P6e.jpg', 70 | 'https://i.imgur.com/AHtwjID.jpg', 71 | 'https://i.imgur.com/jzjgwF8.jpg', 72 | 'https://i.imgur.com/715qewy.jpg', 73 | 'https://i.imgur.com/rwsb6je.jpg', 74 | 'https://i.imgur.com/oMxOnrT.png', 75 | 'https://i.imgur.com/FW4bIwr.png', 76 | 'https://cdn.discordapp.com/attachments/692901031863910493/715026013431791726/unknown.png' 77 | ]; 78 | 79 | exports.run = (message) => { 80 | let file = Math.floor(Math.random() * 5) === 1 81 | ? humanyRice[Math.floor(Math.random() * humanyRice.length)] 82 | : grainyRice[Math.floor(Math.random() * grainyRice.length)]; 83 | 84 | message.channel.send({ embeds: [{ 85 | color: 0x00c140, 86 | image: { 87 | url: file 88 | } 89 | }] }); 90 | }; 91 | 92 | exports.config = { 93 | enabled: true, 94 | permLevel: 1, 95 | perms: ['ATTACH_FILES'], 96 | category: 'eggs' 97 | }; 98 | 99 | exports.meta = { 100 | command: 'rice', 101 | name: 'Rice', 102 | description: 'Here y\'are, Rice.', 103 | help: 'Here y\'are, Rice.' 104 | }; -------------------------------------------------------------------------------- /commands/eggs/roles.js: -------------------------------------------------------------------------------- 1 | exports.run = message => { 2 | message.channel.send(`**server roles**\n${message.guild.roles.cache.filter(r => r.name !== '@everyone').map(r => r.name).join(', ')}`); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 2, 8 | category: 'eggs' 9 | }; 10 | 11 | exports.meta = { 12 | command: 'roles', 13 | name: 'Roles', 14 | description: 'just lists roles', 15 | help: 'actually just lists roles? just for you shatterdpixel#6342 bb' 16 | }; -------------------------------------------------------------------------------- /commands/eggs/ryan.js: -------------------------------------------------------------------------------- 1 | exports.run = message => { 2 | if (message.author.id !== '304716594104369162') return message.channel.send('no.'); 3 | 4 | message.client.daniel = !message.client.daniel; 5 | if (message.client.daniel) message.channel.send('yep, ok.'); 6 | else message.channel.send('alright it\'s off.'); 7 | }; 8 | 9 | exports.config = { 10 | enabled: true, 11 | permLevel: 1, 12 | category: 'eggs' 13 | }; 14 | 15 | exports.meta = { 16 | command: 'ryan', 17 | name: 'Ryan', 18 | description: 'hehehehehehehe', 19 | help: 'hehehehehehehehehehehehehehe' 20 | }; 21 | -------------------------------------------------------------------------------- /commands/eggs/techno.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | message.channel.send('*Technocoder#9418 - 4/22/17 at 11:03 PM from <#219218693928910848>*\n```YOU\'VE HIDDEN THE LAMB SAUCE YOU LAZY FUCKING BASTARD\n```'); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 2, 8 | category: 'eggs' 9 | }; 10 | 11 | exports.meta = { 12 | command: 'techno', 13 | name: 'techno', 14 | description: 'An easter egg based off techno', 15 | help: 'An easter egg based off techno' 16 | }; -------------------------------------------------------------------------------- /commands/eggs/wap.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, args, suffix, client, permLevel, prefix, ipc) => { 2 | message.__ = (string, variables) => { 3 | return i18n.get('commands.play.' + string, message, variables); 4 | }; 5 | 6 | const song = 'https://youtu.be/7tBi4Z7yexY'; 7 | 8 | client.commands.get('play').run(message, [ song ], song, client, permLevel, prefix, ipc).catch(client.errorLog.simple); 9 | }; 10 | 11 | exports.config = { 12 | enabled: 'true', 13 | permLevel: 2, 14 | perms: [ 'EMBED_LINKS', 'SPEAK', 'CONNECT' ], 15 | category: 'eggs' 16 | }; 17 | 18 | exports.meta = { 19 | command: 'wap', 20 | name: 'wap, but gilbert gottfried', 21 | description: 'wap, babey', 22 | help: 'YEAH, YEAH, YEAH' 23 | }; -------------------------------------------------------------------------------- /commands/fun/dice.js: -------------------------------------------------------------------------------- 1 | const maximumRolls = 100; 2 | const diceFormat = /(?[\d]*d)?(?[\d]+)?(?\+[\d]+)?/; 3 | 4 | exports.run = (message, args) => { 5 | let rollCount = 1; 6 | let numberOfSides = 6; 7 | let additive = 0; 8 | 9 | if (args[0]) { 10 | let result = diceFormat.exec(args[0]); 11 | if (result.groups.rollCount) { 12 | let slice = result.groups.rollCount.slice(0, result.groups.rollCount.length - 1); 13 | if (slice.length !== 0) { 14 | let number = parseInt(slice); 15 | if (!number) return message.channel.send(message.__('invalid_number')); 16 | if (number < 1) return message.channel.send(message.__('negative_number')); 17 | rollCount = number; 18 | } 19 | } 20 | 21 | if (result.groups.sideCount) { 22 | let number = parseInt(result.groups.sideCount); 23 | if (!number) return message.channel.send(message.__('invalid_sides')); 24 | if (number < 1) return message.channel.send(message.__('negative_number')); 25 | numberOfSides = number; 26 | } 27 | 28 | if (result.groups.additive) { 29 | let number = parseInt(result.groups.additive); 30 | if (!number) return message.channel.send(message.__('invalid_number')); 31 | additive = number; 32 | } 33 | } 34 | 35 | if (rollCount > maximumRolls) 36 | return message.channel.send(message.__('excessive_rolls')); 37 | 38 | let summationArray = []; 39 | let summation = 0; 40 | for (let i = 0; i < rollCount; i++) { 41 | let num = Math.floor((Math.random() * numberOfSides) + 1); 42 | summationArray.push(num); 43 | summation += num; 44 | } 45 | 46 | if (additive > 0) summation += additive; 47 | 48 | let successMessage; 49 | if (additive === 0 && rollCount === 1) successMessage = message.__('success_roll', { result: summation }); 50 | else if (additive === 0) successMessage = message.__('success_roll_multiple', { roll: '**' + summationArray.join('** + **') + '**', result: summation }); 51 | else successMessage = message.__('success_roll_multiple', { roll: '(**' + summationArray.join('** + **') + '**) + **' + additive + '**', result: summation }); 52 | 53 | if (successMessage.length > 2000) message.channel.send(message.__('success_roll', { result: summation })); 54 | else message.channel.send(successMessage); 55 | }; 56 | 57 | exports.config = { 58 | enabled: true, 59 | permLevel: 1, 60 | category: 'fun' 61 | }; 62 | -------------------------------------------------------------------------------- /commands/fun/endpoll.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args, s, client) => { 2 | if (!args[0]) return message.channel.send(message.__('no_args')); // You're gonna have to provide me with the message ID of the poll you'd like to end. 3 | 4 | if (args[0].length < 17 || args[0].length > 19 || !parseInt(args[0])) return message.channel.send(message.__('invalid_args')); // Please provide a valid message ID (Developer mode must be turned on). See @@help @@endpoll for more details. 5 | 6 | let poll = client.reactionCollectors.get(args[0]); 7 | if (!poll) return message.channel.send(message.__('invalid_poll')); // The ID you provided is of a poll that no longer exists or never existed in the first place. Or it's an invalid ID. Either way, rip. 8 | 9 | if (poll.message.guild.id !== message.guild.id) return message.channel.send(message.__('wrong_guild')); // You're trying to stop a poll in another server? N.. no, okay? No! 10 | 11 | await message.channel.send(message.__('finished')); // You've successfully ended the poll early. Nice. 12 | poll.finish(); 13 | }; 14 | 15 | exports.config = { 16 | enabled: true, 17 | permLevel: 3, 18 | perms: [ 'EMBED_LINKS' ], 19 | category: 'other' 20 | }; 21 | -------------------------------------------------------------------------------- /commands/fun/hug.js: -------------------------------------------------------------------------------- 1 | const { createCanvas, loadImage } = require('canvas'); 2 | 3 | exports.run = async (message, args, suffix, client) => { 4 | let receiverURL = '../media/images/hugArthur.png'; 5 | 6 | if (args[0]) { 7 | let obj = client.findMember(message, suffix); 8 | if (obj) receiverURL = obj.user.displayAvatarURL({ format: 'png' }); 9 | } 10 | 11 | const canvas = createCanvas(300, 350); 12 | const ctx = canvas.getContext('2d'); 13 | 14 | let [ receiverImage, giverImage, backgroundImage ] = await Promise.all([ 15 | loadImage(receiverURL), 16 | loadImage(message.author.displayAvatarURL({ format: 'png' })), 17 | loadImage('../media/images/hug.png') 18 | ]); 19 | 20 | ctx.drawImage(backgroundImage, 0, 0); 21 | 22 | ctx.save(); 23 | ctx.translate(35, 20); 24 | ctx.rotate(-8 * Math.PI / 180); 25 | ctx.drawImage(receiverImage, 0, 0, 110, 110); 26 | ctx.restore(); 27 | 28 | ctx.translate(155, 15); 29 | ctx.rotate(-352 * Math.PI / 180); 30 | ctx.drawImage(giverImage, 0, 0, 110, 110); 31 | 32 | message.channel.send({ files: [{ attachment: canvas.toBuffer(), name: 'hug.png' }] }); 33 | }; 34 | 35 | exports.config = { 36 | enabled: true, 37 | permLevel: 2, 38 | category: 'fun' 39 | }; 40 | -------------------------------------------------------------------------------- /commands/fun/humongoji.js: -------------------------------------------------------------------------------- 1 | exports.run = async message => { 2 | let row = await sql.get(`SELECT humongoji FROM guildOptions WHERE guildID = '${message.guild.id}'`); 3 | 4 | if (!row) { 5 | sql.run(`INSERT INTO guildOptions (guildID, humongoji) VALUES (?, ?)`, [message.guild.id, 'true']); 6 | return message.channel.send(message.__('enabled')); 7 | } 8 | 9 | if (row.humongoji === 'true') { 10 | sql.run(`UPDATE guildOptions SET humongoji = 'false' WHERE guildID = '${message.guild.id}'`); 11 | message.channel.send(message.__('disabled')); 12 | } else { 13 | sql.run(`UPDATE guildOptions SET humongoji = 'true' WHERE guildID = '${message.guild.id}'`); 14 | message.channel.send(message.__('enabled')); 15 | } 16 | }; 17 | 18 | exports.config = { 19 | enabled: true, 20 | permLevel: 5, 21 | category: 'server_management' 22 | }; 23 | -------------------------------------------------------------------------------- /commands/fun/lenny.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, a, suffix) => { 2 | message.channel.send(suffix ? suffix + ' ( ͡° ͜ʖ ͡°)' : '( ͡° ͜ʖ ͡°)'); 3 | }; 4 | 5 | exports.config = { 6 | enabled: true, 7 | permLevel: 1, 8 | category: 'fun' 9 | }; -------------------------------------------------------------------------------- /commands/fun/meme.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | 3 | const MAX_AGE = 1000 * 60 * 15; // maximum subreddit cache age, in ms 4 | const subreddits = [ 'hmmm', 'dankmemes', 'memes', 'funny', 'deepfriedmemes', 'dadjokes', 'wholesomememes', 'meirl', 'me_irl', '2meirl4meirl' ]; 5 | 6 | // two properties: 7 | // lastUpdate: last time (from Date.now()) that subreddit was updated 8 | // data: subreddit data from request, parsed 9 | let subredditCache = {}; 10 | 11 | function updateSubreddit(subreddit) { 12 | return new Promise((resolve, reject) => { 13 | if (subredditCache[subreddit] && Date.now() - subredditCache[subreddit].lastUpdate < MAX_AGE) return resolve(); 14 | 15 | request(`https://imgur.com/r/${subreddit}/hot.json`, (err, response, body) => { 16 | if (err) return reject(); 17 | let json; 18 | 19 | try { 20 | json = JSON.parse(body); 21 | } catch (e) { 22 | return reject(); 23 | } 24 | 25 | if (!json || !json.data || !json.data[0]) return reject(); 26 | 27 | json.data = json.data.filter(data => { 28 | return !data.nsfw && data.num_images >= 1 && data.mimetype && data.mimetype.startsWith('image'); 29 | }); 30 | 31 | if (json.data.length < 1) return reject(); 32 | 33 | subredditCache[subreddit] = { 34 | data: json.data, 35 | lastUpdate: Date.now() 36 | }; 37 | 38 | resolve(); 39 | }); 40 | }); 41 | } 42 | 43 | function getSubredditMeme(subreddit) { 44 | return new Promise((resolve, reject) => { 45 | updateSubreddit(subreddit).then(() => { 46 | let { data } = subredditCache[subreddit]; 47 | 48 | resolve(data[Math.floor(Math.random() * data.length)]); 49 | }).catch(reject); 50 | }); 51 | } 52 | 53 | exports.run = (message, args) => { 54 | let subreddit = args[0] && subreddits.includes(args[0].toLowerCase()) 55 | ? args[0].toLowerCase() 56 | : subreddits[Math.floor(Math.random() * subreddits.length)]; 57 | 58 | getSubredditMeme(subreddit).then(meme => { 59 | message.channel.send({embeds: [{ 60 | title: meme.title, 61 | url: 'https://reddit.com' + meme.reddit, 62 | image: { 63 | url: `https://i.imgur.com/${meme.hash}${meme.ext}` 64 | }, 65 | color: 0x00c140, 66 | footer: { 67 | text: `Posted by u/${meme.author} on r/${subreddit}` 68 | } 69 | }]}) 70 | }).catch(() => { 71 | message.channel.send(message.__('error')); 72 | }); 73 | }; 74 | 75 | exports.config = { 76 | enabled: true, 77 | permLevel: 1, 78 | perms: [ 'EMBED_LINKS' ], 79 | category: 'fun' 80 | }; 81 | 82 | exports.cache = subredditCache; 83 | exports.getSubredditMeme = getSubredditMeme; 84 | -------------------------------------------------------------------------------- /commands/fun/owoify.js: -------------------------------------------------------------------------------- 1 | function owoify(text) { 2 | text = text.replace(/[lr]/g, 'w'); 3 | text = text.replace(/u/g, 'uw'); 4 | text = text.replace(/[LR]/g, 'W'); 5 | text = text.replace(/U/g, 'UW'); 6 | return text; 7 | } 8 | 9 | exports.run = (message, args, suffix) => { 10 | if (!args[0]) return message.channel.send(message.__('no_text')); 11 | message.channel.send({embeds: [{ 12 | description: owoify(suffix), 13 | color: 0x00c140 14 | }]}); 15 | }; 16 | 17 | exports.config = { 18 | enabled: true, 19 | permLevel: 1, 20 | category: 'fun' 21 | }; -------------------------------------------------------------------------------- /commands/fun/poll.js: -------------------------------------------------------------------------------- 1 | const ms = require('ms'); 2 | const moment = require('moment'); 3 | 4 | const { askWithCondition } = require('../../functions/askQuestion'); 5 | 6 | const emojis = [ '🇦', '🇧', '🇨', '🇩', '🇪', '🇫', '🇬', '🇭', '🇮', '🇯' ]; 7 | 8 | const titleCondition = (title) => { 9 | return !!title; 10 | }; 11 | 12 | const optionsCondition = options => { 13 | return options.split('|')[1] && options.split('|').length <= 10; 14 | }; 15 | 16 | const timeCondition = string => { 17 | let time = parseTimeString(string); 18 | return time && time < 604800001; 19 | }; 20 | 21 | function parseTimeString (string) { 22 | let time = 0; 23 | string.split(' ').forEach(s => { 24 | if (!s) return; 25 | let thingy = ms(s); 26 | if (!thingy) return; 27 | time += thingy; 28 | }); 29 | return time; 30 | } 31 | 32 | function emojiDescription (array) { 33 | let newArray = []; 34 | array.forEach((text, i) => { 35 | newArray.push(`${emojis[i]} ${text}`); 36 | }); 37 | return newArray.join('\n'); 38 | } 39 | 40 | async function addReactions (message, number) { 41 | for (let i = 0; i < number; i++) { 42 | await message.react(emojis[i]); 43 | } 44 | } 45 | 46 | function watch (message, options, endDate, client, embed) { 47 | const locale = i18n.getLocaleCode(message.guild); 48 | 49 | client.reactionCollectors.set(message.id, { 50 | message: message, 51 | number: options.length, 52 | options: options, 53 | embed: embed, 54 | locale: locale, 55 | finish: finishThisInstance 56 | }); 57 | 58 | function finishThisInstance() { 59 | finish(message.id, client, locale); 60 | } 61 | 62 | setTimeout(() => { 63 | finishThisInstance(); 64 | }, endDate - Date.now()); 65 | } 66 | 67 | async function finish (messageID, client, locale) { 68 | let obj = client.reactionCollectors.get(messageID); 69 | if (!obj) return; 70 | 71 | let theseEmojis = emojis.slice(0, obj.options.length); 72 | let emojiObject = {}; 73 | let { embed } = obj; 74 | 75 | await Promise.all(obj.message.reactions.cache.map(reaction => { 76 | return new Promise(async resolve => { 77 | await reaction.users.fetch(); 78 | if (!theseEmojis.includes(reaction.emoji.name)) return resolve(); 79 | emojiObject[reaction.emoji.name] = reaction.users.cache.size - 1; 80 | resolve(); 81 | }); 82 | })); 83 | 84 | let total = calculateTotalResults(theseEmojis, emojiObject); 85 | embed.description = finishedEmojiDescription(theseEmojis, emojiObject, obj.options, total); 86 | embed.footer.text = i18n.getString('commands.poll.ended', locale); 87 | embed.title = i18n.getString('commands.poll.finished', locale) + embed.title; 88 | embed.color = 0x42f4a1; 89 | 90 | obj.message.channel.send({ embeds: [ obj.embed ] }); 91 | obj.message.delete().catch(() => {}); 92 | sql.run('DELETE FROM pollReactionCollectors WHERE messageID = ?', [obj.message.id]).catch(console.log); 93 | client.reactionCollectors.delete(messageID); 94 | } 95 | 96 | function finishedEmojiDescription (emojiArray, emojiObject, options, total) { 97 | let final = []; 98 | emojiArray.forEach((emoji, i) => { 99 | let count = emojiObject[emoji]; 100 | let percentage = Math.round(count / total * 100); 101 | let fullEmojis = Math.round(percentage / 10); 102 | final.push(`${options[i]} - ${count} of ${total} - ${percentage}%\n${':large_blue_diamond:'.repeat(fullEmojis)}${':black_small_square:'.repeat(10 - fullEmojis)}`); 103 | }); 104 | return final.join('\n'); 105 | } 106 | 107 | function calculateTotalResults (emojiArray, emojiObject) { 108 | let total = 0; 109 | emojiArray.forEach(emoji => { 110 | total += emojiObject[emoji]; 111 | }); 112 | return total; 113 | } 114 | 115 | exports.run = async (message, a, s, client) => { 116 | const footer = { 117 | text: message.__('footer') 118 | }; 119 | 120 | const titleEmbed = { 121 | title: message.__('title.title'), 122 | description: message.__('title.description'), 123 | color: 0x007c29, 124 | footer: footer 125 | }; 126 | 127 | const optionsEmbed = { 128 | title: message.__('options.title'), 129 | description: message.__('options.description'), 130 | color: 0x00892d, 131 | footer: footer 132 | }; 133 | 134 | const timeEmbed = { 135 | title: message.__('time.title'), 136 | description: message.__('time.description'), 137 | color: 0x008e2e, 138 | footer: footer 139 | }; 140 | 141 | 142 | let title; 143 | let options; 144 | let time; 145 | let embedMessage; 146 | let err; 147 | 148 | try { 149 | let titleObj = await askWithCondition(message.channel, titleEmbed, message.author.id, undefined, 1, undefined, titleCondition); 150 | title = titleObj.response; 151 | let optionsObj = await askWithCondition(message.channel, optionsEmbed, message.author.id, titleObj.message, 1, undefined, optionsCondition); 152 | options = optionsObj.response; 153 | let timeObj = await askWithCondition(message.channel, timeEmbed, message.author.id, optionsObj.message, 1, undefined, timeCondition); 154 | time = timeObj.response; 155 | embedMessage = timeObj.message; 156 | } catch (e) { 157 | err = e; 158 | } 159 | 160 | if (err) return; 161 | 162 | options = options.split('|'); 163 | options.forEach((op, i) => { 164 | options[i] = op.replace(/^ *(.*) *$/g, '$1'); 165 | }); 166 | time = parseTimeString(time); 167 | 168 | let embed = { 169 | title: title, 170 | description: emojiDescription(options), 171 | footer: { 172 | text: message.__('ends') + ' ' 173 | }, 174 | timestamp: moment(Date.now() + time).toISOString(), 175 | color: 0x00c140 176 | }; 177 | 178 | embedMessage.edit({ embeds: [ embed ] }); 179 | 180 | await addReactions(embedMessage, options.length).catch(() => {}); 181 | 182 | let endDate = Date.now() + time; 183 | 184 | await sql.run('INSERT INTO pollReactionCollectors (channelID, messageID, options, endDate, embed) VALUES (?, ?, ?, ?, ?)', 185 | [embedMessage.channel.id, embedMessage.id, JSON.stringify(options), endDate, JSON.stringify(embed)]).catch(console.log); 186 | 187 | watch(embedMessage, options, endDate, client, embed); 188 | }; 189 | 190 | exports.config = { 191 | enabled: true, 192 | permLevel: 3, 193 | perms: [ 'EMBED_LINKS', 'ADD_REACTIONS' ], 194 | category: 'other' 195 | }; 196 | 197 | exports.watch = watch; 198 | exports.emojis = emojis; 199 | -------------------------------------------------------------------------------- /commands/fun/reverse.js: -------------------------------------------------------------------------------- 1 | function reverse(text) { 2 | let array = text.split(''); 3 | array.reverse(); 4 | return array.join(''); 5 | } 6 | 7 | exports.run = (message, args, suffix) => { 8 | if (!args[0]) return message.channel.send(message.__('no_args')); 9 | message.channel.send({embeds: [{ 10 | description: reverse(suffix), 11 | color: 0x00c140 12 | }]}); 13 | }; 14 | 15 | exports.config = { 16 | enabled: true, 17 | permLevel: 1, 18 | category: 'fun' 19 | }; -------------------------------------------------------------------------------- /commands/fun/spacify.js: -------------------------------------------------------------------------------- 1 | function spacify(text) { 2 | let array = text.split(''); 3 | let n = text.length; 4 | let out = ''; 5 | 6 | for (let i = 0; i < n - 1; i++) { 7 | if (array[i] === ' ') out += ' '; 8 | else out += array[i] + ' '; 9 | } 10 | 11 | out += array[n - 1]; 12 | 13 | return out; 14 | } 15 | 16 | exports.run = (message, args, suffix) => { 17 | if (!args[0]) return message.channel.send(message.__('no_args')); 18 | 19 | let desc = '```' + spacify(suffix) + '```'; 20 | if (desc.length > 2048) return message.channel.send(message.__('too_big')); 21 | 22 | message.channel.send({ 23 | embeds: [{ 24 | description: '```' + spacify(suffix) + '```', 25 | color: 0x00c140 26 | }] 27 | }); 28 | }; 29 | 30 | exports.config = { 31 | enabled: true, 32 | permLevel: 1, 33 | category: 'fun' 34 | }; 35 | -------------------------------------------------------------------------------- /commands/fun/spongebob.js: -------------------------------------------------------------------------------- 1 | function alternateCaps(text) { 2 | let array = text.split(''); 3 | let n = text.length; 4 | let out = ''; 5 | let caps = false; 6 | 7 | for (let i = 0; i < n; i++) { 8 | if (!/[A-Za-z]/.test(array[i])) { 9 | out += array[i]; 10 | continue; 11 | } 12 | 13 | if (caps) out += array[i].toUpperCase(); 14 | else out += array[i].toLowerCase(); 15 | 16 | caps = !caps; 17 | } 18 | 19 | return out; 20 | } 21 | 22 | exports.run = (message, args, suffix) => { 23 | if (!args[0]) return message.channel.send(message.__('no_args')); 24 | 25 | message.channel.send({ 26 | embeds: [{ 27 | description: alternateCaps(suffix), 28 | color: 0x00c140 29 | }] 30 | }); 31 | }; 32 | 33 | exports.config = { 34 | enabled: true, 35 | permLevel: 1, 36 | category: 'fun' 37 | }; 38 | -------------------------------------------------------------------------------- /commands/fun/stopwatch.js: -------------------------------------------------------------------------------- 1 | const { timeString } = require('../../struct/Util.js'); 2 | 3 | exports.run = (message, args, s, client) => { 4 | client.shard.send({ action: 'stopwatch', id: message.author.id }).catch(client.errorLog.simple); 5 | client.shardQueue.set(message.author.id, object => { 6 | if (!object.start) message.channel.send(message.__('stopwatch_started')); 7 | else message.channel.send(message.__('stopwatch_stopped', { time: timeString(Math.ceil( (Date.now() - object.start ) / 1000), message) })); 8 | }); 9 | }; 10 | 11 | exports.config = { 12 | enabled: true, 13 | permLevel: 1, 14 | category: 'fun' 15 | }; -------------------------------------------------------------------------------- /commands/help.js: -------------------------------------------------------------------------------- 1 | const permLevel = require('../functions/permLevel'); 2 | 3 | const forEach = function (obj, loop) { 4 | let a = Object.keys(obj); 5 | for (let i = 0; i < a.length; i++) { 6 | loop(obj[a[i]], a[i]); 7 | } 8 | }; 9 | 10 | exports.run = async (message, args, suffix, client, perms, prefix) => { 11 | if (!args[0] || args[0] === 'dev' || args[0] === 'eggs' || args[0] === message.__('chat_flag')) { 12 | let commands = client.commands.filter(c => c.config.permLevel <= (perms === 1 ? 2 : perms)); 13 | let categories = {}; 14 | let fields = []; 15 | commands.forEach((com, name) => { 16 | let meta; 17 | if (com.config.category === 'sound_effects') { 18 | meta = { 19 | command: name, 20 | name: name.substring(0, 1).toUpperCase() + name.substring(1), 21 | description: message.__('sound_effects.description', { name }) 22 | } 23 | } else try { 24 | meta = i18n.getMeta(name, message) || com.meta; 25 | } catch (e) { 26 | if (e.toString() !== `en-US locale missing string commands.${name}.meta`) client.errorLog('Error getting meta for command while generating help'. e); 27 | } 28 | if (!meta) return; 29 | 30 | if (args[0] === 'dev' && client.config.owners.includes(message.author.id) && com.config.category !== 'developer') return; 31 | else if (args[0] === 'eggs' && client.config.owners.includes(message.author.id) && com.config.category !== 'eggs') return; 32 | else if (com.config.category === 'developer' || com.config.category === 'eggs') return; 33 | 34 | if (!categories.hasOwnProperty(com.config.category)) categories[com.config.category] = []; 35 | categories[com.config.category].push(`**${prefix}${meta.command}** \u203a ${meta.description}`); 36 | }); 37 | 38 | forEach(categories, (coms, cat) => { 39 | fields.push({ 40 | name: message.__(`categories.${cat}`), 41 | value: coms.join('\n'), 42 | inline: true 43 | }); 44 | }); 45 | 46 | const invite = await client.generateInvite({ scopes: [ 'bot', 'applications.commands' ], permissions: BigInt(client.config.info.invitePerms) }); 47 | let embed = { 48 | color: 0x00c140, 49 | author: { 50 | name: message.__('arthur_help'), 51 | icon_url: 'https://cdn.discordapp.com/attachments/219218693928910848/361405047608705025/arthur_but_hot.png' 52 | }, 53 | description: `[${message.__('large_embed.invite')}](${invite}) | [GitHub](https://github.com/nikbrandt/Arthur) | [${message.__('large_embed.support_server')}](${client.config.info.guildLink}) | [Trello](https://trello.com/b/wt7ptHpC/arthur) | [${message.__('large_embed.tos')}](https://arthur.wumpler.com/tos) | [${message.__('large_embed.privacy_policy')}](https://arthur.wumpler.com/privacy)\n${message.__('large_embed.description')}`, 54 | fields, 55 | footer: { 56 | text: message.__('large_embed.footer', { prefix }) 57 | } 58 | }; 59 | 60 | if (message.guild) { 61 | if (args[0] === message.__('chat_flag')) message.channel.send({ embeds: [ embed ] }); 62 | else { 63 | let msg = await message.channel.send(message.__('check_dms')); 64 | message.author.send({ embeds: [ embed ] }).catch(() => { 65 | if (msg) msg.edit(message.__('send_failed', { prefix })); 66 | }); 67 | } 68 | } else message.author.send({ embeds: [ embed ] }).catch(() => {}); 69 | } else { 70 | let name = i18n.getCommandFileName(args[0], message) || args[0]; 71 | let command = client.commands.get(name); 72 | if (!command) return message.channel.send(message.__('invalid_command', { command: args[0] })); 73 | 74 | let meta; 75 | 76 | if (command.config.category === 'sound_effects') { 77 | meta = { 78 | name: name.substring(0, 1).toUpperCase() + name.substring(1), 79 | help: message.__('sound_effects.help', { name }) 80 | } 81 | } else try { 82 | meta = i18n.getMeta(name, message) || command.meta; 83 | } catch (e) { 84 | if (e !== `en-US locale missing string commands.${name}.meta`) client.errorLog('Error getting individual command meta for help command display', e); 85 | } 86 | 87 | if (!meta) return message.channel.send(message.__('invalid_command', { command: args[0] })); 88 | 89 | message.channel.send({embeds: [{ 90 | color: 0x00c140, 91 | fields: [ 92 | { 93 | name: meta.name, 94 | value: `${meta.help}\n${message.__('small_embed.usage')} \`${prefix}${name}${meta.usage ? " " + meta.usage : ""}\`${meta.aliases && meta.aliases.length > 0 ? `\n${message.__('small_embed.aliases')} ${meta.aliases.join(', ')}` : ''}`, 95 | inline: true 96 | }, 97 | { 98 | name: message.__('small_embed.advanced'), 99 | value: `${command.config.cooldown ? '**' + Math.round(command.config.cooldown / 1000) + i18n.get('time.abbreviations.seconds', message) + '** ' + message.__('small_embed.cooldown') + '\n' : ''}${command.config.guildCooldown ? '**' + Math.round(command.config.guildCooldown / 1000) + i18n.get('time.abbreviations.seconds', message) + '** ' + message.__('small_embed.server_cooldown') + '\n' : ''}${i18n.get(`perm_levels.${command.config.permLevel}`, message)}\n${command.config.perms ? message.__('small_embed.requires_bot_perms') + i18n.getPermsString(command.config.perms, message) + '\n' : ''}${command.config.userPerms ? message.__('small_embed.requires_member_perms') + i18n.getPermsString(command.config.userPerms, message) : ''}`, 100 | inline: true 101 | } 102 | ], 103 | footer: {text: message.__('small_embed.footer', { category: message.__(`categories.${command.config.category}`) }) } 104 | }]}); 105 | } 106 | }; 107 | 108 | exports.config = { 109 | enabled: true, 110 | permLevel: 1, 111 | perms: ['EMBED_LINKS'], 112 | category: 'other' 113 | }; -------------------------------------------------------------------------------- /commands/i18n/guildlanguage.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args, suffix, client, permLevel) => { 2 | if (!args[0] || permLevel < 4) { 3 | let locale = await i18n.getGuildLocaleString(message.guild.id); 4 | return message.channel.send(message.__('current_language', { locale })); 5 | } 6 | 7 | let indexOfDash = args[0].indexOf('-'); 8 | 9 | if (indexOfDash > 0) args[0] = args[0].substring(0, indexOfDash).toLowerCase() + args[0].substring(indexOfDash).toUpperCase(); 10 | else args[0] = args[0].toLowerCase(); 11 | 12 | args[0] = args[0].trim(); 13 | 14 | let locales = i18n.getLocales(); 15 | 16 | if (!locales.includes(args[0])) return message.channel.send(message.__('invalid_locale', { locales: locales.map(locale => '`' + locale + '`').join(', ') })); 17 | 18 | await i18n.setGuildLocale(message.guild.id, args[0]); 19 | message.channel.send(message.__('locale_set')); 20 | }; 21 | 22 | exports.config = { 23 | enabled: true, 24 | permLevel: 2, 25 | category: 'i18n' 26 | }; 27 | -------------------------------------------------------------------------------- /commands/i18n/langinfo.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, args) => { 2 | if (!args[0]) return message.channel.send(message.__('no_language_specified')); 3 | 4 | let indexOfDash = args[0].indexOf('-'); 5 | 6 | if (indexOfDash > 0) args[0] = args[0].substring(0, indexOfDash).toLowerCase() + args[0].substring(indexOfDash).toUpperCase(); 7 | else args[0] = args[0].toLowerCase(); 8 | 9 | args[0] = args[0].trim(); 10 | 11 | let locale = i18n.getLocale(args[0]); 12 | if (!locale) return message.channel.send(message.__('locale_not_found')); 13 | 14 | let { meta } = locale; 15 | 16 | message.channel.send({ embeds: [{ 17 | title: meta.lang, 18 | description: `:flag_${meta.flag}: *${i18n.get(`meta.translations.${args[0]}`, message)}*\n\n` + message.__('description', { authors: meta.authors.join(', '), percent: meta.percentComplete, code: args[0] }), 19 | color: 0x00c140 20 | }]}); 21 | }; 22 | 23 | exports.config = { 24 | enabled: true, 25 | permLevel: 1, 26 | category: 'i18n' 27 | }; 28 | -------------------------------------------------------------------------------- /commands/i18n/language.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args) => { 2 | if (!args[0]) { 3 | let locale = await i18n.getUserLocaleString(message.author.id); 4 | return message.channel.send(message.__('current', { locale })); 5 | } 6 | 7 | if (args[0].toLowerCase() === message.__('none')) { 8 | await i18n.removeUserLocale(message.author.id); 9 | return message.channel.send(message.__('removed')); 10 | } 11 | 12 | let indexOfDash = args[0].indexOf('-'); 13 | 14 | if (indexOfDash > 0) args[0] = args[0].substring(0, indexOfDash).toLowerCase() + args[0].substring(indexOfDash).toUpperCase(); 15 | else args[0] = args[0].toLowerCase(); 16 | 17 | args[0] = args[0].trim(); 18 | 19 | let locales = i18n.getLocales(); 20 | 21 | if (!locales.includes(args[0])) return message.channel.send(i18n.get('commands.guildlanguage.invalid_locale', message, { locales: locales.map(locale => '`' + locale + '`').join(', ') })); 22 | 23 | await i18n.setUserLocale(message.author.id, args[0]); 24 | message.channel.send(message.__('locale_set')); 25 | }; 26 | 27 | exports.config = { 28 | enabled: true, 29 | permLevel: 1, 30 | category: 'i18n' 31 | }; 32 | -------------------------------------------------------------------------------- /commands/i18n/languages.js: -------------------------------------------------------------------------------- 1 | exports.run = (message) => { 2 | const locales = i18n.getAllLocaleMeta(); 3 | 4 | let descriptionArray = []; 5 | let localLocaleCode = i18n.getLocaleCode(message); 6 | let localLocale = locales.get(localLocaleCode); 7 | 8 | [...locales.keys()].forEach(locale => { 9 | let meta = locales.get(locale); 10 | descriptionArray.push(`:flag_${meta.flag}: \`${locale}\` **${meta.lang}** (${localLocale.translations[locale]}) | ${message.__('percent_complete', { percent: meta.percentComplete } )}`); 11 | }); 12 | 13 | message.channel.send({ embeds: [{ 14 | title: message.__('languages'), 15 | description: descriptionArray.join('\n') + '\n\n' + message.__('description'), 16 | color: 0x00c140 17 | }]}); 18 | }; 19 | 20 | exports.config = { 21 | enabled: true, 22 | permLevel: 1, 23 | category: 'i18n' 24 | }; 25 | -------------------------------------------------------------------------------- /commands/info.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const moment = require('moment'); 3 | 4 | exports.run = async (message, args, asdf, client) => { 5 | const invite = await client.generateInvite({ scopes: [ 'bot', 'applications.commands' ], permissions: BigInt(client.config.info.invitePerms) }); 6 | 7 | let locale = i18n.getLocaleCode(message); 8 | if (locale === 'en-US') locale = 'en'; 9 | 10 | message.channel.send({embeds: [{ 11 | author: { 12 | name: message.__('title'), 13 | icon_url: 'https://cdn.discordapp.com/attachments/219218693928910848/361405047608705025/arthur_but_hot.png' 14 | }, 15 | description: `${message.__('description')}\n [${i18n.get('commands.help.large_embed.invite', message)}](${invite}) | [GitHub](https://github.com/nikbrandt/Arthur) | [${i18n.get('commands.help.large_embed.support_server', message)}](${client.config.info.guildLink}) | [Trello](https://trello.com/b/wt7ptHpC/arthur) | [${i18n.get('commands.help.large_embed.tos', message)}](https://arthur.wumpler.com/tos) | [${i18n.get('commands.help.large_embed.privacy_policy', message)}](https://arthur.wumpler.com/privacy)`, 16 | color: 0x00c140, 17 | fields: [ 18 | { 19 | name: message.__('author'), 20 | value: 'Gymno#4741', 21 | inline: true 22 | }, 23 | { 24 | name: message.__('help'), 25 | value: '@Arthur ' + i18n.get('commands.help.meta.command', message), 26 | inline: true 27 | }, 28 | { 29 | name: message.__('info'), 30 | value: message.__('info_value', { 31 | nodeVersion: process.version, 32 | discordVersion: Discord.version, 33 | uptime: moment.duration(process.uptime() * 1000).locale(locale).humanize(), 34 | shard: client.shard.id, 35 | managerUptime: moment.duration(Date.now() - client.shard.uptimeStart).locale(locale).humanize() 36 | }) 37 | } 38 | ] 39 | }]}); 40 | }; 41 | 42 | exports.config = { 43 | enabled: true, 44 | permLevel: 1, 45 | perms: ['EMBED_LINKS'], 46 | category: 'other' 47 | }; -------------------------------------------------------------------------------- /commands/invite.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args, suffix, client) => { 2 | let invite = await client.generateInvite({ scopes: [ 'bot', 'applications.commands' ], permissions: BigInt(client.config.info.invitePerms) }); 3 | 4 | message.channel.send({embeds: [{ 5 | description: message.__('description', { invite }), 6 | color: 0x00c140 7 | }]}); 8 | }; 9 | 10 | exports.config = { 11 | enabled: true, 12 | permLevel: 1, 13 | perms: ['EMBED_LINKS'], 14 | category: 'other' 15 | }; -------------------------------------------------------------------------------- /commands/levels/leaderboard.js: -------------------------------------------------------------------------------- 1 | const XP = require('../../struct/xp.js'); 2 | 3 | exports.run = async (message, args, suffix, client) => { 4 | let guildRow = await sql.get(`SELECT * FROM guildOptions WHERE guildID = '${message.guild.id}'`); 5 | if (!guildRow || guildRow.levels === 'false') return; 6 | 7 | if (args[0] === message.__('server') || args[0] === message.__('guild') || args[0] === message.__('server_abbreviation') || !args[0] || (args[0].length > 0 && args[0].length < 4 && args[0] !== message.__('global_abbreviation')) || args[0].length === 19) { 8 | let guildID = message.guild.id; 9 | 10 | let page = 1; 11 | if (args[1] || args[0]) { 12 | if ((args[0] === message.__('server') || args[0] === message.__('guild') || args[0] === message.__('server_abbreviation')) && !args[1]) args[1] = '1'; 13 | let tempPage = parseInt(args[0].length < 4 && args[0] !== message.__('server') && args[0] !== message.__('guild') && args[0] !== message.__('server_abbreviation') ? args[0] : args[1], 10); 14 | if (isNaN(tempPage)) return message.channel.send(message.__('invalid_page', { page: args[0].length < 4 ? args[0] : args[1] })); 15 | if (tempPage < 1) return message.channel.send(message.__('negative_or_zero_page')); 16 | page = Math.floor(tempPage); 17 | } 18 | 19 | let rank = await XP.guildRank(message.member); 20 | let list = await XP.guildLeaderboard(guildID, page, client, i18n.getLocaleCode(message)); 21 | if (!list) return message.channel.send(message.__('not_enough_people', { page })); 22 | let guild = client.guilds.cache.get(guildID); 23 | 24 | message.channel.send({embeds: [{ 25 | color: 0x8356ff, 26 | author: { 27 | name: guild.name, 28 | icon_url: guild.iconURL() 29 | }, 30 | footer: { text: message.__('footer', { page, maxPage: list.max, end: !!rank ? ' | ' + message.__('footer_rank', { rank: rank.rank, page: rank.page }) : '' }) }, 31 | description: list.array.join('\n') 32 | }]}); 33 | } 34 | 35 | if (args[0] === message.__('global') || args[0] === message.__('global_abbreviation')) { 36 | let pg = 1; 37 | if (args[1]) { 38 | let tempPage = parseInt(args[1], 10); 39 | if (isNaN(tempPage)) return message.channel.send(message.__('invalid_page', { page: args[0] })); 40 | if (tempPage < 1) return message.channel.send(message.__('negative_or_zero_page')); 41 | pg = Math.floor(tempPage); 42 | } 43 | 44 | let gRank = await XP.globalRank(message.author); 45 | let gList = await XP.globalLeaderboard(pg, client); 46 | if (!gList) return message.channel.send(message.__('not_enough_people', { page: pg })); 47 | 48 | message.channel.send({embeds: [{ 49 | author: { 50 | name: message.__('global_leaderboard'), 51 | icon_url: client.user.displayAvatarURL() 52 | }, 53 | color: 0x8356ff, 54 | footer: { text: message.__('footer', { page: pg, maxPage: gList.max, end: !!gRank ? ' | ' + message.__('footer_rank', { rank: gRank.rank, page: gRank.page }) : '' })}, 55 | description: gList.array.join('\n') 56 | }]}); 57 | } 58 | }; 59 | 60 | exports.config = { 61 | enabled: true, 62 | permLevel: 2, 63 | perms: ['EMBED_LINKS'], 64 | category: 'levels' 65 | }; 66 | -------------------------------------------------------------------------------- /commands/levels/levels.js: -------------------------------------------------------------------------------- 1 | exports.run = async message => { 2 | let row = await sql.get(`SELECT * FROM guildOptions WHERE guildID = '${message.guild.id}'`); 3 | 4 | if (!row) { 5 | sql.run(`INSERT INTO guildOptions (guildID, levels) VALUES (?, ?)`, [message.guild.id, 'true']); 6 | return message.channel.send(message.__('leveling_enabled')); 7 | } 8 | 9 | if (row.levels === 'true') { 10 | sql.run(`UPDATE guildOptions SET levels = 'false' WHERE guildID = '${message.guild.id}'`); 11 | message.channel.send(message.__('leveling_disabled')); 12 | } else { 13 | sql.run(`UPDATE guildOptions SET levels = 'true' WHERE guildID = '${message.guild.id}'`); 14 | message.channel.send(message.__('leveling_enabled')); 15 | } 16 | }; 17 | 18 | exports.config = { 19 | enabled: true, 20 | permLevel: 5, 21 | category: 'server_management' 22 | }; -------------------------------------------------------------------------------- /commands/levels/xp.js: -------------------------------------------------------------------------------- 1 | const XP = require('../../struct/xp.js'); 2 | 3 | exports.run = async (message, args, suffix, client) => { 4 | let guildRow = await sql.get(`SELECT * FROM guildOptions WHERE guildID = '${message.guild.id}'`); 5 | if (!guildRow || guildRow.levels === 'false') return; 6 | 7 | let memObj = client.findMember(message, suffix); 8 | let mem; 9 | if (!memObj) mem = message.member; 10 | else mem = memObj.member; 11 | 12 | let xpObj = await XP.memberXP(mem); 13 | let nextLevel = client.config.xp.levelOne * Math.pow(client.config.xp.mult, xpObj.level); 14 | let neededXP = Math.floor((nextLevel) - xpObj.current) * 10 / 10; 15 | let percent = Math.round(xpObj.current / nextLevel * 1000) / 10; 16 | 17 | let gRank = await XP.globalRank(mem.user); 18 | let rank = await XP.guildRank(mem); 19 | 20 | message.channel.send({embeds: [{ 21 | author: { 22 | name: mem.displayName + '\'s XP', 23 | icon_url: mem.user.displayAvatarURL() 24 | }, 25 | color: 0x8356ff, 26 | description: message.__('description', { xp: xpObj.global, end: gRank ? ' - ' + message.__('rank_on_page', { rank: gRank.rank, page: gRank.page }) : '' }), 27 | fields: [ 28 | { 29 | name: message.__('guild_xp'), 30 | value: message.__('current_xp', { 31 | current: xpObj.current, 32 | total: xpObj.current === xpObj.total ? '' : ', ' + message.__('total') + `: **${xpObj.total}**`, 33 | level: xpObj.level, 34 | rank: rank ? '\n' + message.__('rank') + `: **${rank.rank}**, ` + message.__('page') + `: **${rank.page}**.` : '' 35 | }), 36 | inline: true 37 | }, 38 | { 39 | name: 'Progress', 40 | value: `[${':dollar:'.repeat(parseInt(percent.toString().slice(0, 1), 10))}${':yen:'.repeat(10 - parseInt(percent.toString().slice(0, 1), 10))}]\n**${neededXP}** ${message.__('xp_to_level')} ${xpObj.level + 1}. (${percent}%)`, 41 | inline: true 42 | } 43 | ] 44 | }]}); 45 | }; 46 | 47 | exports.config = { 48 | enabled: true, 49 | permLevel: 2, 50 | perms: ['EMBED_LINKS'], 51 | category: 'levels' 52 | }; 53 | -------------------------------------------------------------------------------- /commands/levels/xpinfo.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, ar, sui, client) => { 2 | let levels = await sql.get(`SELECT levels FROM guildOptions WHERE guildID = '${message.guild.id}'`); 3 | levels = !(!levels || levels.levels === 'false'); 4 | 5 | message.channel.send({embeds: [{ 6 | color: 0x8356ff, 7 | fields: [ 8 | { 9 | name: message.__('leveling_system'), 10 | value: message.__('description', { levels: levels ? message.__('enabled') : message.__('disabled'), prefix: client.config.prefix }) 11 | }, 12 | { 13 | name: message.__('current_settings'), 14 | value: message.__('settings_message', { base: client.config.xp.base, min: client.config.xp.base - client.config.xp.min, max: client.config.xp.base + client.config.xp.max, seconds: client.config.xp.xpAdd / 1000, levelOne: client.config.xp.levelOne, mult: client.config.xp.mult, maxMult: client.config.xp.maxMult }) 15 | } 16 | ] 17 | }]}); 18 | }; 19 | 20 | exports.config = { 21 | enabled: true, 22 | permLevel: 2, 23 | perms: ['EMBED_LINKS'], 24 | category: 'levels' 25 | }; -------------------------------------------------------------------------------- /commands/management/blacklist.js: -------------------------------------------------------------------------------- 1 | function findRole(message, search) { 2 | let rolesCache = message.guild.roles.cache; 3 | let role; 4 | 5 | if (message.mentions.roles.size) role = message.mentions.roles.find(role => role.name !== '@everyone'); 6 | 7 | if (!role) role = rolesCache.get(search); 8 | if (!role) role = rolesCache.find(role => role.name.toLowerCase() === search.toLowerCase()); 9 | if (!role) role = rolesCache.find(role => role.name.toLowerCase().includes(search.toLowerCase())); 10 | 11 | return role; 12 | } 13 | 14 | exports.run = async (message, args, suffix, client) => { 15 | if (!args[0]) return message.channel.send(message.__('no_args')); 16 | 17 | let blacklist = await client.getGuildBlacklist(message.guild.id); 18 | 19 | let role = false; 20 | let obj = client.findMember(message, suffix); 21 | 22 | if (!obj) { 23 | obj = findRole(message, suffix); 24 | role = true; 25 | } 26 | 27 | if (!obj) return message.channel.send(message.__('invalid')); 28 | if (!role) obj = obj.user; 29 | 30 | if (!role && obj.id === message.author.id) return message.channel.send(message.__('self_blacklist')); 31 | if (role && message.member.roles.cache.has(obj.id)) return message.channel.send(message.__('self_role_blacklist')); 32 | if (blacklist.includes(obj.id)) return message.channel.send(message.__('already_blacklisted', { id: obj.id })); 33 | 34 | sql.run(`INSERT INTO guildBlacklist (guildID, ID) VALUES (?, ?)`, [ message.guild.id, obj.id ]); 35 | blacklist.push(obj.id); 36 | 37 | message.channel.send(message.__('blacklisted', { tag: role ? obj.name : obj.tag, id: obj.id })); 38 | }; 39 | 40 | exports.findRole = findRole; 41 | 42 | exports.config = { 43 | enabled: true, 44 | permLevel: 3, 45 | category: 'server_management' 46 | }; -------------------------------------------------------------------------------- /commands/management/levelmessages.js: -------------------------------------------------------------------------------- 1 | exports.run = async message => { 2 | let row = await sql.get(`SELECT levelMessage FROM guildOptions WHERE guildID = '${message.guild.id}'`); 3 | 4 | if (!row) { 5 | sql.run(`INSERT INTO guildOptions (guildID, levelMessage) VALUES (?, ?)`, [message.guild.id, 'false']); 6 | return message.channel.send(message.__('enabled')); 7 | } 8 | 9 | if (row.levelMessage === 'true') { 10 | sql.run(`UPDATE guildOptions SET levelMessage = 'false' WHERE guildID = '${message.guild.id}'`); 11 | message.channel.send(message.__('disabled')); 12 | } else { 13 | sql.run(`UPDATE guildOptions SET levelMessage = 'true' WHERE guildID = '${message.guild.id}'`); 14 | message.channel.send(message.__('enabled')); 15 | } 16 | }; 17 | 18 | exports.config = { 19 | enabled: true, 20 | permLevel: 5, 21 | category: 'server_management' 22 | }; 23 | -------------------------------------------------------------------------------- /commands/management/prefix.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args, s, c, permLevel) => { 2 | let row = await sql.get(`SELECT prefix FROM guildOptions WHERE guildID = '${message.guild.id}'`); 3 | 4 | if (!args[0] || permLevel < 4) { 5 | if (!row) { 6 | message.channel.send(message.__('current', { prefix: 'a.' })); 7 | sql.run(`INSERT INTO guildOptions (guildID, prefix) VALUES (?, ?)`, [message.guild.id, 'a.']); 8 | } else message.channel.send(message.__('current', { prefix: row.prefix })); 9 | } else { 10 | if (args[0].length > 10) return message.channel.send(message.__('too_long')); 11 | 12 | message.channel.send(message.__('updated', { prefix: args[0] })); 13 | if (!row) sql.run(`INSERT INTO guildOptions (guildID, prefix) VALUES (?, ?)`, [message.guild.id, args[0]]); 14 | else sql.run(`UPDATE guildOptions SET prefix = ? WHERE guildID = '${message.guild.id}'`, [ args[0] ]); 15 | } 16 | }; 17 | 18 | exports.config = { 19 | enabled: true, 20 | permLevel: 2, 21 | category: 'server_management' 22 | }; -------------------------------------------------------------------------------- /commands/management/unblacklist.js: -------------------------------------------------------------------------------- 1 | const { findRole } = require('./blacklist'); 2 | 3 | exports.run = async (message, args, suffix, client) => { 4 | let blacklist = await client.getGuildBlacklist(message.guild.id); 5 | 6 | if (!args[0]) return message.channel.send(message.__('no_args')); 7 | 8 | let role = false; 9 | let obj = client.findMember(message, suffix); 10 | 11 | if (!obj) { 12 | obj = findRole(message, suffix); 13 | role = true; 14 | } 15 | 16 | if (!obj) return message.channel.send(i18n.get('commands.blacklist.invalid_user', message)); 17 | if (!role) obj = obj.user; 18 | 19 | if (!blacklist.includes(obj.id)) return message.channel.send(message.__('not_blacklisted')); 20 | 21 | sql.run(`DELETE FROM guildBlacklist WHERE ID = '${obj.id}' AND guildID = '${message.guild.id}'`); 22 | blacklist.splice(blacklist.indexOf(obj.id), 1); 23 | 24 | message.channel.send(message.__('unblacklisted', { tag: role ? obj.name : obj.tag })); 25 | }; 26 | 27 | exports.config = { 28 | enabled: true, 29 | permLevel: 3, 30 | category: 'server_management' 31 | }; -------------------------------------------------------------------------------- /commands/misc/mp3.js: -------------------------------------------------------------------------------- 1 | const ffmpeg = require('fluent-ffmpeg'); 2 | const ytdl = require('ytdl-core'); 3 | const moment = require('moment'); 4 | const fs = require('fs'); 5 | const request = require('request'); 6 | 7 | const soundcloud = require('../../struct/soundcloud'); 8 | const Music = require('../../struct/Music'); 9 | const { timeString } = require('../../struct/Util'); 10 | 11 | const YTRegex = /^(https?:\/\/)?(www\.|m\.|music\.)?(youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/v\/|youtube\.com\/embed\/)([A-z0-9_-]{11})([&?].*)?$/; 12 | 13 | async function youtube(id, message, msg, client) { 14 | let info; 15 | 16 | try { 17 | info = await ytdl.getInfo(id); 18 | } catch (e) { 19 | return msg.edit(message.__('song_not_found')).catch(() => {}); 20 | } 21 | 22 | if (!info) return msg.edit(message.__('song_not_found')).catch(() => {}); 23 | 24 | if (info.videoDetails.isLiveContent) return msg.edit(message.__('livestream')).catch(() => {}); 25 | if (info.videoDetails.lengthSeconds > 1200) return msg.edit(message.__('too_long', { minutes: 20 })).catch(() => {}); 26 | 27 | msg.edit(message.__('downloading_with_time', { seconds: (parseInt(info.videoDetails.lengthSeconds) / 13).toFixed(1) })).catch(() => {}); 28 | let title = info.videoDetails.title; 29 | 30 | let ytdlStream; 31 | 32 | try { 33 | ytdlStream = ytdl.downloadFromInfo(info, { quality: 'highestaudio', requestOptions: { maxRedirects: 10 } }); 34 | } catch (e) { 35 | client.errorLog('Error retrieving ytdl stream in mp3', e); 36 | return msg.edit(message.__('song_not_found')).catch(() => {}); 37 | } 38 | 39 | finish(ytdlStream, title, parseInt(info.videoDetails.lengthSeconds), message, msg, client, info.videoDetails.thumbnail.thumbnails[0].url, `https://youtu.be/${id}`).catch((e) => { 40 | client.errorLog('Error finishing mp3 from YT source', e); 41 | return msg.edit(message.__('song_not_found')).catch(() => {}); 42 | }); 43 | } 44 | 45 | async function finish(stream, title, length, message, msg, client, thumbnail, url) { 46 | title = title.replace(/[^A-z0-9]/g, '_'); 47 | 48 | let index = client.processing.length; 49 | let filePath = `${__dirname}/../../../media/temp/${message.id}.mp3`; 50 | client.processing.push(moment().format('h:mm:ss A') + ' - MP3'); 51 | 52 | ffmpeg(stream, {priority: 20}) 53 | .duration(length + 1) 54 | .audioBitrate(128) 55 | .on('end', () => { 56 | const options = { 57 | url: 'https://file.io', 58 | method: 'POST', 59 | headers: { 60 | 'User-Agent': 'Arthur Discord Bot (github.com/nikbrandt/Arthur)' 61 | }, 62 | formData: { 63 | file: { 64 | value: fs.createReadStream(filePath), 65 | options: { 66 | filename: title + '.mp3' 67 | } 68 | } 69 | } 70 | }; 71 | 72 | request(options, (err, res, body) => { 73 | if (fs.existsSync(filePath)) fs.unlinkSync(filePath); 74 | client.processing.splice(index, 1); 75 | 76 | if (err) { 77 | console.log(err); 78 | console.log(body); 79 | return message.channel.send(message.__('error', { err })); 80 | } 81 | 82 | msg.delete().catch(() => {}); 83 | 84 | try { 85 | body = JSON.parse(body); 86 | } catch (e) { 87 | client.errorLog('Error parsing upload API body in mp3', e); 88 | return message.channel.send(message.__('error', { err: e })); 89 | } 90 | 91 | if (err) message.channel.send(message.__('error', { err })); 92 | 93 | message.channel.send(message.__('song_converted', { user: message.author.toString() }), { 94 | embed: { 95 | title: title, 96 | description: message.__('description', { url: body.link, songURL: url, length: timeString(length, message) }), 97 | thumbnail: { 98 | url: thumbnail 99 | }, 100 | color: 0x42f45c, 101 | footer: { 102 | text: message.__('footer', { tag: message.author.tag }) 103 | } 104 | } 105 | }); 106 | }); 107 | }) 108 | .audioCodec('libmp3lame') 109 | .save(filePath) 110 | .on('error', err => { 111 | if (fs.existsSync(filePath)) fs.unlinkSync(filePath); 112 | client.processing.splice(index, 1); 113 | 114 | msg.delete().catch(() => {}); 115 | 116 | client.errorLog('Error converting to mp3', err); 117 | message.channel.send(message.__('error', { err })); 118 | }); 119 | } 120 | 121 | exports.run = async (message, args, suffix, client) => { 122 | if (!args[0]) return message.channel.send(message.__('no_args')); 123 | let msg = await message.channel.send(message.__('downloading')); 124 | 125 | let id; 126 | 127 | if (soundcloud.regex.test(args[0])) { 128 | let info; 129 | 130 | try { 131 | info = await soundcloud.getInfo(args[0]); 132 | } catch (e) { 133 | return msg.edit(message.__('song_not_found')).catch(() => {}); 134 | } 135 | 136 | let stream = soundcloud(info.id); 137 | let title = info.title; 138 | let length = Math.round(info.duration / 1000); 139 | let thumbnail = info.artwork_url; 140 | 141 | finish(stream, title, length, message, msg, client, thumbnail, args[0]).catch(client.errorLog.simple); 142 | } else if (!YTRegex.test(args[0])) { 143 | id = await Music.attemptYTSearch(suffix, message.client.errorLog); 144 | if (!id) return message.channel.send(message.__('no_results')); 145 | 146 | youtube(id, message, msg, client).catch(client.errorLog.simple); 147 | } else { 148 | id = args[0].match(YTRegex)[4]; 149 | youtube(id, message, msg, client).catch(client.errorLog.simple); 150 | } 151 | }; 152 | 153 | exports.config = { 154 | enabled: true, 155 | permLevel: 1, 156 | perms: ['EMBED_LINKS', 'ATTACH_FILES'], 157 | cooldown: 10000, 158 | category: 'other' 159 | }; 160 | -------------------------------------------------------------------------------- /commands/misc/suggest.js: -------------------------------------------------------------------------------- 1 | const config = require('../../../media/config.json'); 2 | const Trello = require('trello'); 3 | const trello = new Trello(config.trello.key, config.trello.token); 4 | 5 | exports.run = (message, args, suffix, client) => { 6 | if (!args[0]) return message.channel.send(message.__('no_args')); 7 | 8 | let splitified = suffix.split('\n'); 9 | if (splitified[0].length > 256) { 10 | let extra = '...' + splitified[0].substring(253) + '\n\n---'; 11 | splitified[0] = splitified[0].substring(0, 253) + '...'; 12 | splitified.splice(1, 0, extra); 13 | } 14 | 15 | let attachments = message.attachments.map(a => `[${a.name}](${a.url})`); 16 | 17 | let footer = attachments.length 18 | ? `Attached:\n${attachments.join('\n')}\n\n*Suggested by ${message.author.tag} (${message.author.id})*` 19 | : `*Suggested by ${message.author.tag} (${message.author.id})*`; 20 | 21 | trello.addCard(splitified[0], 22 | splitified[1] 23 | ? splitified.slice(1).join('\n') + `\n\n${footer}` 24 | : footer, 25 | config.trello.board 26 | ).then(card => { 27 | let messageOptions = { 28 | embeds: [{ 29 | title: splitified[0], 30 | url: card.shortUrl, 31 | description: splitified[1] ? splitified.slice(1).join('\n') : undefined, 32 | footer: { 33 | text: `Suggested by ${message.author.tag} | ${message.author.id}`, 34 | icon_url: message.author.displayAvatarURL() 35 | }, 36 | color: 0x00c140 37 | }], 38 | files: message.attachments.map(a => {return { attachment: a.url, name: a.name }}) 39 | }; 40 | 41 | client.broadcastEval(`let channel = this.channels.cache.get('${config.trello.channel}'); 42 | if (channel) channel.send(${JSON.stringify(messageOptions)}).then(() => true);`).catch(client.errorLog.simple); 43 | 44 | message.channel.send(message.__('success', { extra: message.guild && message.guild.id === '304428345917964290' ? '' : '\n' + message.__('check_support_server', { link: client.config.info.guildLink }) })); 45 | }).catch(err => { 46 | message.channel.send(message.__('error', { link: client.config.info.guildLink, err })); 47 | }); 48 | }; 49 | 50 | exports.config = { 51 | enabled: true, 52 | permLevel: 1, 53 | category: 'other' 54 | }; 55 | 56 | exports.trello = trello; 57 | -------------------------------------------------------------------------------- /commands/music/forceskip.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, args, suffix, client, permLevel) => { 2 | message.__ = (string, variables) => { 3 | return i18n.get('commands.skip.' + string, message, variables); 4 | }; 5 | 6 | try { 7 | client.commands.get('skip').run(message, [ '-f' ], '-f', client, permLevel); 8 | } catch (error) { 9 | client.errorLog.simple(error); 10 | } 11 | }; 12 | 13 | exports.config = { 14 | enabled: true, 15 | permLevel: 2, 16 | category: 'music' 17 | }; -------------------------------------------------------------------------------- /commands/music/like.js: -------------------------------------------------------------------------------- 1 | const Music = require('../../struct/Music'); 2 | 3 | exports.run = async (message, args, suffix, client) => { 4 | if ((!message.guild.music || !message.guild.music.queue) && !args[0] && !message.attachments.size) return message.channel.send(message.__('no_song_specified')); 5 | 6 | let id; 7 | let type; 8 | let meta; 9 | 10 | if (args[0] || message.attachments.size) { 11 | let object; 12 | let info; 13 | 14 | try { 15 | object = await Music.parseMessage(message, args, suffix, client); 16 | 17 | if (object.type === 1.5 || object.type === 5.5) return message.channel.send(message.__('song_is_playlist')); 18 | 19 | info = await Music.getInfo(object.type, object.id, message, client, 'asdf'); 20 | } catch (err) { 21 | return message.channel.send(err); 22 | } 23 | 24 | id = object.id; 25 | type = object.type; 26 | meta = info.meta; 27 | } else { 28 | id = message.guild.music.queue[0].id; 29 | type = message.guild.music.queue[0].type; 30 | meta = message.guild.music.queue[0].meta; 31 | } 32 | 33 | let dupeCheck = await sql.get(`SELECT count(1) FROM musicLikes WHERE userID = '${message.author.id}' AND id = '${id}'`); 34 | if (dupeCheck['count(1)']) return message.channel.send(message.__('song_already_liked')); 35 | 36 | sql.run(`INSERT INTO musicLikes (userID, type, id, url, title, queueName) VALUES (?, ?, ?, ?, ?, ?)`, [message.author.id, type, id, meta.url, meta.title, meta.queueName]); 37 | 38 | message.channel.send({ 39 | embeds: [{ 40 | author: { 41 | name: `${message.author.username} - ${message.__('song_liked')}` 42 | }, 43 | url: meta.url, 44 | color: 0x427df4, 45 | description: meta.queueName 46 | }] 47 | }); 48 | }; 49 | 50 | exports.config = { 51 | enabled: true, 52 | permLevel: 2, 53 | category: 'music', 54 | cooldown: 1000 55 | }; -------------------------------------------------------------------------------- /commands/music/liked.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args) => { 2 | let rows = await sql.all(`SELECT queueName FROM musicLikes WHERE userID = '${message.author.id}'`); 3 | if (!rows || !rows.length) return message.channel.send(message.__('no_likes')); 4 | 5 | let songArray = []; 6 | 7 | for (let i = 0; i < rows.length; i++) { 8 | songArray.push(`${i + 1}. ${rows[i].queueName}`); 9 | } 10 | 11 | let maxPage = Math.ceil(songArray.length / 10); // check that parsed number is correct 12 | let page = 1; 13 | if (args[0]) { 14 | let num = parseInt(args[0]); 15 | if (!num) return message.channel.send(message.__('invalid_page')); 16 | if (num < 1) return message.channel.send(message.__('negative_page')); 17 | if (num > maxPage) return message.channel.send(message.__('nonexistent_page')); 18 | page = num; 19 | } 20 | 21 | songArray = songArray.slice(page * 10 - 10, page * 10); 22 | 23 | message.channel.send({embeds: [{ 24 | title: message.__('liked_songs', { name: message.member.displayName, s: message.member.displayName.endsWith('s') ? '\'' : '\'s' }), 25 | description: songArray.join('\n'), 26 | color: 0x427df4, 27 | footer: { 28 | text: message.__('footer', { page, maxPage }) 29 | } 30 | }]}) 31 | }; 32 | 33 | exports.config = { 34 | enabled: true, 35 | permLevel: 2, 36 | category: 'music' 37 | }; -------------------------------------------------------------------------------- /commands/music/likedlb.js: -------------------------------------------------------------------------------- 1 | const Music = require('../../struct/Music'); 2 | 3 | exports.run = async (message, args) => { 4 | let array = await Music.likedArray(); 5 | 6 | let maxPage = Math.ceil(array.length / 10); // check that parsed number is correct 7 | let page = 1; 8 | if (args[0]) { 9 | let num = parseInt(args[0]); 10 | if (!num) return message.channel.send(i18n.get('commands.liked.invalid_page', message)); 11 | if (num < 1) return message.channel.send(i18n.get('commands.liked.negative_page', message)); 12 | if (num > maxPage) return message.channel.send(i18n.get('commands.liked.nonexistent_page', message)); 13 | page = num; 14 | } 15 | 16 | array = array.slice(page * 10 - 10, page * 10); // slice array down to 5 songs - the five of current page 17 | let startNum = page * 10 - 9; 18 | let final = []; 19 | 20 | for (let i = 0; i < array.length; i++) { // push each song to the final array, as the song will be displayed 21 | final.push(`**${startNum}**. ${array[i].queueName} - ${array[i].count} ${array[i].count - 1 ? message.__('likes') : message.__('like')}`); 22 | startNum++; 23 | } 24 | 25 | message.channel.send({embeds: [{ 26 | title: message.__('top_liked_songs'), 27 | description: final.join('\n'), 28 | color: 0x427df4, 29 | footer: { 30 | text: message.__('footer', { page, maxPage }) 31 | } 32 | }]}); 33 | }; 34 | 35 | exports.config = { 36 | enabled: true, 37 | permLevel: 2, 38 | category: 'music' 39 | }; -------------------------------------------------------------------------------- /commands/music/loop.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, a, s, c, permLevel) => { 2 | if (!message.guild.music || !message.guild.music.queue) return message.channel.send(message.__('no_music_playing')); 3 | if (!message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.dj', message).toLowerCase()) && !message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.music', message).toLowerCase() && message.guild.music.queue[0].person.id !== message.author.id) && permLevel < 3) return message.channel.send(message.__('no_permissions')); 4 | 5 | let cleanName = message.member.displayName.replace(/@/g, '@\u200b').replace(/ /g, ''); 6 | 7 | message.guild.music.loop = !message.guild.music.loop; 8 | message.channel.send(message.__(message.guild.music.loop ? 'on' : 'off', { name: cleanName })); 9 | }; 10 | 11 | exports.config = { 12 | enabled: true, 13 | permLevel: 2, 14 | category: 'music' 15 | }; 16 | -------------------------------------------------------------------------------- /commands/music/move.js: -------------------------------------------------------------------------------- 1 | function move(array, from, to) { 2 | array.splice(to, 0, array.splice(from, 1)[0]); 3 | } 4 | 5 | exports.run = (message, args, s, c, permLevel) => { 6 | if (!message.guild.music || !message.guild.music.queue) return message.channel.send(message.__('no_music_playing')); 7 | if (!message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.dj', message).toLowerCase()) && !message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.music', message).toLowerCase()) && permLevel < 3) return message.channel.send(message.__('no_permissions')); 8 | 9 | if (!args[0]) return message.channel.send(message.__('missing_song')); 10 | if (!args[1]) return message.channel.send(message.__('missing_position')); 11 | 12 | let song = parseInt(args[0]); 13 | let position = parseInt(args[1]); 14 | 15 | if (!song || !position) return message.channel.send(message.__('invalid_song_or_position')); 16 | if (song < 2 || position < 2) return message.channel.send(message.__('song_or_position_too_low')); 17 | if (song === position) return message.channel.send(message.__('song_and_position_same')); 18 | if (song > message.guild.music.queue.length || position > message.guild.music.queue.length) return message.channel.send(message.__('song_or_position_too_high')); 19 | 20 | move(message.guild.music.queue, song - 1, position - 1); 21 | 22 | message.channel.send(message.__('success', { song: song, pos: position })); 23 | }; 24 | 25 | exports.config = { 26 | enabled: true, 27 | permLevel: 2, 28 | category: 'music' 29 | }; -------------------------------------------------------------------------------- /commands/music/nowplaying.js: -------------------------------------------------------------------------------- 1 | const ytdl = require('ytdl-core'); 2 | 3 | const Music = require('../../struct/Music'); 4 | const { timeString } = require('../../struct/Util.js'); 5 | 6 | exports.run = (message, args, s, client, permLevel) => { 7 | if (args[0] && (args[0] === message.__('notify') || args[0] === message.__('notify_abbreviation'))) { 8 | if (permLevel > 2) client.commands.get('npnotify').run(message); 9 | else message.react(':missingpermissions:407054344874229760').catch(() => {}); 10 | 11 | return; 12 | } 13 | 14 | if (!message.guild.music || !message.guild.music.queue) return message.channel.send(message.__('nothing_playing')); 15 | 16 | const locale = i18n.getLocaleCode(message); 17 | const queueItem = message.guild.music.queue[0]; 18 | 19 | if (queueItem.type === 1) { // YouTube video 20 | const ellapsedTime = Music.calculateEllapsedTime(message.guild); 21 | const embed = queueItem.embed; 22 | 23 | embed.author.name = i18n.getString('struct.music.now_playing', locale); 24 | embed.description = embed.description.split('\n').slice(0, -1).join('\n') + `\n${timeString(ellapsedTime, locale)} ${message.__('of')} ${timeString(queueItem.meta.length, locale)}`; 25 | 26 | message.channel.send({ embeds: [ embed ] }).then(msg => { 27 | Music.addReactionCollector(msg, client, ellapsedTime * 1000); 28 | }); 29 | } else if (queueItem.type === 5) { // soundcloud 30 | let elapsedTime = Math.round(message.guild.me.voice.connection.dispatcher.totalStreamTime / 1000); 31 | 32 | let embed = queueItem.embed; 33 | embed.author.name = i18n.getString('struct.music.now_playing', locale); 34 | embed.description = embed.description.replace(i18n.getString('struct.music.length', locale) + ': ', `**${timeString(elapsedTime, locale)}** ${message.__('of')} `); 35 | 36 | message.channel.send({ embeds: [ embed ] }).then(msg => { 37 | Music.addReactionCollector(msg, client, elapsedTime * 1000); 38 | }); 39 | } else if (queueItem.type >= 2) { // User-provided file 40 | message.channel.send({ 41 | embeds: [{ 42 | author: { 43 | name: i18n.getString('struct.music.now_playing', locale) 44 | }, 45 | url: queueItem.meta.url, 46 | color: 0x7289DA, 47 | description: queueItem.meta.title 48 | }] 49 | }).then(msg => { 50 | Music.addReactionCollector(msg, client); 51 | }); 52 | } 53 | }; 54 | 55 | exports.config = { 56 | enabled: true, 57 | permLevel: 2, 58 | perms: ['EMBED_LINKS'], 59 | category: 'music' 60 | }; 61 | -------------------------------------------------------------------------------- /commands/music/npnotify.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message) => { // npNotify column 2 | let row = await sql.get(`SELECT npNotify FROM guildOptions WHERE guildID = '${message.guild.id}'`); 3 | 4 | if (!row) { 5 | row = 'false'; 6 | await sql.run(`INSERT INTO guildOptions (guildID) VALUES (?)`, [ message.guild.id ]); 7 | } else row = row.npNotify; 8 | 9 | if (row === 'false') { 10 | message.channel.send(message.__('message', { status: message.__('enabled') })); 11 | row = 'true'; 12 | } else { 13 | message.channel.send(message.__('message', { status: message.__('disabled') })); 14 | row = 'false'; 15 | } 16 | 17 | sql.run(`UPDATE guildOptions SET npNotify = '${row}' WHERE guildID = '${message.guild.id}'`); 18 | }; 19 | 20 | exports.config = { 21 | enabled: true, 22 | permLevel: 3, 23 | category: 'music' 24 | }; -------------------------------------------------------------------------------- /commands/music/pause.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, a, s, d, permLevel) => { 2 | if (!message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.dj', message).toLowerCase()) && !message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.music', message).toLowerCase()) && permLevel < 3) return message.react(':missingpermissions:407054344874229760').catch(() => {}); 3 | if (!message.guild.music || !message.guild.music.queue || !message.guild.me.voice || !message.guild.me.voice.connection || !message.guild.me.voice.connection.dispatcher) return message.channel.send(message.__('no_music_playing')); 4 | 5 | if (message.guild.music.playing === false) return message.channel.send(message.__('already_paused')); 6 | 7 | message.guild.me.voice.connection.dispatcher.pause(true); 8 | message.guild.music.playing = false; 9 | message.guild.music.pauseTime = Date.now(); 10 | message.channel.send(message.__('paused')); 11 | }; 12 | 13 | exports.config = { 14 | enabled: true, 15 | permLevel: 2, 16 | category: 'music' 17 | }; -------------------------------------------------------------------------------- /commands/music/playnext.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, args, suffix, client, permLevel, prefix, ipc) => { 2 | message.__ = (string, variables) => { 3 | return i18n.get('commands.play.' + string, message, variables); 4 | }; 5 | 6 | message.playnext = true; 7 | 8 | client.commands.get('play').run(message, args, suffix, client, permLevel, prefix, ipc).catch(client.errorLog.simple); 9 | }; 10 | 11 | exports.config = { 12 | enabled: true, 13 | permLevel: 2, 14 | category: 'music' 15 | }; -------------------------------------------------------------------------------- /commands/music/queue.js: -------------------------------------------------------------------------------- 1 | const { calculateQueueLength } = require('../../struct/Music.js'); 2 | const { timeString } = require('../../struct/Util.js'); 3 | 4 | exports.run = async (message, args) => { 5 | if (!message.guild.music || !message.guild.music.queue) return message.channel.send(message.__('no_music_queued')); 6 | 7 | let i = 1; 8 | if (args[0]) { 9 | let parsed = parseInt(args[0]); 10 | if (!parsed) return message.channel.send(i18n.get('commands.liked.invalid_page', message)); 11 | if (parsed < 1) return message.channel.send(i18n.get('commands.liked.negative_page', message)); 12 | parsed = Math.floor(parsed); 13 | if ((parsed - 1) * 10 > message.guild.music.queue.length) return message.channel.send(message.guild.music.queue.length < 11 14 | ? message.__('only_one_page') 15 | : message.__('only_multiple_pages', { pages: Math.ceil(message.guild.music.queue.length / 10) })); 16 | i = (parsed - 1) * 10 + 1; 17 | } 18 | 19 | let pars = i; 20 | let songArray = []; 21 | let queue = message.guild.music.queue; 22 | 23 | function iterate () { 24 | let obj = queue[i - 1]; 25 | 26 | if (i !== 1) songArray.push(`${i}. ${obj.meta.queueName}`); 27 | 28 | i++; 29 | if (i <= pars + 9 && i <= queue.length) iterate(); 30 | else message.channel.send({ embeds: [{ 31 | title: i18n.get('struct.music.now_playing', message) + ' ' + queue[0].meta.title, 32 | url: queue[0].meta.url, 33 | description: songArray.join('\n'), 34 | color: 0x427df4, 35 | footer: { 36 | text: message.guild.music.queue.length === 1 37 | ? message.__('footer', { page: Math.ceil(pars / 10), 38 | maxPage: Math.ceil(message.guild.music.queue.length / 10), 39 | length: timeString(calculateQueueLength(message.guild), message), 40 | name: message.member.displayName }) 41 | : message.__('footer_plural', { page: Math.ceil(pars / 10), 42 | maxPage: Math.ceil(message.guild.music.queue.length / 10), 43 | songs: message.guild.music.queue.length, 44 | length: timeString(calculateQueueLength(message.guild), message), 45 | name: message.member.displayName }) 46 | } 47 | }]}); 48 | } 49 | 50 | iterate(); 51 | }; 52 | 53 | exports.config = { 54 | enabled: true, 55 | permLevel: 2, 56 | perms: ['EMBED_LINKS'], 57 | category: 'music' 58 | }; 59 | -------------------------------------------------------------------------------- /commands/music/remove.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, args, suffix, client, permLevel) => { 2 | if (!message.guild.music || !message.guild.music.queue) return message.channel.send(message.__('nothing_playing')); 3 | 4 | if (!args[0]) return message.channel.send(message.__('no_args')); 5 | let num = parseInt(args[0]); 6 | if (!num) return message.channel.send(message.__('not_a_number')); 7 | if (num < 1) return message.channel.send(message.__('negative_number')); 8 | if (num === 1) return message.channel.send(message.__('current_song')); 9 | if (num > message.guild.music.queue.length) return message.channel.send(message.__('does_not_exist_yet')); 10 | 11 | if (!message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.dj', message).toLowerCase()) && !message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.music', message).toLowerCase()) && permLevel < 3 && message.guild.music.queue[num - 1].person.id !== message.author.id) return; 12 | 13 | message.guild.music.queue.splice(num - 1, 1); 14 | message.channel.send(message.__('success', { num, name: message.member.displayName.replace(/@/g, '@\u200b').replace(/ /g, '') })); 15 | }; 16 | 17 | exports.config = { 18 | enabled: true, 19 | permLevel: 2, 20 | category: 'music' 21 | }; -------------------------------------------------------------------------------- /commands/music/resume.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, a, s, d, permLevel) => { 2 | if (!message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.dj', message).toLowerCase()) && !message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.music', message).toLowerCase()) && permLevel < 3) return message.react(':missingpermissions:407054344874229760').catch(() => {}); 3 | if (!message.guild.music || !message.guild.music.queue) return message.channel.send(message.__('nothing_playing')); 4 | 5 | if (message.guild.music.playing === true) return message.channel.send(message.__('already_playing')); 6 | 7 | message.guild.me.voice.connection.dispatcher.resume(); 8 | message.guild.music.playing = true; 9 | message.guild.music.startTime += Date.now() - message.guild.music.pauseTime; 10 | message.guild.music.pauseTime = null; 11 | 12 | message.channel.send(message.__('resumed')); 13 | }; 14 | 15 | exports.config = { 16 | enabled: true, 17 | permLevel: 2, 18 | category: 'music' 19 | }; -------------------------------------------------------------------------------- /commands/music/shuffle.js: -------------------------------------------------------------------------------- 1 | function shuffle(array) { // Fisher-Yates shuffle, https://bost.ocks.org/mike/shuffle/ 2 | let m = array.length, t, i; 3 | while (m) { 4 | i = Math.floor(Math.random() * m--); 5 | t = array[m]; 6 | array[m] = array[i]; 7 | array[i] = t; 8 | } 9 | return array; 10 | } 11 | 12 | exports.run = (message, args, suffix, client, permLevel) => { 13 | if (!message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.dj', message).toLowerCase()) && !message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.music', message).toLowerCase()) && permLevel < 3) return message.react(':missingpermissions:407054344874229760').catch(() => {}); 14 | if (!message.guild.music || !message.guild.music.queue) return message.channel.send(message.__('no_queue')); 15 | if (message.guild.music.queue.length < 3) return message.channel.send(message.__('too_small')); // Kappa 16 | 17 | let queue = message.guild.music.queue; 18 | let first = queue.shift(); 19 | let shuffled = shuffle(queue); 20 | shuffled.unshift(first); 21 | 22 | message.guild.music.queue = shuffled; 23 | message.channel.send(message.__('shuffled')); 24 | }; 25 | 26 | exports.config = { 27 | enabled: true, 28 | permLevel: 2, 29 | category: 'music' 30 | }; 31 | 32 | exports.shuffle = shuffle; -------------------------------------------------------------------------------- /commands/music/skip.js: -------------------------------------------------------------------------------- 1 | function skip (message) { 2 | if (message.guild.me.voice && message.guild.me.voice.connection && message.guild.me.voice.connection.dispatcher) message.guild.me.voice.connection.dispatcher.end(); 3 | else message.guild.music = {}; 4 | message.channel.send(message.__('skipped', { user: message.member.displayName.replace(/@/g, '@\u200b').replace(/ /g, '') })); 5 | } 6 | 7 | exports.run = (message, args, s, d, permLevel) => { 8 | if (!message.guild.music || !message.guild.music.queue || !message.guild.me.voice || !message.guild.me.voice.connection) return message.channel.send(message.__('no_music')); 9 | 10 | let canForceSkip = false; 11 | if (message.member.roles.cache.some(r => r.name.toLowerCase() === i18n.get('struct.music.dj', message).toLowerCase()) || message.member.roles.cache.some(r => r.name.toLowerCase() === i18n.get('struct.music.music', message).toLowerCase()) || permLevel > 3 || message.guild.music.queue[0].person.id === message.author.id) canForceSkip = true; 12 | 13 | if (args[0] === message.__('force_command_flag') && canForceSkip) return skip(message); 14 | 15 | if (message.guild.music.queue[0].voteSkips) { 16 | if (message.guild.music.queue[0].voteSkips.includes(message.author.id)) { 17 | let index = message.guild.music.queue[0].voteSkips.indexOf(message.author.id); 18 | message.guild.music.queue[0].voteSkips.splice(index, 1); 19 | return message.channel.send(message.__('vote_skip_removed', { user: message.member.displayName.replace(/@/g, '@\u200b') })); 20 | } 21 | 22 | message.guild.music.queue[0].voteSkips.push(message.author.id); 23 | } 24 | else message.guild.music.queue[0].voteSkips = [ message.author.id ]; 25 | 26 | let skipNum = Math.round((message.guild.me.voice.connection.channel.members.size - 1) / 2); 27 | if (message.guild.music.queue[0].voteSkips.length >= skipNum) return skip(message); 28 | 29 | message.channel.send(message.__('vote_skip_registered', { 30 | user: message.member.displayName.replace(/@/g, '@\u200b'), 31 | votes: skipNum - message.guild.music.queue[0].voteSkips.length, 32 | part: message.guild.music.queue[0].voteSkips.length, 33 | whole: skipNum, 34 | forceMessage: canForceSkip ? '\n' + message.__('force_message') : '' 35 | })); 36 | }; 37 | 38 | exports.config = { 39 | enabled: true, 40 | permLevel: 2, 41 | category: 'music' 42 | }; 43 | -------------------------------------------------------------------------------- /commands/music/stop.js: -------------------------------------------------------------------------------- 1 | exports.run = (message, a, s, d, permLevel) => { 2 | if (!message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.dj', message).toLowerCase()) 3 | && !message.member.roles.cache.find(r => r.name.toLowerCase() === i18n.get('struct.music.music', message).toLowerCase()) 4 | && permLevel < 3 5 | && ((!message.guild.music || !message.guild.music.queue) ? true : message.guild.music.queue.find(item => item.requester !== message.author.id) || message.guild.me.voice.connection.channel.members.size > 2)) return message.react(':missingpermissions:407054344874229760').catch(() => {}); 6 | 7 | if (!message.guild.music || !message.guild.music.queue) return message.channel.send(message.__('no_music')); 8 | 9 | message.channel.send(message.__('stopped', { user: message.member.displayName.replace(/@/g, '@\u200b').replace(/ /g, '') })); 10 | if (message.guild.me.voice && message.guild.me.voice.connection) message.guild.me.voice.connection.disconnect(); 11 | message.guild.music = {}; 12 | }; 13 | 14 | exports.config = { 15 | enabled: true, 16 | permLevel: 2, 17 | category: 'music' 18 | }; -------------------------------------------------------------------------------- /commands/music/unlike.js: -------------------------------------------------------------------------------- 1 | exports.run = async (message, args) => { 2 | let rows = await sql.all(`SELECT title, id FROM musicLikes WHERE userID = '${message.author.id}'`); 3 | if (!rows || !rows.length) return message.channel.send(message.__('no_likes')); 4 | 5 | let num; 6 | 7 | if (!args[0] && (!message.guild.music && !message.guild.music.queue)) return message.channel.send(message.__('no_args')); 8 | 9 | if (args[0]) { 10 | num = parseInt(args[0]); 11 | if (!num) return message.channel.send(message.__('invalid_number')); 12 | if (num < 1) return message.channel.send(message.__('negative_number')); 13 | if (num > rows.length) return message.channel.send(message.__('too_high_number')); 14 | } else { 15 | if (!message.guild.me.voice) return message.channel.send(message.__('no_voice_connection')); 16 | let index = rows.findIndex(o => o.id === message.guild.music.queue[0].id); 17 | if (index === -1) return message.channel.send(message.__('song_not_liked')); 18 | num = index + 1; 19 | } 20 | 21 | let title = rows[num - 1].title; 22 | await sql.run(`DELETE FROM musicLikes WHERE userID = '${message.author.id}' AND id = '${rows[num - 1].id}'`); 23 | message.channel.send({embeds: [{ 24 | title: message.__('song_unliked'), 25 | description: `${title}`, 26 | color: 0x427df4 27 | }]}); 28 | }; 29 | 30 | exports.config = { 31 | enabled: true, 32 | permLevel: 2, 33 | category: 'music' 34 | }; -------------------------------------------------------------------------------- /events/guildCreate.js: -------------------------------------------------------------------------------- 1 | const defaultChannel = require('../functions/defaultChannel'); 2 | 3 | module.exports = (client, guild) => { 4 | // check for existence of guild settings in sql 5 | sql.get(`SELECT guildID FROM guildOptions WHERE guildID = '${guild.id}'`).then(row => { 6 | if (!!row) return; // if there are guild settings, don't send message 7 | 8 | // create guild settings with default values 9 | sql.run(`INSERT INTO guildOptions (guildID) VALUES (?)`, [guild.id]).then(() => { 10 | let channel = defaultChannel(guild); 11 | 12 | if (channel && channel.permissionsFor(guild.me).has('SEND_MESSAGES')) channel.send( 13 | 'Aye! My name\'s Arthur, one of those multipurpose bots who does things.\n' + 14 | 'Thanks for adding me to your server\nMy prefix is currently `a.`, change it with the `prefix` command.\n' + 15 | 'To change the server\'s language, use `serverlanguage`, and to change your own, use `language`. (If you know a non-English language, help translate! Ask in the support server.)\n' + 16 | 'For more help, use `a.help`.\n' + 17 | 'If you have any problems, feel free to join the support server (in the help commmand).\n' + 18 | 'By using this bot, you agree to the TOS and Privacy Policy linked on https://arthur.wumpler.com' + 19 | 'Enjoy!' 20 | ).catch(() => {}); 21 | }) 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /events/messageReactionAdd.js: -------------------------------------------------------------------------------- 1 | const { emojis } = require('../commands/fun/poll'); 2 | 3 | module.exports = (client, reaction, user) => { 4 | if (!reaction) return; 5 | if (!client.reactionCollectors.has(reaction.message.id)) return; 6 | if (user.id === client.user.id) return; 7 | 8 | let obj = client.reactionCollectors.get(reaction.message.id); 9 | 10 | let currentEmojis = emojis.slice(0, obj.number); 11 | if (!currentEmojis.includes(reaction.emoji.name)) return; 12 | 13 | let reactions = reaction.message.reactions.cache.filter(reaction => currentEmojis.includes(reaction.emoji.name)); 14 | let total = 0; 15 | 16 | reactions.forEach(reaction => { 17 | if (reaction.users.cache.has(user.id)) total++; 18 | }); 19 | 20 | if (total > 1) reaction.users.remove(user).catch(() => {}); 21 | }; -------------------------------------------------------------------------------- /events/ready.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const fs = require('fs'); 3 | 4 | const { watch } = require('../commands/fun/poll'); 5 | const ipc = require('../struct/ipc'); 6 | 7 | async function game (client) { 8 | let games = [ 9 | [ 'Invite me to your server with the "invite" command.', 'PLAYING' ], 10 | [ `${(await client.broadcastEval('this.guilds.cache.size')).reduce((prev, cur) => prev + cur, 0)} servers do their things`, 'WATCHING' ], 11 | [ `${(await client.broadcastEval('this.users.cache.size')).reduce((prev, cur) => prev + cur, 0)} users very closely`, 'WATCHING' ], 12 | [ 'with the webshot command', 'PLAYING' ], 13 | [ 'The lovely mp3 command', 'LISTENING' ], 14 | [ 'what do i put here', 'PLAYING' ], 15 | [ 'SOUNDCLOUD SUPPOOORRRRTTTT', 'LISTENING' ], 16 | [ 'talk to me im lonely', 'PLAYING' ], 17 | [ 'honestly the best way to code is by etching engravings into your hard drive', 'PLAYING' ], 18 | [ 'enable hot levels with the levels command', 'PLAYING' ], 19 | [ 'are you feeling sad? look at a cat and feel happy for a few seconds.', 'PLAYING' ], 20 | [ 'feeling like you need help with life? have the 8ball make all your choices.', 'PLAYING' ], 21 | [ 'don\'t like my prefix? weirdo. change it with the "prefix" command.', 'PLAYING' ], 22 | [ '( ͡° ͜ʖ ͡°)', 'PLAYING' ], 23 | [ 'if you feel like this bot is crap, feel free to \'a.suggest\' on how to make it less crap.', 'PLAYING' ], 24 | [ 'join my server so my dev feels popular - click "Support Server" in the help command', 'PLAYING' ], 25 | [ 'did you know that you\'re a nerd?', 'PLAYING' ], 26 | [ 'mmm sexy polls i think', 'PLAYING' ], 27 | [ 'you.', 'WATCHING' ], 28 | [ 'the screams of.. uh.. nevermind..', 'LISTENING' ], 29 | [ `simultaneously with all of you ;)`, 'PLAYING' ], 30 | [ 'the Earth burn as I hitch a ride with the Vogons', 'WATCHING' ], 31 | [ 'Help translate Arthur! Join the support server and ask how you can.', 'PLAYING' ] 32 | ]; 33 | 34 | let array = games[Math.floor(Math.random() * games.length)]; 35 | client.user.setActivity(`${array[0]} | @Arthur help`, { type: array[1] }).catch(() => {}); 36 | } 37 | 38 | function sendStats(client) { 39 | client.shard.send({ 40 | action: 'updateStats', 41 | commands: client.commandStatsObject, 42 | daily: client.dailyStatsObject, 43 | weekly: client.weeklyStatsObject 44 | }).then(() => { 45 | client.commandStatsObject = {}; 46 | client.dailyStatsObject = {}; 47 | client.weeklyStatsObject = {}; 48 | }); 49 | } 50 | 51 | function cleanProcesses(client) { 52 | /* client.voice.connections.forEach(connection => { 53 | if (!connection.channel 54 | || connection.channel.members.size < 2 55 | || !connection.channel.guild 56 | || !connection.channel.guild.music 57 | || !connection.channel.guild.music.queue 58 | ) { 59 | if (connection.channel.guild && connection.channel.guild.music) connection.channel.guild.music = {}; 60 | 61 | connection.disconnect(); 62 | connection.channel.leave(); 63 | } 64 | }); */ // wooh what even is voice 65 | 66 | client.processing.forEach((item, i) => { 67 | if (typeof item !== 'string') return; 68 | 69 | let start = moment(item.split(' - ')[0], 'h:mm:ss A').valueOf(); 70 | if (Date.now() - start > 600000) client.processing.splice(i, 1); // 600000 ms = 10 minutes 71 | }); 72 | } 73 | 74 | module.exports = client => { 75 | if (Date.now() - client.loadStart > 300000) return; 76 | console.log(`\n${client.test ? 'Testbot' : 'Arthur'} has started! Currently in ${client.guilds.cache.size} guilds, attempting to serve ${client.users.cache.size} users. (${Date.now() - client.loadStart} ms)\n`); 77 | 78 | /*if (!client.test)*/ ipc(client); 79 | 80 | client.recentMessages = {}; 81 | client.lastRecentMessageID = 0; 82 | 83 | client.user.setActivity('Nevermind! Click me.'); 84 | 85 | // TODO: uncomment if/when Arthur is alive again 86 | // game(client); 87 | // setInterval(() => { 88 | // game(client); 89 | // }, 1000 * 60 * 5); 90 | 91 | setInterval(() => { 92 | cleanProcesses(client); 93 | }, 600000); 94 | 95 | if (!client.test) { 96 | setInterval(() => { 97 | sendStats(client); 98 | }, 30000);/* 99 | 100 | dbots.getLikes(client); 101 | setInterval(() => { 102 | dbots.getLikes(client); 103 | }, 600000);*/ 104 | } 105 | 106 | sql.all('SELECT * FROM pollReactionCollectors').then(results => { 107 | let parsed = []; 108 | 109 | results.forEach(obj => { 110 | let options = JSON.parse(obj.options); 111 | let embed = JSON.parse(obj.embed); 112 | parsed.push({ 113 | channelID: obj.channelID, 114 | messageID: obj.messageID, 115 | options, 116 | endDate: obj.endDate, 117 | embed 118 | }); 119 | }); 120 | 121 | parsed.forEach(obj => { 122 | let channel = client.channels.cache.get(obj.channelID); 123 | if (!channel) return; 124 | channel.messages.fetch(obj.messageID).then(msg => { 125 | watch(msg, obj.options, obj.endDate, client, obj.embed); 126 | }).catch(() => { 127 | sql.run('DELETE FROM pollReactionCollectors WHERE messageID = ?', [obj.messageID]).catch(console.log); 128 | }) 129 | }); 130 | }).catch(client.errorLog.simple); 131 | 132 | const crashPath = require('path').join(__basedir, '..', 'media', 'temp', 'crash.txt'); 133 | if (fs.existsSync(crashPath)) fs.readFile(crashPath, 'utf8', (err, data) => { 134 | if (err) return client.errorLog('Error loading previous error', err); 135 | 136 | let datarray = data.split('\n'); 137 | const code = datarray.shift(); 138 | const stack = datarray.join('\n'); 139 | 140 | client.errorLog('Previous crash error', { stack, code }, true); 141 | 142 | fs.unlink(crashPath, err => { 143 | if (err) client.errorLog('Could not delete previous crash.txt file', err); 144 | }); 145 | }); 146 | }; 147 | -------------------------------------------------------------------------------- /events/voiceStateUpdate.js: -------------------------------------------------------------------------------- 1 | let timeouts = {}; 2 | 3 | module.exports = (client, old, member) => { 4 | if (timeouts.hasOwnProperty(member.guild.id)) clearTimeout(timeouts[member.guild.id]); 5 | 6 | timeouts[member.guild.id] = setTimeout(() => { 7 | if (member.guild.me.voice && member.guild.me.voice.connection && (member.guild.me.voice.connection.channel.members.size === 1 || !member.guild.me.voice.connection.channel.members.some(m => m.user.bot === false))) { 8 | member.guild.music = {}; 9 | member.guild.me.voice.connection.disconnect(); 10 | } 11 | 12 | delete timeouts[member.guild.id]; 13 | }, 1000 * 60) 14 | }; -------------------------------------------------------------------------------- /functions/askQuestion.js: -------------------------------------------------------------------------------- 1 | const defaultErrorEmbed = { 2 | title: 'Command canceled', 3 | description: 'You did not respond in time.', 4 | footer: { 5 | text: 'L.' 6 | }, 7 | color: 0xff0000 8 | }; 9 | 10 | const defaultCancelEmbed = { 11 | title: 'Command canceled', 12 | description: 'You cancelled the command.', 13 | color: 0xff0000 14 | }; 15 | 16 | const defaultAttemptEmbed = { 17 | title: 'Command canceled', 18 | description: 'You took too many attempts', 19 | footer: { 20 | text: 'L.' 21 | }, 22 | color: 0xff0000 23 | }; 24 | 25 | /** 26 | * Ask a question in a channel and resend the message if a condition is not met 27 | * @param {TextChannel} channel Channel to send the message to 28 | * @param {object} embed Embed message to send 29 | * @param {string} authorID User ID to collect message from 30 | * @param {Message} [message] Message to edit instead of sending a message 31 | * @param {number} [attempt] Attempt number 32 | * @param {object} [errorEmbed] Embed to send as an error embed 33 | * @param {function} condition Condition to match against message content 34 | * @param {number} [time=60000] Time in ms to wait for an answer 35 | * @param {function} [resolve] Promise resolve function 36 | * @param {function} [reject] Promise reject function 37 | * @returns {Promise} 38 | */ 39 | function askWithCondition (channel, embed, authorID, message, attempt, errorEmbed, condition, time, resolve, reject) { 40 | if (resolve) { 41 | askQuestion(channel, embed, authorID, message, attempt, errorEmbed, time).then(object => { 42 | if (!condition(object.response)) return askWithCondition(channel, embed, authorID, object.message, object.attempt, errorEmbed, condition, time, resolve, reject); 43 | resolve(object); 44 | }).catch(err => { 45 | reject(err); 46 | }); 47 | } else { 48 | return new Promise((resolve, reject) => { 49 | askQuestion(channel, embed, authorID, message, attempt, errorEmbed, time).then(object => { 50 | if (!condition(object.response)) return askWithCondition(channel, embed, authorID, object.message, object.attempt, errorEmbed, condition, time, resolve, reject); 51 | resolve(object); 52 | }).catch(err => { 53 | reject(err); 54 | }); 55 | }); 56 | } 57 | } 58 | 59 | /** 60 | * Ask a question in a channel 61 | * @param {TextChannel} channel Channel to send the message to 62 | * @param {object} embed Embed message to send 63 | * @param {string} authorID User ID to collect message from 64 | * @param {Message} [message] Message to edit instead of sending a message 65 | * @param {number} [attempt] Attempt number 66 | * @param {object} [errorEmbed] Embed to send as an error embed 67 | * @param {number} [time] Time in ms to wait for an answer 68 | * @returns {Promise} 69 | */ 70 | function askQuestion (channel, embed, authorID, message, attempt, errorEmbed, time) { 71 | return new Promise(async (resolve, reject) => { 72 | if (attempt > 5) { 73 | message ? await message.edit({ content: '', embeds: [ defaultAttemptEmbed ] }).catch(() => {}) : await channel.send({ embeds: [ defaultAttemptEmbed ] }); 74 | return reject('attempts'); 75 | } 76 | 77 | embed.footer.text = embed.footer.text.replace(/Attempt [1-5]/g, `Attempt ${attempt}`); 78 | const embedMessage = message ? await message.edit({ content: '', embeds: [ embed ] }).catch(() => {}) : await channel.send({ embeds: [ embed ] }); 79 | 80 | const filter = m => m.author.id === authorID; 81 | 82 | channel.awaitMessages({ filter, max: 1, time: time ? time : 60000, errors: [ 'time' ] }) 83 | .then(async collected => { 84 | let message = collected.first(); 85 | 86 | message.delete().catch(() => {}); 87 | 88 | if (message.content.toLowerCase() === 'cancel') { 89 | await embedMessage.edit({ content: '', embeds: [ defaultCancelEmbed ] }).catch(() => {}); 90 | return reject('cancel'); 91 | } 92 | 93 | resolve({ 94 | response: message.content, 95 | message: embedMessage, 96 | attempt: attempt + 1 97 | }); 98 | }).catch(() => { 99 | embedMessage.edit({ embeds: [ errorEmbed ? errorEmbed : defaultErrorEmbed ] }).catch(() => {}); 100 | reject('response'); 101 | }); 102 | }); 103 | } 104 | 105 | module.exports = askQuestion; 106 | askQuestion.askWithCondition = askWithCondition; -------------------------------------------------------------------------------- /functions/commandLoader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const { errorLog } = require('./eventLoader'); 4 | 5 | let count = 0; 6 | let i = -1; 7 | 8 | const loadCmd = (path, command, client, reload) => { 9 | let commandPath = `${__dirname}/../commands/${path}`; 10 | 11 | if (reload) delete require.cache[require.resolve(commandPath)]; 12 | let file = require(commandPath); 13 | 14 | client.commands.set(command.replace(/.js/g, ''), file); 15 | count++; 16 | i++; 17 | if (i % 10 === 0) { 18 | console.log(''); 19 | process.stdout.write(' '); 20 | } 21 | process.stdout.write(`${command.slice(0, -3)} `); 22 | }; 23 | 24 | const soundEffects = (client) => { 25 | process.stdout.write(' '); 26 | let effects = fs.readdirSync('../media/sounds'); 27 | effects.forEach(file => { 28 | let basename = file.replace('.mp3', ''); 29 | client.commands.set(basename, { 30 | run: (message, args, suffix, client) => { 31 | message.__ = (string, variables) => { 32 | return i18n.get('commands.play.' + string, message, variables); 33 | }; 34 | 35 | client.commands.get('play').run(message, ['file', basename], suffix, client); 36 | }, 37 | config: { 38 | enabled: true, 39 | permLevel: 2, 40 | aliases: [], 41 | perms: ['EMBED_LINKS', 'SPEAK', 'CONNECT'], 42 | guildCooldown: 1000, 43 | category: 'sound_effects' 44 | } 45 | }); 46 | count++; 47 | process.stdout.write(file.slice(0, -4) + ' '); 48 | }) 49 | }; 50 | 51 | exports.loadCmd = loadCmd; 52 | 53 | module.exports = (client, reload) => { 54 | let start = Date.now(); 55 | process.stdout.write('Loading commands..'); 56 | count = 0; 57 | 58 | const files = fs.readdirSync(`${__dirname}/../commands`); 59 | 60 | files.forEach(f => { 61 | try { 62 | if (fs.statSync(`${__dirname}/../commands/${f}`).isDirectory()) { 63 | let dFiles = fs.readdirSync(`${__dirname}/../commands/${f}`).filter(fi => fs.statSync(`${__dirname}/../commands/${f}/${fi}`).isFile()); 64 | dFiles.forEach(dF => { 65 | try { 66 | if (dF !== '.DS_Store') loadCmd(`${f}/${dF}`, dF, client, reload); 67 | } catch (err) { 68 | errorLog(`Error loading ${f}/${dF}`, err); 69 | } 70 | 71 | }); 72 | } else { 73 | if (f !== '.DS_Store') loadCmd(f, f, client); 74 | } 75 | } catch (err) { 76 | errorLog(`Error loading ${f}`, err); 77 | } 78 | }); 79 | 80 | console.log(); 81 | 82 | console.log('Generating sound effect commands..'); 83 | soundEffects(client); 84 | 85 | console.log(); 86 | 87 | console.log(`Success! Loaded ${count} commands in ${Date.now() - start} ms.\n`); 88 | return [ count, Date.now() - start ]; 89 | }; 90 | -------------------------------------------------------------------------------- /functions/dbots.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | 3 | const config = require('../../media/config.json'); 4 | const { broadcastEval } = require.main.exports; 5 | 6 | let guilds; 7 | let prevShards; 8 | 9 | exports.post = async () => { 10 | let shards = await broadcastEval('this.guilds.cache.size'); 11 | 12 | let totalGuilds = shards.reduce((prev, cur) => prev + cur, 0); 13 | if (guilds === totalGuilds) return; 14 | guilds = totalGuilds; 15 | 16 | let orgOpts = { 17 | url: `https://top.gg/api/bots/${config.website.client_id}/stats`, 18 | method: 'POST', 19 | json: true, 20 | headers: { 21 | Authorization: config.dbotsAuth.org.bot 22 | }, 23 | body: { 24 | shards: shards, 25 | shard_count: shards.length 26 | } 27 | }; 28 | 29 | try { 30 | request(orgOpts); 31 | } catch (e) {} 32 | 33 | shards.forEach((shard, i) => { 34 | if (prevShards && prevShards[i] === shard) return; 35 | 36 | let pwOpts = { 37 | uri: `https://discord.bots.gg/api/v1/bots/${config.website.client_id}/stats`, 38 | method: 'POST', 39 | json: true, 40 | headers: { 41 | Authorization: config.dbotsAuth.pw 42 | }, 43 | body: { 44 | guildCount: shard, 45 | shardCount: shards.length, 46 | shardId: i 47 | } 48 | }; 49 | 50 | try { 51 | request(pwOpts); 52 | } catch (e) {} 53 | }); 54 | 55 | prevShards = shards; 56 | }; 57 | 58 | exports.getLikes = client => { 59 | let opts = { 60 | uri: `https://discordbots.org/api/bots/329085343800229889/votes?onlyids=1`, 61 | method: 'GET', 62 | json: true, 63 | headers: { 64 | Authorization: config.dbotsAuth.org.bot 65 | } 66 | }; 67 | 68 | request(opts, (err, response, body) => { 69 | client.dbotsUpvotes = body; 70 | }) 71 | }; 72 | -------------------------------------------------------------------------------- /functions/defaultChannel.js: -------------------------------------------------------------------------------- 1 | module.exports = guild => { 2 | let textChannels = guild.channels.cache.filter(g => g.type === 'GUILD_TEXT'); 3 | if (textChannels.size < 1) return undefined; 4 | 5 | return textChannels.find(c => c.name === 'general') || 6 | textChannels.get(guild.id) || 7 | textChannels.first(); 8 | }; 9 | -------------------------------------------------------------------------------- /functions/eventLoader.js: -------------------------------------------------------------------------------- 1 | const Discord = require('discord.js'); 2 | const config = require('../../media/config.json'); 3 | const statusWebhookClient = new Discord.WebhookClient({ id: config.statusLog.id, token: config.statusLog.token}); 4 | const errorWebhookClient = new Discord.WebhookClient({ id: config.errorLog.id, token: config.errorLog.token }); 5 | const fs = require('fs'); 6 | 7 | let lastHeartbeat = Date.now(); 8 | 9 | function statusUpdate (embed) { 10 | if (!(process.argv[2] && process.argv[2] === 'test')) statusWebhookClient.send({ embeds: [ embed ] }).catch(() => {}); 11 | } 12 | 13 | const errorLog = (shortError, error, noConsole) => { 14 | if (!noConsole) { 15 | console.error(shortError); 16 | console.error(error.stack ? error.stack : error); 17 | } 18 | 19 | let { stack, code} = error; 20 | if (!stack) stack = error; 21 | 22 | if (process.argv[2] && process.argv[2] === 'test') { 23 | if (process.env['ARTHUR_ERRORLOG'] !== 'true') return; 24 | code += ' | Testbot error'; 25 | } 26 | 27 | errorWebhookClient.send({ 28 | embeds: [ { 29 | title: shortError, 30 | description: '```js\n' + stack + '```', 31 | footer: { 32 | text: code + ` | Shard ${errorLog.shardID}` + (errorLog.lastCommand ? ` | last command: ${errorLog.lastCommand}` : '') 33 | }, 34 | timestamp: new Date().toISOString(), 35 | color: 0xff0000 36 | } ] 37 | }).catch((e) => { 38 | console.error('Error sending error webhook:\n', e.stack); 39 | }); 40 | }; 41 | 42 | errorLog.simple = error => { 43 | errorLog(`Simple error log error`, error); 44 | }; 45 | 46 | const rawEvents = { 47 | MESSAGE_REACTION_ADD: 'messageReactionAdd', 48 | MESSAGE_REACTION_REMOVE: 'messageReactionRemove' 49 | }; 50 | 51 | exports.load = client => { 52 | let start = Date.now(); 53 | let events = fs.readdirSync('./events'); 54 | console.log(`Loading ${events.length} events..`); 55 | process.stdout.write(' '); 56 | 57 | client.errorLog = errorLog; 58 | 59 | events.forEach(file => { 60 | let eventName = file.split('.')[0]; 61 | let event = require(`../events/${file}`); 62 | 63 | client.on(eventName, event.bind(null, client)); 64 | process.stdout.write(`${file.slice(0, -3)} `); 65 | }); 66 | 67 | console.log(); 68 | console.log(`Loaded ${events.length} events in ${Date.now() - start} ms.\n`); 69 | 70 | client.on('raw', async event => { 71 | if (!rawEvents.hasOwnProperty(event.t)) return; 72 | 73 | const { d: data } = event; 74 | const user = await client.users.fetch(data.user_id); 75 | const channel = client.channels.cache.get(data.channel_id); // || await user.createDM(); if using DM reactions 76 | 77 | if (!channel || channel.messages.cache.has(data.message_id)) return; 78 | 79 | const message = await channel.messages.fetch(data.message_id).catch(() => {}); 80 | if (!message) return; 81 | 82 | const emojiKey = (data.emoji.id) ? `${data.emoji.name}:${data.emoji.id}` : data.emoji.name; 83 | const reaction = message.reactions.cache.get(emojiKey) ? await message.reactions.cache.get(emojiKey).fetch() : undefined; 84 | 85 | if (!reaction || !reaction.message) return; 86 | 87 | client.emit(rawEvents[event.t], reaction, user); 88 | }); 89 | 90 | client.on('debug', d => { 91 | if (d.startsWith('[VOICE')) return; 92 | 93 | if (d.includes('Session invalidated')) statusUpdate({ 94 | title: 'Session Invalidated', 95 | timestamp: new Date().toISOString(), 96 | color: 0xf47742, 97 | }); 98 | 99 | if (d.includes('eartbeat')) { 100 | lastHeartbeat = Date.now(); 101 | return; 102 | } 103 | 104 | console.log(d); 105 | }); 106 | 107 | client.on('error', err => { 108 | errorLog('Discord.JS Client Error', err); 109 | }); 110 | 111 | process.on('unhandledPromiseRejection', (err, promise) => { 112 | errorLog(`Unhandled Promise Rejection at ${promise}`, err); 113 | }); 114 | 115 | process.on('unhandledRejection', (err, promise) => { 116 | if (err.code === 'ERR_IPC_CHANNEL_CLOSED') process.exit(); 117 | errorLog(`Unhandled Rejection Error at ${promise}`, err); 118 | }); 119 | 120 | process.on('uncaughtException', err => { 121 | console.error(err); 122 | fs.writeFileSync(__basedir + '/../media/temp/crash.txt', err.code + '\n' + err.stack); 123 | process.exit(err.code); 124 | }); 125 | }; 126 | 127 | exports.statusUpdate = statusUpdate; 128 | exports.errorLog = errorLog; 129 | -------------------------------------------------------------------------------- /functions/findMember.js: -------------------------------------------------------------------------------- 1 | module.exports = (message, string) => { 2 | let member; 3 | 4 | if (message.mentions.members.size) member = message.mentions.members.find(mem => mem.id !== message.client.user.id); 5 | 6 | if (!member && !!string) { 7 | let find = message.guild.members.cache.find(mem => mem.user.tag.toUpperCase() === string.toUpperCase()); 8 | if (!find) find = message.guild.members.cache.find(mem => mem.user.username.toUpperCase() === string.toUpperCase()); 9 | if (!find) find = message.guild.members.cache.find(mem => mem.displayName.toUpperCase() === string.toUpperCase()); 10 | if (!find) find = message.guild.members.cache.get(string); 11 | if (!find) return undefined; 12 | else member = find; 13 | } 14 | 15 | if (!member) member = message.member; 16 | 17 | return { 18 | user: member.user, 19 | member: member 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /functions/permLevel.js: -------------------------------------------------------------------------------- 1 | const { Permissions } = require('discord.js'); 2 | 3 | exports.pl = client => { 4 | client.permLevel = message => { 5 | let permLevel = 2; 6 | 7 | if (client.ownerId === message.author.id) return 10; 8 | if (client.config.owners.includes(message.author.id)) return 9; 9 | if (!message.author) return 0; 10 | if (!message.guild) return 1; 11 | 12 | try { 13 | let mod = message.guild.roles.cache.find(r => /mod$|moderator.*/.test(r.name.toLowerCase())); 14 | let admin = message.guild.roles.cache.find(r => r.name.toLowerCase().includes('admin')); 15 | if (mod && message.member.roles.cache.has(mod.id)) permLevel = 3; 16 | if (message.member.permissions.has(Permissions.FLAGS.MANAGE_GUILD)) permLevel = 4; 17 | if ((admin && message.member.roles.cache.has(admin.id)) || message.member.permissions.has('ADMINISTRATOR')) permLevel = 5; 18 | } catch (e) {} 19 | 20 | if (message.author.id === message.guild.ownerId) permLevel = 6; 21 | 22 | return permLevel; 23 | 24 | /* 25 | 0 - not a person (webhook/pinned message) 26 | 1 - in a DM 27 | 2 - regular guild member 28 | 3 - mod 29 | 4 - admin 30 | 5 - manage server perm 31 | 6 - guild owner 32 | 9 - bot owner 33 | 10 - gymno 34 | */ 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const sqlog = fs.createWriteStream('../media/sql.log'); 4 | 5 | const sqlite = require('sqlite'); 6 | const sqlite3 = require('sqlite3'); 7 | let sql; 8 | 9 | exports.broadcastEval = broadcastEval; // such that the post() function below has it defined 10 | 11 | const config = require('../media/config'); 12 | const { post } = require('./functions/dbots'); 13 | const { errorLog } = require('./functions/eventLoader'); 14 | errorLog.shardID = 'Manager'; 15 | 16 | const test = !!(process.argv[2] && process.argv[2] === 'test'); 17 | 18 | if (!test) { 19 | let tempItems = fs.readdirSync('../media/temp'); 20 | if (tempItems) tempItems.forEach(i => { 21 | fs.unlinkSync(`../media/temp/${i}`); 22 | }); 23 | } 24 | 25 | const { ShardingManager } = require('discord.js'); 26 | const manager = new ShardingManager('./bot.js', { 27 | token: test ? config.testToken : config.token, 28 | shardArgs: test ? [ 'test' ] : [] 29 | }); 30 | 31 | sqlite.open({ 32 | filename: test ? '../media/database/db.sqlite' : '/mnt/ramdisk/database/db.sqlite', 33 | driver: sqlite3.cached.Database 34 | }).then(db => { 35 | sql = db; 36 | 37 | manager.spawn().catch(errorLog.simple); 38 | }).catch(errorLog.simple); 39 | 40 | let stopwatchUserObject = {}; 41 | 42 | let commandStatsObject = JSON.parse(fs.readFileSync('../media/stats/commands.json')); 43 | let dailyStatsObject = JSON.parse(fs.readFileSync('../media/stats/daily.json')); 44 | let weeklyStatsObject = JSON.parse(fs.readFileSync('../media/stats/weekly.json')); 45 | 46 | let queue = new Map(); 47 | let queueID = 0; 48 | 49 | manager.on('shardCreate', shard => { 50 | console.log(`Launched shard ${shard.id}`); 51 | 52 | shard.on('ready', () => { 53 | shard.send({ 54 | action: 'uptime', 55 | uptime: Date.now() - Math.floor(process.uptime() * 1000), 56 | id: shard.id 57 | }).catch(errorLog.simple); 58 | }); 59 | 60 | shard.on('message', message => { 61 | switch (message.action) { 62 | case 'sql': { 63 | let timeline = {start: Date.now()}; 64 | let { id, query, args, type } = message; 65 | 66 | sql[type](query, args).then(result => { 67 | timeline.sqlFinished = Date.now() - timeline.start; 68 | sqlThen(shard, id, result, timeline); 69 | }).catch(error => { 70 | timeline.sqlFinished = Date.now() - timeline.start; 71 | timeline.error = true; 72 | sqlCatch(shard, id, error, timeline); 73 | }); 74 | 75 | break; 76 | } 77 | 78 | case 'stopwatch': { 79 | let { id } = message; 80 | 81 | if (stopwatchUserObject[id]) { 82 | shard.send({ action: 'stopwatch', id: id, start: stopwatchUserObject[id] }).catch(() => {}); 83 | delete stopwatchUserObject[id]; 84 | } else { 85 | stopwatchUserObject[id] = Date.now(); 86 | shard.send({ action: 'stopwatch', id: id }).catch(() => {}); 87 | } 88 | 89 | break; 90 | } 91 | 92 | case 'broadcastEval': { 93 | let { script, id } = message; 94 | let internalID = queueID++; 95 | 96 | queue.set(internalID, { 97 | shards: 0, 98 | returnShard: shard, 99 | returnID: id, 100 | results: {} 101 | }); 102 | 103 | manager.shards.forEach(shard => { 104 | sendWhenReady(shard, { 105 | action: 'eval', 106 | script: script, 107 | id: internalID 108 | }, () => { 109 | shard.emit({ 110 | action: 'eval', 111 | error: 'Shard took too long to become ready.', 112 | id: internalID 113 | }) 114 | }); 115 | }); 116 | 117 | break; 118 | } 119 | 120 | case 'eval': { 121 | let { error, result, id } = message; 122 | 123 | if (!queue.has(id)) return; 124 | let queueObj = queue.get(id); 125 | 126 | if (error) { 127 | queue.delete(id); 128 | 129 | if (queueObj.internal) return queueObj.internal.reject(error); 130 | 131 | return queueObj.returnShard.send({ 132 | action: 'broadcastEval', 133 | error: error, 134 | id: queueObj.returnID 135 | }).catch(errorLog.simple); 136 | } 137 | 138 | queueObj.shards++; 139 | queueObj.results[shard.id] = result; 140 | 141 | if (queueObj.shards < manager.shards.size) return; 142 | 143 | let results = []; 144 | for (let i = 0; i < queueObj.shards; i++) { 145 | results.push(queueObj.results[i]); 146 | } 147 | 148 | if (queueObj.internal) queueObj.internal.resolve(results); 149 | else queueObj.returnShard.send({ 150 | action: 'broadcastEval', 151 | result: results, 152 | id: queueObj.returnID 153 | }).catch(errorLog.simple); 154 | 155 | queue.delete(id); 156 | 157 | break; 158 | } 159 | 160 | case 'updateStats': { 161 | addValues(message.commands, commandStatsObject); 162 | addValues(message.daily, dailyStatsObject); 163 | addValues(message.weekly, weeklyStatsObject); 164 | 165 | break; 166 | } 167 | 168 | case 'getStats': { 169 | switch (message.type) { 170 | case 'commands': 171 | shard.send({ action: 'stats', id: message.id, value: commandStatsObject }).catch(errorLog.simple); 172 | break; 173 | case 'daily': 174 | shard.send({ action: 'stats', id: message.id, value: dailyStatsObject[message.arg] }).catch(errorLog.simple); 175 | break; 176 | case 'weekly': 177 | shard.send({ action: 'stats', id: message.id, value: weeklyStatsObject[message.arg] }).catch(errorLog.simple); 178 | break; 179 | } 180 | 181 | break; 182 | } 183 | 184 | case 'restart': { 185 | process.exit(); 186 | break; 187 | } 188 | } 189 | }); 190 | }); 191 | 192 | function sendWhenReady(shard, message, error, retry = 0) { 193 | if (retry > 20) return error(); 194 | 195 | if (shard.ready) shard.send(message); 196 | else setTimeout(() => { 197 | sendWhenReady(shard, message, error, ++retry); 198 | }, 1000); 199 | } 200 | 201 | function addValues(from, to) { 202 | for (let key in from) { 203 | if (typeof from[key] !== 'number') { 204 | if (!to[key]) to[key] = {}; 205 | addValues(from[key], to[key]); 206 | } else { 207 | if (!to[key]) to[key] = from[key]; 208 | else to[key] += from[key]; 209 | } 210 | } 211 | } 212 | 213 | function sqlThen(shard, id, result, timeline) { 214 | shard.send({ 215 | action: 'sql', 216 | id: id, 217 | result: result 218 | }).catch(errorLog.simple).then(() => { 219 | handleSQLTimeline(timeline); 220 | }); 221 | } 222 | 223 | function sqlCatch(shard, id, error, timeline) { 224 | errorLog('SQL error', error); 225 | 226 | shard.send({ 227 | action: 'sql', 228 | id: id, 229 | error: error 230 | }).catch(errorLog.simple).then(() => { 231 | handleSQLTimeline(timeline); 232 | }); 233 | } 234 | 235 | function handleSQLTimeline(timeline) { 236 | timeline.finished = Date.now() - timeline.start; 237 | if (timeline.finished < 1000) return; 238 | 239 | sqlog.write(`SQL rec at ${timeline.start}, finished ${timeline.sqlFinished} ms later, sent off ${timeline.finished - timeline.sqlFinished} ms later, total ${timeline.finished} ms.\n`); 240 | } 241 | 242 | 243 | function broadcastEval(script) { 244 | return new Promise((resolve, reject) => { 245 | let id = queueID++; 246 | 247 | queue.set(id, { 248 | shards: 0, 249 | results: {}, 250 | internal: { resolve, reject } 251 | }); 252 | 253 | manager.shards.forEach(shard => { 254 | shard.send({ 255 | action: 'eval', 256 | script: script, 257 | id: id 258 | }).catch(reject); 259 | }); 260 | }); 261 | } 262 | 263 | setInterval(() => { 264 | fs.writeFileSync('../media/stats/commands.json', JSON.stringify(commandStatsObject)); 265 | fs.writeFileSync('../media/stats/daily.json', JSON.stringify(dailyStatsObject)); 266 | fs.writeFileSync('../media/stats/weekly.json', JSON.stringify(weeklyStatsObject)); 267 | }, 30000); 268 | 269 | if (!test) setInterval(() => { 270 | post().catch(errorLog.simple); 271 | }, 1000 * 60 * 2); 272 | -------------------------------------------------------------------------------- /locales/ro Română.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "authors": [ 4 | "Tudor#3877" 5 | ], 6 | "flag": "ro", 7 | "translations": { 8 | "cs": "Cehă", 9 | "en-US": "Engleză, Statele Unite", 10 | "es-ES": "Spaniolă", 11 | "nl": "Olandeză", 12 | "pl": "Polonă", 13 | "pt": "Portugheză", 14 | "pt-BR": "Portugheză, Brazilia", 15 | "ro": "Română" 16 | } 17 | }, 18 | "permissions": { 19 | "MANAGE_SERVER": "Gestionează server-ul", 20 | "ADMINISTRATOR": "Administrator", 21 | "EMBED_LINKS": "Încorporează link-uri", 22 | "ATTACH_FILES": "Atașează fișiere", 23 | "ADD_REACTIONS": "Adaugă reacții", 24 | "SPEAK": "Vorbește", 25 | "CONNECT": "Conectează-te" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arthur", 3 | "version": "0.1.0", 4 | "description": "A discord bot, probably a rewrite of Marvin", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "discord", 11 | "bot", 12 | "discord.js", 13 | "arthur", 14 | "marvin" 15 | ], 16 | "author": "Nikolas Brandt", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/nikbrandt/Arthur.git" 20 | }, 21 | "license": "MIT", 22 | "dependencies": { 23 | "@discordjs/opus": "^0.8.0", 24 | "bufferutil": "^4.0.3", 25 | "canvas": "^2.8.0", 26 | "capture-website": "^1.4.0", 27 | "discord.js": "^13.10.3", 28 | "erlpack": "github:discord/erlpack", 29 | "file-type": "^12.4.2", 30 | "fluent-ffmpeg": "^2.1.2", 31 | "hoek": "^5.0.4", 32 | "libsodium-wrappers": "^0.7.9", 33 | "moment": "^2.29.1", 34 | "ms": "^2.1.3", 35 | "needle": "^2.6.0", 36 | "node-expat": "^2.4.0", 37 | "node-fetch": "^2.6.1", 38 | "node-ipc": "^9.2.0", 39 | "request": "^2.88.2", 40 | "request-promise": "^4.2.6", 41 | "sqlite": "^4.0.23", 42 | "sqlite3": "^5.0.2", 43 | "trello": "^0.10.0", 44 | "utf-8-validate": "^5.0.5", 45 | "xml2json": "^0.11.2", 46 | "ytdl-core": "^4.8.3", 47 | "ytpl": "^1.0.1", 48 | "ytsr": "^3.5.0", 49 | "zlib-sync": "^0.1.7" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /struct/ArthurClient.js: -------------------------------------------------------------------------------- 1 | const { Client, Collection } = require('discord.js'); 2 | const loadCommands = require('../functions/commandLoader'); 3 | const eventLoader = require('../functions/eventLoader'); 4 | const permLevel = require('../functions/permLevel.js'); 5 | const findMember = require('../functions/findMember.js'); 6 | const i18n = require('../struct/i18n'); 7 | const config = require('../../media/config.json'); 8 | 9 | class ArthurClient extends Client { 10 | constructor (options) { 11 | super(options); 12 | 13 | this.loadStart = Date.now(); 14 | this.queueCount = 0; 15 | 16 | this.test = !!(process.argv[2] && process.argv[2] === 'test'); 17 | this.processing = []; 18 | 19 | this.commandStatsObject = {}; 20 | this.dailyStatsObject = {}; 21 | this.weeklyStatsObject = {}; 22 | 23 | this.config = config; 24 | this.ownerId = config.owners[0]; 25 | this.commands = new Collection(); 26 | this.aliases = new Collection(); 27 | this.reactionCollectors = new Collection(); 28 | this.shardQueue = new Map(); 29 | this.shardErrorQueue = new Map(); 30 | this.guildBlacklists = new Map(); 31 | this.hardBlacklist; 32 | 33 | loadCommands(this); 34 | 35 | this.i18n = new i18n(this); 36 | global.i18n = this.i18n; 37 | 38 | permLevel.pl(this); 39 | this.findMember = findMember; 40 | } 41 | 42 | async init () { 43 | eventLoader.load(this); 44 | await this.i18n.init(); 45 | this.totalXP = (await sql.get('SELECT SUM(global) FROM (SELECT DISTINCT userID, global FROM xp)'))['SUM(global)']; 46 | 47 | this.login(this.test ? this.config.testToken : this.config.token).catch(this.errorLog.simple); 48 | } 49 | 50 | broadcastEval(script) { 51 | return new Promise((resolve, reject) => { 52 | if (!this.readyTimestamp) reject('Client not ready yet.'); 53 | 54 | let id = this.queueCount++; 55 | 56 | this.shardQueue.set(id, resolve); 57 | this.shardErrorQueue.set(id, reject); 58 | 59 | this.shard.send({ 60 | action: 'broadcastEval', 61 | script: script, 62 | id: id 63 | }).catch(reject); 64 | }); 65 | } 66 | 67 | async getGuildBlacklist(guildID) { 68 | if (this.guildBlacklists.has(guildID)) return this.guildBlacklists.get(guildID); 69 | else { 70 | let blacklist = await sql.all('SELECT ID FROM guildBlacklist WHERE guildID = ?', guildID); 71 | this.guildBlacklists.set(guildID, blacklist.map(o => o.ID)); 72 | 73 | return blacklist; 74 | } 75 | } 76 | 77 | async checkHardBlacklist(id) { 78 | if (this.hardBlacklist) return this.hardBlacklist.includes(id); 79 | else { 80 | let blacklist = await sql.all('SELECT id FROM hardBlacklist'); 81 | blacklist = blacklist.map(o => o.id); 82 | this.hardBlacklist = blacklist; 83 | 84 | return blacklist.includes(id); 85 | } 86 | } 87 | } 88 | 89 | module.exports = ArthurClient; 90 | -------------------------------------------------------------------------------- /struct/Util.js: -------------------------------------------------------------------------------- 1 | class Util { 2 | /** 3 | * Similar to Array#map, return an array based on input function 4 | * @param {object} object The object to map 5 | * @param {function} func The function to call on each object entry, with parameters `key` and `value`. Returning `null` ignores disincludes current iteration. 6 | * @returns {Array} The resulting array 7 | */ 8 | static objectMap(object, func) { 9 | let keys = Object.keys(object); 10 | let values = Object.values(object); 11 | 12 | let out = []; 13 | for (let i = 0; i < keys.length; i++) { 14 | let res = func(keys[i], values[i]); 15 | if (res === null) continue; 16 | out.push(res); 17 | } 18 | 19 | return out; 20 | } 21 | 22 | /** 23 | * Convert an input duration into a human readable time string (e.g. 4h 22m 39s) 24 | * @param {number} seconds The input duration, in seconds 25 | * @param {Message|Guild|User|string} resolvable A resolvable for use with i18n (to get the 'h', 'm', and 's') 26 | */ 27 | static timeString(seconds, resolvable) { 28 | let h = i18n.get('time.abbreviations.hours', resolvable); 29 | let m = i18n.get('time.abbreviations.minutes', resolvable); 30 | let s = i18n.get('time.abbreviations.seconds', resolvable); 31 | 32 | let hours = Math.floor(seconds / 3600); 33 | seconds -= hours * 3600; 34 | 35 | let mins = Math.floor(seconds / 60); 36 | seconds -= mins * 60; 37 | 38 | return ((hours ? hours + h + ' ' : '') + (mins ? mins + m + ' ' : '') + (seconds || (!hours && !mins) ? seconds + s: '')).trim(); 39 | } 40 | } 41 | 42 | module.exports = Util; -------------------------------------------------------------------------------- /struct/soundcloud.js: -------------------------------------------------------------------------------- 1 | const needle = require('needle'); 2 | const Discord = require('discord.js'); 3 | 4 | const { timeString } = require('./Util.js'); 5 | const config = require('../../media/config.json'); 6 | const clientID = config.soundcloudID; 7 | 8 | const soundcloudRegex = /^(https:\/\/)?soundcloud.com\/.+\/[^/\n\s]+$/; 9 | 10 | let cache = new Map(); 11 | let timeouts = {}; 12 | 13 | const soundcloud = module.exports = function soundcloud (id) { 14 | return new Promise(async resolve => { 15 | let res = await needle('get', id + `?client_id=${clientID}`, { json: true }); 16 | return resolve(res.body.url); 17 | }); 18 | }; 19 | 20 | soundcloud.getInfo = getInfo; 21 | soundcloud.search = search; 22 | soundcloud.constructInfoFromMeta = constructInfoFromMeta; 23 | soundcloud.regex = soundcloudRegex; 24 | 25 | function getInfo (url, localeResolvable) { 26 | return new Promise(async (resolve, reject) => { 27 | if (!soundcloudRegex.test(url)) return reject(i18n.get('struct.soundcloud.invalid_url', localeResolvable)); 28 | if (url.indexOf('?') > 0) url = url.substring(0, url.indexOf('?')); 29 | 30 | if (cache.has(url)) return resolve(cache.get(url)); 31 | 32 | let { body } = await needle('get', `https://api-v2.soundcloud.com/resolve?url=${encodeURIComponent(url)}&client_id=${clientID}`, { json: true }).catch(err => { 33 | reject(err); 34 | }); 35 | 36 | if (!body) return; 37 | 38 | if (body.errors && body.errors[1].error_message.includes('404')) return reject(i18n.get('struct.soundcloud.sound_nonexistant', localeResolvable)); 39 | if (body.kind !== 'track' && body.kind !== 'playlist') return reject(i18n.get('struct.soundcloud.invalid_type', localeResolvable, { type: body.kind })); 40 | 41 | addToCache(body); 42 | if (body.kind === 'track') body.id = body.media.transcodings[1].url; 43 | resolve(body); 44 | }); 45 | } 46 | 47 | /** 48 | * Search for a song 49 | * @param term 50 | * @param localeResolvable 51 | * @returns {Promise} 52 | */ 53 | function search (term, localeResolvable) { 54 | return new Promise(async (resolve, reject) => { 55 | let { body } = await needle('get', `https://api-v2.soundcloud.com/search/tracks?q=${encodeURIComponent(term)}&limit=1&client_id=${clientID}`, { json: true }).catch(err => { 56 | reject(err); 57 | }); 58 | 59 | if (!body) return reject(i18n.get('struct.soundcloud.no_search_results', localeResolvable)); 60 | 61 | if (!body.collection || body.collection.length === 0) return reject(i18n.get('struct.soundcloud.no_search_results', localeResolvable)); 62 | 63 | addToCache(body.collection[0]); 64 | resolve(body.collection[0]); 65 | }); 66 | } 67 | 68 | /** 69 | * Converts track meta from soundcloud API to getInfo friendly format 70 | * @param {object} meta Track meta from soundcloud API 71 | * @param {Message} message Message that started the getInfo request 72 | * @param {string} title The title being used in the embed of the song ("Now Playing" or "Added to queue", i18n'd) 73 | * @returns {object} Object with in getInfo format 74 | */ 75 | function constructInfoFromMeta(meta, message, title) { 76 | if (!meta.user || !meta.title || !meta.duration) return null; 77 | 78 | let time = timeString(Math.round(meta.duration / 1000), message); 79 | 80 | meta.title = Discord.Util.escapeMarkdown(meta.title); 81 | meta.user.username = Discord.Util.escapeMarkdown(meta.user.username); 82 | 83 | return { 84 | meta: { 85 | url: meta.permalink_url, 86 | title: `${meta.title} (${time})`, 87 | queueName: `[${meta.title}](${meta.permalink_url}) - ${time}`, 88 | id: meta.media ? meta.media.transcodings[1].url : meta.id, 89 | length: Math.round(meta.duration / 1000) 90 | }, 91 | embed: { 92 | author: { 93 | name: title, 94 | icon_url: meta.user.avatar_url 95 | }, 96 | color: 0xff8800, 97 | description: `[${meta.title}](${meta.permalink_url})\n${i18n.get('commands.nowplaying.by', message)} [${meta.user.username}](${meta.user.permalink_url})\n${message._('length')}: ${time}`, 98 | thumbnail: { 99 | url: meta.artwork_url 100 | }, 101 | footer: { 102 | text: i18n.get('commands.nowplaying.footer', message, {tag: message.author.tag}) 103 | } 104 | }, 105 | ms: meta.duration 106 | }; 107 | } 108 | 109 | function addToCache(item) { 110 | let id = item.permalink_url; 111 | 112 | if (item.kind === 'track') cache.set(id, getNecessarySongMeta(item)); 113 | else if (item.kind === 'playlist') { 114 | let tracks = []; 115 | item.tracks.forEach(track => { 116 | let id = track.permalink_url; 117 | let info = getNecessarySongMeta(track); 118 | 119 | if (!info) return; 120 | 121 | cache.set(id, info); 122 | setCacheTimeout(id); 123 | tracks.push(info); 124 | }); 125 | 126 | cache.set(id, { 127 | permalink_url: id, 128 | kind: 'playlist', 129 | tracks: tracks 130 | }); 131 | } 132 | 133 | setCacheTimeout(id); 134 | } 135 | 136 | function getNecessarySongMeta(song) { 137 | if (!song.user || !song.title || !song.duration) return null; 138 | 139 | return { 140 | duration: song.duration, 141 | title: song.title, 142 | permalink_url: song.permalink_url, 143 | user: { 144 | username: song.user.username, 145 | avatar_url: song.user.avatar_url, 146 | permalink_url: song.user.permalink_url 147 | }, 148 | id: song.media ? song.media.transcodings[1].url : song.id, 149 | artwork_url: song.artwork_url, 150 | kind: 'track' 151 | } 152 | } 153 | 154 | function setCacheTimeout(id) { 155 | if (timeouts[id]) clearTimeout(timeouts[id]); 156 | timeouts[id] = setTimeout(() => { 157 | cache.delete(id); 158 | delete timeouts[id]; 159 | }, 1000 * 60 * 15); 160 | } 161 | -------------------------------------------------------------------------------- /struct/xp.js: -------------------------------------------------------------------------------- 1 | const config = require('../../media/config.json'); 2 | 3 | const emojiForNumber = num => { 4 | let emojiArray = ['👑', ':two:', ':three:', ':four:', ':five:']; 5 | if (num <= 5 && num >= 1) return emojiArray[num - 1]; 6 | else return num + '.'; 7 | }; 8 | 9 | class XP { 10 | static async memberXP (member) { 11 | let row = await sql.get(`SELECT * FROM xp WHERE userID = ${member.user.id} AND guildID = ${member.guild.id}`); 12 | if (!row) { 13 | sql.run(`INSERT INTO xp (userID, guildID, current, total, level, mult, global, lastXP) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [member.user.id, member.guild.id, 0, 0, 0, 1, 0, 0]); 14 | return { 15 | current: 0, 16 | total: 0, 17 | level: 0, 18 | mult: 0 19 | } 20 | } 21 | return { 22 | current: row.current, 23 | total: row.total, 24 | level: row.level, 25 | mult: row.mult, 26 | global: row.global 27 | } 28 | } 29 | 30 | static async addXP (message, guildRow) { 31 | if (message.channel.type !== 'GUILD_TEXT') return; 32 | if (message.channel.id === '304429222477299712') return; 33 | 34 | if (!guildRow || guildRow.levels === 'false') return; 35 | 36 | let rows = await sql.all(`SELECT * FROM xp WHERE userID = '${message.author.id}'`); 37 | let global = Math.max.apply(Math, rows.map(a => a.global)); 38 | 39 | let row = await sql.get(`SELECT * FROM xp WHERE userID = '${message.author.id}' AND guildID = '${message.guild.id}'`); 40 | if (!row) return sql.run(`INSERT INTO xp (userID, guildID, current, total, level, mult, global, lastXP, lastMessage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [message.author.id, message.guild.id, 0, 0, 0, 1, 0, 0, Date.now()]); 41 | 42 | if (Date.now() - row.lastXP < config.xp.xpAdd) return sql.run(`UPDATE xp SET lastMessage = ${Date.now()} WHERE userID = '${message.author.id}' AND guildID = '${message.guild.id}'`); 43 | 44 | let mult; 45 | if (Date.now() - row.lastMessage < 10000) mult = row.mult * 1.02; 46 | else if (Date.now() - row.lastMessage < 25000) mult = row.mult * 1.01; 47 | else if (Date.now() - row.lastMessage < 50000) mult = row.mult * 1.005; 48 | else mult = 1; 49 | if (mult > config.xp.maxMult) mult = config.xp.maxMult; 50 | sql.run(`UPDATE xp SET mult = ${mult} WHERE userID = '${message.author.id}' AND guildID = '${message.guild.id}'`); 51 | 52 | let base = Math.round(mult * config.xp.base); 53 | let min = base - config.xp.min; 54 | let max = base + config.xp.max; 55 | let add = Math.round(Math.random() * (max - min) + min); 56 | message.client.totalXP += add; 57 | 58 | let levels = []; 59 | let level = 0; 60 | let temp = 0; 61 | 62 | for (let i = 0; i < 101; i++) { 63 | temp += Math.round(config.xp.levelOne * Math.pow(config.xp.mult, i)); 64 | levels.push(temp); 65 | if (temp < row.total) level = i + 1; 66 | } 67 | 68 | if (row.total + add >= levels[level] && level + 1 > row.level) { 69 | sql.run(`UPDATE xp SET level = ${level + 1} WHERE guildID = '${message.guild.id}' AND userID = '${message.author.id}'`); 70 | sql.run(`UPDATE xp SET current = ${row.total + add - levels[level]} WHERE userID = '${message.author.id}' AND guildID = '${message.guild.id}'`); 71 | if (guildRow.levelMessage === 'true') message.channel.send(i18n.get('struct.xp.level_up', message.guild, { mention: message.author.toString(), level: level + 1})).catch(() => {}); 72 | } else sql.run(`UPDATE xp SET current = ${row.current + add} WHERE userID = '${message.author.id}' AND guildID = '${message.guild.id}'`); 73 | 74 | sql.run(`UPDATE xp SET total = ${row.total + add} WHERE userID = '${message.author.id}' AND guildID = '${message.guild.id}'`); 75 | sql.run(`UPDATE xp SET lastXP = ${Date.now()} WHERE userID = '${message.author.id}' AND guildID = '${message.guild.id}'`); 76 | sql.run(`UPDATE xp SET lastMessage = ${Date.now()} WHERE userID = '${message.author.id}' AND guildID = '${message.guild.id}'`); 77 | sql.run(`UPDATE xp SET global = ${global + add} WHERE userID = '${message.author.id}'`); 78 | } 79 | 80 | static async globalRank (user) { 81 | let rows = await sql.all('SELECT DISTINCT userID, global FROM xp'); 82 | rows = rows.sort((a, b) => { 83 | return a.global < b.global ? 1 : -1; 84 | }); 85 | 86 | let index = rows.findIndex(x => x.userID === user.id); 87 | if (typeof index !== 'number') return false; 88 | 89 | return { 90 | rank: index + 1, 91 | page: Math.ceil((index + 1) / 5) 92 | }; 93 | } 94 | 95 | static async guildRank (member) { 96 | let rows = await sql.all(`SELECT userID, total FROM xp WHERE guildID = '${member.guild.id}'`); 97 | rows = rows.sort((a, b) => { 98 | return a.total < b.total ? 1 : -1; 99 | }); 100 | 101 | let index = rows.findIndex(x => x.userID === member.user.id); 102 | if (typeof index !== 'number') return false; 103 | 104 | return { 105 | rank: index + 1, 106 | page: Math.ceil((index + 1) / 5) 107 | }; 108 | } 109 | 110 | static async guildLeaderboard (guildID, page, client, locale) { 111 | let entries = await sql.all(`SELECT userID, level, total FROM xp WHERE guildID = '${guildID}'`); 112 | 113 | if (!entries) return { 114 | max: 1, 115 | array: [ i18n.getString('struct.xp.no_one_talked_yet', locale) ] 116 | }; 117 | 118 | entries = entries.sort((a, b) => { 119 | return a.total < b.total ? 1 : -1; 120 | }); 121 | 122 | let maxPage = Math.ceil(entries.length / 5); 123 | if (page > maxPage) return false; 124 | 125 | let pageArray = []; 126 | entries = entries.slice(page * 5 - 5, page * 5); 127 | let startNum = page * 5 - 4; 128 | 129 | for (let entry of entries) { 130 | let user = await client.users.fetch(entry.userID); 131 | pageArray.push(`${emojiForNumber(startNum)} ${startNum === 1 ? '**' : ''} ${user.username} | Level ${entry.level} | ${entry.total} xp${startNum === 1 ? '**' : ''}`); 132 | startNum++; 133 | } 134 | 135 | return { 136 | max: maxPage, 137 | array: pageArray 138 | }; 139 | } 140 | 141 | static async globalLeaderboard (page, client) { 142 | let entries = await sql.all(`SELECT DISTINCT userID, global FROM xp`); 143 | 144 | if (!entries) { 145 | client.errorLog('No sqlite entries', new Error('No SQL entries in global leaderboard func')); 146 | return { 147 | max: 1, 148 | array: ['no entries found - you shouldn\'t ever see this - please report it'] 149 | }; 150 | } 151 | 152 | entries = entries.sort((a, b) => { 153 | return a.global < b.global ? 1 : -1; 154 | }); 155 | 156 | let maxPage = Math.ceil(entries.length / 5); 157 | if (page > maxPage) return false; 158 | 159 | let pageArray = []; 160 | entries = entries.slice(page * 5 - 5, page * 5); 161 | let startNum = page * 5 - 4; 162 | 163 | for (let entry of entries) { 164 | let user = await client.users.fetch(entry.userID); 165 | pageArray.push(`${emojiForNumber(startNum)} ${startNum === 1 ? '**' : ''} ${user.username} | ${entry.global} xp${startNum === 1 ? '**' : ''}`); 166 | startNum++; 167 | } 168 | 169 | return { 170 | max: maxPage, 171 | array: pageArray 172 | }; 173 | } 174 | } 175 | 176 | module.exports = XP; 177 | --------------------------------------------------------------------------------