├── .env.example
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── LICENSE
├── README.md
├── bot.js
├── commands
├── avatar.js
├── birthday.js
├── brackets.js
├── cfg.js
├── describe.js
├── dev.js
├── export.js
├── feedback.js
├── find.js
├── group.js
├── help.js
├── import.js
├── invite.js
├── list.js
├── listng.js
├── privacy.js
├── register.js
├── remove.js
├── rename.js
├── showuser.js
├── stats.js
├── tag.js
└── togglebrackets.js
├── events
├── debug.js
├── disconnect.js
├── error.js
├── guildDelete.js
├── hello.js
├── messageCreate.js
├── messageReactionAdd.js
├── messageUpdate.js
├── rawWS.js
├── ready.js
├── shardDisconnect.js
├── shardPreReady.js
├── shardReady.js
├── shardResume.js
├── unknown.js
└── warn.js
├── index.js
├── modules
├── blacklist.json
├── cmd.js
├── db.js
├── ipc.js
├── lang.js
├── msg.js
├── paginator.js
├── proxy.js
├── redis.js
└── util.js
└── package.json
/.env.example:
--------------------------------------------------------------------------------
1 | DISCORD_TOKEN=MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs
2 | DISCORD_OWNERID=99326032288423936
3 |
4 | BOT_INVITE=431544605209788416
5 | SUPPORT_INVITE=rHxMbt2
6 |
7 | PGUSER=postgres
8 | PGHOST=postgres
9 | PGDATABASE=postgres
10 | PGPASSWORD=postgres
11 | PGPORT=5432
12 | REDISURL=redis://localhost:6379
13 |
14 | SENTRY_DSN=https://(key)@sentry.io/(id)
15 | DEFAULT_PREFIX=tul!
16 | DEFAULT_LANG=tupper
17 |
18 | DEV=
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push, pull_request]
3 | jobs:
4 | build:
5 | runs-on: ubuntu-latest
6 | steps:
7 | - uses: actions/checkout@v2
8 | - name: Install modules
9 | run: npm install
10 | - name: Run ESLint
11 | run: ./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | # Dependency directory
64 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
65 | node_modules
66 |
67 | #privacy policy
68 | privacy.txt
69 |
70 | #config files
71 | auth.json
72 | servercfg.json
73 | tulpae.json
74 | webhooks.json
75 | backups
76 | package-lock.json
77 | recovery*
78 | .vscode/
79 | .vs/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2020 Runi and contributors
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 | # This is the open-source fork of the public [Tupperbox bot](https://discord.com/oauth2/authorize?client_id=431544605209788416&scope=bot&permissions=536996928).
2 | This fork is not currently being worked on. Pull requests or issues opened on this fork may not be looked at for a while. In the future more features are planned to be brought to this fork for those of you hosting your own versions of Tupperbox.
3 | If you have an issue with the [live version of Tupperbox](https://discord.com/oauth2/authorize?client_id=431544605209788416&scope=bot&permissions=536996928) then please join the [support server](https://discord.com/invite/rHxMbt2) for help.
4 |
5 | # Tupperbox
6 | A Discord bot written in [eris](https://github.com/abalabahaha/eris) for proxying user messages through webhooks to emulate users having multiple Discord accounts.
7 |
8 | * [Click here to invite the public bot to your server!](https://discord.com/oauth2/authorize?client_id=431544605209788416&scope=bot&permissions=536996928)
9 |
10 | * [Click here to join our support server!](https://discord.com/invite/rHxMbt2)
11 |
12 | # Local Installation
13 | The self-hosted version of Tupperbox requires Node.js (must be at least v14), PostgreSQL (v11, preferably v12) and Redis (stable, currently v6.0.8). You can download Node.js [here](https://nodejs.org/en/download/), PostgreSQL [here](https://www.postgresql.org/download/) and Redis [here (Linux)](https://redis.io/download) or [here (Windows)](https://www.memurai.com/).
14 |
15 | Once Node.js is installed, run `npm install` from the bot directory to install the bot's dependencies. (Note: you may have to run `npm -g install windows-build-tools` first if on Windows)
16 |
17 | The bot expects a file in the same directory named `.env` with its configuration info. An example configuration can be found in the `.env.example` file.
18 |
19 | * The PG-prefixed variables should be filled in with the connection info to your PostgreSQL database set up during installation.
You need a **database**, a **user** with associated **password** with full write access to that database, and the **host IP** of the machine running the server (localhost if it's the same machine).
20 |
21 | * SENTRY_DSN is a link to a registered Sentry project. See https://sentry.io/ for more information on Sentry.
This is **optional**.
22 |
23 | * Edit `DEFAULT_PREFIX`, `DEFAULT_LANG` as desired.
24 |
25 | * `BOT_INVITE` is the bot's user ID, used in the `tul!invite` command. `SUPPORT_INVITE` is the invite ID (**not invite link**) to the bot's support server, used in the `tul!feedback` command.
Remove either of these to remove the respective bot commands.
26 |
27 | * Leave `REDISURL` alone unless you change the port Redis runs on or you host it on another machine.
28 |
--------------------------------------------------------------------------------
/bot.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const Sentry = require("@sentry/node");
3 | Sentry.init({dsn: process.env.SENTRY_DSN });
4 |
5 | const Base = require("eris-sharder").Base;
6 |
7 | class Tupperbox extends Base {
8 | constructor(bot) {
9 | super(bot);
10 | }
11 |
12 | launch() {
13 | let bot = this.bot;
14 | bot.base = this;
15 | bot.sentry = Sentry;
16 | bot.db = require("./modules/db");
17 | bot.msg = require("./modules/msg");
18 | bot.cmd = require("./modules/cmd");
19 | bot.proxy = require("./modules/proxy");
20 | bot.paginator = require("./modules/paginator");
21 | bot.recent = {};
22 | bot.cmds = {};
23 | bot.dialogs = {};
24 | bot.owner = process.env.DISCORD_OWNERID;
25 | bot.defaultCfg = { prefix: process.env.DEFAULT_PREFIX, lang: process.env.DEFAULT_LANG };
26 | try { bot.blacklist = require("./modules/blacklist.json"); }
27 | catch(e) { bot.blacklist = []; }
28 | require("./modules/ipc")(bot);
29 | require("./modules/util")(bot);
30 |
31 | let files = fs.readdirSync("./commands");
32 | files.forEach(file => {
33 | bot.cmds[file.slice(0,-3)] = require("./commands/"+file);
34 | });
35 |
36 | files = fs.readdirSync("./events");
37 | files.forEach(file => {
38 | bot.on(file.slice(0,-3), (...args) => require("./events/"+file)(...args,bot));
39 | });
40 |
41 | process.on("message", message => {
42 | if(bot.ipc[message.name]) bot.ipc[message.name](message);
43 | });
44 |
45 | setInterval(() => bot.updateStatus(),3600000); //every hour
46 | bot.updateStatus();
47 |
48 | if (!process.env.BOT_INVITE)
49 | delete bot.cmds.invite;
50 |
51 | if (!process.env.SUPPORT_INVITE)
52 | delete bot.cmds.feedback;
53 |
54 | if(!fs.existsSync("privacy.txt")) {
55 | console.warn("no privacy command");
56 | delete bot.cmds.privacy;
57 | }
58 | }
59 | }
60 |
61 | module.exports = Tupperbox;
62 |
--------------------------------------------------------------------------------
/commands/avatar.js:
--------------------------------------------------------------------------------
1 | const validUrl = require("valid-url");
2 | const request = require("got");
3 | const probe = require("probe-image-size");
4 | const {article,proper} = require("../modules/lang");
5 |
6 | module.exports = {
7 | help: cfg => "View or change " + article(cfg) + " " + cfg.lang + "'s avatar",
8 | usage: cfg => [
9 | `avatar - Show the ${cfg.lang}'s current avatar.`,
10 | `avatar [url] - If url is specified or an image is uploaded with the command, change the ${cfg.lang}'s avatar.`,
11 | `avatar clear - Set the ${cfg.lang}'s avatar to the default avatar.`,
12 | ],
13 | permitted: () => true,
14 | desc: cfg => "It's possible to simply upload the new avatar as an attachment while running the command instead of providing the URL. If a URL is provided, it must be a direct link to an image - that is, the URL should end in .jpg or .png or another common image filetype.\n\nDue to Discord limitations, avatars can't be over 1mb in size and either the width or height of the avatar must be less than 1024.",
15 | groupArgs: true,
16 | execute: async (bot, msg, args, cfg) => {
17 | if(!args[0]) return bot.cmds.help.execute(bot, msg, ["avatar"], cfg);
18 |
19 | let clear = false;
20 |
21 | //check arguments
22 | let name = msg.attachments[0] ? args.join(" ") : args[0];
23 | let member = await bot.db.members.get(msg.author.id, name);
24 | if(!member) return `You don't have ${article(cfg)} ${cfg.lang} named '${name}' registered.`;
25 | if(!args[1] && !msg.attachments[0]) return member.avatar_url;
26 |
27 | // check if we're clearing
28 | if (!msg.attachments[0] && args[1] == "clear") {
29 | clear = true;
30 | args[1] = "https://i.imgur.com/ZpijZpg.png";
31 | }
32 |
33 | if(!validUrl.isWebUri(args[1]) && !msg.attachments[0]) return "Malformed url.";
34 |
35 | //check image is valid
36 | let url = msg.attachments[0] ? msg.attachments[0].url : args[1];
37 | if (!clear) {
38 | let head;
39 | try { head = await request.head(url); }
40 | catch(e) { return "I was unable to access that URL. Please try another."; }
41 | if(!head.headers["content-type"] || !head.headers["content-type"].startsWith("image")) return "I couldn't find an image at that URL. Make sure it's a direct link (ends in .jpg or .png for example).";
42 | if(Number(head.headers["content-length"]) > 1048575) {
43 | return "That image is too large and Discord will not accept it. Please use an image under 1mb.";
44 | }
45 | let res;
46 | try { res = await probe(url); }
47 | catch(e) { return "There was a problem checking that image. Please try another."; }
48 | if(Math.min(res.width,res.height) >= 1024) return "That image is too large and Discord will not accept it. Please use an image where width or height is less than 1024 pixels.";
49 | }
50 |
51 | //update member
52 | await bot.db.members.update(msg.author.id,name,"avatar_url",url);
53 | return `Avatar ${clear ? "cleared" : "changed"} successfully.` + (msg.attachments[0] ? "\nNote: if the message you just used to upload the avatar with gets deleted, the avatar will eventually no longer appear." : "");
54 | }
55 | };
--------------------------------------------------------------------------------
/commands/birthday.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "View or change " + article(cfg) + " " + cfg.lang + "'s birthday, or see upcoming birthdays",
5 | usage: cfg => ["birthday [name] [date] -\n\tIf name and date are specified, set the named " + cfg.lang + "'s birthday to the date.\n\tIf name only is specified, show the " + cfg.lang + "'s birthday.\n\tIf neither are given, show the next 5 birthdays on the server.",
6 | "birthday [name] clear/remove/none/delete - Unset a birthday for the given " + cfg.lang + "."],
7 | desc: cfg => "Date must be given in format MM/DD/YY and are stored in UTC.",
8 | permitted: () => true,
9 | groupArgs: true,
10 | execute: async (bot, msg, args, cfg) => {
11 | if(!args[0]) {
12 | let targets = msg.channel.guild ? await bot.findAllUsers(msg.channel.guild.id) : [msg.author];
13 | let members = (await bot.db.query("SELECT *, birthday + date_trunc('year', age(birthday + 1)) + interval '1 year' as anniversary FROM Members WHERE birthday IS NOT NULL AND user_id IN (select(unnest($1::text[]))) ORDER BY anniversary LIMIT 5;",[targets.map(u => u.id)])).rows;
14 | if(!members[0]) return "No " + cfg.lang + "s on this server have birthdays set.";
15 | return "Here are the next few upcoming " + cfg.lang + " birthdays in this server (UTC):\n" + members.map(t => (bot.checkMemberBirthday(t) ? `${t.name}: Birthday today! \uD83C\uDF70` : `${t.name}: ${t.anniversary.toLocaleDateString("en-US",{timeZone:"UTC"})}`)).join("\n");
16 | }
17 |
18 | //check arguments
19 | let member = await bot.db.members.get(msg.author.id,args[0]);
20 | if(!member) return `You don't have ${article(cfg)} ${cfg.lang} named '${args[0]}' registered.`;
21 | if(!args[1]) return member.birthday ? "Current birthday: " + member.birthday.toDateString() + "\nTo remove it, try " + cfg.prefix + "birthday " + member.name + " clear" : "No birthday currently set for " + args[0];
22 | if(["clear","remove","none","delete"].includes(args[1])) {
23 | await bot.db.members.update(msg.author.id,member.name,"birthday",null);
24 | return "Birthday cleared.";
25 | }
26 | if(!(new Date(args[1]).getTime())) return "I can't understand that date. Please enter in the form MM/DD/YYYY with no spaces.";
27 |
28 | //update member
29 | let date = new Date(args[1]);
30 | await bot.db.members.update(msg.author.id,args[0],"birthday",date);
31 | return `${proper(cfg.lang)} '${args[0]}' birthday set to ${date.toDateString()}.`;
32 | }
33 | };
--------------------------------------------------------------------------------
/commands/brackets.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "View or change " + article(cfg) + " " + cfg.lang + "'s brackets",
5 | usage: cfg => ["brackets [brackets] - if brackets are given, change the " + cfg.lang + "'s brackets, if not, simply echo the current ones",
6 | "brackets add - add another set of brackets to proxy with",
7 | "brackets remove - remove a set of brackets, unless it's the last one"],
8 | desc: cfg => "Brackets must be the word 'text' surrounded by any symbols or letters, i.e. `[text]` or `>>text`",
9 | permitted: () => true,
10 | groupArgs: true,
11 | execute: async (bot, msg, args, cfg) => {
12 | if(!args[0]) return bot.cmds.help.execute(bot, msg, ["brackets"], cfg);
13 |
14 | //check arguments
15 | let name = (args[0] == "add" || args[0] == "remove") ? args[1] : args[0];
16 | let member = await bot.db.members.get(msg.author.id,name);
17 | if(!member) return `You don't have ${article(cfg)} ${cfg.lang} named '${name}' registered.`;
18 | if(!args[1]) return `Brackets for ${args[0]}: ${bot.getBrackets(member)}`;
19 | let brackets = msg.content.slice(msg.content.indexOf(name)+name.length+1).trim().split("text");
20 | if(brackets.length < 2) return "No 'text' found to detect brackets with. For the last part of your command, enter the word 'text' surrounded by any characters.\nThis determines how the bot detects if it should replace a message.";
21 | if(!brackets[0] && !brackets[1]) return "Need something surrounding 'text'.";
22 | if(args[0] == "add") {
23 | member.brackets = member.brackets.concat(brackets);
24 | await bot.db.members.update(msg.author.id,member.name,"brackets",member.brackets);
25 | return "Brackets added.";
26 | } else if(args[0] == "remove") {
27 | let index = -1;
28 | for(let i=0; i "Configure server-specific settings",
4 | usage: cfg => ["cfg prefix - Change the bot's prefix.",
5 | "cfg rename - Change all instances of the default name 'member' in bot replies in this server to the specified term.",
6 | "cfg log [channel] - Enable the bot to send a log of all " + cfg.lang + " messages and some basic info like who registered them. Useful for having a searchable channel and for distinguishing between similar names. To disable logging, run with no channel argument.",
7 | "cfg blacklist - Add or remove channels to the bot's proxy blacklist - users will be unable to proxy in blacklisted channels.",
8 | "cfg cmdblacklist - Add or remove channels to the bot's command blacklist - users will be unable to issue commands in blacklisted channels."],
9 |
10 | permitted: (msg) => (msg.member && msg.member.permission.has("manageGuild")),
11 | execute: async (bot, msg, args, cfg) => {
12 | if(msg.channel.type == 1) return "This command cannot be used in private messages.";
13 |
14 | let gid = msg.channel.guild.id;
15 | let channels, out;
16 | switch(args[0]) {
17 | case "prefix":
18 | if(!args[1]) return "Missing argument 'prefix'.";
19 | let prefix = args.slice(1).join(" ");
20 |
21 | await bot.db.config.update(gid,"prefix",prefix,bot.defaultCfg);
22 | return `Prefix changed to ${prefix}\nThis means that all commands must now be preceded by your chosen prefix rather than \`${cfg.prefix}\`. If this was changed by mistake, run \`${prefix}cfg prefix ${process.env.DEFAULT_PREFIX}\` to return to default behavior.`;
23 |
24 | case "roles":
25 | return "This feature has been disabled indefinitely.";
26 |
27 | case "rename":
28 | if(!args[1]) return "Missing argument 'newname'";
29 | let lang = args.slice(1).join(" ");
30 | await bot.db.config.update(gid,"lang",lang,bot.defaultCfg);
31 | return "Entity name changed to " + lang;
32 |
33 | case "log":
34 | if(!args[1]) {
35 | await bot.db.config.update(gid,"log_channel",null,bot.defaultCfg);
36 | return "Logging channel unset. Logging is now disabled.";
37 | }
38 | let channel = bot.resolveChannel(msg,args[1]);
39 | if(!channel) return "Channel not found.";
40 | await bot.db.config.update(gid,"log_channel",channel.id,bot.defaultCfg);
41 | return `Logging channel set to <#${channel.id}>`;
42 |
43 | case "blacklist":
44 | if(!args[1]) {
45 | let blacklist = (await bot.db.blacklist.getAll(gid)).filter(bl => bl.is_channel && bl.block_proxies);
46 | if(blacklist[0]) return `Currently blacklisted channels: ${blacklist.map(bl => "<#"+bl.id+">").join(" ")}`;
47 | return "No channels currently blacklisted.";
48 | }
49 | switch(args[1]) {
50 | case "add":
51 | if(!args[2]) return "Must provide name/mention/id of channel to blacklist.";
52 | channels = args.slice(2).map(arg => bot.resolveChannel(msg,arg)).map(ch => { if(ch) return ch.id; else return ch; });
53 | if(!channels.find(ch => ch != undefined)) return `Could not find ${channels.length > 1 ? "those channels" : "that channel"}.`;
54 | if(channels.some(ch => ch == undefined)) {
55 | out = "Could not find these channels: ";
56 | for(let i = 0; i < channels.length; i++)
57 | if(!channels[i]) out += args.slice(2)[i];
58 | return out;
59 | }
60 | for(let i=0; i 1 ? "s" : ""} blacklisted successfully.`;
62 |
63 | case "remove":
64 | if(!args[2]) return "Must provide name/mention/id of channel to allow.";
65 | channels = args.slice(2).map(arg => bot.resolveChannel(msg,arg)).map(ch => { if(ch) return ch.id; else return ch; });
66 | if(!channels.find(ch => ch != undefined)) return `Could not find ${channels.length > 1 ? "those channels" : "that channel"}.`;
67 | if(channels.some(ch => ch == undefined)) {
68 | out = "Could not find these channels: ";
69 | for(let i = 0; i < channels.length; i++)
70 | if(!channels[i]) out += args.slice(2)[i] + " ";
71 | return out;
72 | }
73 | for(let i=0; i 1 ? "s" : ""} removed from blacklist.`;
75 |
76 | default:
77 | return "Invalid argument: must be 'add' or 'remove'";
78 | }
79 |
80 | case "cmdblacklist":
81 | if(!args[1]) {
82 | let blacklist = (await bot.db.blacklist.getAll(gid)).filter(bl => bl.is_channel && bl.block_commands);
83 | if(blacklist[0]) return `Currently blacklisted channels: ${blacklist.map(bl => "<#"+bl.id+">").join(" ")}`;
84 | return "No channels currently cmdblacklisted.";
85 | }
86 | switch(args[1]) {
87 | case "add":
88 | if(!args[2]) return "Must provide name/mention/id of channel to cmdblacklist.";
89 | channels = args.slice(2).map(arg => bot.resolveChannel(msg,arg)).map(ch => { if(ch) return ch.id; else return ch; });
90 | if(!channels.find(ch => ch != undefined)) return `Could not find ${channels.length > 1 ? "those channels" : "that channel"}.`;
91 | if(channels.some(ch => ch == undefined)) {
92 | out = "Could not find these channels: ";
93 | for(let i = 0; i < channels.length; i++)
94 | if(!channels[i]) out += args.slice(2)[i];
95 | return out;
96 | }
97 | for(let i=0; i 1 ? "s" : ""} blacklisted successfully.`;
99 |
100 | case "remove":
101 | if(!args[2]) return "Must provide name/mention/id of channel to allow.";
102 | channels = args.slice(2).map(arg => bot.resolveChannel(msg,arg)).map(ch => { if(ch) return ch.id; else return ch; });
103 | if(!channels.find(ch => ch != undefined)) return `Could not find ${channels.length > 1 ? "those channels" : "that channel"}.`;
104 | if(channels.some(ch => ch == undefined)) {
105 | out = "Could not find these channels: ";
106 | for(let i = 0; i < channels.length; i++)
107 | if(!channels[i]) out += args.slice(2)[i] + " ";
108 | return out;
109 | }
110 | for(let i=0; i 1 ? "s" : ""} removed from cmdblacklist.`;
112 |
113 | default:
114 | return "Invalid argument: must be 'add' or 'remove'";
115 | }
116 |
117 | default:
118 | return bot.cmds.help.execute(bot, msg, ["cfg"], cfg);
119 | }
120 | }
121 | };
122 |
--------------------------------------------------------------------------------
/commands/describe.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "View or change " + article(cfg) + " " + cfg.lang + "'s description",
5 | usage: cfg => ["describe [desc] - if desc is specified, change the " + cfg.lang + "'s describe, if not, simply echo the current one",
6 | "describe [name] clear/remove/none/delete - Unset a description for the given " + cfg.lang + "."],
7 | permitted: () => true,
8 | groupArgs: true,
9 | execute: async (bot, msg, args, cfg) => {
10 | if(!args[0]) return bot.cmds.help.execute(bot, msg, ["describe"], cfg);
11 |
12 | //check arguments
13 | let member = await bot.db.members.get(msg.author.id,args[0]);
14 | if(!member) return `You don't have ${article(cfg)} ${cfg.lang} named '${args[0]}' registered.`;
15 | if(!args[1]) return member.description ? "Current description: " + member.description + "\nTo remove it, try " + cfg.prefix + "describe '" + member.name + "' clear" : "No description currently set for " + member.name;
16 | if(["clear","remove","none","delete"].includes(args[1])) {
17 | await bot.db.members.update(msg.author.id,member.name,"description",null);
18 | return "Description cleared.";
19 | }
20 |
21 | //update member
22 | let temp = msg.content.slice(msg.content.indexOf(args[0]) + args[0].length);
23 | let desc = temp.slice(temp.indexOf(args[1]));
24 | await bot.db.members.update(msg.author.id,args[0],"description",desc.slice(0,1023));
25 | if(desc.length > 1023) return "Description updated, but was truncated due to Discord embed limits.";
26 | return "Description updated successfully.";
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/commands/dev.js:
--------------------------------------------------------------------------------
1 | const util = require("util");
2 |
3 | module.exports = {
4 | permitted: (msg, bot) => msg.author.id == bot.owner,
5 | execute: async (bot, msg, args, cfg) => {
6 | if (msg.author.id != bot.owner) return;
7 | switch(args.shift()) {
8 | case "eval":
9 | let out;
10 | try {
11 | out = await eval(msg.content.slice(cfg.prefix.length + 9).trim());
12 | } catch(e) { out = e.toString(); }
13 | return util.inspect(out).split(process.env.DISCORD_TOKEN).join("[[ TOKEN ]]").slice(0,2000);
14 | case "reload":
15 | process.send({name: "broadcast", msg: {name: "reload", type: args[0], targets: args.slice(1), channel: msg.channel.id}});
16 | if(args[0] == "ipc") process.send({name:"reloadIPC"});
17 | return "Reload command sent!";
18 | case "blacklist":
19 | await bot.banAbusiveUser(args.shift(), msg.channel.id);
20 | break;
21 | }
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/commands/export.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | help: cfg => "Export your data to a file",
3 | usage: cfg => ["export [name] - Get a .json file of your data that you can import to compatible bots. If a name is specified, will export only that " + cfg.lang + "."],
4 | permitted: () => true,
5 | groupArgs: true,
6 | cooldown: msg => 600000,
7 | execute: async (bot, msg, args, cfg, members) => {
8 | let data = { tuppers: [], groups: []};
9 | if(!args[0]) data = { tuppers: members, groups: (await bot.db.groups.getAll(msg.author.id)) };
10 | else {
11 | for (let arg of args) {
12 | let tup = await bot.db.members.get(msg.author.id, arg);
13 | if(!tup) return `You don't have a registered ${cfg.lang} with the name '${arg}'.`;
14 | data.tuppers.push(tup);
15 | }
16 | }
17 | if(data.tuppers.length == 0 && data.groups.length == 0) return "You don't have anything to export.";
18 | try {
19 | let channel = await msg.author.getDMChannel(); //get the user's DM channel
20 | let exportMsg = await bot.send(channel,"",{name:"tuppers.json",file:Buffer.from(JSON.stringify(data))}); //send it to them in DMs
21 | await bot.send(channel, `<${exportMsg.attachments[0].url}>`);
22 | if (msg.channel.guild) return "Sent you a DM!";
23 | } catch (e) {
24 | if (e.code != 50007) throw e;
25 | return `<${(await bot.send(msg.channel,"I couldn't access your DMs; sending publicly: ",{name:"tuppers.json",file:Buffer.from(JSON.stringify(data))})).attachments[0].url}>`;
26 | }
27 | }
28 | };
29 |
--------------------------------------------------------------------------------
/commands/feedback.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | help: cfg => "Get a link to the official support server!",
3 | usage: cfg => ["feedback - get a link to the support server"],
4 | permitted: msg => true,
5 | execute: (bot, msg, args, cfg) => {
6 | return "https://discord.gg/" + process.env.SUPPORT_INVITE;
7 | }
8 | };
9 |
--------------------------------------------------------------------------------
/commands/find.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "Find and display info about " + cfg.lang + "s by name",
5 | usage: cfg => ["find - Attempts to find " + article(cfg) + " " + cfg.lang + " with exactly the given name, and if none are found, tries to find " + cfg.lang + "s with names containing the given name."],
6 | permitted: (msg) => true,
7 | groupArgs: true,
8 | execute: async (bot, msg, args, cfg) => {
9 | if(!args[0]) return bot.cmds.help.execute(bot, msg, ["find"], cfg);
10 |
11 | //do search
12 | let search = args.join(" ").toLowerCase();
13 | let targets;
14 | if(msg.channel.type == 1)
15 | targets = [msg.author];
16 | else {
17 | await bot.sendChannelTyping(msg.channel.id);
18 | targets = await bot.findAllUsers(msg.channel.guild.id);
19 | }
20 | let results = (await bot.db.query("SELECT * FROM Members WHERE user_id IN (select(unnest($1::text[]))) AND (CASE WHEN tag IS NULL THEN LOWER(name) LIKE '%' || $2 || '%' ELSE (LOWER(name) || LOWER(tag)) LIKE '%' || $2 || '%' END) LIMIT 25",[targets.map(u => u.id),search])).rows;
21 | if(!results[0]) return `Couldn't find ${article(cfg)} ${cfg.lang} named '${search}'.`;
22 |
23 | //return single match
24 | if(results.length == 1) {
25 | let t = results[0];
26 | let host = targets.find(u => u.id == t.user_id);
27 | let group = null;
28 | if(t.group_id) group = await bot.db.groups.getById(t.group_id);
29 | let val = `User: ${host ? host.username + "#" + host.discriminator : "Unknown user " + t.user_id}\n`;
30 | let embed = { embed: {
31 | author: {
32 | name: t.name,
33 | icon_url: t.url
34 | },
35 | description: val + bot.paginator.generateMemberField(bot, t,group,val.length).value,
36 | }};
37 | return embed;
38 | }
39 |
40 | //build paginated list of results
41 | let embeds = [];
42 | let current = { embed: {
43 | title: "Results",
44 | fields: []
45 | }};
46 | for(let i=0; i= 5) {
49 | embeds.push(current);
50 | current = { embed: {
51 | title: "Results",
52 | fields: []
53 | }};
54 | }
55 | let group = null;
56 | if(t.group_id) group = await bot.db.groups.getById(t.group_id);
57 | let host = targets.find(u => u.id == t.user_id);
58 | let val = `User: ${host ? host.username + "#" + host.discriminator : "Unknown user " + t.user_id}\n`;
59 | current.embed.fields.push({name: t.name, value: val + bot.paginator.generateMemberField(bot, t,group,val.length).value});
60 | }
61 |
62 | embeds.push(current);
63 | if(embeds.length > 1) {
64 | for(let i = 0; i < embeds.length; i++)
65 | embeds[i].embed.title += ` (page ${i+1}/${embeds.length} of ${results.length} results)`;
66 | return bot.paginator.paginate(bot, msg, embeds);
67 | }
68 | return embeds[0];
69 | }
70 | };
71 |
--------------------------------------------------------------------------------
/commands/group.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "View or change your groups",
5 | usage: cfg => ["group create - Add a new group with the given name",
6 | "group delete - Remove a group, all " + cfg.lang + "s in the group will be reassigned to empty group",
7 | "group add - Add an existing " + cfg.lang + " to the named group (use * to select all groupless " + cfg.lang + "s)",
8 | "group remove - Remove a member from the named group (use * to empty the group)",
9 | "group list - Short list of your groups and their " + cfg.lang + "s",
10 | "group rename - Rename a group",
11 | "group tag - Give the group a tag, to be displayed after group member names and personal tags",
12 | "group describe - Give the group a description"],
13 | permitted: () => true,
14 | groupArgs: true,
15 | execute: async (bot, msg, args, cfg, members) => {
16 | let name,existing,group,tup;
17 | switch(args[0]) {
18 | case "create":
19 | if(!args[1]) return "No group name given.";
20 | name = args.slice(1).join(" ");
21 | existing = await bot.db.groups.get(msg.author.id, name);
22 | if(existing) return `You already have a group named '${name}'.`;
23 | await bot.db.groups.add(msg.author.id, bot.noVariation(name));
24 | return `Group created. Add ${cfg.lang}s to it with "${cfg.prefix}group add ${args.length < 3 ? name : "'" + name + "'"} [name...]".`;
25 |
26 | case "delete":
27 | if(!args[1]) return "No group name given.";
28 | if(args[1] == "*") {
29 | await bot.db.groups.deleteAll(msg.author.id);
30 | return "All groups deleted and members set to no group.";
31 | }
32 | name = args.slice(1).join(" ");
33 | existing = await bot.db.groups.get(msg.author.id, name);
34 | if(!existing) return `You don't have a group named '${name}'.`;
35 | await bot.db.groups.delete(existing.id);
36 | return "Group deleted, members have been set to no group.";
37 |
38 | case "add":
39 | if(!args[1]) return "No group name given.";
40 | if(!args[2]) return `No ${cfg.lang} name given.`;
41 | group = await bot.db.groups.get(msg.author.id, args[1]);
42 | if(!group) return `You don't have a group named '${args[1]}'.`;
43 | args = args.slice(2);
44 |
45 | if (args.length == 1) {
46 | if (args[0] == "*") {
47 | for (tup of members.filter(t => t.group_id == null)) {
48 | await bot.db.groups.addMember(group.id,tup.id);
49 | }
50 | return `All groupless ${cfg.lang}s assigned to group ${group.name}.`;
51 | }
52 |
53 | tup = await bot.db.members.get(msg.author.id, args[0]);
54 | if(!tup) return `You don't have a registered ${cfg.lang} named '${args[0]}'.`;
55 | await bot.db.groups.addMember(group.id,tup.id);
56 | return `${proper(cfg.lang)} '${tup.name}' group set to '${group.name}'.`;
57 | }
58 |
59 | let addedMessage = `${proper(cfg.lang)}s added to group:`;
60 | let notAddedMessage = `${proper(cfg.lang)}s not found:`;
61 | let baseLength = 2000 - (addedMessage.length + notAddedMessage.length);
62 | let originalLength = { addedMessage: addedMessage.length, notAddedMessage: notAddedMessage.length, };
63 |
64 | for (let arg of args) {
65 | let tup = await bot.db.members.get(msg.author.id, arg);
66 | if (tup) {
67 | await bot.db.groups.addMember(group.id,tup.id);
68 | if ((addedMessage.length + notAddedMessage.length + arg.length) < baseLength) addedMessage += ` '${arg}'`; else addedMessage += " (...)";
69 | } else {
70 | if ((addedMessage.length + notAddedMessage.length + arg.length) < baseLength) notAddedMessage += ` '${arg}'`; else notAddedMessage += " (...)";
71 | }
72 | }
73 | if (addedMessage.length == originalLength.addedMessage) return `No ${cfg.lang}s added to group.`;
74 | if (notAddedMessage.length == originalLength.notAddedMessage) return addedMessage;
75 | return `${addedMessage}\n${notAddedMessage}`;
76 |
77 | case "remove":
78 | if(!args[1]) return "No group name given.";
79 | if(!args[2]) return `No ${cfg.lang} name given.`;
80 | group = await bot.db.groups.get(msg.author.id, args[1]);
81 | if(!group) return `You don't have a group named '${args[1]}'.`;
82 | args = args.slice(2);
83 |
84 | if (args.length == 1) {
85 | if (args[0] == "*") {
86 | await bot.db.groups.removeMembers(group.id);
87 | return `All ${cfg.lang}s set to no group.`;
88 | }
89 | tup = await bot.db.members.get(msg.author.id, args[0]);
90 | if(!tup) return `You don't have a registered ${cfg.lang} named '${args[0]}'.`;
91 | await bot.db.members.removeGroup(tup.id);
92 | return `${proper(cfg.lang)} '${tup.name}' group unset.`;
93 | }
94 |
95 | let removedMessage = `${proper(cfg.lang)}s removed from group:`;
96 | let notRemovedMessage = `${proper(cfg.lang)}s not found:`;
97 | let rBaseLength = 2000 - (removedMessage.length + notRemovedMessage.length);
98 | let rOriginalLength = { removedMessage: removedMessage.length, notRemovedMessage: notRemovedMessage.length, };
99 |
100 | for (let arg of args) {
101 | tup = await bot.db.members.get(msg.author.id, arg);
102 | if (tup) {
103 | await bot.db.members.removeGroup(tup.id);
104 | if ((removedMessage.length + notRemovedMessage.length + arg.length) < rBaseLength) removedMessage += ` '${arg}'`; else removedMessage += " (...)";
105 | } else {
106 | if ((removedMessage.length + notRemovedMessage.length + arg.length) < rBaseLength) notRemovedMessage += ` '${arg}'`; else notRemovedMessage += " (...)";
107 | }
108 | }
109 | if (removedMessage.length == rOriginalLength.removedMessage) return `No ${cfg.lang}s found that could be removed from this group.`;
110 | if (notRemovedMessage.length == rOriginalLength.notRemovedMessage) return removedMessage;
111 | return `${removedMessage}\n${notRemovedMessage}`;
112 |
113 | case "list":
114 | let groups = await bot.db.groups.getAll(msg.author.id);
115 | if(!groups[0]) return `You have no groups. Try \`${cfg.prefix}group create \` to make one.`;
116 | let extra = {
117 | title: `${msg.author.username}#${msg.author.discriminator}'s registered groups`,
118 | author: {
119 | name: msg.author.username,
120 | icon_url: msg.author.avatarURL
121 | }
122 | };
123 | if(members.find(t => !t.group_id))
124 | groups.push({name: "No Group", id: null});
125 | let embeds = await bot.paginator.generatePages(bot, groups,g => {
126 | let mms = members.filter(t => t.group_id == g.id).map(t => t.name).join(", ");
127 | let field = {
128 | name: g.name,
129 | value: `${g.tag ? "Tag: " + g.tag + "\n" : ""}${g.description ? "Description: " + g.description + "\n" : ""} ${mms ? `Members: ${mms}` : "No members."}`
130 | };
131 | if(field.value.length > 1020) field.value = field.value.slice(0,1020) + "...";
132 | return field;
133 | },extra);
134 |
135 | if(embeds[1]) return bot.paginator.paginate(bot, msg, embeds);
136 | return embeds[0];
137 |
138 | case "tag":
139 | if(!args[1]) return "No group name given.";
140 | group = await bot.db.groups.get(msg.author.id, args[1]);
141 | if(!group) return `You don't have a group named '${args[1]}'.`;
142 | if(!args[2]) return group.tag ? "Current tag: " + group.tag + "\nTo remove it, try " + cfg.prefix + "group tag " + group.name + " clear" : "No tag currently set.";
143 | if(["clear","remove","none","delete"].includes(args[2])) {
144 | await bot.db.groups.update(msg.author.id,group.name,"tag",null);
145 | return "Tag cleared.";
146 | }
147 | let tag = args.slice(2).join(" ").trim();
148 | if(tag.length > 25) return "That tag is far too long. Please pick one shorter than 25 characters.";
149 | await bot.db.groups.update(msg.author.id, group.name, "tag", bot.noVariation(args.slice(2).join(" ")));
150 | return "Tag set. Group members will attempt to have their group tags displayed when proxying, if there's enough room.";
151 |
152 | case "rename":
153 | if(!args[1]) return "No group name given.";
154 | group = await bot.db.groups.get(msg.author.id, args[1]);
155 | if(!group) return `You don't have a group named '${args[1]}'.`;
156 | if(!args[2]) return "No new name given.";
157 | let newname = args.slice(2).join(" ").trim();
158 | let group2 = await bot.db.groups.get(msg.author.id, newname);
159 | if(group2 && group2.id != group.id) return `You already have a group named '${newname}'.`;
160 | await bot.db.groups.update(msg.author.id, group.name, "name", bot.noVariation(newname));
161 | return "Group renamed to '" + newname + "'.";
162 |
163 | case "describe":
164 | if(!args[1]) return "No group name given.";
165 | group = await bot.db.groups.get(msg.author.id, args[1]);
166 | if(!group) return `You don't have a group named '${args[1]}'.`;
167 | if(!args[2]) return group.description ? "Current description: " + group.description + "\nTo remove it, try " + cfg.prefix + "group describe " + group.name + " clear" : "No description currently set.";
168 | if(["clear","remove","none","delete"].includes(args[2])) {
169 | await bot.db.groups.update(msg.author.id,group.name,"description",null);
170 | return "Description cleared.";
171 | }
172 | let description = args.slice(2).join(" ").trim();
173 | await bot.db.groups.update(msg.author.id, group.name, "description", description.slice(0,2000));
174 | if(description.length > 2000) return "Description updated, but was cut to 2000 characters to fit within Discord embed limits.";
175 | return "Description updated.";
176 |
177 | default:
178 | return bot.cmds.help.execute(bot, msg, ["group"], cfg);
179 | }
180 | }
181 | };
182 |
--------------------------------------------------------------------------------
/commands/help.js:
--------------------------------------------------------------------------------
1 | const {article} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "Print this message, or get help for a specific command",
5 | usage: cfg => ["help - print list of commands",
6 | "help [command] - get help on a specific command"],
7 | permitted: () => true,
8 | execute: async (bot, msg, args, cfg) => {
9 | //help for a specific command
10 | if(args[0]) {
11 | if(bot.cmds[args[0]] && bot.cmds[args[0]].usage) {
12 | let output = { embed: {
13 | title: "Bot Command | " + args[0],
14 | description: bot.cmds[args[0]].help(cfg) + "\n\n**Usage:**\n",
15 | timestamp: new Date().toJSON(),
16 | color: 0x999999,
17 | author: {
18 | name: "Tupperbox",
19 | icon_url: bot.user.avatarURL
20 | },
21 | footer: {
22 | text: "If something is wrapped in <> or [], do not include the brackets when using the command. They indicate whether that part of the command is required <> or optional []."
23 | }
24 | }};
25 | for(let u of bot.cmds[args[0]].usage(cfg))
26 | output.embed.description += `${cfg.prefix + u}\n`;
27 | if(bot.cmds[args[0]].desc)
28 | output.embed.description += `\n${bot.cmds[args[0]].desc(cfg)}`;
29 | return output;
30 | }
31 | return "Command not found.";
32 | }
33 |
34 | //general help
35 | let output = { embed: {
36 | title: "Tupperbox | Help",
37 | description: "I am Tupperbox, a bot that allows you to send messages as other pseudo-users using Discord webhooks.\nTo get started, register " + article(cfg) + " " + cfg.lang + " with `" + cfg.prefix + "register` and enter a message with the brackets you set!\nExample: `" + cfg.prefix + "register test [text]` to register with brackets as []\n`[Hello!]` to proxy the message 'Hello!'\n\n**Command List**\nType `"+cfg.prefix+"help command` for detailed help on a command.\n" + String.fromCharCode(8203) + "\n",
38 | color: 0x999999,
39 | author: {
40 | name: "Tupperbox",
41 | icon_url: bot.user.avatarURL
42 | }
43 | }};
44 | for(let cmd of Object.keys(bot.cmds)) {
45 | if(bot.cmds[cmd].help)
46 | output.embed.description += `**${cfg.prefix + cmd}** - ${bot.cmds[cmd].help(cfg)}\n`;
47 | }
48 | output.embed.fields = [{ name: "\u200b", value: `Single or double quotes can be used in any command to specify multi-word arguments!\n\nProxy tips:\nReact with \u274c to a recent proxy to delete it (if you sent it)!\nReact with \u2753 to a recent proxy to show who sent it in DM!\n\n${process.env.SUPPORT_INVITE ? "Questions? Join the support server: [invite](https://discord.gg/" + process.env.SUPPORT_INVITE + ")" : "" }\nNow accepting donations to cover server costs! [patreon](https://www.patreon.com/tupperbox)\nInvite the bot to your server --> [click](https://discord.com/oauth2/authorize?client_id=${process.env.BOT_INVITE}&scope=bot&permissions=536996928)`}];
49 | return output;
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/commands/import.js:
--------------------------------------------------------------------------------
1 | const request = require("got");
2 |
3 | module.exports = {
4 | help: cfg => "Import your data from a file",
5 | usage: cfg => ["import [link] - Attach a compatible .json file or supply a link to a file when using this command. Data files can be obtained from compatible bots like me and Pluralkit."],
6 | permitted: () => true,
7 | desc: cfg => "Importing data acts as a merge, meaning if there are any " + cfg.lang + "s already registered with the same name as one being imported, the values will be updated instead of registering a new one.",
8 | cooldown: msg => 300000,
9 | tupperbox: async (bot, msg, client, data, oldData) => {
10 |
11 | let added = 0;
12 | let updated = 0;
13 |
14 | await client.query("BEGIN");
15 |
16 | for (let g of data.groups) {
17 | let old = oldData.groups.find(gr => g.name == gr.name) || {};
18 | if(!old.name) { //update existing entry
19 | added++;
20 | await bot.db.groups.add(msg.author.id, g.name, client);
21 | } else updated++;
22 | await client.query("UPDATE Groups SET description = $1, tag = $2 WHERE user_id = $3 AND name = $4",
23 | [g.description || null, g.tag || null, msg.author.id, g.name]);
24 | }
25 |
26 | for (let t of data.tuppers) {
27 | let old = oldData.tuppers.find(tu => t.name == tu.name) || {};
28 |
29 | if(!old.name) { //update existing entry
30 | added++;
31 | await bot.db.members.add(msg.author.id,{name: t.name,brackets: t.brackets}, client);
32 | } else updated++;
33 |
34 | // todo: there *must* be a better way of doing this
35 | await client.query("UPDATE Members SET avatar_url = $1, posts = $2, show_brackets = $3, birthday = $4, description = $5, tag = $6, brackets = $7 WHERE user_id = $8 AND name = $9",
36 | [t.avatar_url || old.avatar_url, Math.max(old.posts || 0,t.posts || 0), t.show_brackets || false, t.birthday || null, t.description || null, t.tag || null, t.brackets || old.brackets, msg.author.id, t.name]);
37 |
38 | if(old.group_id != t.group_id) {
39 | let grp = data.groups.find(g => g.id == t.group_id);
40 | let validGroup = grp ? (await bot.db.groups.get(msg.author.id,grp.name)) : null;
41 | if(validGroup)
42 | await client.query("UPDATE Members SET group_id = $1, group_pos = (SELECT GREATEST(COUNT(group_pos),MAX(group_pos)+1) FROM Members WHERE group_id = $1) WHERE user_id = $2 AND name = $3", [validGroup.id,msg.author.id, t.name]);
43 | }
44 | }
45 | await client.query("COMMIT");
46 | return `Import successful. Added ${added} entries and updated ${updated} entries.`;
47 | },
48 | pluralkit: async (bot, msg, client, data, oldData) => {
49 | let sysName = data.name || msg.author.username;
50 |
51 | let systemGroup = await bot.db.groups.get(msg.author.id,sysName);
52 | if(!systemGroup) {
53 | await bot.db.groups.add(msg.author.id,sysName);
54 | await bot.db.query("UPDATE Groups SET description = $1, tag = $2 WHERE user_id = $3 AND name = $4",[data.description || null, data.tag || null, msg.author.id, sysName]);
55 | systemGroup = await bot.db.groups.get(msg.author.id, sysName);
56 | }
57 |
58 | let added = 0;
59 | let updated = 0;
60 | await client.query("BEGIN");
61 |
62 | for (let t of data.members) {
63 | let old = oldData.tuppers.find(tu => t.name == tu.name) || {};
64 | let newBrackets = (t.proxy_tags.length == 0) ? [`${t.name}:`,""] : t.proxy_tags.map(pt => [pt.prefix || "", pt.suffix || ""]).reduce((acc,val) => acc.concat(val),[]);
65 |
66 | if(!old.name) { //update existing entry
67 | added++;
68 | await bot.db.members.add(msg.author.id,{name:t.name,brackets:newBrackets},client);
69 | } else updated++;
70 |
71 | await client.query("UPDATE Members SET avatar_url = $1, posts = $2, birthday = $3, description = $4, group_id = $5, group_pos = (SELECT GREATEST(COUNT(group_pos),MAX(group_pos)+1) FROM Members WHERE group_id = $5), brackets = $6::text[] WHERE user_id = $7 AND name = $8",
72 | [t.avatar_url || old.avatar_url || "https://i.imgur.com/ZpijZpg.png", t.message_count || 0, t.birthday || null, t.description || null, old.group_id || systemGroup.id, newBrackets, msg.author.id, t.name]);
73 | }
74 | await client.query("COMMIT");
75 |
76 | if (await bot.db.groups.memberCount(systemGroup.id) == 0) await bot.db.groups.delete(msg.author.id,systemGroup.id);
77 | return `Import successful. Added ${added} entries and updated ${updated} entries.`;
78 | },
79 | execute: async (bot, msg, args, cfg, members) => {
80 | let file = msg.attachments[0] ?? args[0];
81 | if(!file) return "Please attach or link to a .json file to import when running this command.\nYou can get a file by running the export command from me or Pluralkit.";
82 |
83 | let data;
84 | try {
85 | data = JSON.parse((await request(msg.attachments[0] ? msg.attachments[0].url : args[0])).body);
86 | } catch(e) {
87 | return "Please attach a valid .json file.";
88 | }
89 |
90 | if (!data.tuppers && !data.switches) return "Unknown file format. Please notify the creator by joining the support server. " + (process.env.SUPPORT_INVITE ? `https://discord.gg/${process.env.SUPPORT_INVITE}` : "");
91 |
92 | if ((data.tuppers && (data.tuppers.length > 3000 || data.groups.length > 500)) || (data.switches && (data.members.length > 3000))) return "Data too large for import. Please visit the support server for assistance. " + (process.env.SUPPORT_INVITE ? `https://discord.gg/${process.env.SUPPORT_INVITE}` : "");
93 |
94 | let confirm = await bot.confirm(msg, "Warning: This will overwrite your data. Only use this command with a recent, untampered .json file generated from the export command from either me or PluralKit. Please reply 'yes' if you wish to continue.");
95 | if (confirm !== true) return confirm;
96 |
97 | let old = {
98 | tuppers: members,
99 | groups: await bot.db.groups.getAll(msg.author.id),
100 | };
101 |
102 | let client = await bot.db.connect();
103 | try {
104 | if(data.tuppers) return await module.exports.tupperbox(bot, msg, client, data, old);
105 | else if(data.switches) return await module.exports.pluralkit(bot, msg, client, data, old);
106 | } catch(e) {
107 | bot.err(msg,e,false);
108 | if(client) await client.query("ROLLBACK");
109 | return `Something went wrong importing your data. This may have resulted in a partial import. Please check the data and try again. (${e.code || e.message})`;
110 | } finally {
111 | if(client) client.release();
112 | }
113 | }
114 | };
115 |
--------------------------------------------------------------------------------
/commands/invite.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | help: cfg => "Invite the bot to your server",
3 | usage: cfg => ["invite - sends the bot's invite URL to this channel, which can be used to invite the bot to another server in which you have Manage Server permissions"],
4 | permitted: (msg) => true,
5 | execute: (bot, msg, args, cfg) => {
6 | return `Click/tap this link to invite the bot to your server: \nIf you are on mobile and are having issues, copy the link and paste it into a browser.`;
7 | }
8 | };
--------------------------------------------------------------------------------
/commands/list.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | help: cfg => "Get a detailed list of yours or another user's registered " + cfg.lang + "s",
3 | usage: cfg => ["list [user] - Sends a list of the user's registered " + cfg.lang + "s, their brackets, post count, and birthday (if set). If user is not specified it defaults to the message author.\nThe bot will provide reaction emoji controls for navigating long lists: Arrows navigate through pages, # jumps to a specific page, ABCD jumps to a specific " + cfg.lang + ", and the stop button deletes the message."],
4 | permitted: () => true,
5 | cooldown: msg => 60000,
6 | execute: async (bot, msg, args, cfg, _members, ng = false) => {
7 |
8 | //get target list
9 | let target;
10 | if(args[0]) {
11 | target = await bot.resolveUser(msg, args.join(" "));
12 | } else target = msg.author;
13 | if(!target) return "User not found.";
14 |
15 | //generate paginated list with groups
16 | let groups = await bot.db.groups.getAll(target.id);
17 | if(groups[0] && !ng) {
18 | let members = await bot.db.members.getAll(target.id);
19 | if(!members[0]) return (target.id == msg.author.id) ? "You have not registered any " + cfg.lang + "s." : "That user has not registered any " + cfg.lang + "s.";
20 | if(members.find(t => !t.group_id)) groups.push({name: "Ungrouped", id: null});
21 | let embeds = [];
22 | for(let i=0; i t.group_id == groups[i].id), t => bot.paginator.generateMemberField(bot, t),extra);
32 | if(add[add.length-1].embed.fields.length < 5 && groups[i+1]) add[add.length-1].embed.fields.push({
33 | name: "\u200b",
34 | value: `Next page: group ${groups[i+1].name}`
35 | });
36 | embeds = embeds.concat(add);
37 | }
38 |
39 | for(let i=0; i 1) embeds[i].embed.title += ` (page ${i+1}/${embeds.length}, ${members.length} total)`;
42 | }
43 |
44 | if(embeds[1]) return bot.paginator.paginate(bot, msg,embeds);
45 | return embeds[0];
46 | }
47 | let members = await bot.db.members.getAll(target.id);
48 | if(!members[0]) return (target.id == msg.author.id) ? "You have not registered any " + cfg.lang + "s." : "That user has not registered any " + cfg.lang + "s.";
49 |
50 | //generate paginated list
51 | let extra = {
52 | title: `${target.username}#${target.discriminator}'s registered ${cfg.lang}s`,
53 | author: {
54 | name: target.username,
55 | icon_url: target.avatarURL
56 | }
57 | };
58 |
59 | let embeds = await bot.paginator.generatePages(bot, members, async t => {
60 | let group = null;
61 | if(t.group_id) group = await bot.db.groups.getById(t.group_id);
62 | return bot.paginator.generateMemberField(bot, t,group);
63 | }, extra);
64 | if(embeds[1]) return bot.paginator.paginate(bot, msg, embeds);
65 | return embeds[0];
66 | }
67 | };
68 |
--------------------------------------------------------------------------------
/commands/listng.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | help: cfg => "Like list, but without showing group info.",
3 | usage: cfg => ["list [user] - Sends a list of the user's registered " + cfg.lang + "s, their brackets, post count, and birthday (if set). If user is not specified it defaults to the message author.\nThe bot will provide reaction emoji controls for navigating long lists: Arrows navigate through pages, # jumps to a specific page, ABCD jumps to a specific " + cfg.lang + ", and the stop button deletes the message."],
4 | permitted: () => true,
5 | cooldown: msg => 60000,
6 | execute: async (bot, msg, args, cfg, members) => {
7 | return bot.cmds.list.execute(bot,msg,args,cfg,members,true);
8 | }
9 | };
10 |
--------------------------------------------------------------------------------
/commands/privacy.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs").promises;
2 | let policy = null;
3 | fs.readFile("./privacy.txt").then(file => policy = file.toString()).catch((err) => { if(err.code != "ENOENT") console.warn(err); });
4 |
5 | module.exports = {
6 | help: cfg => "View my privacy policy.",
7 | usage: cfg => ["privacy - show the privacy policy"],
8 | permitted: msg => true,
9 | execute: (bot, msg, args, cfg) => {
10 | return policy;
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/commands/register.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "Register a new " + cfg.lang + "",
5 | usage: cfg => ["register - Register a new " + cfg.lang + ".\n\t - the " + cfg.lang + "'s name, for multi-word names surround this argument in single or double quotes.\n\t - the word 'text' surrounded by any characters on one or both sides"],
6 | desc: cfg => "Upload an image when using this command to quickly set that image as the avatar!\n\nExample use: `register Test >text<` - registers " + article(cfg) + " " + cfg.lang + " named 'Test' that is triggered by messages surrounded by ><\nBrackets can be anything, one sided or both. For example `text<<` and `T:text` are both valid\nNote that you can enter multi-word names by surrounding the full name in single or double quotes `'like this'` or `\"like this\"`.",
7 | permitted: () => true,
8 | cooldown: msg => 15000,
9 | groupArgs: true,
10 | execute: async (bot, msg, args, cfg, members) => {
11 | if(!args[0]) return bot.cmds.help.execute(bot, msg, ["register"], cfg);
12 |
13 | //check arguments
14 | let brackets = msg.content.slice(msg.content.indexOf(args[0], msg.content.indexOf("register")+8)+args[0].length+1).trim().split("text");
15 | let name = bot.sanitizeName(args[0]);
16 | let member = (await bot.db.query("SELECT name,brackets FROM Members WHERE user_id = $1::VARCHAR(32) AND (LOWER(name) = LOWER($2::VARCHAR(76)) OR brackets = $3)",[msg.author.id,name,brackets || []])).rows[0];
17 | if(!args[1]) return "Missing argument 'brackets'. Try `" + cfg.prefix + "help register` for usage details.";
18 | if(name.length < 1 || name.length > 76) return "Name must be between 1 and 76 characters.";
19 | if(brackets.length < 2) return "No 'text' found to detect brackets with. For the last part of your command, enter the word 'text' surrounded by any characters.\nThis determines how the bot detects if it should replace a message.";
20 | if(!brackets[0] && !brackets[1]) return "Need something surrounding 'text'.";
21 | if(member && member.name.toLowerCase() == name.toLowerCase()) return `${proper(cfg.lang)} named '${name}' under your user account already exists.`;
22 | if(member && member.brackets[0] == brackets[0] && member.brackets[1] == brackets[1]) return proper(cfg.lang) + " with those brackets under your user account already exists.";
23 | if(members.length >= 5000) return `Maximum ${cfg.lang}s reached.`;
24 | let daysOld = bot.ageOf(msg.author);
25 | if((daysOld < 30 && members.Length >= 500) || (daysOld < 14 && members.Length >= 100)) return `Maximum ${cfg.lang}s reached for your account age.`;
26 | let avatar = msg.attachments[0] ? msg.attachments[0].url : "https://i.imgur.com/ZpijZpg.png";
27 |
28 | //add member
29 | await bot.db.members.add(msg.author.id,{name,avatarURL:avatar,brackets:brackets.slice(0,2)});
30 | return {
31 | content: `${proper(cfg.lang)} registered!`,
32 | embed: {
33 | title: name,
34 | description: `**Brackets:**\t${brackets[0]}text${brackets[1]}\n**Avatar URL:**\t${avatar}\n\nTry typing: \`${brackets[0]}hello${brackets[1]}\``,
35 | footer: {
36 | text: "If the brackets look wrong, try re-registering using \"quotation marks\" around the name!"
37 | }
38 | }
39 | };
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/commands/remove.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "Unregister " + article(cfg) + " " + cfg.lang + "",
5 | usage: cfg => ["remove - Unregister the named " + cfg.lang + " from your list",
6 | "remove * - Unregister ALL of your " + cfg.lang + "s (requires confirmation)"],
7 | permitted: () => true,
8 | groupArgs: true,
9 | execute: async (bot, msg, args, cfg, members) => {
10 | if(!args[0]) return bot.cmds.help.execute(bot, msg, ["remove"], cfg);
11 |
12 | //check arguments
13 | if(args[0] == "*") {
14 | if (members.length == 0) return "You don't have anything to remove.";
15 | let confirm = await bot.confirm(msg, `Warning: This will remove ALL of your ${cfg.lang}s. Reply 'yes' to continue or anything else to cancel.`);
16 | if (confirm !== true) return confirm;
17 | await bot.db.members.clear(msg.author.id);
18 | return `All ${cfg.lang}s removed.`;
19 | }
20 | else if (args.length == 1) {
21 | let name = args.join(" ");
22 | let member = await bot.db.members.get(msg.author.id,name);
23 | if(!member) return `You don't have ${article(cfg)} ${cfg.lang} named '${name}' registered.`;
24 | await bot.db.members.delete(msg.author.id, name);
25 | return proper(cfg.lang) + " unregistered.";
26 | } else {
27 | let removedMessage = `${proper(cfg.lang)}s removed:`;
28 | let notRemovedMessage = `${proper(cfg.lang)}s not found:`;
29 | let baseLength = 2000 - (removedMessage.length + notRemovedMessage.length);
30 | let rOriginalLength = { removedMessage: removedMessage.length, notRemovedMessage: notRemovedMessage.length, };
31 |
32 | for (let arg of args) {
33 | let tup = await bot.db.members.get(msg.author.id, arg);
34 | if (tup) {
35 | await bot.db.members.delete(msg.author.id, arg);
36 | if ((removedMessage.length + notRemovedMessage.length + arg.length) < baseLength) removedMessage += ` '${arg}'`; else removedMessage += " (...)";
37 | } else {
38 | if ((removedMessage.length + notRemovedMessage.length + arg.length) < baseLength) notRemovedMessage += ` '${arg}'`; else notRemovedMessage += " (...)";
39 | }
40 | }
41 | if (removedMessage.length == rOriginalLength.removedMessage) return `No ${cfg.lang}s found.`;
42 | if (notRemovedMessage.length == rOriginalLength.notRemovedMessage) return removedMessage;
43 | return `${removedMessage}\n${notRemovedMessage}`;
44 | }
45 | }
46 | };
--------------------------------------------------------------------------------
/commands/rename.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "Change " + article(cfg) + " " + cfg.lang + "'s name",
5 | usage: cfg => ["rename - Set a new name for the " + cfg.lang + ""],
6 | desc: cfg => "Use single or double quotes around multi-word names `\"like this\"` or `'like this'`.",
7 | permitted: () => true,
8 | groupArgs: true,
9 | execute: async (bot, msg, args, cfg) => {
10 | if(!args[0]) return bot.cmds.help.execute(bot, msg, ["rename"], cfg);
11 |
12 | //check arguments
13 | let member = await bot.db.members.get(msg.author.id,args[0]);
14 | if(!args[1]) return "Missing argument 'newname'.";
15 | let newname = bot.sanitizeName(args[1]);
16 | let newMember = await bot.db.members.get(msg.author.id,newname);
17 | if(newname.length < 1 || newname.length > 76) return "New name must be between 1 and 76 characters.";
18 | if(!member) return `You don't have ${article(cfg)} ${cfg.lang} named '${args[0]}' registered.`;
19 | if(newMember && newMember.id != member.id) return "You already have " + article(cfg) + " " + cfg.lang + " with that new name.";
20 |
21 | //update member
22 | await bot.db.members.update(msg.author.id,args[0],"name",newname);
23 | return proper(cfg.lang) + " renamed successfully.";
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/commands/showuser.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | help: cfg => "Show the user that registered the " + cfg.lang + " that last spoke",
3 | usage: cfg => ["showuser - Finds the user that registered the " + cfg.lang + " that last sent a message in this channel"],
4 | permitted: (msg) => true,
5 | execute: (bot, msg, args, cfg) => {
6 | if(!bot.recent[msg.channel.id]) return "No " + cfg.lang + "s have spoken in this channel since I last started up, sorry.";
7 | let recent = bot.recent[msg.channel.id][0];
8 | bot.send(msg.channel, { content: `That proxy was sent by <@!${recent.user_id}> (tag at time of sending: ${recent.tag} - id: ${recent.user_id}).`, allowedMentions: { users: false } });
9 | }
10 | };
--------------------------------------------------------------------------------
/commands/stats.js:
--------------------------------------------------------------------------------
1 | const util = require("util");
2 |
3 | module.exports = {
4 | help: cfg => "Show info about the bot.",
5 | usage: cfg => ["stats - Show list of technical info about the bot's status"],
6 | desc: cfg => "Displays a list of useful technical information about the bot's running processes. Lists technical details of all clusters, useful for monitoring recent outages and the progress of ongoing restarts. Displays the total memory usage and allocation of the bot, along with how many servers the bot is serving. Displays which shard is currently supporting this server and which cluster that shard is a part of.",
7 | permitted: msg => true,
8 | execute: (bot, msg, args, cfg) => {
9 | process.send({name: "postStats", channelID: msg.channel.id, shard: msg.channel.guild ? msg.channel.guild.shard.id : 0, cluster: bot.base.clusterID});
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/commands/tag.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "Remove or change " + article(cfg) + " " + cfg.lang + "'s tag (displayed next to name when proxying)",
5 | usage: cfg => ["tag [tag] - if tag is given, change the " + cfg.lang + "'s tag, if not, show the current one.",
6 | "tag [name] clear/remove/none/delete - Unset a tag for the given " + cfg.lang + ".",
7 | "tag * - clear tag for all " + cfg.lang + "s"],
8 | desc: cfg => proper(article(cfg)) + " " + cfg.lang + "'s tag is shown next to their name when speaking.",
9 | permitted: () => true,
10 | groupArgs: true,
11 | execute: async (bot, msg, args, cfg) => {
12 | if(!args[0]) return bot.cmds.help.execute(bot, msg, ["tag"], cfg);
13 |
14 | //check arguments & clear tag if empty
15 | if(args[0] == "*") {
16 | if(args[1]) return "Cannot mass assign tags due to name limits.";
17 | await bot.db.members.clearTags(msg.author.id);
18 | return "Tag cleared for all " + cfg.lang + "s.";
19 | }
20 | let member = await bot.db.members.get(msg.author.id,args[0]);
21 | if(!member) return `You don't have ${article(cfg)} ${cfg.lang} named '${args[0]}' registered.`;
22 | if(!args[1]) return member.tag ? "Current tag: " + member.tag + "\nTo remove it, try " + cfg.prefix + "tag " + member.name + " clear" : "No tag currently set for " + args[0];
23 | if(["clear","remove","none","delete"].includes(args[1])) {
24 | await bot.db.members.update(msg.author.id,member.name,"tag",null);
25 | return "Tag cleared.";
26 | }
27 | if (args.slice(1).join(" ").length > 25) return "That tag is too long. Please use one with less than 25 characters.";
28 |
29 | //update member
30 | await bot.db.members.update(msg.author.id,args[0],"tag",bot.noVariation(args.slice(1).join(" ")));
31 | return "Tag updated successfully.";
32 | }
33 | };
--------------------------------------------------------------------------------
/commands/togglebrackets.js:
--------------------------------------------------------------------------------
1 | const {article,proper} = require("../modules/lang");
2 |
3 | module.exports = {
4 | help: cfg => "Toggles whether the brackets are included or stripped in proxied messages for the given " + cfg.lang,
5 | usage: cfg => ["togglebrackets - toggles showing brackets on or off for the given " + cfg.lang],
6 | permitted: () => true,
7 | groupArgs: true,
8 | execute: async (bot, msg, args, cfg) => {
9 | if(!args[0]) return bot.cmds.help.execute(bot, msg, ["togglebrackets"], cfg);
10 |
11 | //check arguments
12 | let member = await bot.db.members.get(msg.author.id,args[0]);
13 | if(!member) return `You don't have ${article(cfg)} ${cfg.lang} named '${args[0]}' registered.`;
14 |
15 | //update member
16 | await bot.db.members.update(msg.author.id,args[0],"show_brackets",!member.show_brackets);
17 | return `Now ${member.show_brackets ? "hiding" : "showing"} brackets in proxied messages for ${member.name}.`;
18 | }
19 | };
--------------------------------------------------------------------------------
/events/debug.js:
--------------------------------------------------------------------------------
1 | let discordBanned = false;
2 |
3 | module.exports = (data,shard,bot) => {
4 | if(typeof data != "string") return console.log(data);
5 | if(data.includes("op\":")) {
6 | if(!data.includes("op\":1")) return console.log(`Shard ${shard} sent: ${data.replace(bot.token, "##TOKEN##")}`);
7 | }
8 | if(data.includes(" 429 (")) {
9 | if(!discordBanned) console.log(data);
10 | if(data.includes("You are being blocked from accessing our API temporarily due to exceeding our rate limits frequently") && !discordBanned) discordBanned = true;
11 | }
12 | if(data.includes("left | Reset")) return;
13 | if(data.includes("close") || data.includes("reconnect")) {
14 | console.log(`Shard ${shard} ${data}`);
15 | }
16 | };
--------------------------------------------------------------------------------
/events/disconnect.js:
--------------------------------------------------------------------------------
1 | module.exports = async bot => {
2 | console.log("Bot disconnected!");
3 | };
--------------------------------------------------------------------------------
/events/error.js:
--------------------------------------------------------------------------------
1 | module.exports = (err,id,bot) => {
2 | console.error(`(Shard ${id})`,err);
3 | };
--------------------------------------------------------------------------------
/events/guildDelete.js:
--------------------------------------------------------------------------------
1 | module.exports = (guild,bot) => {
2 | bot.db.config.delete(guild.id);
3 | };
--------------------------------------------------------------------------------
/events/hello.js:
--------------------------------------------------------------------------------
1 | module.exports = (trace,id,bot) => {
2 | console.log(`Shard ${id} hello!`);
3 | };
--------------------------------------------------------------------------------
/events/messageCreate.js:
--------------------------------------------------------------------------------
1 | module.exports = async (msg,bot) => {
2 | bot.msg(bot, msg);
3 | };
4 |
--------------------------------------------------------------------------------
/events/messageReactionAdd.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = async (message, emoji, userID, bot) => {
3 | if(emoji.user && emoji.user.bot) return;
4 |
5 | if(emoji.name == "\u274c" && bot.recent[message.channel.id] && bot.recent[message.channel.id].find(r => r.user_id == userID && message.id == r.id)) {
6 | if(!message.channel.guild || message.channel.permissionsOf(bot.user.id).has("manageMessages"))
7 | bot.deleteMessage(message.channel.id,message.id);
8 | return;
9 | }
10 | else if(emoji.name == "\u2753" && bot.recent[message.channel.id]) return await bot.proxy.sendMsgInfo(bot, message, emoji, userID);
11 |
12 | if(!bot.paginator.cache[message.id] || bot.paginator.cache[message.id].user != userID || !bot.paginator.buttons.includes(emoji.name)) return;
13 |
14 | bot.paginator.handleReaction(bot, message, emoji, userID);
15 | };
16 |
--------------------------------------------------------------------------------
/events/messageUpdate.js:
--------------------------------------------------------------------------------
1 | module.exports = async (msg,_,bot) => {
2 | // occasionally errors on bot message embeds for some reason?
3 | if (!msg.author) return;
4 | // ignore messages sent more than 10 minutes ago
5 | if (Date.now() - msg.timestamp > 1000*60*10) return;
6 | bot.msg(bot, msg, true);
7 | };
8 |
--------------------------------------------------------------------------------
/events/rawWS.js:
--------------------------------------------------------------------------------
1 | module.exports = (packet, shard, bot) => {
2 | if(packet.op != 0 && packet.op != 11)
3 | console.log(`Shard ${shard} received: ${JSON.stringify(packet)}`);
4 | };
--------------------------------------------------------------------------------
/events/ready.js:
--------------------------------------------------------------------------------
1 | module.exports = bot => {
2 | bot.updateStatus();
3 | };
4 |
--------------------------------------------------------------------------------
/events/shardDisconnect.js:
--------------------------------------------------------------------------------
1 | module.exports = async (err,id,bot) => {
2 | console.log(`Shard ${id} disconnected with code ${err ? err.code : "unknown"}: ${err ? err.reason : "unknown reason"}`);
3 | };
--------------------------------------------------------------------------------
/events/shardPreReady.js:
--------------------------------------------------------------------------------
1 | module.exports = (id,bot) => {
2 | console.log(`Shard ${id} pre-ready!`);
3 | };
--------------------------------------------------------------------------------
/events/shardReady.js:
--------------------------------------------------------------------------------
1 | module.exports = (id,bot) => {
2 | console.log(`Shard ${id} ready!`);
3 | };
--------------------------------------------------------------------------------
/events/shardResume.js:
--------------------------------------------------------------------------------
1 | module.exports = (id,bot) => {
2 | console.log(`Shard ${id} resumed!`);
3 | };
--------------------------------------------------------------------------------
/events/unknown.js:
--------------------------------------------------------------------------------
1 | module.exports = (packet,id,bot) => {
2 | console.log(`Shard ${id} unknown packet:`,packet);
3 | };
--------------------------------------------------------------------------------
/events/warn.js:
--------------------------------------------------------------------------------
1 | module.exports = (msg,id,bot) => {
2 | console.log(`Warning from shard ${id}: ${msg}`);
3 | };
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const Sharder = require("eris-sharder").Master;
3 | const cluster = require("cluster");
4 |
5 | const init = async () => {
6 | if(cluster.isMaster) {
7 | try {
8 | require("./auth.json");
9 | throw new Error("outdated");
10 | } catch(e) {
11 | if(e.message == "outdated") throw new Error("auth.json is outdated, please use the .env file instead! See the github page for more info");
12 | }
13 |
14 | await require("./modules/db").init();
15 | }
16 |
17 | let sharder = new Sharder("Bot " + process.env.DISCORD_TOKEN,"/bot.js",{
18 | clientOptions: {
19 | disableEvents: {
20 | GUILD_BAN_ADD: true,
21 | GUILD_BAN_REMOVE: true,
22 | MESSAGE_DELETE: true,
23 | MESSAGE_DELETE_BULK: true,
24 | TYPING_START: true,
25 | VOICE_STATE_UPDATE: true
26 | },
27 | messageLimit: 0,
28 | guildSubscriptions: false,
29 | allowedMentions: { roles: false },
30 | restMode: true,
31 | ratelimiterOffset: 5,
32 | intents: [
33 | "guilds",
34 | "guildMessages",
35 | "guildMessageReactions",
36 | "directMessages",
37 | "directMessageReactions"
38 | ],
39 | maxConcurrency: "auto"
40 | },
41 | stats: true,
42 | debug: true,
43 | shards: +process.env.SHARDS,
44 | clusters: +process.env.CLUSTERS || process.env.DEV? 1 : undefined,
45 | name: "Tupperbox",
46 | clusterTimeout: 0.1
47 | });
48 |
49 | sharder.eris.on("debug",console.log);
50 |
51 |
52 | if(cluster.isMaster) {
53 | let events = require("./modules/ipc.js");
54 |
55 | cluster.on("message",(worker,message) => {
56 | if(message.name == "reloadIPC") {
57 | delete require.cache[require.resolve("./modules/ipc.js")];
58 | events = require("./modules/ipc.js");
59 | console.log("Reloaded IPC plugin!");
60 | } else if(events[message.name]) {
61 | events[message.name](worker,message,sharder);
62 | }
63 | });
64 | }
65 | };
66 |
67 | init();
68 |
--------------------------------------------------------------------------------
/modules/blacklist.json:
--------------------------------------------------------------------------------
1 | [ ]
--------------------------------------------------------------------------------
/modules/cmd.js:
--------------------------------------------------------------------------------
1 | const cache = require("../modules/redis");
2 |
3 | module.exports = async ({msg, bot, members, cfg, dmChannel}) => {
4 |
5 | let targetChannel = dmChannel || msg.channel;
6 | let content = msg.content.substr(cfg.prefix.length).trim();
7 | let args = content.split(" ");
8 | let cmdName = args.shift();
9 | let cmd = bot.cmds[cmdName];
10 |
11 | if (cmd && msg.channel.guild && !msg.channel.permissionsOf(bot.user.id).has("embedLinks") && cmdName != "dev") return bot.send(targetChannel, "I need 'Embed Links' permissions to run commands.");
12 | if (!cmd || !bot.checkPermissions(bot, cmd, msg, args)) return;
13 |
14 | let key = msg.author.id + cmdName;
15 | let cd = await cache.cooldowns.get(key);
16 | if (cd) return bot.send(targetChannel,`You're using that too quickly! Try again in ${Math.ceil((cd - Date.now())/1000)} seconds`);
17 | if(cmd.cooldown && !process.env.DEV) cache.cooldowns.set(key, cmd.cooldown(msg));
18 |
19 | if(cmd.groupArgs) args = bot.getMatches(content,/“(.+?)”|‘(.+?)’|"(.+?)"|'(.+?)'|(\S+)/gi).slice(1);
20 |
21 | try {
22 | let output = await cmd.execute(bot, msg, args, cfg, members);
23 | if(output && (typeof output == "string" || output.embed)) {
24 | if(dmChannel) {
25 | let add = "This message sent to you in DM because I am lacking permissions to send messages in the original channel.";
26 | if(output.embed) output.content = add;
27 | else output += "\n" + add;
28 | }
29 | bot.send(targetChannel,output,null,true,msg.author);
30 | }
31 | } catch(e) {
32 | bot.err(msg,e);
33 | }
34 |
35 | };
--------------------------------------------------------------------------------
/modules/db.js:
--------------------------------------------------------------------------------
1 | const { Pool } = require("pg");
2 | const fs = require("fs");
3 | const cache = require("./redis");
4 |
5 | let pool = new Pool();
6 |
7 | const question = q => {
8 | let rl = require("readline").createInterface({input:process.stdin,output:process.stdout});
9 | return new Promise((res,rej) => {
10 | rl.question(q, ans => { rl.close(); res(ans); });
11 | });
12 | };
13 |
14 | const blacklistBitfield = (blockProxies, blockCommands) => {
15 | let blacklist = 0;
16 | if (blockCommands) blacklist |= 1;
17 | if (blockProxies) blacklist |= 2;
18 | return blacklist;
19 | };
20 |
21 | module.exports = {
22 |
23 | init: async () => {
24 | process.stdout.write("Checking postgres connection... ");
25 | (await (await pool.connect()).release());
26 | process.stdout.write("ok!\nChecking tables...");
27 | //move members after
28 | await pool.query(`
29 | create or replace function create_constraint_if_not_exists (
30 | t_name text, c_name text, constraint_sql text
31 | )
32 | returns void AS
33 | $$
34 | begin
35 | -- Look for our constraint
36 | if not exists (select constraint_name
37 | from information_schema.constraint_column_usage
38 | where table_name = t_name and constraint_name = c_name) then
39 | execute constraint_sql;
40 | end if;
41 | end;
42 | $$ language 'plpgsql';
43 |
44 | CREATE TABLE IF NOT EXISTS webhooks(
45 | id VARCHAR(32) PRIMARY KEY,
46 | channel_id VARCHAR(32) NOT NULL,
47 | token VARCHAR(100) NOT NULL
48 | );
49 | CREATE TABLE IF NOT EXISTS servers(
50 | id VARCHAR(32) PRIMARY KEY,
51 | prefix TEXT NOT NULL,
52 | lang TEXT NOT NULL,
53 | lang_plural TEXT,
54 | log_channel VARCHAR(32)
55 | );
56 | CREATE TABLE IF NOT EXISTS blacklist(
57 | id VARCHAR(32) NOT NULL,
58 | server_id VARCHAR(32) NOT NULL,
59 | is_channel BOOLEAN NOT NULL,
60 | block_proxies BOOLEAN NOT NULL,
61 | block_commands BOOLEAN NOT NULL,
62 | PRIMARY KEY (id, server_id)
63 | );
64 | CREATE TABLE IF NOT EXISTS groups(
65 | id SERIAL PRIMARY KEY,
66 | user_id VARCHAR(32) NOT NULL,
67 | name TEXT NOT NULL,
68 | description TEXT,
69 | tag VARCHAR(32)
70 | );
71 | CREATE TABLE IF NOT EXISTS members(
72 | id SERIAL PRIMARY KEY,
73 | user_id VARCHAR(32) NOT NULL,
74 | name VARCHAR(80) NOT NULL,
75 | position INTEGER NOT NULL,
76 | avatar_url TEXT NOT NULL,
77 | brackets TEXT[] NOT NULL,
78 | posts INTEGER NOT NULL,
79 | show_brackets BOOLEAN NOT NULL,
80 | birthday DATE,
81 | description TEXT,
82 | tag VARCHAR(32),
83 | group_id INTEGER,
84 | UNIQUE (user_id,name)
85 | );
86 | CREATE TABLE IF NOT EXISTS global_blacklist(
87 | user_id VARCHAR(50) PRIMARY KEY
88 | );
89 |
90 | ALTER TABLE groups
91 | ADD COLUMN IF NOT EXISTS position INTEGER;
92 | ALTER TABLE members
93 | ADD COLUMN IF NOT EXISTS group_pos INTEGER;
94 |
95 | SELECT create_constraint_if_not_exists('groups','groups_user_id_name_key',
96 | 'ALTER TABLE groups ADD CONSTRAINT groups_user_id_name_key UNIQUE (user_id, name);'
97 | );
98 | SELECT create_constraint_if_not_exists('groups','members_group_id_fkey',
99 | 'ALTER TABLE members ADD CONSTRAINT members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id);'
100 | );`);
101 |
102 | await pool.query("CREATE INDEX CONCURRENTLY IF NOT EXISTS members_lower_idx ON members(lower(name))");
103 | await pool.query("CREATE INDEX CONCURRENTLY IF NOT EXISTS webhooks_channelidx ON webhooks(channel_id);");
104 |
105 | console.log("ok!\nChecking for data to import...");
106 | let found = false;
107 | //check tulpae.json
108 | try {
109 | let members = require("../tulpae.json");
110 | found = true;
111 | if((await question("Found tulpae.json file. Import to database? (yes/no)\n") != "yes")) console.log("Ignoring file.");
112 | else {
113 | console.log("Beginning import.");
114 | let count = 0;
115 | let keys = Object.keys(members);
116 | for(let id of keys) {
117 | count++;
118 | console.log(`\tImporting user ${id} (${count} of ${keys.length})`);
119 | for(let i=0;i { console.error(e); })
126 | .then(() => {
127 | console.log(`\tuser ${a} - ${member.name} done`);
128 | conn.release();
129 | });
130 | }
131 | }
132 | await pool.end();
133 | pool = new Pool();
134 | fs.unlink("./tulpae.json", err => { if(err) console.error(err); });
135 | }
136 | } catch(e) { if(e.code != "MODULE_NOT_FOUND") console.log(e);}
137 | //check webhooks.json
138 | try {
139 | let webhooks = require("../webhooks.json");
140 | found = true;
141 | if((await question("Found webhooks.json file. Import to database? (yes/no)\n") != "yes")) console.log("Ignoring file.");
142 | else {
143 | console.log("Beginning import.");
144 | let count = 0;
145 | let keys = Object.keys(webhooks);
146 | for(let id of keys) {
147 | count++;
148 | console.log(`\tImporting webhook for channel ${id} (${count} of ${keys.length})`);
149 | let conn = await pool.connect();
150 | conn.query("INSERT INTO Webhooks VALUES ($1,$2,$3)", [webhooks[id].id,id,webhooks[id].token])
151 | .catch(e => { console.error(e); })
152 | .then(() => {
153 | console.log(`\twebhook ${id} done`);
154 | conn.release();
155 | });
156 | }
157 | await pool.end();
158 | pool = new Pool();
159 | fs.unlink("./webhooks.json", err => { if(err) console.error(err); });
160 | }
161 | } catch(e) { if(e.code != "MODULE_NOT_FOUND") console.log(e);}
162 | //check servercfg.json
163 | try {
164 | let config = require("../servercfg.json");
165 | found = true;
166 | if((await question("Found servercfg.json file. Import to database? (yes/no)\n") != "yes")) console.log("Ignoring file.");
167 | else {
168 | console.log("Beginning import.");
169 | let count = 0;
170 | let keys = Object.keys(config);
171 | for(let id of keys) {
172 | count++;
173 | let cfg = config[id];
174 | console.log(`\tImporting config for server ${id} (${count} of ${keys.length})`);
175 | let conn = await pool.connect();
176 | conn.query("INSERT INTO Servers VALUES ($1,$2,$3,$4,$5)", [id,cfg.prefix,cfg.lang,null,cfg.log || null])
177 | .catch(e => { console.error(e); })
178 | .then(async () => {
179 | if(cfg.blacklist) for(let bl of cfg.blacklist) await module.exports.blacklist.update(id,bl,true,true,null).then(() => console.log(`${id} - blacklist updated`));
180 | if(cfg.cmdblacklist) for(let bl of cfg.cmdblacklist) await module.exports.blacklist.update(id,bl,true,null,true).then(() => console.log(`${id} - blacklist updated`));
181 | conn.release();
182 | }).catch(e => { throw e; });
183 | }
184 | await pool.end();
185 | pool = new Pool();
186 | fs.unlink("./servercfg.json", err => { if(err) console.error(err); });
187 | }
188 | } catch(e) { if(e.code != "MODULE_NOT_FOUND") console.log(e);}
189 | if(!found) console.log("Data OK.");
190 | process.stdout.write("Checking Redis connection...");
191 | await cache.redis.set("test", 1);
192 | if(await cache.redis.get("test") != 1) throw new Error("Cache integrity check failed");
193 | await cache.redis.del("test");
194 | await cache.redis.flushall();
195 | console.log("ok!");
196 | },
197 |
198 | connect: () => pool.connect(),
199 | end: async () => { return await pool.end(); },
200 |
201 | query: (text, params, callback) => {
202 | return pool.query(text, params, callback);
203 | },
204 |
205 | members: {
206 | add: async (userID, member, client) =>
207 | await (client || module.exports).query("insert into Members (user_id, name, position, avatar_url, brackets, posts, show_brackets) values ($1::VARCHAR(32), $2, (select greatest(count(position), max(position)+1) from Members where user_id = $1::VARCHAR(32)), $3, $4, 0, false)", [userID, member.name, member.avatarURL || "https://i.imgur.com/ZpijZpg.png",member.brackets]),
208 |
209 | get: async (userID, name) =>
210 | (await module.exports.query("select * from Members where user_id = $1 and lower(name) = lower($2)", [userID, name])).rows[0],
211 |
212 | getAll: async (userID) =>
213 | (await module.exports.query("select * from Members where user_id = $1 order by group_pos, position", [userID])).rows,
214 |
215 | count: async () =>
216 | (await module.exports.query("SELECT COUNT(*) FROM Members")).rows[0].count,
217 |
218 | export: async (userID, name) =>
219 | (await module.exports.query("select name, avatar_url, brackets, posts, show_brackets, birthday, description, tag, group_id, group_pos from Members where user_id = $1 and lower(name) = lower($2)", [userID, name])).rows[0],
220 |
221 | update: async (userID, name, column, newVal) =>
222 | await module.exports.query(`update Members set ${column} = $1 where user_id = $2 and lower(name) = lower($3)`, [newVal, userID, name]),
223 |
224 | removeGroup: async (memberID) =>
225 | await module.exports.query("update Members set group_id = null, group_pos = null where id = $1", [memberID]),
226 |
227 | removeAllGroups: async (userID) =>
228 | await module.exports.query("update Members set group_id = null, group_pos = null where user_id = $1", [userID]),
229 |
230 | removeAllTags: async (userID) =>
231 | await module.exports.query("update Members set tag = null where user_id = $1", [userID]),
232 |
233 | delete: async (userID, name) =>
234 | await module.exports.query("delete from Members where user_id = $1 and lower(name) = lower($2)", [userID, name]),
235 |
236 | clearTags: async (userID) =>
237 | await module.exports.query("update Members set tag = null where user_id = $1", [userID]),
238 |
239 | clear: async (userID) =>
240 | await module.exports.query("delete from Members where user_id = $1", [userID]),
241 | },
242 |
243 | groups: {
244 | get: async (userID, name) => {
245 | return (await pool.query("SELECT * FROM Groups WHERE user_id = $1 AND LOWER(name) = LOWER($2)", [userID, name])).rows[0];
246 | },
247 |
248 | getById: async (id) => {
249 | return (await pool.query("select * from Groups where id = $1", [id])).rows[0];
250 | },
251 |
252 | getAll: async (userID) => {
253 | return (await pool.query("SELECT * FROM Groups WHERE user_id = $1 ORDER BY position", [userID])).rows;
254 | },
255 |
256 | memberCount: async (groupID) =>
257 | (await module.exports.query("select count(name) from Members where group_id = $1", [groupID])).rows[0].count,
258 |
259 | add: async (userID, name, client) => {
260 | return await (client || pool).query("INSERT INTO Groups (user_id, name, position) VALUES ($1::VARCHAR(32), $2, (SELECT GREATEST(COUNT(position),MAX(position)+1) FROM Groups WHERE user_id = $1::VARCHAR(32)))", [userID, name]);
261 | },
262 |
263 | addMember: async (groupID, memberID) =>
264 | await module.exports.query("UPDATE Members SET group_id = $1, group_pos = (SELECT GREATEST(COUNT(group_pos),MAX(group_pos)+1) FROM Members WHERE group_id = $1) WHERE id = $2", [groupID,memberID]),
265 |
266 | update: async (userID, name, column, newVal) => {
267 | return await pool.query(`UPDATE Groups SET ${column} = $1 WHERE user_id = $2 AND LOWER(name) = LOWER($3)`, [newVal, userID, name]);
268 | },
269 |
270 | removeMembers: async (id) =>
271 | await pool.query("update Members set group_id = null, group_pos = null where group_id = $1", [id]),
272 |
273 | delete: async (id) => {
274 | await module.exports.groups.removeMembers(id);
275 | await pool.query("DELETE FROM Groups WHERE id = $1", [id]);
276 | },
277 |
278 | deleteAll: async (userID) => {
279 | await pool.query("UPDATE Members SET group_id = null, group_pos = null WHERE user_id = $1", [userID]);
280 | await pool.query("DELETE FROM Groups WHERE user_id = $1", [userID]);
281 | },
282 | },
283 |
284 | config: {
285 | add: async (serverID, cfg) => {
286 | return await pool.query("INSERT INTO Servers(id, prefix, lang) VALUES ($1, $2, $3)", [serverID,cfg.prefix,cfg.lang]);
287 | },
288 |
289 | get: async (serverID) => {
290 | let cfg = await cache.config.get(serverID);
291 | if(cfg) return cfg;
292 | cfg = ((await pool.query("SELECT prefix, lang, lang_plural, log_channel FROM Servers WHERE id = $1", [serverID])).rows[0]);
293 | if(cfg) cache.config.set(serverID, cfg);
294 | return cfg;
295 | },
296 |
297 | update: async (serverID, column, newVal, cfg) => {
298 | await pool.query("INSERT INTO Servers(id, prefix, lang) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;",[serverID,cfg.prefix,cfg.lang]);
299 | let updated = (await pool.query(`UPDATE Servers SET ${column} = $1 WHERE id = $2 RETURNING prefix, lang, lang_plural, log_channel`, [newVal,serverID])).rows[0];
300 | if(updated) return await cache.config.set(serverID, updated);
301 | },
302 |
303 | delete: async (serverID) => {
304 | cache.config.delete(serverID);
305 | return await pool.query("DELETE FROM Servers WHERE id = $1", [serverID]);
306 | },
307 | },
308 |
309 | blacklist: {
310 | get: async (channel) => {
311 | if (!channel.guild) return 2; // DM channel: commands OK, proxy NO
312 | let blacklistCache = await cache.blacklist.get(channel.id);
313 | if (blacklistCache) return blacklistCache;
314 |
315 | let blacklist;
316 |
317 | let dbBlacklist = (await module.exports.query("select * from blacklist where server_id = $1 and id = $2", [channel.guild.id, channel.id])).rows;
318 | if (dbBlacklist.length == 0) blacklist = 0;
319 |
320 | blacklist = blacklistBitfield(dbBlacklist.filter(x => x.block_proxies).length > 0, dbBlacklist.filter(x => x.block_commands).length > 0);
321 | cache.blacklist.set(channel.id, blacklist);
322 | return blacklist;
323 | },
324 |
325 | getAll: async (serverID) => {
326 | return (await pool.query("SELECT * FROM Blacklist WHERE server_id = $1", [serverID])).rows;
327 | },
328 |
329 | update: async (serverID, id, isChannel, blockProxies, blockCommands) => {
330 | cache.blacklist.delete(id);
331 | return await pool.query("INSERT INTO Blacklist VALUES ($1,$2,$3,CASE WHEN $4::BOOLEAN IS NULL THEN false ELSE $4::BOOLEAN END,CASE WHEN $5::BOOLEAN IS NULL THEN false ELSE $5::BOOLEAN END) ON CONFLICT (id,server_id) DO UPDATE SET block_proxies = (CASE WHEN $4::BOOLEAN IS NULL THEN Blacklist.block_proxies ELSE EXCLUDED.block_proxies END), block_commands = (CASE WHEN $5::BOOLEAN IS NULL THEN Blacklist.block_commands ELSE EXCLUDED.block_commands END)",[id,serverID,isChannel,blockProxies,blockCommands]);
332 | },
333 |
334 | delete: async (guildID, channelID) => {
335 | cache.blacklist.delete(channelID);
336 | return await module.exports.query("delete from Blacklist where server_id = $1 and id = $2", [guildID, channelID]);
337 | },
338 |
339 | },
340 |
341 | webhooks: {
342 | get: async (channelID) =>
343 | (await module.exports.query("select * from Webhooks where channel_id = $1", [channelID])).rows[0],
344 |
345 | set: async (hook) =>
346 | await module.exports.query("insert into Webhooks values ($1, $2, $3)", [hook.id, hook.channel_id, hook.token]),
347 |
348 | delete: async (channelID) =>
349 | await module.exports.query("delete from Webhooks where channel_id = $1", [channelID]),
350 | },
351 |
352 | getGlobalBlacklisted: async (id) => {
353 | return (await pool.query("SELECT * FROM global_blacklist WHERE user_id = $1", [id])).rows[0];
354 | },
355 |
356 | };
357 |
--------------------------------------------------------------------------------
/modules/ipc.js:
--------------------------------------------------------------------------------
1 | const cluster = require("cluster");
2 | const os = require("os");
3 |
4 | const dhm = t => {
5 | let cd = 24 * 60 * 60 * 1000, ch = 60 * 60 * 1000, cm = 60 * 1000, cs = 1000;
6 | let d = Math.floor(t/cd), h = Math.floor((t-d*cd)/ch), m = Math.floor((t-d*cd-h*ch)/cm), s = Math.floor((t-d*cd-h*ch-m*cm)/cs);
7 | return `${d}d ${h}h ${m}m ${s}s`;
8 | };
9 |
10 | let masterExports = () => {
11 | this.postStats = (wrk,msg,shrd) => {
12 | if(!msg.channelID) return;
13 | let guilds = shrd.stats.stats.clusters.reduce((a,b)=>a+b.guilds,0);
14 | shrd.eris.createMessage(msg.channelID,
15 | "```"+shrd.stats.stats.clusters.sort((a,b) => a.cluster-b.cluster).map(c =>
16 | `Cluster ${c.cluster}${c.cluster < 10 ? " " : ""} - ${c.ram.toFixed(1)} MB RAM - ${c.guilds} servers (up ${dhm(c.uptime)})`).join("\n")
17 | +`\n\nTotal memory used: ${(shrd.stats.stats.totalRam/1000000).toFixed(1)} MB/${(os.totalmem()/1000000).toFixed(1)} MB\nTotal servers: ${guilds}\n16 shards per cluster\n\nRequest received on Shard ${msg.shard} (Cluster ${msg.cluster})` + "```"
18 | );
19 | },
20 |
21 | this.restartCluster = (wrk,msg,shrd) => {
22 | if(msg.id == null) return;
23 | cluster.workers[shrd.clusters.get(msg.id).workerID].kill();
24 | };
25 |
26 | return this;
27 | };
28 |
29 | const types = ["command", "module", "event"];
30 | const modules = ["blacklist", "cmd", "db", "msg", "paginator", "proxy", "redis"];
31 |
32 | let botExports = (bot) => {
33 |
34 | this.reload = async msg => {
35 |
36 | if(!msg.type || !msg.targets || !msg.channel) return;
37 |
38 | let out = "";
39 | msg.targets.forEach(async (arg) => {
40 | try {
41 | let path = `../${msg.type}s/${arg}`;
42 |
43 | if (types.includes(msg.type)) delete require.cache[require.resolve(path)];
44 | if (msg.type == "command") bot.cmds[arg] = require(path);
45 |
46 | else if(msg.type == "event") {
47 | bot.removeAllListeners(arg);
48 | let func = require(path);
49 | bot.on(arg, (...a) => func(...a,bot));
50 | }
51 |
52 | else if(msg.type == "module") {
53 | if (arg == "db") await bot.db.end();
54 | if (modules.includes(arg)) bot[arg] = require(`../modules/${arg + (arg == "blacklist" ? ".json" : "")}`);
55 | else switch(arg) {
56 |
57 | case "util":
58 | require("../modules/util")(bot);
59 | break;
60 |
61 | case "ipc":
62 | process.send({name:"reloadIPC"});
63 | require("../modules/ipc")(bot);
64 | break;
65 | }}
66 |
67 | out += `${arg} reloaded\n`;
68 | } catch(e) {
69 | out += `Could not reload ${arg} (${e.code}) - ${e.stack}\n`;
70 | }
71 | });
72 |
73 | console.log(out);
74 | },
75 |
76 | this.eval = async msg => {
77 | if(!msg.code) return;
78 | let result = await eval(msg.code);
79 | console.log(result);
80 | };
81 |
82 | bot.ipc = this;
83 | };
84 |
85 | if (cluster.isMaster) module.exports = masterExports();
86 | else module.exports = botExports;
87 |
--------------------------------------------------------------------------------
/modules/lang.js:
--------------------------------------------------------------------------------
1 | const vowels = ["a","e","i","o","u"];
2 |
3 | module.exports = {
4 | proper: text => {
5 | return text.substring(0,1).toUpperCase() + text.substring(1);
6 | },
7 | article: cfg => {
8 | return vowels.includes(cfg.lang.slice(0,1)) ? "an" : "a";
9 | }
10 | };
--------------------------------------------------------------------------------
/modules/msg.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = async (bot,msg,edit) => {
3 |
4 | if(msg.author.bot || msg.type != 0) return;
5 | if(msg.channel.guild && bot.blacklist.includes(msg.channel.guild.id)) return;
6 | if(await bot.db.getGlobalBlacklisted(msg.author.id)) return;
7 |
8 | let blacklist = +(await bot.db.blacklist.get(msg.channel));
9 | let cfg = msg.channel.guild ? (await bot.db.config.get(msg.channel.guild.id) ?? { ...bot.defaultCfg }) : { ...bot.defaultCfg };
10 | let members = await bot.db.members.getAll(msg.author.id);
11 |
12 | let permissions;
13 | let dmChannel;
14 |
15 | if (msg.channel.guild) {
16 | permissions = msg.channel.permissionsOf(bot.user.id);
17 | if(!permissions.has("readMessages")) return;
18 | if (!permissions.has("sendMessages")) {
19 | try { dmChannel = await bot.getDMChannel(msg.author.id); }
20 | catch(e) { if(e.code != 50007) bot.err(msg,e,false); return; }
21 | }
22 | }
23 |
24 | let dialogKey = msg.channel.id + msg.author.id;
25 |
26 | if (bot.dialogs[dialogKey]) {
27 | bot.dialogs[dialogKey](msg);
28 | delete bot.dialogs[dialogKey];
29 | return;
30 | }
31 |
32 | if (msg.content == `<@${bot.user.id}>` || msg.content == `<@!${bot.user.id}>`)
33 | return bot.send(msg.channel,
34 | `Hello! ${msg.channel.guild ? "This server's" : "My"} prefix is \`${cfg.prefix}\`. Try \`${cfg.prefix}help\` for help${(msg.channel.guild && cfg.prefix != process.env.DEFAULT_PREFIX) ? ` or \`${cfg.prefix}cfg prefix ${process.env.DEFAULT_PREFIX}\` to reset the prefix` : ""}.`
35 | );
36 |
37 | if (msg.content.startsWith(cfg.prefix)) {
38 | if (!edit && (!(blacklist & 1) || msg.member.permission.has("manageGuild"))) await bot.cmd({ msg, bot, members, cfg, dmChannel});
39 | return;
40 | }
41 |
42 | if (members[0] && !(blacklist & 2) && msg.channel.guild && !dmChannel) bot.proxy.executeProxy({ msg, bot, members, cfg });
43 |
44 | };
45 |
--------------------------------------------------------------------------------
/modules/paginator.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports = {
3 | buttons: ["\u23ea", "\u2b05", "\u27a1", "\u23e9", "\u23f9", "\u0023\u20e3", "\uD83D\uDD20"],
4 | cache: {},
5 |
6 | paginate: async (bot, msg, data) => {
7 | if(!(msg.channel.type == 1)) {
8 | let perms = msg.channel.permissionsOf(bot.user.id);
9 | if(!perms.has("readMessages") || !perms.has("sendMessages") || !perms.has("embedLinks")) return;
10 | if(!perms.has("addReactions") || !perms.has("readMessageHistory")) {
11 | await bot.send(msg.channel, data[0]);
12 | if(!perms.has("addReactions")) return "'Add Reactions' permission missing, cannot use reaction module.exports.buttons. Only first page shown.";
13 | else return "'Read Message History' permission missing, cannot use reaction module.exports.buttons. (Discord requires this permission to add reactions.) Only first page shown.";
14 | }
15 | }
16 | let m = await bot.send(msg.channel, data[0]);
17 | module.exports.cache[m.id] = {
18 | user: msg.author.id,
19 | pages: data,
20 | index: 0
21 | };
22 | setTimeout(() => {
23 | if(!module.exports.cache[m.id]) return;
24 | if(msg.channel.guild && msg.channel.permissionsOf(bot.user.id).has("manageMessages"))
25 | bot.removeMessageReactions(msg.channel.id,m.id).catch(bot.ignoreDeletion); //discard "Unknown Message" - no way to know if the message has been deleted
26 | delete module.exports.cache[m.id];
27 | }, 900000);
28 | for(let i=0; i {
33 | let embeds = [];
34 | let current = { embed: {
35 | title: extra.title,
36 | author: extra.author,
37 | description: extra.description,
38 | footer: extra.footer,
39 | fields: []
40 | }};
41 |
42 | for(let i=0; i 1) {
58 | for(let i = 0; i < embeds.length; i++)
59 | embeds[i].embed.title += ` (page ${i+1}/${embeds.length}, ${arr.length} total)`;
60 | }
61 | return embeds;
62 | },
63 |
64 | generateMemberField: (bot, member,group = null,add = 0) => {
65 | let out = {
66 | name: member.name.trim().length < 1 ? member.name + "\u200b" : member.name,
67 | value: `${(group != null) ? "Group: " + group.name + "\n" : ""}${member.tag ? ("Tag: " + member.tag + "\n") : ""}Brackets: ${bot.getBrackets(member)}\nAvatar URL: ${member.avatar_url}${member.birthday ? ("\nBirthday: "+member.birthday.toDateString()) : ""}\nTotal messages sent: ${member.posts}${member.description ? ("\n"+member.description) : ""}`
68 | };
69 | if(out.value.length + add > 1023) out.value = out.value.slice(0,1020-add) + "...";
70 | return out;
71 | },
72 |
73 | handleReaction: async (bot, message, emoji, userID) => {
74 | let data = module.exports.cache[message.id];
75 | try {
76 | if(message.channel.type != 1 && message.channel.permissionsOf(bot.user.id).has("manageMessages"))
77 | await bot.removeMessageReaction(message.channel.id, message.id, emoji.name, userID);
78 | } catch(e) {
79 | if(!e.message.startsWith("Request timed out") && e.code != 500 && e.code != 10008) bot.err(message,e,false);
80 | }
81 | let msg1,msg2;
82 | switch(emoji.name) {
83 | case "\u23ea": // first page
84 | data.index = 0;
85 | break;
86 |
87 | case "\u2b05": // previous page
88 | data.index--;
89 | if(data.index < 0) data.index = data.pages.length - 1;
90 | break;
91 |
92 | case "\u27a1": // next page
93 | data.index++;
94 | if(data.index >= data.pages.length) data.index = 0;
95 | break;
96 |
97 | case "\u23e9": // last page
98 | data.index = data.pages.length-1;
99 | break;
100 |
101 | case "\u23f9": // stop
102 | delete module.exports.cache[message.id];
103 | if(message.channel.type != null && message.channel.type != 1 && !message.channel.permissionsOf(bot.user.id).has("manageMessages")) return;
104 | try {
105 | return await bot.deleteMessage(message.channel.id, message.id);
106 | } catch(e) {
107 | return bot.err(message, e, false);
108 | }
109 |
110 | case "\u0023\u20e3": //go to num
111 | if(bot.dialogs[message.channel.id + userID]) return;
112 | try {
113 | msg1 = await bot.send(message.channel, "Enter a page number to go to.");
114 | message.author = {id: userID};
115 | msg2 = await bot.waitMessage(message);
116 | if(!isNaN(Number(msg2.content))) {
117 | data.index = Math.round(Number(msg2.content)-1);
118 | if(data.index < 0) data.index = 0;
119 | if(data.index >= data.pages.length) data.index = data.pages.length - 1;
120 | } else {
121 | msg1.edit("Invalid number.");
122 | let id = msg1.id;
123 | //setTimeout(() => bot.deleteMessage(message.channel.id,id).catch(bot.ignoreDeletion), 3000);
124 | msg1 = null;
125 | }
126 | } catch(e) {
127 | if(e == "timeout") {
128 | msg1.edit("Timed out - canceling.").catch(bot.ignoreDeletion);
129 | let id = msg1.id;
130 | /*setTimeout(() => {
131 | bot.deleteMessage(message.channel.id,id).catch(bot.ignoreDeletion);
132 | },3000);*/
133 | msg1 = null;
134 | } else {
135 | bot.err(message, e, false);
136 | }
137 | }
138 | //if(msg1) msg1.delete().catch(bot.ignoreDeletion);
139 | //if(msg2 && msg2.channel.type != 1) msg2.delete().catch(bot.ignoreDeletion);
140 | break;
141 |
142 | case "\ud83d\udd20": //find in list
143 | if(bot.dialogs[message.channel.id + userID]) return;
144 | try {
145 | msg1 = await bot.send(message.channel, "Enter text to search for.");
146 | message.author = {id: userID};
147 | msg2 = await bot.waitMessage(message);
148 | let search = msg2.content.toLowerCase();
149 | let searchFunc = test => {
150 | for(let i = 0; i < data.pages.length; i++) {
151 | if(!data.pages[i].embed.fields || data.pages[i].embed.fields.length == 0) continue;
152 | for(let j = 0; j < data.pages[i].embed.fields.length; j++) {
153 | if(test(data.pages[i].embed.fields[j])) {
154 | return i;
155 | }
156 | }
157 | }
158 | return -1;
159 | };
160 | let res = searchFunc(f => f.name.toLowerCase() == search);
161 | if(res < 0) res = searchFunc(f => f.name.toLowerCase().includes(search));
162 | if(res < 0) res = searchFunc(f => f.value.toLowerCase().includes(search));
163 | if(res < 0) {
164 | msg1.edit("No result found.").catch(bot.ignoreDeletion);
165 | let id = msg1.id;
166 | /*setTimeout(() => {
167 | bot.deleteMessage(message.channel.id,id).catch(bot.ignoreDeletion);
168 | },3000);*/
169 | msg1 = null;
170 | } else data.index = res;
171 | } catch(e) {
172 | if(e == "timeout") {
173 | msg1.edit("Timed out - canceling.").catch(bot.ignoreDeletion);
174 | let id = msg1.id;
175 | /*setTimeout(() => {
176 | bot.deleteMessage(message.channel.id,id).catch(bot.ignoreDeletion);
177 | },3000);*/
178 | msg1 = null;
179 | } else {
180 | bot.err(message, e, false);
181 | }
182 | }
183 | //if(msg1) msg1.delete().catch(bot.ignoreDeletion);
184 | //if(msg2 && msg2.channel.type != 1) msg2.delete().catch(bot.ignoreDeletion);
185 | break;
186 | }
187 | try {
188 | await bot.editMessage(message.channel.id, message.id, data.pages[data.index]).catch(bot.ignoreDeletion); //ignore message already deleted
189 | } catch(e) {
190 | bot.err(message, e, false);
191 | }
192 | },
193 |
194 | };
--------------------------------------------------------------------------------
/modules/proxy.js:
--------------------------------------------------------------------------------
1 | const request = require("got");
2 | const strlen = require("string-length");
3 | let tagRegex = /(@[\s\S]+?#0000|@\S+)/g;
4 |
5 | module.exports = {
6 |
7 | checkMember: (msg, member, clean) => {
8 | for(let i=0; i (member.brackets[i*2].length + member.brackets[i*2+1].length)))
10 | return i;
11 | }
12 | return -1;
13 | },
14 |
15 | fetchWebhook: async (bot, channel) => {
16 | let q = await bot.db.webhooks.get(channel.id);
17 | if(q) {
18 | try {
19 | if (await bot.getWebhook(q.id, q.token)) return q;
20 | } catch (e) {
21 | if (e.code != 10015) throw e;
22 | await bot.db.webhooks.delete(channel.id);
23 | return await module.exports.fetchWebhook(bot, channel);
24 | }
25 | }
26 | else if(!channel.permissionsOf(bot.user.id).has("manageWebhooks"))
27 | throw { permission: "Manage Webhooks" };
28 | else {
29 | let hook;
30 | try {
31 | hook = await channel.createWebhook({ name: "Tupperhook" });
32 | } catch(e) {
33 | if(e.code == 30007) {
34 | let wbhooks = await channel.getWebhooks();
35 | for(let i=0; i {
49 | let files = [];
50 | for(let i = 0; i < msg.attachments.length; i++) {
51 | let head;
52 | try {
53 | head = await request.head(msg.attachments[i].url);
54 | } catch(e) { }
55 | if(head && head.headers["content-length"] && Number(head.headers["content-length"]) > 8388608) throw new Error("toolarge");
56 | files.push({ file: (await request(msg.attachments[i].url, {encoding: null})).body , name: msg.attachments[i].filename });
57 | }
58 | return files;
59 | },
60 |
61 | cleanName: (bot, msg, un) => {
62 | //discord treats astral characters (many emojis) as one character, so add an invisible char to make it two
63 | let len = strlen(un);
64 | if(len == 0) un += "\u17B5\u17B5";
65 | else if(len == 1) un += "\u17B5";
66 |
67 | //discord collapses same-name messages, so if two would be sent by different users, break them up with a tiny space
68 | if(bot.recent[msg.channel.id] && msg.author.id !== bot.recent[msg.channel.id][0].user_id && un === bot.recent[msg.channel.id][0].name) {
69 | un = un.substring(0,1) + "\u200a" + un.substring(1);
70 | }
71 |
72 | //discord prevents the name 'clyde' being used in a webhook, so break it up with a tiny space
73 | un = un.replace(/(c)(lyde)/gi, "$1\u200a$2");
74 | if(un.length > 80) un = un.slice(0,78) + "..";
75 |
76 | return un;
77 | },
78 |
79 | getName: async (bot, member) =>
80 | `${member.name}${member.tag ? " " + member.tag : ""}${bot.checkMemberBirthday(member) ? "\uD83C\uDF70" : ""}${member.group_id ? " " + ((await bot.db.groups.getById(member.group_id)).tag ?? "") : ""}`.trim(),
81 |
82 | getRecentMentions: (bot, msg, content) =>
83 | bot.recent[msg.channel.id] ? content.replace(tagRegex,match => {
84 | let includesDiscrim = match.endsWith("#0000");
85 | let found = bot.recent[msg.channel.id].find(r => (includesDiscrim ? r.name == match.slice(1,-5) : r.rawname.toLowerCase() == match.slice(1).toLowerCase()));
86 | return found ? `${includesDiscrim ? match.slice(0,-5) : match} (<@${found.user_id}>)` : match;
87 | }) : content,
88 |
89 | logProxy: async (bot, msg, cfg, member, content, webmsg) => {
90 | if(cfg.log_channel && msg.channel.guild.channels.has(cfg.log_channel)) {
91 | let logchannel = msg.channel.guild.channels.get(cfg.log_channel);
92 | if(logchannel.type != 0 || typeof(logchannel.createMessage) != "function") {
93 | cfg.log_channel = null;
94 | bot.send(msg.channel, "Warning: There is a log channel configured but it is not a text channel. Logging has been disabled.");
95 | await bot.db.config.update(msg.channel.guild.id,"log_channel",null,bot.defaultCfg);
96 | }
97 | else if(!logchannel.permissionsOf(bot.user.id).has("sendMessages") || !logchannel.permissionsOf(bot.user.id).has("readMessages")) {
98 | bot.send(msg.channel, "Warning: There is a log channel configured but I do not have permission to send messages to it. Logging has been disabled.");
99 | await bot.db.config.update(msg.channel.guild.id,"log_channel",null,bot.defaultCfg);
100 | }
101 | else bot.send(logchannel, {embed: {
102 | title: member.name,
103 | description: content + "\n",
104 | fields: [
105 | { name: "Registered by", value: `<@!${msg.author.id}> (${msg.author.id})`, inline: true},
106 | { name: "Channel", value: `<#${msg.channel.id}> (${msg.channel.id})`, inline: true },
107 | { name: "\u200b", value: "\u200b", inline: true},
108 | { name: "Original Message", value: `[jump](https://discord.com/channels/${msg.channel.guild ? msg.channel.guild.id : "@me"}/${msg.channel.id}/${webmsg.id})`, inline: true},
109 | { name: "Attachments", value: msg.attachments[0] ? msg.attachments.map((att, i) => `[link ${i+1}](${att.url})`).join(", ") : "None", inline: true},
110 | { name: "\u200b", value: "\u200b", inline: true},
111 | ],
112 | thumbnail: {url: member.avatar_url},
113 | footer: {text: `Message ID ${webmsg.id}`}
114 | }});
115 | }
116 | },
117 |
118 | updateRecent: (bot, msg, data) => {
119 | if(!bot.recent[msg.channel.id]) {
120 | bot.recent[msg.channel.id] = [];
121 | }
122 | bot.recent[msg.channel.id].unshift(data);
123 | if(bot.recent[msg.channel.id].length > 5) bot.recent[msg.channel.id] = bot.recent[msg.channel.id].slice(0,5);
124 | },
125 |
126 | replaceMessage: async (bot, msg, cfg, member, content, retry = 2) => {
127 |
128 | if (retry == 0) return;
129 |
130 | const hook = await module.exports.fetchWebhook(bot, msg.channel);
131 |
132 | let ratelimit = bot.requestHandler.ratelimits[`/webhooks/${hook.id}/:token?wait=true`];
133 | if(ratelimit && ratelimit._queue.length > 5) {
134 | let res = { message: "autoban", notify: false };
135 | //ratelimit._queue = [];
136 | if(!ratelimit.expire || Date.now() > ratelimit.expire) {
137 | ratelimit.expire = Date.now() + 10000;
138 | res.notify = true;
139 | }
140 | throw res;
141 | }
142 |
143 | const data = {
144 | wait: true,
145 | content: module.exports.getRecentMentions(bot, msg, content),
146 | username: module.exports.cleanName(bot, msg, await module.exports.getName(bot, member)),
147 | avatarURL: member.avatar_url,
148 | };
149 |
150 | if(msg.attachments[0]) data.file = await module.exports.getAttachments(msg);
151 |
152 | if(data.content.trim().length == 0 && !data.file) throw { message: "empty" };
153 |
154 | let webmsg;
155 | try {
156 | webmsg = await bot.executeWebhook(hook.id,hook.token,data);
157 | } catch (e) {
158 | if(e.code == 504 || e.code == "EHOSTUNREACH") {
159 | return await module.exports.replaceMessage(bot, msg,cfg,member,content,retry-1);
160 | } else if(e.code == 40005) {
161 | throw new Error("toolarge");
162 | } else throw e;
163 | }
164 |
165 | module.exports.logProxy(bot, msg, cfg, member, content, webmsg);
166 |
167 | bot.db.members.update(member.user_id,member.name,"posts",member.posts+1);
168 |
169 | if(!bot.recent[msg.channel.id] && !msg.channel.permissionsOf(bot.user.id).has("manageMessages"))
170 | bot.send(msg.channel, "Warning: I do not have permission to delete messages. Both the original message and proxied message will show.");
171 |
172 | module.exports.updateRecent(bot, msg, {
173 | user_id: msg.author.id,
174 | name: data.username,
175 | rawname: member.name,
176 | id: webmsg.id,
177 | tag: `${msg.author.username}#${msg.author.discriminator}`
178 | });
179 |
180 | return true;
181 | },
182 |
183 | findInMessage: (bot, msg, members, cfg) => {
184 |
185 | let clean = msg.cleanContent || msg.content;
186 | clean = clean.replace(/()|(<@!?\d+?>)/,"cleaned");
187 | let cleanarr = clean.split("\n");
188 | let lines = msg.content.split("\n");
189 | let replace = [];
190 | let current = null;
191 |
192 | for(let i = 0; i < lines.length; i++) {
193 | let found = false;
194 | members.forEach(t => {
195 | let res = module.exports.checkMember(msg, t, cleanarr[i]);
196 | if(res >= 0) {
197 | if(t.brackets[res*2+1].length == 0) current = t;
198 | else current = null;
199 | found = true;
200 | replace.push([msg,cfg,t,t.show_brackets ? lines[i] : lines[i].substring(t.brackets[res*2].length, lines[i].length-t.brackets[res*2+1].length)]);
201 | }
202 | });
203 | if(!found && current)
204 | replace[replace.length-1][3] += "\n"+lines[i];
205 | }
206 |
207 | if(replace.length < 2) replace = [];
208 |
209 | if(!replace[0]) {
210 | for(let t of members) {
211 | let res = module.exports.checkMember(msg, t, clean);
212 | if(res >= 0) {
213 | replace.push([msg, cfg, t, t.show_brackets ? msg.content : msg.content.substring(t.brackets[res*2].length, msg.content.length-t.brackets[res*2+1].length)]);
214 | break;
215 | }
216 | }
217 | }
218 |
219 | return replace;
220 | },
221 |
222 | executeProxy: async ({msg,bot,members,cfg}) => {
223 |
224 | let replace = module.exports.findInMessage(bot, msg, members, cfg);
225 |
226 | if(!replace[0]) return false;
227 |
228 | try {
229 | if(replace.length > 7) {
230 | //console.log(`Potential abuse by ${msg.author.id} - ${replace.length} proxies at once in ${msg.channel.id}!`);
231 | return bot.send(msg.channel, "Proxy refused: too many proxies in one message!");
232 | }
233 | for(let r of replace) {
234 | await module.exports.replaceMessage(bot, ...r);
235 | }
236 | let perms = msg.channel.permissionsOf(bot.user.id);
237 | if(perms.has("manageMessages") && perms.has("readMessages"))
238 | // todo: do we want to try deleting multiple times, as previously done in the deletion queue?
239 | msg.delete().catch(bot.ignoreDeletion);
240 | return true;
241 | } catch(e) {
242 | if(e.message == "empty") bot.send(msg.channel, "Cannot proxy empty message.");
243 | else if(e.permission == "Manage Webhooks") bot.send(msg.channel, "Proxy failed because I don't have 'Manage Webhooks' permission in this channel.");
244 | else if(e.message == "toolarge") bot.send(msg.channel, "Message not proxied because bots can't send attachments larger than 8mb. Sorry!");
245 | else if(e.message == "autoban") {
246 | if(e.notify) bot.send(msg.channel, "Proxies refused due to spam!");
247 | console.log(`Potential spam by ${msg.author.id}!`);
248 | }
249 | else if(e.code != 10008) bot.err(msg, e); //discard "Unknown Message" errors
250 | }
251 | },
252 |
253 | sendMsgInfo: async (bot, message, emoji, userID) => {
254 |
255 | let recent = bot.recent[message.channel.id].find(r => message.id == r.id);
256 | if(!recent) return;
257 | let response = { content: `That proxy was sent by <@!${recent.user_id}> (tag at time of sending: ${recent.tag} - id: ${recent.user_id}).`, allowedMentions: { users: false } };
258 | let target;
259 | try {
260 | target = await bot.getDMChannel(userID);
261 | await bot.send(target,response);
262 | } catch(e) {
263 | target = message.channel;
264 | response.content = `<@${userID}>: ${response.content}\n(also I am unable to DM you!)`;
265 | await bot.send(target,response);
266 | }
267 | await bot.removeMessageReaction(message.channel.id, message.id, emoji.name, userID);
268 | return;
269 |
270 | },
271 | };
272 |
--------------------------------------------------------------------------------
/modules/redis.js:
--------------------------------------------------------------------------------
1 | const redis = new (require("ioredis"))(process.env.REDISURL);
2 |
3 | module.exports = {
4 | redis,
5 |
6 | cooldowns: {
7 | get: async (key) =>
8 | await redis.get(`cooldowns/${key}`),
9 |
10 | set: async (key, time) =>
11 | await redis.set(`cooldowns/${key}`, Date.now() + time, "px", time),
12 |
13 | update: async (key, time) =>
14 | await redis.pexpire(`cooldowns/${key}`, time),
15 | },
16 |
17 | config: {
18 | // TODO: rewrite this with a hashmap
19 | get: async (guildID) =>
20 | JSON.parse(await redis.get(`config/${guildID}`)),
21 |
22 | set: async (guildID, config) =>
23 | await redis.set(`config/${guildID}`, JSON.stringify(Object.fromEntries(Object.entries(config).filter(ent => ent[1] !== null)))),
24 |
25 | delete: async (guildID) =>
26 | await redis.del(`config/${guildID}`),
27 | },
28 |
29 | blacklist: {
30 | get: async (channelID) =>
31 | await redis.hget("blacklist", channelID),
32 |
33 | set: async (channelID, value) =>
34 | await redis.hset("blacklist", channelID, value),
35 |
36 | delete: async (channelID) =>
37 | await redis.hdel("blacklist", channelID),
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/modules/util.js:
--------------------------------------------------------------------------------
1 | module.exports = bot => {
2 | bot.cooldowns = {};
3 |
4 | bot.err = (msg, error, tell = true) => {
5 | if(error.message.startsWith("Request timed out") || error.code == 500 || error.code == "ECONNRESET" || error.code == "EHOSTUNREACH") return; //Internal discord errors don't need reporting
6 | console.error(`[ERROR ch:${msg.channel.id} usr:${msg.author ? msg.author.id : "UNKNOWN"}]\n(${error.code}) ${error.stack} `);
7 | if(tell && msg.channel) bot.send(msg.channel,`There was an error performing the operation. Please report this to the support server if issues persist. (${error.code || error.message})`).catch(e => {});
8 | bot.sentry.captureException(error);
9 | };
10 |
11 | bot.updateStatus = async () => {
12 | bot.editStatus({ name: `${bot.defaultCfg.prefix}help | ${(+(await bot.db.members.count())).toLocaleString()} registered`});
13 | };
14 |
15 | bot.ageOf = user => {
16 | return (Date.now() - user.createdAt)/(1000*60*60*24);
17 | };
18 |
19 | bot.getBrackets = member => {
20 | let out = [];
21 | for(let i=0; i {
28 | let targets = [];
29 | let amtFound = 1000;
30 | let lastId = "0";
31 | while(amtFound == 1000) {
32 | let found = await bot.requestHandler.request("GET", `/guilds/${guildID}/members`, true, {limit:1000,after:lastId});
33 | amtFound = found.length;
34 | if(found.length > 0) lastId = found[found.length-1].user.id;
35 | targets = targets.concat(found.map(m => m.user));
36 | }
37 | return targets;
38 | };
39 |
40 | bot.checkMemberBirthday = member => {
41 | if(!member.birthday) return false;
42 | let now = new Date();
43 | return member.birthday.getUTCDate() == now.getUTCDate() && member.birthday.getUTCMonth() == now.getUTCMonth();
44 | };
45 |
46 | bot.resolveUser = async (msg, text) => {
47 | let uid = /<@!?(\d+)>/.test(text) && text.match(/<@!?(\d+)>/)[1] || text;
48 | if (/^\d+$/.test(uid)) {
49 | let target = null;
50 | target = await bot.getRESTUser(uid).catch(e => { if(e.code != 10013) throw e; return null; }); //return null if user wasn't found, otherwise throw
51 | if (target && target.user) target = target.user;
52 | return target;
53 | } else return null;
54 | };
55 |
56 | bot.resolveChannel = (msg, text) => {
57 | let g = msg.channel.guild;
58 | return g.channels.get(/<#(\d+)>/.test(text) && text.match(/<#(\d+)>/)[1]) || g.channels.get(text); /*|| g.channels.find(m => m.name.toLowerCase() == text.toLowerCase())*/
59 | };
60 |
61 | bot.checkPermissions = (bot, cmd, msg, args) =>
62 | (msg.author.id === bot.owner) || (cmd.permitted(msg,bot));
63 |
64 | bot.waitMessage = (msg) => {
65 | return new Promise((res, rej) => {
66 | bot.dialogs[msg.channel.id + msg.author.id] = res;
67 | setTimeout(() => {
68 | if(bot.dialogs[msg.channel.id + msg.author.id] != undefined) {
69 | delete bot.dialogs[msg.channel.id + msg.author.id];
70 | rej("timeout");
71 | }
72 | }, 10000);
73 | });
74 | };
75 |
76 | bot.confirm = async (msg, text) => {
77 | let response;
78 | try {
79 | await bot.send(msg.channel, text);
80 | response = await bot.waitMessage(msg);
81 | if(response.content.toLowerCase() != "yes") return "Canceling operation.";
82 | } catch(e) {
83 | if(e == "timeout") return "Response timed out. Canceling.";
84 | else throw e;
85 | }
86 | return true;
87 | };
88 |
89 | bot.send = async (channel, message, file, retry = 2) => {
90 | if(!channel.id) return;
91 | let msg;
92 | try {
93 | if(bot.announcement && message.embed) {
94 | if(!message.content) message.content = "";
95 | message.content += "\n"+bot.announcement;
96 | }
97 | msg = await channel.createMessage(message, file);
98 | } catch(e) {
99 | if(e.message.startsWith("Request timed out") || (e.code >= 500 && e.code <= 599) || e.code == "EHOSTUNREACH") {
100 | if(retry > 0) return bot.send(channel,message,file,retry-1);
101 | else return;
102 | } else throw e;
103 | }
104 | return msg;
105 | };
106 |
107 | bot.sanitizeName = name => {
108 | return name.trim();
109 | };
110 |
111 | bot.noVariation = word => {
112 | return word.replace(/[\ufe0f]/g,"");
113 | };
114 |
115 | bot.banAbusiveUser = async (userID, notifyChannelID) => {
116 | if(userID == bot.user.id) return;
117 | let membersDeleted = await bot.db.members.clear(userID);
118 | let blacklistedNum = 0;
119 | try {
120 | blacklistedNum = (await bot.db.query("INSERT INTO global_blacklist values($1::VARCHAR(50))",[userID])).rowCount;
121 | } catch(e) { console.log(e.message); }
122 | console.log(`blacklisted ${blacklistedNum} user ${userID} and deleted ${membersDeleted.rowCount} tuppers`);
123 | bot.createMessage(notifyChannelID,`User <@${userID}> (${userID}) is now blacklisted for abuse.`);
124 | };
125 |
126 | bot.getMatches = (string, regex) => {
127 | var matches = [];
128 | var match;
129 | while (match = regex.exec(string)) {
130 | match.splice(1).forEach(m => { if(m) matches.push(m); });
131 | }
132 | return matches;
133 | };
134 |
135 | bot.ignoreDeletion = (e) => {
136 | if(e.code != 10008) throw e;
137 | };
138 |
139 | };
140 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tupperbox",
3 | "description": "tupperbox bot for discord message proxying",
4 | "homepage": "https://github.com/Keterr/Tupperbox/blob/master/README.md",
5 | "eslintConfig": {
6 | "env": {
7 | "es6": true,
8 | "node": true
9 | },
10 | "extends": "eslint:recommended",
11 | "parserOptions": {
12 | "sourceType": "module",
13 | "ecmaVersion": 2020
14 | },
15 | "rules": {
16 | "indent": ["error", "tab"],
17 | "quotes": ["error", "double"],
18 | "semi": ["error", "always"],
19 | "no-console": "off",
20 | "no-unused-vars": "off",
21 | "no-cond-assign": "off",
22 | "no-case-declarations": "off",
23 | "no-empty": "off"
24 | }
25 | },
26 | "dependencies": {
27 | "@sentry/node": "^5.7.1",
28 | "bufferutil": "^4.0.1",
29 | "dotenv": "^6.2.0",
30 | "eris": "github:keterr/eris#dev",
31 | "eris-sharder": "^1.10.0",
32 | "got": "^9.6.0",
33 | "ioredis": "^4.16.0",
34 | "pako": "^1.0.10",
35 | "pg": "^8.2.1",
36 | "probe-image-size": "^3.2.0",
37 | "string-length": "^2.0.0",
38 | "valid-url": "^1.0.9",
39 | "winston": "^3.2.1",
40 | "zlib-sync": "^0.1.6"
41 | },
42 | "devDependencies": {
43 | "eslint": "^7.10.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------