summaryrefslogtreecommitdiff
path: root/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/src')
-rw-r--r--server/src/API/API.ts33
-rw-r--r--server/src/API/routers/GuildRouter.ts44
-rw-r--r--server/src/API/routers/OAuth2Router.ts71
-rw-r--r--server/src/Bot.ts5
-rw-r--r--server/src/Config.ts12
-rw-r--r--server/src/client/BotClient.ts103
-rw-r--r--server/src/commands/.todo44
-rw-r--r--server/src/commands/animals/Bunny.ts36
-rw-r--r--server/src/commands/animals/Cat.ts36
-rw-r--r--server/src/commands/animals/Dog.ts36
-rw-r--r--server/src/commands/animals/Duck.ts36
-rw-r--r--server/src/commands/animals/Fox.ts36
-rw-r--r--server/src/commands/animals/Owl.ts36
-rw-r--r--server/src/commands/anime/Darling.ts90
-rw-r--r--server/src/commands/anime/Douse.ts27
-rw-r--r--server/src/commands/anime/Waifu.ts32
-rw-r--r--server/src/commands/bot/Info.ts61
-rw-r--r--server/src/commands/bot/Invite.ts27
-rw-r--r--server/src/commands/bot/Ping.ts23
-rw-r--r--server/src/commands/bot/Sin.ts35
-rw-r--r--server/src/commands/bot/Suggest.ts35
-rw-r--r--server/src/commands/emma/FanArt.ts132
-rw-r--r--server/src/commands/emma/UglyCat.ts27
-rw-r--r--server/src/commands/fun/Advice.ts29
-rw-r--r--server/src/commands/fun/Clapify.ts39
-rw-r--r--server/src/commands/fun/DateFact.ts52
-rw-r--r--server/src/commands/fun/DayFact.ts41
-rw-r--r--server/src/commands/fun/FML.ts33
-rw-r--r--server/src/commands/fun/Fact67
-rw-r--r--server/src/commands/fun/GitHubZen.ts29
-rw-r--r--server/src/commands/fun/Hello.ts23
-rw-r--r--server/src/commands/fun/Insult.ts29
-rw-r--r--server/src/commands/fun/NumberFact.ts41
-rw-r--r--server/src/commands/fun/Onion.ts35
-rw-r--r--server/src/commands/fun/Opinion.ts34
-rw-r--r--server/src/commands/fun/PayRespects.ts24
-rw-r--r--server/src/commands/fun/Rate.ts33
-rw-r--r--server/src/commands/fun/Say.ts40
-rw-r--r--server/src/commands/fun/Spoiler.ts39
-rw-r--r--server/src/commands/fun/Uwufy.ts48
-rw-r--r--server/src/commands/fun/YearFact.ts41
-rw-r--r--server/src/commands/fun/YoMomma.ts29
-rw-r--r--server/src/commands/minigames/8Ball.ts31
-rw-r--r--server/src/commands/minigames/Coinflip.ts50
-rw-r--r--server/src/commands/minigames/RollDie.ts29
-rw-r--r--server/src/commands/minigames/RussianRoulette.ts27
-rw-r--r--server/src/commands/mod/Ban.ts64
-rw-r--r--server/src/commands/mod/Kick.ts57
-rw-r--r--server/src/commands/mod/Prune.ts39
-rw-r--r--server/src/commands/mod/Slowmode.ts61
-rw-r--r--server/src/commands/mod/Unban.ts38
-rw-r--r--server/src/commands/nsfw/Danbooru.ts77
-rw-r--r--server/src/commands/nsfw/Gelbooru.ts77
-rw-r--r--server/src/commands/nsfw/Rule34.ts68
-rw-r--r--server/src/commands/owner/DM.ts90
-rw-r--r--server/src/commands/owner/IP.ts27
-rw-r--r--server/src/commands/owner/Reload.ts40
-rw-r--r--server/src/commands/owner/ServerCount.ts24
-rw-r--r--server/src/commands/owner/Status.ts35
-rw-r--r--server/src/commands/owner/Username.ts35
-rw-r--r--server/src/commands/reaction/List.ts46
-rw-r--r--server/src/commands/reaction/New.ts168
-rw-r--r--server/src/commands/reaction/Remove.ts61
-rw-r--r--server/src/commands/server/Goodbye.ts81
-rw-r--r--server/src/commands/server/MemberCount.ts24
-rw-r--r--server/src/commands/server/OldestMember.ts39
-rw-r--r--server/src/commands/server/PFP.ts63
-rw-r--r--server/src/commands/server/Poll.ts42
-rw-r--r--server/src/commands/server/RandomMember.ts23
-rw-r--r--server/src/commands/server/Server.ts39
-rw-r--r--server/src/commands/server/User.ts75
-rw-r--r--server/src/commands/server/Welcome.ts81
-rw-r--r--server/src/commands/util/Categories.ts155
-rw-r--r--server/src/commands/util/Help.ts100
-rw-r--r--server/src/database/index.ts5
-rw-r--r--server/src/database/models/DarlingModel.ts10
-rw-r--r--server/src/database/models/FanArtModel.ts12
-rw-r--r--server/src/database/models/GoodbyeModel.ts12
-rw-r--r--server/src/database/models/ReactionGuildModel.ts16
-rw-r--r--server/src/database/models/ReactionModel.ts36
-rw-r--r--server/src/database/models/WelcomeModel.ts12
-rw-r--r--server/src/database/structures/SettingsProvider.ts212
-rw-r--r--server/src/database/utils/Constants.ts8
-rw-r--r--server/src/inhibitors/sendMessages.ts18
-rw-r--r--server/src/json/8ball.json37
-rw-r--r--server/src/listeners/client/ReadyListener.ts59
-rw-r--r--server/src/listeners/client/channelDelete.ts21
-rw-r--r--server/src/listeners/client/debug.ts15
-rw-r--r--server/src/listeners/client/emojiDelete.ts20
-rw-r--r--server/src/listeners/client/guildCreate.ts23
-rw-r--r--server/src/listeners/client/messageDelete.ts20
-rw-r--r--server/src/listeners/client/messageReactionAdd.ts65
-rw-r--r--server/src/listeners/client/messageReactionRemove.ts55
-rw-r--r--server/src/listeners/client/roleDelete.ts17
-rw-r--r--server/src/structures/Interfaces.ts16
-rw-r--r--server/src/structures/OAuth2.ts62
-rw-r--r--server/src/utils/Logger.ts44
-rw-r--r--server/src/utils/Utils.ts5
98 files changed, 4389 insertions, 0 deletions
diff --git a/server/src/API/API.ts b/server/src/API/API.ts
new file mode 100644
index 0000000..8a6eb0f
--- /dev/null
+++ b/server/src/API/API.ts
@@ -0,0 +1,33 @@
+import { AkairoClient } from 'discord-akairo';
+import express, { Application } from 'express';
+import { createServer } from 'http';
+import cors from 'cors';
+import OAuth2 from '../structures/OAuth2';
+
+import OAuth2Router from './routers/OAuth2Router';
+import GuildRouter from './routers/GuildRouter';
+
+export default class API {
+ protected client: AkairoClient;
+ protected server: Application;
+ protected oauth: OAuth2;
+
+ public constructor(client: AkairoClient) {
+ this.client = client;
+ this.oauth = new OAuth2(this.client);
+ }
+
+ public start(): void {
+ this.server = express();
+ this.server.use(express.json());
+ this.server.use(cors({
+ origin: true,
+ credentials: true
+ }));
+
+ new OAuth2Router(this.server, this.client, this.oauth);
+ new GuildRouter(this.server, this.client);
+
+ createServer(this.server).listen(8088, (): void => console.log('API is online.'));
+ }
+} \ No newline at end of file
diff --git a/server/src/API/routers/GuildRouter.ts b/server/src/API/routers/GuildRouter.ts
new file mode 100644
index 0000000..01a8a9b
--- /dev/null
+++ b/server/src/API/routers/GuildRouter.ts
@@ -0,0 +1,44 @@
+import { Router, Request, Response, Application } from 'express';
+import { AkairoClient } from 'discord-akairo';
+import { Guild } from 'discord.js';
+import { authorization } from '../../Config';
+
+export default class GuildRouter {
+ protected app: Application;
+ protected client: AkairoClient;
+ protected router: Router;
+
+ public constructor(app: Application, client: AkairoClient) {
+ this.app = app;
+ this.client = client;
+ this.router = Router();
+
+ this.app.use(this.router);
+
+ this.router.get('/v1/get/guild/:id', (req: Request, res: Response) => {
+ const guild: Guild = this.client.guilds.cache.get(req.params.id);
+ if (!guild) return res.status(404).send({ message: 'Guild Not Found' });
+
+ return res.status(200).send({
+ name: guild.name,
+ owner: guild.owner.user.tag,
+ members: guild.memberCount
+ });
+ });
+
+ this.router.post('/v1/post/guild-name/:id', (req: Request, res: Response) => {
+ if (req.headers.authorization !== authorization) return res.status(401).send({ message: 'Unauthorized' });
+
+ const guild: Guild = this.client.guilds.cache.get(req.params.id);
+ if (!guild) return res.status(404).send({ message: 'Guild Not Found' });
+
+ if (!req.body.name) return res.status(404).send({ message: 'No Guild Name Provided' });
+ if (req.body.name.length > 32) return res.status(400).send({ message: 'Guild Name Exceeds 32 Characters' });
+ if (!guild.me.permissions.has('MANAGE_GUILD')) return res.status(401).send({ message: 'Cannot Manage Guild' });
+
+ guild.setName(req.body.name);
+
+ return res.status(201).send(req.body);
+ });
+ }
+} \ No newline at end of file
diff --git a/server/src/API/routers/OAuth2Router.ts b/server/src/API/routers/OAuth2Router.ts
new file mode 100644
index 0000000..60b0410
--- /dev/null
+++ b/server/src/API/routers/OAuth2Router.ts
@@ -0,0 +1,71 @@
+import { Router, Request, Response, Application } from 'express';
+import { AkairoClient } from 'discord-akairo';
+import fetch from 'node-fetch';
+import session from 'express-session';
+import OAuth2 from '../../structures/OAuth2';
+import { callbackUrl, authorization, clientID, redirectUri, clientSecret } from '../../Config';
+
+export default class OAuth2Router {
+ protected app: Application;
+ protected client: AkairoClient;
+ protected router: Router;
+ protected oauth: OAuth2;
+
+ public constructor(app: Application, client: AkairoClient, oauth: OAuth2) {
+ this.app = app;
+ this.client = client;
+ this.router = Router();
+ this.oauth = oauth;
+
+ this.app.use(session({
+ secret: authorization,
+ resave: false,
+ saveUninitialized: false,
+ cookie: {
+ secure: 'auto',
+ sameSite: false,
+ httpOnly: false,
+ maxAge: 6048e5
+ }
+ }));
+
+ this.app.use(this.router);
+
+ this.router.get('/oauth/login', (req: Request, res: Response) => {
+ return res.redirect(`https://discord.com/api/oauth2/authorize?client_id=${clientID}&redirect_uri=${encodeURIComponent(callbackUrl)}&response_type=code&scope=${encodeURIComponent('identify guilds')}`);
+ });
+
+ this.router.get('/oauth/logout', (req: Request, res: Response) => {
+ req.session.destroy(null);
+ return res.redirect(redirectUri);
+ });
+
+ this.router.get('/oauth/callback', (req: Request, res: Response) => {
+ fetch('https://discord.com/api/oauth2/token', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded'
+ },
+ //@ts-ignore
+ body: new URLSearchParams({
+ 'client_id': clientID,
+ 'client_secret': clientSecret,
+ 'grant_type': 'authorization_code',
+ 'code': req.query.code,
+ 'redirect_uri': callbackUrl,
+ 'scope': 'identify'
+ })
+ })
+ .then(response => response.json())
+ .then(response => {
+ req.session.token = response['access_token'];
+ res.redirect(redirectUri);
+ });
+ });
+
+ this.router.get('/oauth/details', async (req: Request, res: Response) => {
+ const details = await this.oauth.resolveInformation(req);
+ return res.status(200).send(details);
+ });
+ }
+} \ No newline at end of file
diff --git a/server/src/Bot.ts b/server/src/Bot.ts
new file mode 100644
index 0000000..899aff4
--- /dev/null
+++ b/server/src/Bot.ts
@@ -0,0 +1,5 @@
+import { token, owners } from './Config';
+import BotClient from './client/BotClient';
+
+const client: BotClient = new BotClient({ token, owners });
+client.start(); \ No newline at end of file
diff --git a/server/src/Config.ts b/server/src/Config.ts
new file mode 100644
index 0000000..8be44ed
--- /dev/null
+++ b/server/src/Config.ts
@@ -0,0 +1,12 @@
+export const token: string = "NzEyMDg4MzY5MjA2OTE5MjY5.XxZ9gQ.Qf-wdR-rRG4-4ImmOf7No3XdkP0";
+export const prefix: string = "uwu$";
+export const owners: string[] = ['217348698294714370'];
+export const authorization: string = "rex-1337";
+// This is all devifier's information, change this in production.
+export const clientID: string = "712088369206919269";
+export const clientSecret: string = "qpj1nsOo7HWVyNh6lY8Z8I8IvtV0bPiM";
+export const redirectUri: string = "http://localhost:3000";
+export const callbackUrl: string = "http://localhost:8088/oauth/callback"; // 8080
+export const colour: string = "ecb1d1";
+export const validIDs: string[] = [];
+export const mongoDBUri: string = "mongodb://sin:[email protected]:47107/heroku_4qrjvmb9"; \ No newline at end of file
diff --git a/server/src/client/BotClient.ts b/server/src/client/BotClient.ts
new file mode 100644
index 0000000..da23a7b
--- /dev/null
+++ b/server/src/client/BotClient.ts
@@ -0,0 +1,103 @@
+import { AkairoClient, CommandHandler, ListenerHandler, InhibitorHandler } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { join } from 'path';
+import { prefix, owners } from '../Config';
+import { logger } from '../utils/Logger';
+import { SettingsProvider } from '../database';
+import { Logger } from 'winston';
+
+declare module 'discord-akairo' {
+ interface AkairoClient {
+ commandHandler: CommandHandler;
+ listenerHandler: ListenerHandler;
+ logger: Logger;
+ settings: SettingsProvider;
+ }
+}
+
+interface BotOptions {
+ token?: string;
+ owners?: string[];
+ // prefix?: string;
+}
+
+export default class BotClient extends AkairoClient {
+ public readonly config: BotOptions;
+
+ public logger = logger;
+
+ public inhibitorHandler: InhibitorHandler = new InhibitorHandler(this, {
+ directory: join(__dirname, '..', 'inhibitors')
+ });
+
+ public listenerHandler: ListenerHandler = new ListenerHandler(this, {
+ directory: join(__dirname, '..', 'listeners')
+ });
+
+ public commandHandler: CommandHandler = new CommandHandler(this, {
+ directory: join(__dirname, '..', 'commands'),
+ /* prefix: async (msg: Message): Promise<string> => {
+ if (msg.guild) {
+ const doc = this.settings.cache.guilds.get(msg.guild.id);
+ if (doc?.prefix) return doc.prefix;
+ }
+ return this.config.prefix
+ }, */
+ prefix,
+ allowMention: true,
+ defaultCooldown: 6e4, // 60000 - 6 - count the zeroes... = 4, so its 6e4
+ ignorePermissions: owners,
+ // Extra stuff
+ argumentDefaults: {
+ prompt: {
+ modifyStart: (_: Message, str: string): string => `${str}\n\nType \`cancel\` to cancel the command...`,
+ modifyRetry: (_: Message, str: string): string => `${str}\n\nType \`cancel\` to cancel the command...`,
+ timeout: 'You took too long, the command has now been cancelled...',
+ ended: 'You exceeded the maximum amount of tries, this command has now been cancelled...',
+ cancel: 'This command has been cancelled...',
+ retries: 3,
+ time: 3e4
+ },
+ otherwise: ''
+ }
+ });
+
+ public settings: SettingsProvider = new SettingsProvider(this);
+
+ public constructor(config: BotOptions) {
+ super({
+ ownerID: config.owners,
+ messageCacheMaxSize: 50,
+ messageSweepInterval: 900,
+ messageCacheLifetime: 300,
+ partials: ['MESSAGE', 'REACTION']
+ });
+
+ this.config = config;
+
+ this.on('shardError', (err: Error, id: any): Logger => this.logger.warn(`[SHARD ${id} ERROR] ${err.message}`, err.stack))
+ .on('warn', (warn: any): Logger => this.logger.warn(`[CLIENT WARN] ${warn}`));
+ }
+
+ private async init(): Promise<this> {
+ await this.settings.init();
+ this.commandHandler.useInhibitorHandler(this.inhibitorHandler);
+ this.commandHandler.useListenerHandler(this.listenerHandler);
+ this.listenerHandler.setEmitters({
+ commandHandler: this.commandHandler,
+ listenerHandler: this.listenerHandler,
+ inhibitorHandler: this.inhibitorHandler,
+ process
+ });
+ this.commandHandler.loadAll();
+ this.listenerHandler.loadAll();
+ this.inhibitorHandler.loadAll();
+
+ return this;
+ }
+
+ public async start(): Promise<string> {
+ await this.init();
+ return this.login(this.config.token);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/.todo b/server/src/commands/.todo
new file mode 100644
index 0000000..0420950
--- /dev/null
+++ b/server/src/commands/.todo
@@ -0,0 +1,44 @@
+Commands:
+ [ ] Animals
+ [x] Anime
+ [ ] Bot
+ [ ] Crypto
+ [ ] Emma
+ [ ] Fun
+ [ ] Aesthetic
+ [ ] Cultured Text
+ [ ] Dogeify
+ [ ] Draw Cards
+ [ ] Embed
+ [ ] Emoji
+ [ ] FML
+ [ ] Gay
+ [ ] Howify
+ [ ] Insult
+ [ ] IQ
+ [ ] KMK
+ [ ] Lorem
+ [ ] Motivate
+ [ ] Oddcase
+ [ ] Offspring
+ [ ] Onion
+ [ ] Quote
+ [ ] RoastWilly/ RoastWillyC
+ [ ] Rock, Paper, Scissors
+ [ ] ShowerThoughts
+ [ ] Smash or Pass
+ [ ] Spongebob
+ [ ] Stretch
+ [ ] Subreddit
+ [ ] Surreal
+ [ ] Minecraft
+ [ ] Moderation
+ [ ] NSFW
+ [ ] Roleplay
+ [ ] Server
+ [ ] User
+ [ ] Utility
+ [ ] Voice
+ [ ] Zero Two
+ [x] Douse
+ [ ] Darling \ No newline at end of file
diff --git a/server/src/commands/animals/Bunny.ts b/server/src/commands/animals/Bunny.ts
new file mode 100644
index 0000000..5325002
--- /dev/null
+++ b/server/src/commands/animals/Bunny.ts
@@ -0,0 +1,36 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+import { colour } from '../../Config';
+
+export default class BunnyAnimals extends Command {
+ public constructor() {
+ super('bunny', {
+ aliases: ['bunny'],
+ category: 'animals',
+ description: {
+ content: 'Gives you a random bunny!',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ clientPermissions: ['EMBED_LINKS']
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const animal = await Axios.get(`https://api.bunnies.io/v2/loop/random/?media=gif,png`).catch(err => {
+ console.error(err);
+ msg.reply('Woops, there was an error with the (http://api.bunies.io/) API.');
+ });
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor('bunnies.io')
+ //@ts-ignore
+ .setImage(animal.data.media.gif);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/animals/Cat.ts b/server/src/commands/animals/Cat.ts
new file mode 100644
index 0000000..8c37d2c
--- /dev/null
+++ b/server/src/commands/animals/Cat.ts
@@ -0,0 +1,36 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+import { colour } from '../../Config';
+
+export default class CatAnimals extends Command {
+ public constructor() {
+ super('cat', {
+ aliases: ['cat'],
+ category: 'animals',
+ description: {
+ content: 'Gives you a random cat!',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ clientPermissions: ['EMBED_LINKS']
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const animal = await Axios.get(`https://aws.random.cat/meow`).catch(err => {
+ console.error(err);
+ msg.reply('Woops, there was an error with the (http://random.cat/) API.');
+ });
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor('bunnies.io', 'https://i.imgur.com/Ik0Gf0r.png', 'https://random.cat')
+ //@ts-ignore
+ .setImage(animal.data.file);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/animals/Dog.ts b/server/src/commands/animals/Dog.ts
new file mode 100644
index 0000000..c44131e
--- /dev/null
+++ b/server/src/commands/animals/Dog.ts
@@ -0,0 +1,36 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+import { colour } from '../../Config';
+
+export default class DogAnimals extends Command {
+ public constructor() {
+ super('dog', {
+ aliases: ['dog'],
+ category: 'animals',
+ description: {
+ content: 'Gives you a random dog!',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ clientPermissions: ['EMBED_LINKS']
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const animal = await Axios.get(`https://dog.ceo/api/breeds/image/random`).catch(err => {
+ console.error(err);
+ msg.reply('Woops, there was an error with the (http://dog.ceo/api) API.');
+ });
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor('dog.ceo', 'https://dog.ceo/img/favicon.png', 'https://dog.ceo/dog-api/')
+ //@ts-ignore
+ .setImage(animal.data.message);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/animals/Duck.ts b/server/src/commands/animals/Duck.ts
new file mode 100644
index 0000000..c1b75f9
--- /dev/null
+++ b/server/src/commands/animals/Duck.ts
@@ -0,0 +1,36 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+import { colour } from '../../Config';
+
+export default class DuckAnimals extends Command {
+ public constructor() {
+ super('duck', {
+ aliases: ['duck'],
+ category: 'animals',
+ description: {
+ content: 'Gives you a random duck!',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ clientPermissions: ['EMBED_LINKS']
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const animal = await Axios.get(`https://random-d.uk/api/v1/random?type=gif`).catch(err => {
+ console.error(err);
+ msg.reply('Woops, there was an error with the (http://random-d.uk/api/) API.');
+ });
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor('random-d.uk', 'https://random-d.uk/favicon.ico', 'https://random-d.uk')
+ //@ts-ignore
+ .setImage(animal.data.url);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/animals/Fox.ts b/server/src/commands/animals/Fox.ts
new file mode 100644
index 0000000..fc887ef
--- /dev/null
+++ b/server/src/commands/animals/Fox.ts
@@ -0,0 +1,36 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+import { colour } from '../../Config';
+
+export default class FoxAnimals extends Command {
+ public constructor() {
+ super('fox', {
+ aliases: ['fox'],
+ category: 'animals',
+ description: {
+ content: 'Gives you a random fox!',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ clientPermissions: ['EMBED_LINKS']
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const animal = await Axios.get(`https://randomfox.ca/floof/`).catch(err => {
+ console.error(err);
+ msg.reply('Woops, there was an error with the (http://randomfox.ca/floof/) API.');
+ });
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor('randomfox.ca')
+ //@ts-ignore
+ .setImage(animal.data.image);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/animals/Owl.ts b/server/src/commands/animals/Owl.ts
new file mode 100644
index 0000000..bec7a5b
--- /dev/null
+++ b/server/src/commands/animals/Owl.ts
@@ -0,0 +1,36 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+import { colour } from '../../Config';
+
+export default class OwlAnimals extends Command {
+ public constructor() {
+ super('owl', {
+ aliases: ['owl'],
+ category: 'animals',
+ description: {
+ content: 'Gives you a random owl!',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ clientPermissions: ['EMBED_LINKS']
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const animal = await Axios.get(`http://pics.floofybot.moe/owl`).catch(err => {
+ console.error(err);
+ msg.reply('Woops, there was an error with the (http://pics.floofybot.moe/owl) API.');
+ });
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor('pics.floofybot.moe/owl', 'http://pics.floofybot.moe/assets/favicon.svg', 'http://pics.floofybot.moe/')
+ //@ts-ignore
+ .setImage(animal.data.image);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/anime/Darling.ts b/server/src/commands/anime/Darling.ts
new file mode 100644
index 0000000..5b5a7bb
--- /dev/null
+++ b/server/src/commands/anime/Darling.ts
@@ -0,0 +1,90 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Darling from '../../database/models/DarlingModel';
+import mongoose from 'mongoose';
+import { mongoDBUri } from '../../Config';
+mongoose.connect(mongoDBUri, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true
+});
+
+export default class DarlingAnime extends Command {
+ public constructor() {
+ super('darling', {
+ aliases: ['darling'],
+ category: 'anime',
+ description: {
+ content: 'Allows you to set, check or delete the/ a server\'s darling.',
+ usage: '[type]',
+ examples: [
+ '',
+ 'set',
+ 'remove',
+ 'check'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ args: [
+ {
+ id: 'type',
+ type: 'string',
+ prompt: {
+ start: 'Would you like to set, check or delete the current darling?',
+ retries: 3,
+ retry: 'Sorry, that was not a valid type.'
+ }
+ }
+ ],
+ userPermissions: ['MANAGE_GUILD']
+ });
+ }
+
+ public exec(msg: Message, { type }): Promise<Message> | any {
+ const darling = new Darling({
+ _id: mongoose.Types.ObjectId(),
+ username: msg.author.username,
+ userID: msg.author.id,
+ guildname: msg.guild.name,
+ guildID: msg.guild.id,
+ channelname: msg.channel,
+ channelID: msg.channel.id,
+ time: msg.createdAt
+ });
+
+ return Darling.findOne({ guildID: msg.guild.id }, async (error, guild) => {
+ if (error) return console.error(error);
+
+ if (guild) {
+ if (type === 'remove') {
+ //@ts-ignore
+ if (msg.author.id !== guild.userID || msg.author.id !== msg.guild.owner.id)
+ return msg.reply('Only my darling or the guild owner can remove the current darling.');
+
+ await Darling.findOneAndDelete({ guildID: msg.guild.id });
+ return msg.channel.send('The current darling has been removed!');
+ } else if (type === 'set') {
+ //@ts-ignore
+ return msg.channel.send(`I already have a darling! It's **${guild.username}**! To set a new darling, either the current darling or the guild owner has to do \`${this.client.commandHandler.prefix}darling remove\`.`);
+ } else if (type === 'check') {
+ //@ts-ignore
+ return msg.channel.send(`My darling is ${guild.username}.`);
+ }
+ } else if (!guild) {
+ if (type === 'remove') {
+ return msg.channel.send('There is no darling set in this server.');
+ } else if (type === 'set') {
+ await darling.save().catch(err => console.error(err));
+ const quotes = [
+ 'I think I have taken a liking to you. Won\'t you be my darling?',
+ 'I like the look in your eyes. It makes my heart race. You are now my darling!',
+ 'Wow, your taste makes my heart race. It bites and lingers... The taste of danger. You are now my darling!'
+ ];
+ return msg.channel.send(quotes[Math.floor(Math.random() * quotes.length)]);
+ } else if (type === 'check') {
+ return msg.reply(`I haven't found my darling yet! To set one, do ${this.client.commandHandler.prefix}darling set.`);
+ }
+ }
+ });
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/anime/Douse.ts b/server/src/commands/anime/Douse.ts
new file mode 100644
index 0000000..02b5771
--- /dev/null
+++ b/server/src/commands/anime/Douse.ts
@@ -0,0 +1,27 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class DouseAnime extends Command {
+ public constructor() {
+ super('douse', {
+ aliases: ['douse'],
+ category: 'anime',
+ description: {
+ content: 'Douses Zero Two.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setImage('https://i.pinimg.com/originals/6a/c8/26/6ac826e3d0cbd64eb4f42c12a73fcdb8.gif');
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/anime/Waifu.ts b/server/src/commands/anime/Waifu.ts
new file mode 100644
index 0000000..043b8ac
--- /dev/null
+++ b/server/src/commands/anime/Waifu.ts
@@ -0,0 +1,32 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+import request from 'node-superfetch';
+import Util from '../../utils/Utils';
+
+export default class WaifuAnime extends Command {
+ public constructor() {
+ super('waifu', {
+ aliases: ['waifu', 'thiswaifudoesnotexist'],
+ category: 'anime',
+ description: {
+ content: 'Sends a randomly generated waifu with a backstory. WARNING: don\'t get too attatched.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const num = Math.floor(Math.random() * 100000);
+ const { text } = await request.get(`https://www.thiswaifudoesnotexist.net/snippet-${num}.txt`);
+ const embed = this.client.util.embed()
+ .setDescription(Util.shorten(text, 1000))
+ .setColor(colour)
+ .setThumbnail(`https://www.thiswaifudoesnotexist.net/example-${num}.jpg`);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/bot/Info.ts b/server/src/commands/bot/Info.ts
new file mode 100644
index 0000000..cda8f7a
--- /dev/null
+++ b/server/src/commands/bot/Info.ts
@@ -0,0 +1,61 @@
+import { Command, version as akairoversion } from 'discord-akairo';
+import { Message, version as djsversion } from 'discord.js';
+import { stripIndents } from 'common-tags';
+import * as moment from 'moment';
+import 'moment-duration-format';
+import { colour, owners } from '../../Config';
+
+export default class InfoBot extends Command {
+ public constructor() {
+ super('info', {
+ aliases: ['info', 'stats', 'uptime'],
+ category: 'bot',
+ description: {
+ content: 'Provides some information/ stats on the bot.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ // @ts-ignore
+ const duration = moment.duration(this.client.uptime!).format(' D[d] H[h] m[m] s[s]');
+ const embed = this.client.util.embed()
+ .setTitle(`${this.client.user!.username} Stats`)
+ .setColor(colour)
+ .setThumbnail(this.client.user!.displayAvatarURL())
+ .addField(`\`⏰\` Uptime`, duration, true)
+ .addField(`\`💾\`Memory Usage`, `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)}MB`, true)
+ .addField(
+ `\`📊\` General Stats`,
+ // • Servers: ${this.client.guilds.cache.size.toLocaleString('en-US')}
+ stripIndents`
+ • Channels: ${this.client.channels.cache.size.toLocaleString('en-US')}
+ • Users: ${this.client.guilds.cache
+ .reduce((prev, val) => prev + val.memberCount, 0)
+ .toLocaleString('en-US')}
+ `, true)
+ /* .addField(
+ '`👴` Reaction Role Stats',
+ stripIndents`
+ • Current: ${this.client.settings.cache.reactions.filter(r => r.active).size}
+ • Lifetime: ${this.client.settings.cache.reactions.size}
+ `,
+ true,
+ ) */
+ .addField(
+ '`📚` Library Info',
+ stripIndents`
+ [\`Akairo Framework\`](https://discord-akairo.github.io/#/): ${akairoversion}
+ [\`Discord.js\`](https://discord.js.org/#/): ${djsversion}
+ `, true)
+ .addField('`👧` Lead Developer', (await this.client.fetchApplication()).owner!.toString(), true)
+ .setFooter(`For more information about ${this.client.users.resolve(owners[0]).tag}, use ${this.client.commandHandler.prefix}sin`,
+ `${this.client.users.resolve(owners[0]).avatarURL()}`);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/bot/Invite.ts b/server/src/commands/bot/Invite.ts
new file mode 100644
index 0000000..a25a20c
--- /dev/null
+++ b/server/src/commands/bot/Invite.ts
@@ -0,0 +1,27 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class InviteBot extends Command {
+ public constructor() {
+ super('invite', {
+ aliases: ['invite'],
+ category: 'bot',
+ description: {
+ content: 'Gives you the bot\'s invite link.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setDescription('To invite the bot, please use [this link](https://kyzer.co/discord/bots/uwufier/).');
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/bot/Ping.ts b/server/src/commands/bot/Ping.ts
new file mode 100644
index 0000000..d7857c7
--- /dev/null
+++ b/server/src/commands/bot/Ping.ts
@@ -0,0 +1,23 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class PingBot extends Command {
+ public constructor() {
+ super('ping', {
+ aliases: ['ping'],
+ category: 'bot',
+ description: {
+ content: 'Check the latency of the ping to the Discord API.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ return msg.channel.send(`Pong! \`${this.client.ws.ping}ms\`.`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/bot/Sin.ts b/server/src/commands/bot/Sin.ts
new file mode 100644
index 0000000..66e1f2f
--- /dev/null
+++ b/server/src/commands/bot/Sin.ts
@@ -0,0 +1,35 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+import { stripIndents } from 'common-tags';
+
+export default class SinBot extends Command {
+ public constructor() {
+ super('sin', {
+ aliases: ['sin'],
+ category: 'bot',
+ description: {
+ content: 'Will get you more information about Sin, the lead developer.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .addField('Sin\'s Stuff', stripIndents`
+ 💎 [GitHub](https://github.com/8cy)
+ 🎀 [NPM](https://www.npmjs.com/~sinny)
+ 🎨 [Twitter](https://twitter.com/__cpuid)
+ 🎁 [Website](https://kyzer.co)
+ ✨ [YouTube](https://youtube.com/s1nny)
+ 🎐 [Top.gg Vote](https://discordbots.org/bot/699473263998271489/vote)
+ `, false)
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/bot/Suggest.ts b/server/src/commands/bot/Suggest.ts
new file mode 100644
index 0000000..7478182
--- /dev/null
+++ b/server/src/commands/bot/Suggest.ts
@@ -0,0 +1,35 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { owners } from '../../Config';
+
+export default class SuggestBot extends Command {
+ public constructor() {
+ super('suggest', {
+ aliases: ['suggest'],
+ category: 'bot',
+ description: {
+ content: 'Suggest a feature that the bot should add.',
+ usage: '[suggestion]',
+ examples: [
+ 'walter command please'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'suggestion',
+ type: 'string',
+ prompt: {
+ start: 'What would you like to suggest?'
+ },
+ match: 'rest'
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { suggestion }): Promise<Message> {
+ await this.client.users.resolve(owners[0]).send(`**${msg.author.tag}** suggest; *${suggestion}*`);
+ return msg.channel.send('Thank you for your suggestion!');
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/emma/FanArt.ts b/server/src/commands/emma/FanArt.ts
new file mode 100644
index 0000000..3cab365
--- /dev/null
+++ b/server/src/commands/emma/FanArt.ts
@@ -0,0 +1,132 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import FanArt from '../../database/models/FanArtModel';
+import mongoose from 'mongoose';
+import { mongoDBUri, colour } from '../../Config';
+mongoose.connect(mongoDBUri, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true
+});
+
+export default class FanArtEmma extends Command {
+ public constructor() {
+ super('fanart', {
+ aliases: ['fanart', 'art'],
+ category: 'emma',
+ description: {
+ content: 'Allows you to set, check or delete the/ a server fanart channel.',
+ usage: '[type]',
+ examples: [
+ '',
+ 'set',
+ 'remove',
+ 'check'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ args: [
+ {
+ id: 'type',
+ type: 'string',
+ prompt: {
+ start: 'Would you like to set, check or delete the fanart channel?',
+ retries: 3,
+ retry: 'Sorry, that was not a valid type.'
+ }
+ },
+ {
+ id: 'comment',
+ type: 'string'
+ }
+ ],
+ userPermissions: ['MANAGE_GUILD']
+ });
+ }
+
+ public exec(msg: Message, { type, comment }): Promise<Message> | any {
+ if (msg.guild.id.toString() !== '663964105983393793') return;
+ const welcome = new FanArt({
+ _id: mongoose.Types.ObjectId(),
+ username: msg.author.username,
+ userID: msg.author.id,
+ guildname: msg.guild.name,
+ guildID: msg.guild.id,
+ channelname: msg.channel,
+ channelID: msg.channel.id,
+ time: msg.createdAt
+ });
+
+ const validTypes = ['set', 'remove', 'check'];
+
+ if (type === 'submit') {
+ FanArt.findOne({ guildID: msg.guild.id }, async (error, guild) => {
+ if (error) return console.log(error);
+
+ //@ts-ignore
+ let fanartServer = this.client.guilds.cache.get(guild.guildID);
+ //@ts-ignore
+ let fanartChannel = guild.channelID;
+
+ if (msg.attachments.size) {
+ msg.attachments.forEach(fanart => {
+ if (fanart.url) {
+ //@ts-ignore
+ return fanartServer.channels.cache.get(fanartChannel).send(`**New fanart submitted!**\nFanart by <@${msg.author.id}>.\n\n**Comment**\n${comment ? comment : 'None.'}\n\n**Video** ` + fanart.url)
+ .then(m => {
+ m.react('😍');
+ m.react('😂');
+ m.react('😁');
+ m.react('😳');
+ m.react('😱');
+ });
+ } else {
+ return msg.reply(`No attachment was submitted! If you need help, please do \`${this.client.commandHandler.prefix}fanart help\`.`);
+ }
+ });
+ } else {
+ return msg.reply(`No attachment was submitted! If you need help, please do \`${this.client.commandHandler.prefix}fanart help\`.`);
+ }
+ });
+ } else if (type === 'help') {
+ const embed = this.client.util.embed()
+ .setTitle('Fanart - Help')
+ .setColor(colour)
+ .setDescription('How to submmit fanart:')
+ .setThumbnail(msg.guild.iconURL())
+ .addField('#1', 'Go to the `#media` channel.')
+ .addField('#2', 'Click on the add media button in the bottom left corner of your screen and select a video or image.')
+ .addField('#3', 'In the message section, please put `uwu!art submit`.')
+ .addField('#4 (Optional)', 'If you would like, you can also put a comment on your fanart, you can do this by adding an extra string to the end of your submit command. e.g. `uwu!art submit this is where the comment goes!`, if you followed the steps correctly, your comment should be `this is where the comment goes!')
+ .addField('Admin Stuff', `If you are an admin or moderator who would like to set/ remove a fanart channel, you can do this by going to to the channel you would like to set as the new fanart channel and doing \`${this.client.commandHandler.prefix}fanart set\`, this will set the current channel as the fanart channel. To remove a fanart channel, just do \`${this.client.commandHandler.prefix}fanart remove\`.`)
+ .addField('More Admin Info', 'You can only have **ONE** fanart channel (I think, I haven\'t tested it lol. If you change the name of the fanart channel, you will have to re-register with the bot by simply removing and re-setting the fanart channel');
+ return msg.channel.send(embed);
+ } else if (validTypes.includes(type)) {
+ return FanArt.findOne({ guildID: msg.guild.id }, async (error, guild) => {
+ if (error) return console.error(error);
+
+ if (guild) {
+ if (type === 'remove') {
+ await FanArt.findOneAndDelete({ guildID: msg.guild.id });
+ return msg.channel.send('The current fanart channel has been unset!');
+ } else if (type === 'set') {
+ //@ts-ignore
+ return msg.channel.send(`There already is a fanart channel set! It's ${guild.channelname}`);
+ } else if (type === 'check') {
+ //@ts-ignore
+ return msg.channel.send(`The current fanart channel is ${guild.channelname}!`);
+ }
+ } else if (!guild) {
+ if (type === 'remove') {
+ return msg.channel.send('There is no current fanart channel set for this guild!');
+ } else if (type === 'set') {
+ await welcome.save().catch(err => console.error(err));
+ return msg.channel.send(`The fanart channel has been set to ${msg.channel!}`);
+ } else if (type === 'check') {
+ return msg.reply(`There is no current fanart channel set for this guild! To set one, do ${this.client.commandHandler.prefix}fanart set in the channel you want to set it in!`);
+ }
+ }
+ });
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/emma/UglyCat.ts b/server/src/commands/emma/UglyCat.ts
new file mode 100644
index 0000000..95193fe
--- /dev/null
+++ b/server/src/commands/emma/UglyCat.ts
@@ -0,0 +1,27 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class UglyCatEmma extends Command {
+ public constructor() {
+ super('uglycat', {
+ aliases: ['uglycat', 'ugycat'],
+ category: 'fun',
+ description: {
+ content: 'Ugly Cat.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setImage('https://i.pinimg.com/originals/4d/19/0f/4d190f1307b35e7155bb4b898e19d545.jpg');
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Advice.ts b/server/src/commands/fun/Advice.ts
new file mode 100644
index 0000000..17035c0
--- /dev/null
+++ b/server/src/commands/fun/Advice.ts
@@ -0,0 +1,29 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+
+export default class AdviceFun extends Command {
+ public constructor() {
+ super('advice', {
+ aliases: ['advice'],
+ category: 'fun',
+ description: {
+ content: 'Gives you a random piece of advice.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const response = await Axios.get('http://api.adviceslip.com/advice').catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error regarding the (http://numbersapi.com) API.');
+ });
+ //@ts-ignore
+ return msg.reply(response.data.slip.advice);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Clapify.ts b/server/src/commands/fun/Clapify.ts
new file mode 100644
index 0000000..3e6e0fd
--- /dev/null
+++ b/server/src/commands/fun/Clapify.ts
@@ -0,0 +1,39 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class ClapifyFun extends Command {
+ public constructor() {
+ super('clapify', {
+ aliases: ['clapify'],
+ category: 'fun',
+ description: {
+ content: 'Clapifies your specified text.',
+ usage: '[text]',
+ examples: [
+ 'clap this lol'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'text',
+ type: 'string',
+ prompt: {
+ start: 'What would you like to clapify?'
+ },
+ match: 'rest'
+ },
+ {
+ id: 'deleteinitialmessage',
+ flag: ['-delete', '-d'],
+ match: 'flag'
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { text, deleteinitialmessage }): Promise<Message> {
+ if (deleteinitialmessage) msg.delete();
+ return msg.channel.send(text.split(' ').join('👏'));
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/DateFact.ts b/server/src/commands/fun/DateFact.ts
new file mode 100644
index 0000000..6fe94bc
--- /dev/null
+++ b/server/src/commands/fun/DateFact.ts
@@ -0,0 +1,52 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+
+export default class DateFactFun extends Command {
+ public constructor() {
+ super('datefact', {
+ aliases: ['datefact'],
+ category: 'fun',
+ description: {
+ content: 'Grabs a fact about a specified date.',
+ usage: '[numeric day] [numeric month]',
+ examples: [
+ '8 4'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'day',
+ type: 'integer',
+ prompt: {
+ start: 'What day would you like to get facts for? (Numeric value)',
+ retry: 'That is not a valid day, please try again.',
+ retries: 3
+ },
+ default: 'random',
+ },
+ {
+ id: 'month',
+ type: 'integer',
+ prompt: {
+ start: 'What month would you like to get facts for? (Numeric value)',
+ retry: 'That is not a valid month, please try again.',
+ retries: 3
+ },
+ default: 'random',
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { day, month }): Promise<Message> {
+ const uri = `http://numbersapi.com/${month === 'random' || day === 'random' ? 'random' : `${month}/${day}/date`}`;
+ const fact = await Axios.get(uri).catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error regarding the (http://numbersapi.com) API.');
+ });
+ //@ts-ignore
+ return msg.reply(fact.data);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/DayFact.ts b/server/src/commands/fun/DayFact.ts
new file mode 100644
index 0000000..df5d3b7
--- /dev/null
+++ b/server/src/commands/fun/DayFact.ts
@@ -0,0 +1,41 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+
+export default class DayFactFun extends Command {
+ public constructor() {
+ super('dayfact', {
+ aliases: ['dayfact'],
+ category: 'fun',
+ description: {
+ content: 'Grabs a fact about a specified day.',
+ usage: '[numeric day]',
+ examples: [
+ '8'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'day',
+ type: 'integer',
+ prompt: {
+ start: 'What day would you like to get facts for? (Numeric value)',
+ retry: 'That is not a valid day, please try again.',
+ retries: 3
+ },
+ default: 'random',
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { day }): Promise<Message> {
+ const fact = await Axios.get(`http://numbersapi.com/${day}/date`).catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error with the (http://numbersapi.com) API.');
+ });
+ //@ts-ignore
+ return msg.reply(fact.data);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/FML.ts b/server/src/commands/fun/FML.ts
new file mode 100644
index 0000000..041622c
--- /dev/null
+++ b/server/src/commands/fun/FML.ts
@@ -0,0 +1,33 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import request from 'node-superfetch';
+import * as cheerio from 'cheerio';
+
+export default class FMLFun extends Command {
+ public constructor() {
+ super('fml', {
+ aliases: ['fml'],
+ category: 'fun',
+ description: {
+ content: 'Gives you a random FML.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ //@ts-ignore
+ const { text } = await request.get('http://www.fmylife.com/random').catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error with the (http://www.fmylife.com/random) API.');
+ });
+ const $ = cheerio.load(text, { normalizeWhitespace: true });
+ const fml = $('a.article-link').first().text().trim();
+ //@ts-ignore
+ return msg.reply(fml);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Fact b/server/src/commands/fun/Fact
new file mode 100644
index 0000000..c942b45
--- /dev/null
+++ b/server/src/commands/fun/Fact
@@ -0,0 +1,67 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import request from 'node-superfetch';
+
+export default class FactFun extends Command {
+ public constructor() {
+ super('fact', {
+ aliases: ['fact', 'facts'],
+ category: 'fun',
+ description: {
+ content: 'Grabs a random fact.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const article = await this.randomWikipediaArticle();
+ const body = await request.get('https://en.wikipedia.org/w/api.php')
+ .query({
+ action: 'query',
+ prop: 'extracts',
+ format: 'json',
+ titles: article,
+ exintro: '',
+ explaintext: '',
+ redirects: '',
+ //@ts-ignore
+ formatversion: 2
+ })
+ .catch(err => {
+ console.error(err);
+ msg.reply('Woops, there was an error regarding the (http://en.wikipedia.org) API.');
+ });
+ //@ts-ignore
+ let fact = body.query.pages[0].extract;
+ if (fact.length > 200) {
+ const facts = fact.split('.');
+ fact = `${facts[0]}.`;
+ if (fact.length < 200 && facts.length > 1) fact += `${facts[1]}.`;
+ }
+ return msg.reply(fact);
+ }
+
+ public async randomWikipediaArticle() {
+ const { body } = await request.get('https://en.wikipedia.org/w/api.php')
+ .query({
+ action: 'query',
+ list: 'random',
+ //@ts-ignore
+ rnnamespace: 0,
+ //@ts-ignore
+ rnlimit: 1,
+ format: 'json',
+ //@ts-ignore
+ formatversion: 2
+ });
+ //@ts-ignore
+ if (!body.query.random[0].title) return 'Facts are hard to find sometimes.';
+ //@ts-ignore
+ return body.query.random[0].title;
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/GitHubZen.ts b/server/src/commands/fun/GitHubZen.ts
new file mode 100644
index 0000000..a1ffeee
--- /dev/null
+++ b/server/src/commands/fun/GitHubZen.ts
@@ -0,0 +1,29 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+
+export default class GitHubZenFun extends Command {
+ public constructor() {
+ super('githubzen', {
+ aliases: ['githubzen', 'github-zen'],
+ category: 'fun',
+ description: {
+ content: 'Gives you a random GitHub design philosophy.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const text = await Axios.get('https://api.github.com/zen').catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error with the (http://api.github.com) API.');
+ });
+ //@ts-ignore
+ return msg.reply(text.data);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Hello.ts b/server/src/commands/fun/Hello.ts
new file mode 100644
index 0000000..a866d39
--- /dev/null
+++ b/server/src/commands/fun/Hello.ts
@@ -0,0 +1,23 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class HelloFun extends Command {
+ public constructor() {
+ super('hello', {
+ aliases: ['hello', 'hi'],
+ category: 'fun',
+ description: {
+ content: 'Say hello!',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ return msg.reply('Hi!');
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Insult.ts b/server/src/commands/fun/Insult.ts
new file mode 100644
index 0000000..941824f
--- /dev/null
+++ b/server/src/commands/fun/Insult.ts
@@ -0,0 +1,29 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+
+export default class InsultFun extends Command {
+ public constructor() {
+ super('insult', {
+ aliases: ['insult', 'roast', 'roastwilly'],
+ category: 'fun',
+ description: {
+ content: 'Gives you a random insult.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const response = await Axios.get('https://evilinsult.com/generate_insult.php?lang=en&type=json').catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error regarding the (http://numbersapi.com) API.');
+ });
+ //@ts-ignore
+ return msg.reply(response.data.insult);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/NumberFact.ts b/server/src/commands/fun/NumberFact.ts
new file mode 100644
index 0000000..2739218
--- /dev/null
+++ b/server/src/commands/fun/NumberFact.ts
@@ -0,0 +1,41 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+
+export default class NumberFactFun extends Command {
+ public constructor() {
+ super('numberfact', {
+ aliases: ['numberfact', 'number-fact', 'numfact', 'num-fact'],
+ category: 'fun',
+ description: {
+ content: 'Grabs a facts about a specified number.',
+ usage: '[number]',
+ examples: [
+ '8'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'number',
+ type: 'integer',
+ prompt: {
+ start: 'What number would you like to get facts for? (Numeric value)',
+ retry: 'That is not a valid number, please try again.',
+ retries: 3
+ },
+ default: 'random',
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { number }): Promise<Message> {
+ const fact = await Axios.get(`http://numbersapi.com/${number}`).catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error with the (http://numbersapi.com) API.');
+ });
+ //@ts-ignore
+ return msg.reply(fact.data);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Onion.ts b/server/src/commands/fun/Onion.ts
new file mode 100644
index 0000000..e056ec7
--- /dev/null
+++ b/server/src/commands/fun/Onion.ts
@@ -0,0 +1,35 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { stripIndents } from 'common-tags';
+import RSS from 'rss-parser';
+
+export default class OnionFun extends Command {
+ public constructor() {
+ super('onion', {
+ aliases: ['onion', 'theonion', 'the-onion'],
+ category: 'fun',
+ description: {
+ content: 'Gives you a random Onion article.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const parser = new RSS();
+ const feed = await parser.parseURL('https://www.theonion.com/rss').catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error regarding the (http://numbersapi.com) API.');
+ });
+ //@ts-ignore
+ const article = feed.items[Math.floor(Math.random() * feed.items.length)];
+ return msg.reply(stripIndents`
+ ${article.title}
+ ${article.link}
+ `);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Opinion.ts b/server/src/commands/fun/Opinion.ts
new file mode 100644
index 0000000..012af7e
--- /dev/null
+++ b/server/src/commands/fun/Opinion.ts
@@ -0,0 +1,34 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class OpinionFun extends Command {
+ public constructor() {
+ super('opinion', {
+ aliases: ['opinion'],
+ category: 'fun',
+ description: {
+ content: 'Determines the bot\'s opinion on something. WARNING: do not take these seriously.',
+ usage: '[question]',
+ examples: [
+ 'avocadoes'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'question',
+ type: 'string',
+ prompt: {
+ start: 'What would you like to get an opinion on?'
+ },
+ match: 'rest'
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { question }): Promise<Message> {
+ const opinions = ['👍', '👎'];
+ return msg.reply(`*${question}* ${opinions[Math.floor(Math.random() * opinions.length)]}`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/PayRespects.ts b/server/src/commands/fun/PayRespects.ts
new file mode 100644
index 0000000..a7c9c68
--- /dev/null
+++ b/server/src/commands/fun/PayRespects.ts
@@ -0,0 +1,24 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { MessageReaction } from 'discord.js';
+
+export default class PayRespectsFun extends Command {
+ public constructor() {
+ super('payrespects', {
+ aliases: ['payrespects', 'respect', 'f'],
+ category: 'fun',
+ description: {
+ content: 'Press F to pay respects.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message | MessageReaction> {
+ return msg.channel.send('Press F to pay respects').then(m => m.react('🇫'));
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Rate.ts b/server/src/commands/fun/Rate.ts
new file mode 100644
index 0000000..d377c11
--- /dev/null
+++ b/server/src/commands/fun/Rate.ts
@@ -0,0 +1,33 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class RateFun extends Command {
+ public constructor() {
+ super('rate', {
+ aliases: ['rate'],
+ category: 'fun',
+ description: {
+ content: 'Determines the bot\'s rating on something. WARNING: do not take these seriously.',
+ usage: '[question/ item/ topic]',
+ examples: [
+ 'avocadoes'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'item',
+ type: 'string',
+ prompt: {
+ start: 'What would you like to get a rating on?'
+ },
+ match: 'rest'
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { item }): Promise<Message> {
+ return msg.reply(`I'd give *${item}* a rating of **${Math.floor(Math.random() * 10) + 1}/ 10**!`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Say.ts b/server/src/commands/fun/Say.ts
new file mode 100644
index 0000000..876f539
--- /dev/null
+++ b/server/src/commands/fun/Say.ts
@@ -0,0 +1,40 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { validIDs, owners } from '../../Config';
+
+export default class SayFun extends Command {
+ public constructor() {
+ super('say', {
+ aliases: ['say'],
+ category: 'fun',
+ description: {
+ content: 'Allows you to speak as the bot.',
+ usage: '[text]',
+ examples: [
+ 'hi this is bot'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'text',
+ type: 'string',
+ prompt: {
+ start: 'What would you like to say?'
+ },
+ match: 'rest'
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { text }): Promise<Message> {
+ console.log(text)
+ if (validIDs.includes(msg.author.id) || owners.includes(msg.author.id)) {
+ msg.delete();
+ return msg.channel.send(text);
+ }
+
+ return msg.delete();
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Spoiler.ts b/server/src/commands/fun/Spoiler.ts
new file mode 100644
index 0000000..fb061f9
--- /dev/null
+++ b/server/src/commands/fun/Spoiler.ts
@@ -0,0 +1,39 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class SpoilerFun extends Command {
+ public constructor() {
+ super('spoiler', {
+ aliases: ['spoiler'],
+ category: 'fun',
+ description: {
+ content: 'Turn every character in a specified phrase as a ||s||||p||||o||||i||||l||||e||||r||.',
+ usage: '[text]',
+ examples: [
+ 'hide this lol'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'text',
+ type: 'string',
+ prompt: {
+ start: 'What would you like to *spoil* (hide)?'
+ },
+ match: 'rest'
+ },
+ {
+ id: 'deleteinitialmessage',
+ flag: ['-delete', '-d'],
+ match: 'flag'
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { text, deleteinitialmessage }): Promise<Message> {
+ if (deleteinitialmessage) msg.delete();
+ return msg.channel.send(text.replace(/./g, '||$&||'));
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/Uwufy.ts b/server/src/commands/fun/Uwufy.ts
new file mode 100644
index 0000000..15aff06
--- /dev/null
+++ b/server/src/commands/fun/Uwufy.ts
@@ -0,0 +1,48 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class UwufyFun extends Command {
+ public constructor() {
+ super('uwufy', {
+ aliases: ['uwufy', 'owofy'],
+ category: 'fun',
+ description: {
+ content: 'Uwufys a specified string.',
+ usage: '[text]',
+ examples: [
+ 'how are you doing today?'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'text',
+ type: 'string',
+ prompt: {
+ start: 'What would you like to uwufy?'
+ },
+ match: 'rest'
+ },
+ {
+ id: 'deleteinitialmessage',
+ flag: ['-delete', '-d'],
+ match: 'flag'
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { text, deleteinitialmessage }): Promise<Message> {
+ if (deleteinitialmessage) msg.delete();
+ text.replace(/(?:l|r)/g, 'w');
+ text.replace(/(?:L|R)/g, 'W');
+ text.replace(/!+/g, ` >w< `);
+
+ const f = (Math.random() < 0.25)
+ if (f) {
+ let c = text.charAt(0);
+ text = c + '-' + text
+ }
+ return msg.channel.send(`*${text}*`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/YearFact.ts b/server/src/commands/fun/YearFact.ts
new file mode 100644
index 0000000..e6b2208
--- /dev/null
+++ b/server/src/commands/fun/YearFact.ts
@@ -0,0 +1,41 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+
+export default class YearFactFun extends Command {
+ public constructor() {
+ super('yearfact', {
+ aliases: ['yearfact'],
+ category: 'fun',
+ description: {
+ content: 'Grabs a fact about a specified year.',
+ usage: '[numeric year]',
+ examples: [
+ '1995'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'year',
+ type: 'integer',
+ prompt: {
+ start: 'What year would you like to get facts for? (Numeric value)',
+ retry: 'That is not a valid year, please try again.',
+ retries: 3
+ },
+ default: 'random',
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { year }): Promise<Message> {
+ const fact = await Axios.get(`http://numbersapi.com/${year}/year`).catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error with the (http://numbersapi.com) API.');
+ });
+ //@ts-ignore
+ return msg.reply(fact.data);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/fun/YoMomma.ts b/server/src/commands/fun/YoMomma.ts
new file mode 100644
index 0000000..f156e8d
--- /dev/null
+++ b/server/src/commands/fun/YoMomma.ts
@@ -0,0 +1,29 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+
+export default class YoMommaFun extends Command {
+ public constructor() {
+ super('yomomma', {
+ aliases: ['yomomma', 'yo-momma'],
+ category: 'fun',
+ description: {
+ content: 'Grabs a "Yo Momma" joke.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ const fact = await Axios.get('https://api.yomomma.info/').catch(err => {
+ console.error(err);
+ return msg.reply('Woops, there was an error with the (https://api.yomomma.info/) API.');
+ });
+ //@ts-ignore
+ return msg.reply(fact.data.joke);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/minigames/8Ball.ts b/server/src/commands/minigames/8Ball.ts
new file mode 100644
index 0000000..2ff3904
--- /dev/null
+++ b/server/src/commands/minigames/8Ball.ts
@@ -0,0 +1,31 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import * as EightBallResponses from '../../json/8ball.json'
+import { colour } from '../../Config';
+
+export default class EightBallMinigames extends Command {
+ public constructor() {
+ super('8ball', {
+ aliases: ['8ball', '8b', '8-ball', '8-b'],
+ category: 'minigames',
+ description: {
+ content: 'Shake the magic 8 Ball for a fortune!',
+ usage: '[question]',
+ examples: [
+ 'will I ever get married?'
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ let randomResponse = EightBallResponses.standard[Math.floor(Math.random() * EightBallResponses.standard.length)];
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor('The 8-ball says',
+ 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fd/8-Ball_Pool.svg/500px-8-Ball_Pool.svg.png')
+ .setDescription(`\`${randomResponse}\``);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/minigames/Coinflip.ts b/server/src/commands/minigames/Coinflip.ts
new file mode 100644
index 0000000..1962b00
--- /dev/null
+++ b/server/src/commands/minigames/Coinflip.ts
@@ -0,0 +1,50 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class CoinflipMinigames extends Command {
+ public constructor() {
+ super('coinflip', {
+ aliases: ['coinflip', 'flipcoin', 'coin-flip', 'flip-coin'],
+ category: 'minigames',
+ description: {
+ content: 'Flip a coin.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'type',
+ type: 'string',
+ prompt: {
+ start: 'What type of coinflip would you like?',
+ retry: 'That is not a valid type',
+ optional: true
+ }
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { type }): Promise<Message> {
+ let outcomes;
+ let quantum = false;
+ if (type === 'quantum' || type === 'q') {
+ outcomes = ['NaN', '0', 'null', 'undefined', ''];
+ quantum = true;
+ } else {
+ outcomes = ['heads!', 'tails!'];
+ }
+ const side = outcomes[Math.floor(Math.random() * outcomes.length)];
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor(`The ${quantum ? 'quantum' : ''} coin landed on`,
+ 'https://i.imgur.com/pr7JCce.png')
+ .setDescription(`\`${side}\``);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/minigames/RollDie.ts b/server/src/commands/minigames/RollDie.ts
new file mode 100644
index 0000000..8631874
--- /dev/null
+++ b/server/src/commands/minigames/RollDie.ts
@@ -0,0 +1,29 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class RollDieMinigames extends Command {
+ public constructor() {
+ super('rolldie', {
+ aliases: ['rolldie', 'rolldice', 'roll-die', 'roll-dice'],
+ category: 'minigames',
+ description: {
+ content: 'Roll a die.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ const sides = [1, 2, 3, 4, 5, 6];
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor('The die landed on', 'https://i.imgur.com/dK18NpV.png')
+ .setDescription(`\`${sides[Math.floor(Math.random() * sides.length)]}\``);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/minigames/RussianRoulette.ts b/server/src/commands/minigames/RussianRoulette.ts
new file mode 100644
index 0000000..c52a4c8
--- /dev/null
+++ b/server/src/commands/minigames/RussianRoulette.ts
@@ -0,0 +1,27 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class RussianRouletteMinigames extends Command {
+ public constructor() {
+ super('russianroulette', {
+ aliases: ['russianroulette', 'rr'],
+ category: 'minigames',
+ description: {
+ content: 'Play a round of Russian Roulette.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ const chamber = Math.floor(Math.random() * 6);
+ if (chamber === 0)
+ return msg.reply('💥 *Bang.* You lose.');
+ else
+ return msg.reply("🔫 *Click.* You survived.");
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/mod/Ban.ts b/server/src/commands/mod/Ban.ts
new file mode 100644
index 0000000..d91731e
--- /dev/null
+++ b/server/src/commands/mod/Ban.ts
@@ -0,0 +1,64 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class BanMod extends Command {
+ public constructor() {
+ super('ban', {
+ aliases: ['ban', 'banish'],
+ category: 'moderation',
+ description: {
+ content: 'Ban a specified user from the server.',
+ usage: '[user] [reason(s)]',
+ examples: [
+ '@sin#1337',
+ '@sin#1337 too cool'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ clientPermissions: ['BAN_MEMBERS'],
+ userPermissions: ['BAN_MEMBERS'],
+ args: [
+ {
+ id: 'user',
+ type: 'string',
+ prompt: {
+ start: 'Which user would you like to ban?',
+ retry: 'That doesn\' seem to be a user, please try again!'
+ }
+ },
+ {
+ id: 'reason',
+ type: 'string',
+ prompt: {
+ start: 'For what reason would you like to ban this user?',
+ optional: true
+ },
+ match: 'rest'
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { user, reason }): Promise<Message> {
+ if (msg.mentions.members.first()) user = msg.mentions.members.first().id;
+
+ if (user === this.client.user.id) return msg.channel.send('You can\'t ban me!');
+ if (!reason) reason = 'No reason has been specified.';
+ if (user === msg.author.id) return msg.channel.send('You can\'t ban yourself!');
+
+ if (msg.mentions.members.first()) {
+ user = msg.mentions.members.first();
+ await user.send(`You have been banned from **${msg.guild.name}** for the following reason(s): "**${reason}**".`);
+ // .catch(() => console.log('[ERROR] Could not send message to banned user.'));
+
+ return user.ban({ reason: `Banned by: ${msg.author.username} for the following reason(s): ${reason}.`})
+ .then(() => msg.reply(`${user.user.username} was successfully banned with the following reason(s): "**${reason}**".`))
+ .catch(err => console.error(err));
+ } else {
+ msg.guild.members.ban(user, { reason: `Banned by: ${msg.author.username} for the following reason(s): ${reason}.`})
+ .then(() => msg.reply(`User ID ${user} was successfully banned with the following reason(s): "**${reason}**".`))
+ .catch(err => console.error(err));
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/mod/Kick.ts b/server/src/commands/mod/Kick.ts
new file mode 100644
index 0000000..3295c2a
--- /dev/null
+++ b/server/src/commands/mod/Kick.ts
@@ -0,0 +1,57 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class KickMod extends Command {
+ public constructor() {
+ super('kick', {
+ aliases: ['kick'],
+ category: 'moderation',
+ description: {
+ content: 'Kick a specified user from the server.',
+ usage: '[user] [reason(s)]',
+ examples: [
+ '@sin#1337',
+ '@sin#1337 too cool'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ clientPermissions: ['KICK_MEMBERS'],
+ userPermissions: ['KICK_MEMBERS'],
+ args: [
+ {
+ id: 'user',
+ type: 'string',
+ prompt: {
+ start: 'Which user would you like to kick?',
+ retry: 'That doesn\' seem to be a user, please try again!'
+ }
+ },
+ {
+ id: 'reason',
+ type: 'string',
+ prompt: {
+ start: 'For what reason would you like to kick this user?',
+ optional: true
+ },
+ match: 'rest'
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { user, reason }): Promise<Message> {
+ if (msg.mentions.members.first()) user = msg.mentions.members.first().id;
+
+ if (user === this.client.user.id) return msg.channel.send('You can\'t kick me!');
+ if (!reason) reason = 'No reason has been specified.';
+ if (user === msg.author.id) return msg.channel.send('You can\'t kick yourself!');
+
+ await user.send(`You have been kick from **${msg.guild.name}** for the following reason(s): "**${reason}**".`);
+ // .catch(() => console.log('[ERROR] Could not send message to banned user.'));
+
+ return user.kick({ reason: `Kicked by: ${msg.author.username} for the following reason(s): ${reason}.`})
+ .then(() => msg.reply(`${user.user.username} was successfully kick with the following reason(s): "**${reason}**".`))
+ .catch(err => console.error(err));
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/mod/Prune.ts b/server/src/commands/mod/Prune.ts
new file mode 100644
index 0000000..bf56846
--- /dev/null
+++ b/server/src/commands/mod/Prune.ts
@@ -0,0 +1,39 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { TextChannel } from 'discord.js';
+
+export default class PruneMod extends Command {
+ public constructor() {
+ super('prune', {
+ aliases: ['prune', 'clear', 'purge'],
+ category: 'moderation',
+ description: {
+ content: 'Bulk delete a specified amount of message from the server.',
+ usage: '[amount]',
+ examples: [
+ '50'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ clientPermissions: ['MANAGE_MESSAGES'],
+ userPermissions: ['MANAGE_MESSAGES'],
+ args: [
+ {
+ id: 'amount',
+ type: 'integer',
+ prompt: {
+ start: 'How many messages would you like to delete?'
+ }
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { amount }): Promise<Message> {
+ if (amount <= 100) amount = 99;
+ (msg.channel as TextChannel).bulkDelete(amount, true);
+ return msg.reply('Due to Discord API limitations, the amount of messages you have specified has been rounded down to **99**. (This message will automatically be deleted in 3 seconds.)')
+ .then(m => m.delete({ timeout: 3000 }));
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/mod/Slowmode.ts b/server/src/commands/mod/Slowmode.ts
new file mode 100644
index 0000000..1d626ec
--- /dev/null
+++ b/server/src/commands/mod/Slowmode.ts
@@ -0,0 +1,61 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class SlowmodeMod extends Command {
+ public constructor() {
+ super('slowmode', {
+ aliases: ['slowmode', 'slow', 'cooldown'],
+ category: 'moderation',
+ description: {
+ content: 'Add a specified amount of slowmode to the current channel.',
+ usage: '[amount 1-120] [time slowmode should be active for]',
+ examples: [
+ '5 60'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ clientPermissions: ['MANAGE_CHANNELS'],
+ userPermissions: ['MANAGE_CHANNELS'],
+ args: [
+ {
+ id: 'amount',
+ type: 'integer',
+ prompt: {
+ start: 'What amount of slowmode would you like to add to the channel?'
+ }
+ },
+ {
+ id: 'realtime',
+ type: 'integer',
+ prompt: {
+ start: 'How long would you like the slowmode to last?',
+ optional: true
+ }
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { amount, realtime }): Promise<Message> {
+ try {
+ if (amount > 120) return msg.channel.send('Due to Discord API limitations, slow mode can only be a max of **120** seconds or less!');
+
+ // msg.channel.setRateLimitPerUser(amount);
+
+ if (realtime) {
+ let time = 60000 * realtime;
+ msg.channel.send(`Slowmode has now been set to ${amount} seconds and will end in ${realtime} minutes!`);
+ setTimeout(() => {
+ // msg.channel.setRateLimitPerUser(0);
+ return msg.channel.send('Slowmode has now been disabled!');
+ }, time);
+ } else {
+ if (amount == 0) return msg.channel.send('Slowmode has now been disabled!');
+ return msg.channel.send(`Slowmode has now been set to ${amount} seconds!`);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/mod/Unban.ts b/server/src/commands/mod/Unban.ts
new file mode 100644
index 0000000..7e66af1
--- /dev/null
+++ b/server/src/commands/mod/Unban.ts
@@ -0,0 +1,38 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class UnbanMod extends Command {
+ public constructor() {
+ super('unban', {
+ aliases: ['unban'],
+ category: 'moderation',
+ description: {
+ content: 'Unban a specified user from the server.',
+ usage: '[user id]',
+ examples: [
+ '50'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ clientPermissions: ['BAN_MEMBERS'],
+ userPermissions: ['BAN_MEMBERS'],
+ args: [
+ {
+ id: 'user',
+ type: 'integer',
+ prompt: {
+ start: 'Which user would you like to unban?',
+ retry: 'That doesn\' seem to be a user ID, please try again!'
+ }
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { user }): Promise<Message> {
+ return msg.guild.members.unban(user.toString()) // Does this really need to be returned?
+ .then(() => { return msg.reply(`User ID ${user} was successfully unbanned!`)})
+ .catch(() => { return msg.reply('Could not unban the specified user, are they banned in the first place?')});
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/nsfw/Danbooru.ts b/server/src/commands/nsfw/Danbooru.ts
new file mode 100644
index 0000000..15e08fa
--- /dev/null
+++ b/server/src/commands/nsfw/Danbooru.ts
@@ -0,0 +1,77 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+import { colour } from '../../Config';
+
+export default class DanbooruNSFW extends Command {
+ public constructor() {
+ super('danbooru', {
+ aliases: ['danbooru'],
+ category: 'nsfw',
+ description: {
+ content: 'Danbooru.',
+ usage: '[tag]',
+ examples: [
+ '',
+ 'minecraft'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'tag',
+ type: 'string',
+ prompt: {
+ start: 'What tag would you like? (Only one is supported at this time because I have no idea how this API works...)',
+ optional: true
+ }
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { tag }): Promise<Message> {
+ //@ts-ignore
+ if (!msg.channel.nsfw) return msg.reply('This is not an NSFW marked channel!');
+
+ const tags = await tag.trim().toLowerCase();
+ const denylist = ['loli', 'shota', 'cub', 'young', 'child', 'baby', 'guro', 'gore', 'vote', 'scat', 'poop', 'kid', 'kiddie', 'kiddy', 'cp', 'shit', 'turd', 'feces', 'excrement', 'excrete'];
+
+ if (tags && denylist.includes(tags)) return msg.reply('A denylisted word was used! ⛔');
+
+ const response = await Axios.get(`https://danbooru.donmai.us/posts.json?limit=200&tags=${tags}+-rating:safe`)
+ .catch(error => {
+ console.error(error);
+ return msg.reply('Woops, there was an error regarding the (https://danbooru.donmai.us) API.');
+ });
+
+ //@ts-ignore
+ const randomInt = Math.floor(Math.random() * response.data.length);
+
+ //@ts-ignore
+ if (denylist.includes(response.data[randomInt].tags)) {
+ return msg.reply('Sorry! This image had a tag that was denylisted! ⛔');
+ }
+
+ let getRating = (rating: string) => {
+ switch (rating) {
+ case 's': return 'Safe'; break;
+ case 'q': return 'Questionable'; break;
+ case 'e': return 'Explicit'; break;
+ case 'u': return 'Unrated'; break;
+ }
+ }
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setTitle(`Danbooru - ${!tags ? 'Random Image' : tags}`)
+ //@ts-ignore
+ .setDescription(`[Source](https://danbooru.donmai.us/posts/${response.data[randomInt].id})`)
+ //@ts-ignore
+ .setImage(response.data[randomInt].file_url)
+ .setTimestamp()
+ //@ts-ignore
+ .setFooter(`Score: ${response.data[randomInt].score} | Rating: ${getRating(response.data[randomInt].rating)}`, msg.author.avatarURL());
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/nsfw/Gelbooru.ts b/server/src/commands/nsfw/Gelbooru.ts
new file mode 100644
index 0000000..a858ea1
--- /dev/null
+++ b/server/src/commands/nsfw/Gelbooru.ts
@@ -0,0 +1,77 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+import { colour } from '../../Config';
+
+export default class GelbooruNSFW extends Command {
+ public constructor() {
+ super('gelbooru', {
+ aliases: ['gelbooru'],
+ category: 'nsfw',
+ description: {
+ content: 'Gelbooru.',
+ usage: '[tag]',
+ examples: [
+ '',
+ 'minecraft'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'tag',
+ type: 'string',
+ prompt: {
+ start: 'What tag would you like? (Only one is supported at this time because I have no idea how this API works...)',
+ optional: true
+ }
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { tag }): Promise<Message> {
+ //@ts-ignore
+ if (!msg.channel.nsfw) return msg.reply('This is not an NSFW marked channel!');
+
+ const tags = await tag.trim().toLowerCase();
+ const denylist = ['loli', 'shota', 'cub', 'young', 'child', 'baby', 'guro', 'gore', 'vote', 'scat', 'poop', 'kid', 'kiddie', 'kiddy', 'cp', 'shit', 'turd', 'feces', 'excrement', 'excrete'];
+
+ if (tags && denylist.includes(tags)) return msg.reply('A denylisted word was used! ⛔');
+
+ const response = await Axios.get(`https://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=100&tags=${tags}+-rating:safe&json=1`)
+ .catch(error => {
+ console.error(error);
+ return msg.reply('Woops, there was an error regarding the (https://gelbooru.com) API.');
+ });
+
+ //@ts-ignore
+ const randomInt = Math.floor(Math.random() * response.data.length);
+
+ //@ts-ignore
+ if (denylist.includes(response.data[randomInt].tags)) {
+ return msg.reply('Sorry! This image had a tag that was denylisted! ⛔');
+ }
+
+ let getRating = (rating: string) => {
+ switch (rating) {
+ case 's': return 'Safe'; break;
+ case 'q': return 'Questionable'; break;
+ case 'e': return 'Explicit'; break;
+ case 'u': return 'Unrated'; break;
+ }
+ }
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setTitle(`Gelbooru - ${!tags ? 'Random Image' : tags}`)
+ //@ts-ignore
+ .setDescription(`[Source](https://gelbooru.com/index.php?page=post&s=view&id=${response.data[randomInt].id})`)
+ //@ts-ignore
+ .setImage(response.data[randomInt].file_url)
+ .setTimestamp()
+ //@ts-ignore
+ .setFooter(`Score: ${response.data[randomInt].score} | Rating: ${getRating(response.data[randomInt].rating)}`, msg.author.avatarURL());
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/nsfw/Rule34.ts b/server/src/commands/nsfw/Rule34.ts
new file mode 100644
index 0000000..bea3af1
--- /dev/null
+++ b/server/src/commands/nsfw/Rule34.ts
@@ -0,0 +1,68 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Axios from 'axios';
+import { colour } from '../../Config';
+
+export default class Rule34NSFW extends Command {
+ public constructor() {
+ super('rule34', {
+ aliases: ['rule34', 'r34'],
+ category: 'nsfw',
+ description: {
+ content: 'If it exists, theres porn of it. If there isn\'t, there will be.',
+ usage: '[tag]',
+ examples: [
+ '',
+ 'minecraft'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'tag',
+ type: 'string',
+ prompt: {
+ start: 'What tag would you like? (Only one is supported at this time because I have no idea how this API works...)',
+ optional: true
+ }
+ }
+ ]
+ });
+ }
+
+ public async exec(msg: Message, { tag }): Promise<Message> {
+ //@ts-ignore
+ if (!msg.channel.nsfw) return msg.reply('This is not an NSFW marked channel!');
+
+ const tags = await tag.trim().toLowerCase();
+ const denylist = ['loli', 'shota', 'cub', 'young', 'child', 'baby', 'guro', 'gore', 'vote', 'scat', 'poop', 'kid', 'kiddie', 'kiddy', 'cp', 'shit', 'turd', 'feces', 'excrement', 'excrete'];
+
+ if (tags && denylist.includes(tags)) return msg.reply('A denylisted word was used! ⛔');
+
+ const response = await Axios.get(`http://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags=${tags}+-rating:safe&json=1`)
+ .catch(error => {
+ console.error(error);
+ return msg.reply('Woops, there was an error regarding the (https://rule34.xxx) API.');
+ });
+
+ //@ts-ignore
+ const randomInt = Math.floor(Math.random() * response.data.length);
+
+ //@ts-ignore
+ if (denylist.includes(response.data[randomInt].tags)) {
+ return msg.reply('Sorry! This image had a tag that was denylisted! ⛔');
+ }
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setTitle(`Rule34 - ${!tags ? 'Random Image' : tags}`)
+ //@ts-ignore
+ .setDescription(`[Source](https://rule34.xxx/index.php?page=post&s=view&id=${response.data[randomInt].id})`)
+ //@ts-ignore
+ .setImage(`https://rule34.xxx/images/${response.data[randomInt].directory}/${response.data[randomInt].image}`)
+ .setTimestamp()
+ //@ts-ignore
+ .setFooter(`Score: ${response.data[randomInt].score} | Rating: ${getRating(response.data[randomInt].rating)}`, msg.author.avatarURL());
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/owner/DM.ts b/server/src/commands/owner/DM.ts
new file mode 100644
index 0000000..e0e793a
--- /dev/null
+++ b/server/src/commands/owner/DM.ts
@@ -0,0 +1,90 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class DMOwner extends Command {
+ public constructor() {
+ super('dm', {
+ aliases: ['dm', 'pm'],
+ category: 'owner',
+ description: {
+ content: 'DM a specified user.',
+ usage: '[user id] [message]',
+ examples: [
+ '217348698294714370 hi'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'user',
+ type: 'string'
+ },
+ {
+ id: 'type',
+ type: 'string',
+ prompt: {
+ start: 'What type of DM would you like to send the specified user?',
+ retry: 'That is not a valid DM type!'
+ }
+ },
+ {
+ id: 'text',
+ type: 'string',
+ prompt: {
+ start: 'What would you like to send to the specified user?'
+ },
+ match: 'rest'
+ }
+ ],
+ ownerOnly: true
+ });
+ }
+
+ public exec(msg: Message, { user, type, text }): Promise<Message> {
+ if (type == 'embed') {
+ function uuidv4() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+ let r = Math.random() * 16 | 0, v = c == 'x' ? r : (4 & 0x3 | 0x8);
+ return v.toString(16);
+ });
+ }
+
+ const uuid = uuidv4();
+
+ user = this.client.users.resolve(user);
+ if (!user) return msg.channel.send('An incorrect user ID was provided.');
+
+ const embed = this.client.util.embed()
+ .setTitle('You received a message from the developer!')
+ .setColor(colour)
+ .setDescription(text)
+ .setFooter(`If you wish to respond, use the following command: ${this.client.commandHandler.prefix}feedback --reply ${uuid} <message>`)
+ .setTimestamp();
+
+ let attachment = (msg.attachments).array();
+ if (attachment[0]) {
+ this.client.users.resolve(user).send(embed, { files: [attachment[0].url] })
+ .then(() => { return msg.channel.send(`A DM has successfully been sent to ${user.username}.`)})
+ .catch(() => { return msg.channel.send(`Could not send a DM to ${user.username}.`)});
+ } else {
+ this.client.users.resolve(user).send(embed)
+ .then(() => { return msg.channel.send(`A DM has successfully been sent to ${user.tag}.`)})
+ .catch(() => { return msg.channel.send(`Could not send a DM to ${user.tag}.`)});
+ }
+ } else if (type === 'normal') {
+ let attachment = (msg.attachments).array();
+ if (attachment[0]) {
+ this.client.users.resolve(user).send(text, { files: [attachment[0].url] })
+ .then(() => { return msg.channel.send(`A DM has successfully been sent to ${user.username}.`)})
+ .catch(() => { return msg.channel.send(`Could not send a DM to ${user.username}.`)});
+ } else {
+ this.client.users.resolve(user).send(text)
+ .then(() => { return msg.channel.send(`A DM has successfully been sent to ${user.tag}.`)})
+ .catch(() => { return msg.channel.send(`Could not send a DM to ${user.tag}.`)});
+ }
+ }
+
+ return;
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/owner/IP.ts b/server/src/commands/owner/IP.ts
new file mode 100644
index 0000000..244c11f
--- /dev/null
+++ b/server/src/commands/owner/IP.ts
@@ -0,0 +1,27 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import request from 'node-superfetch';
+
+export default class IPOwner extends Command {
+ public constructor() {
+ super('ip', {
+ aliases: ['ip'],
+ category: 'owner',
+ description: {
+ content: 'Gives you the bot\'s IP address.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ ownerOnly: true
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message> {
+ let { body } = await request.get('https://api.ipify.org').query({ format: 'json' });
+ //@ts-ignore
+ return msg.reply(`${this.client.user.username}'s IP address is **${body.ip}**. *Which script kiddie in chat asked you to send this zzz. -Sin*`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/owner/Reload.ts b/server/src/commands/owner/Reload.ts
new file mode 100644
index 0000000..ac7bd1f
--- /dev/null
+++ b/server/src/commands/owner/Reload.ts
@@ -0,0 +1,40 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class ReloadOwner extends Command {
+ public constructor() {
+ super('reload', {
+ aliases: ['reload', 'reboot', 'reloadlistener'],
+ category: 'owner',
+ description: {
+ content: 'Reload a command.',
+ usage: '[command]',
+ examples: [
+ 'ping'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'command',
+ type: 'string',
+ prompt: {
+ start: 'What command would you like to reload?',
+ },
+ match: 'rest'
+ }
+ ],
+ ownerOnly: true
+ });
+ }
+
+ public exec(msg: Message, { command }): Promise<Message> {
+ if (msg.util.parsed.alias == 'reloadlistener') {
+ this.client.listenerHandler.reload(command);
+ return msg.channel.send(`Successfully reloaded the listener ${command}`);
+ } else {
+ this.handler.reload(command);
+ return msg.channel.send(`Successfully reloaded the command ${command}`);
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/owner/ServerCount.ts b/server/src/commands/owner/ServerCount.ts
new file mode 100644
index 0000000..5068c22
--- /dev/null
+++ b/server/src/commands/owner/ServerCount.ts
@@ -0,0 +1,24 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class ServerCountOwner extends Command {
+ public constructor() {
+ super('servercount', {
+ aliases: ['servercount', 'server-count'],
+ category: 'owner',
+ description: {
+ content: 'Check the amount of servers the bot is in.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ ownerOnly: true
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ return msg.channel.send(`${this.client.user.username} is currently in **${this.client.guilds.cache.size}** server(s).`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/owner/Status.ts b/server/src/commands/owner/Status.ts
new file mode 100644
index 0000000..8bf8dad
--- /dev/null
+++ b/server/src/commands/owner/Status.ts
@@ -0,0 +1,35 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class StatusOwner extends Command {
+ public constructor() {
+ super('status', {
+ aliases: ['status'],
+ category: 'owner',
+ description: {
+ content: 'Change the status of the bot.',
+ usage: '[status message]',
+ examples: [
+ 'hello, world!'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'status',
+ type: 'string',
+ prompt: {
+ start: 'Which status would you like to give me?',
+ },
+ match: 'rest'
+ }
+ ],
+ ownerOnly: true
+ });
+ }
+
+ public exec(msg: Message, { status }): Promise<Message> {
+ this.client.user.setActivity(status);
+ return msg.channel.send(`My status has not been set to ${status}!`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/owner/Username.ts b/server/src/commands/owner/Username.ts
new file mode 100644
index 0000000..df6d6c4
--- /dev/null
+++ b/server/src/commands/owner/Username.ts
@@ -0,0 +1,35 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class UsernameOwner extends Command {
+ public constructor() {
+ super('username', {
+ aliases: ['username'],
+ category: 'owner',
+ description: {
+ content: 'Change the username of the bot.',
+ usage: '[username]',
+ examples: [
+ 'Aki'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'username',
+ type: 'string',
+ prompt: {
+ start: 'What username would you like to give me?',
+ },
+ match: 'rest'
+ }
+ ],
+ ownerOnly: true
+ });
+ }
+
+ public exec(msg: Message, { username }): Promise<Message> {
+ this.client.user.setUsername(username);
+ return msg.channel.send(`My username has now been set to ${username}!`)
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/reaction/List.ts b/server/src/commands/reaction/List.ts
new file mode 100644
index 0000000..36c1156
--- /dev/null
+++ b/server/src/commands/reaction/List.ts
@@ -0,0 +1,46 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { oneLine } from 'common-tags';
+import { colour } from '../../Config';
+
+export default class ListReaction extends Command {
+ public constructor() {
+ super('reactionlist', {
+ aliases: ['reactionlist', 'reactionls'],
+ category: 'reactions',
+ description: {
+ content: 'Lists all current reaction roles.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ userPermissions: ['MANAGE_ROLES'],
+ channel: 'guild'
+ });
+ }
+
+ public async exec(msg: Message): Promise<Message | Message[]> {
+ const reactions = this.client.settings.cache.reactions.filter(r => r.guildID === msg.guild!.id && r.active);
+ if (!reactions.size) return msg.reply('you have no live reaction roles!');
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setTitle('Live Reaction Roles')
+ .setDescription(
+ reactions
+ .map(r => {
+ const emoji = r.emojiType === 'custom' ? this.client.emojis.cache.get(r.emoji) : r.emoji;
+ return oneLine`[\`${r.id}\`] ${emoji}
+ ${this.client.channels.cache.get(r.channelID) || '#deleted-channel'}
+ ${msg.guild!.roles.cache.get(r.roleID) || '@deleted-role'}
+ `;
+ })
+ .join('\n')
+ .substring(0, 2048),
+ );
+
+ return msg.reply({ embed });
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/reaction/New.ts b/server/src/commands/reaction/New.ts
new file mode 100644
index 0000000..c695dbc
--- /dev/null
+++ b/server/src/commands/reaction/New.ts
@@ -0,0 +1,168 @@
+import { Command } from 'discord-akairo';
+import { Message, MessageReaction, Permissions, Role, TextChannel, User } from 'discord.js';
+import { stripIndents } from 'common-tags';
+import * as nodemoji from 'node-emoji';
+import { colour } from '../../Config';
+
+export default class NewReaction extends Command {
+ public constructor() {
+ super('reactionnew', {
+ aliases: ['reactionnew', 'reactionadd'],
+ category: 'reactions',
+ description: {
+ content: 'Create a new reaction role.',
+ usage: '[type] [channel] [message id] [emoji] [role]',
+ examples: [
+ '1 #welcome 603009228180815882 🍕 @Pizza Lover'
+ ]
+ },
+ ratelimit: 3,
+ userPermissions: ['MANAGE_ROLES'],
+ clientPermissions: ['ADD_REACTIONS', 'MANAGE_ROLES', 'MANAGE_MESSAGES'],
+ channel: 'guild'
+ });
+ }
+
+ public *args(m: Message): object {
+ const type = yield {
+ type: 'number',
+ prompt: {
+ start: stripIndents`
+ What type of reaction role do you wish to create?
+
+ \`[1]\` for react to add and remove. *Classic*
+ ~~\`[2]\` for react to add only.
+ \`[3]\` for react to delete only.~~
+ `,
+ restart: stripIndents`
+ Please provide a valid number for which type of reaction role do you wish to create?
+
+ \`[1]\` Both react to add and remove. *Classic*
+ ~~\`[2]\` Only react to add.
+ \`[3]\` Only react to remove role.~~
+ `,
+ },
+ };
+
+ const channel = yield {
+ type: 'textChannel',
+ prompt: {
+ start: "What channel of the message you'd like to add this reaction role to?",
+ retry: 'Please provide a valid channel.',
+ },
+ };
+
+ const message = yield {
+ type: async (_: Message, str: string): Promise<null | Message> => {
+ if (str) {
+ try {
+ const m = await channel.messages.fetch(str);
+ if (m) return m;
+ } catch {}
+ }
+ return null;
+ },
+ prompt: {
+ start: 'What is the ID of the message you want to add that reaction role to?',
+ retry: 'Please provide a valid message ID.',
+ },
+ };
+
+ const emoji = yield {
+ type: async (_: Message, str: string): Promise<string | null> => {
+ if (str) {
+ const unicode = nodemoji.find(str);
+ if (unicode) return unicode.emoji;
+
+ const custom = this.client.emojis.cache.find(r => r.toString() === str);
+ if (custom) return custom.id;
+ return null;
+ }
+
+ const message = await m.channel.send(
+ stripIndents`Please **react** to **this** message with the emoji you wish to use?
+ If it's a custom emoji, please ensure I'm in the server that it's from!`,
+ );
+ // Please **react** to **this** message or respond with the emoji you wish to use?
+ // If it's a custom emoji, please ensure I'm in the server that it's from!
+
+ const collector = await message.awaitReactions((_: MessageReaction, u: User): boolean => m.author.id === u.id, {
+ max: 1,
+ });
+ if (!collector || collector.size !== 1) return null;
+
+ const collected = collector.first()!;
+
+ if (collected.emoji.id) {
+ const emoji = this.client.emojis.cache.find(e => e.id === collected.emoji.id);
+ if (emoji) return emoji.id;
+ return null;
+ }
+
+ return null;
+ },
+ prompt: {
+ start:
+ "Please **respond** to **this** message with the emoji you wish to use? If it's a custom emoji, please ensure I'm in the server that it's from!",
+ retry:
+ "Please **respond** to **this** message with a valid emoji. If it's a custom emoji, please ensure I'm in the server that it's from!",
+ },
+ };
+
+ const role = yield {
+ type: 'role',
+ match: 'rest',
+ prompt: {
+ start: 'What role would you like to apply when they react?',
+ retry: 'Please provide a valid role.',
+ },
+ };
+
+ return { type, channel, message, emoji, role };
+ }
+
+ public async exec(msg: Message, { type, channel, message, emoji, role }: { type: number; channel: TextChannel; message: Message; emoji: string; role: Role }): Promise<Message | Message[] | void> {
+ if (!channel.permissionsFor(this.client.user!.id)!.has(Permissions.FLAGS.ADD_REACTIONS))
+ return msg.reply(`I'm missing the permissions to react in ${channel}!`);
+
+ const reaction = await message.react(emoji).catch((err: Error) => err);
+
+ if (reaction instanceof Error)
+ return msg.reply(`an error occurred when trying to react to that message: \`${reaction}\`.`);
+
+ const id = this.makeID();
+
+ await this.client.settings.new('reaction', {
+ guildID: msg.guild!.id,
+ messageID: message.id,
+ userID: msg.author.id,
+ channelID: channel.id,
+ id,
+ emoji,
+ emojiType: emoji.length >= 3 ? 'custom' : 'unicode',
+ roleID: role.id,
+ uses: 0,
+ type,
+ });
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setTitle('New Reaction Role!')
+ .setDescription("Please make sure my highest role is above the one you're trying to assign!")
+ .addField('🔢 Reference ID', id)
+ .addField('🏠 Channel', `${channel} \`[${channel.id}]\``)
+ .addField('💬 Message', `\`${message.id}\``)
+ .addField('🍕 Emoji', emoji.length >= 3 ? `${emoji} \`[${emoji}]\`` : emoji)
+ .addField('💼 Role', `${role} \`[${role.id}]\``);
+ return msg.channel.send({ embed });
+ }
+
+ public makeID(times?: number): string {
+ const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ return 'X'
+ .repeat(times || 4)
+ .split('')
+ .map(() => possible.charAt(Math.floor(Math.random() * possible.length)))
+ .join('');
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/reaction/Remove.ts b/server/src/commands/reaction/Remove.ts
new file mode 100644
index 0000000..0a58cdd
--- /dev/null
+++ b/server/src/commands/reaction/Remove.ts
@@ -0,0 +1,61 @@
+import { Command } from 'discord-akairo';
+import { Message, TextChannel } from 'discord.js';
+import { Reaction } from '../../database/models/ReactionModel';
+
+export default class RemoveReaction extends Command {
+ public constructor() {
+ super('reactionremove', {
+ aliases: ['reactionremove', 'reactionrm', 'reactiondelete', 'reactiondel'],
+ category: 'reactions',
+ description: {
+ content: 'Removes a reaction from a message via an discriminator.',
+ usage: '[discriminator]',
+ examples: [
+ '[fV9k]'
+ ]
+ },
+ ratelimit: 3,
+ userPermissions: ['MANAGE_ROLES'],
+ channel: 'guild',
+ args: [
+ {
+ id: 'reaction',
+ type: (msg: Message, str: string): Reaction | null => {
+ const req = this.client.settings.cache.reactions.find(r => r.id === str && r.guildID === msg.guild!.id);
+ if (!req) return null;
+ return req;
+ },
+ match: 'rest',
+ prompt: {
+ start: "Please provide the unique identifier for the reaction you'd like to delete.",
+ retry:
+ "Please provide a valid identifier for the reaction role you'd like to delete. You can also delete the whole message to delete reaction roles on it.",
+ },
+ },
+ ],
+ });
+ }
+
+ public async exec(msg: Message, { reaction }: { reaction: Reaction }): Promise<Message | Message[] | void> {
+ this.client.logger.info(reaction);
+ try {
+ const chan = this.client.channels.cache.get(reaction.channelID) as TextChannel;
+ if (!chan) throw new Error("That channel doesn't exist!");
+ const message = await chan.messages.fetch(reaction.messageID);
+ if (!message) throw new Error("That message doesn't exist!");
+ await message.reactions.cache.get(reaction.emoji)!.users.remove(this.client.user!.id);
+ } catch (err) {
+ this.client.logger.error(`[ERROR in REMOVE CMD]: ${err}.`);
+ }
+
+ this.client.settings.set(
+ 'reaction',
+ { messageID: reaction.messageID },
+ {
+ active: false,
+ },
+ );
+
+ return msg.reply('successfully deleted that reaction role.');
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/server/Goodbye.ts b/server/src/commands/server/Goodbye.ts
new file mode 100644
index 0000000..bc341b8
--- /dev/null
+++ b/server/src/commands/server/Goodbye.ts
@@ -0,0 +1,81 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Goodbye from '../../database/models/GoodbyeModel';
+import mongoose from 'mongoose';
+import { mongoDBUri } from '../../Config';
+mongoose.connect(mongoDBUri, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true
+});
+
+export default class GoodbyeServer extends Command {
+ public constructor() {
+ super('goodbye', {
+ aliases: ['goodbye'],
+ category: 'server',
+ description: {
+ content: 'Allows you to set, check or delete the/ a server goodbye message.',
+ usage: '[type]',
+ examples: [
+ '',
+ 'set',
+ 'remove',
+ 'check'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ args: [
+ {
+ id: 'type',
+ type: 'string',
+ prompt: {
+ start: 'Would you like to set, check or delete the goodbye channel?',
+ retries: 3,
+ retry: 'Sorry, that was not a valid type.'
+ }
+ }
+ ],
+ userPermissions: ['MANAGE_GUILD']
+ });
+ }
+
+ public exec(msg: Message, { type }): Promise<Message> | any {
+ const goodbye = new Goodbye({
+ _id: mongoose.Types.ObjectId(),
+ username: msg.author.username,
+ userID: msg.author.id,
+ guildname: msg.guild.name,
+ guildID: msg.guild.id,
+ channelname: msg.channel,
+ channelID: msg.channel.id,
+ time: msg.createdAt
+ });
+
+ return Goodbye.findOne({ guildID: msg.guild.id }, async (error, guild) => {
+ if (error) return console.error(error);
+
+ if (guild) {
+ if (type === 'remove') {
+ await Goodbye.findOneAndDelete({ guildID: msg.guild.id });
+ return msg.channel.send('The current goodbye channel has been unset!');
+ } else if (type === 'set') {
+ //@ts-ignore
+ return msg.channel.send(`There already is a goodbye channel set! It's ${guild.channelname}`);
+ } else if (type === 'check') {
+ //@ts-ignore
+ return msg.channel.send(`The current goodbye channel is ${guild.channelname}!`);
+ }
+ } else if (!guild) {
+ if (type === 'remove') {
+ return msg.channel.send('There is no current goodbye channel set for this guild!');
+ } else if (type === 'set') {
+ await goodbye.save().catch(err => console.error(err));
+ return msg.channel.send(`The goodbye channel has been set to ${msg.channel!}`);
+ } else if (type === 'check') {
+ return msg.reply(`There is no current goodbye channel set for this guild! To set one, do ${this.client.commandHandler.prefix}goodbye set in the channel you want to set it in!`);
+ }
+ }
+ });
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/server/MemberCount.ts b/server/src/commands/server/MemberCount.ts
new file mode 100644
index 0000000..ee870fe
--- /dev/null
+++ b/server/src/commands/server/MemberCount.ts
@@ -0,0 +1,24 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class MemberCountServer extends Command {
+ public constructor() {
+ super('membercount', {
+ aliases: ['membercount', 'mc', 'member-count', 'members'],
+ category: 'bot',
+ description: {
+ content: 'Grabs the current server\'s member count.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild'
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ return msg.reply(`There are **${msg.guild.memberCount}** members in **${msg.guild.name}**.`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/server/OldestMember.ts b/server/src/commands/server/OldestMember.ts
new file mode 100644
index 0000000..6fea716
--- /dev/null
+++ b/server/src/commands/server/OldestMember.ts
@@ -0,0 +1,39 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { formatDistance, formatRelative } from 'date-fns';
+
+export default class OldestMemberServer extends Command {
+ public constructor() {
+ super('oldestmember', {
+ aliases: ['oldestmember'],
+ category: 'bot',
+ description: {
+ content: 'Grabs the current server\'s oldest member (registration time).',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild'
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ const oldest = msg.guild.members.cache.sort((member1, member2) => {
+ const timestamp1 = member1.user.createdTimestamp;
+ const timestamp2 = member2.user.createdTimestamp;
+
+ if (timestamp1 > timestamp2)
+ return 1;
+ else if (timestamp1 < timestamp2)
+ return -1;
+ return 0
+ }).first().user;
+
+ const { createdAt } = oldest;
+ const age = formatDistance(createdAt, new Date());
+ const date = formatRelative(createdAt, new Date());
+ return msg.reply(`${oldest.tag} is the oldest member in this server! Their account's age is **${age}** old (created **${date}**).`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/server/PFP.ts b/server/src/commands/server/PFP.ts
new file mode 100644
index 0000000..d5ac00b
--- /dev/null
+++ b/server/src/commands/server/PFP.ts
@@ -0,0 +1,63 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class PFPServer extends Command {
+ public constructor() {
+ super('pfp', {
+ aliases: ['pfp', 'avatar', 'avi'],
+ category: 'server',
+ description: {
+ content: 'Grabs a specified user\'s profile picture.',
+ usage: '[user]',
+ examples: [
+ '@sin#1337'
+ ]
+ },
+ ratelimit: 3,
+ args: [
+ {
+ id: 'user',
+ type: 'user',
+ prompt: {
+ start: 'Which user\'s avatar would you like to grab?',
+ retries: 3,
+ retry: 'Please choose a valid user.',
+ optional: true
+ }
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { user }): Promise<Message> {
+ let embed = this.client.util.embed()
+ .setColor(colour)
+
+ if (!user) {
+ let format = msg.author.displayAvatarURL({ dynamic: true }).substr(msg.author.displayAvatarURL({ dynamic: true}).length - 3);
+ if (format == 'gif') {
+ embed.setAuthor(msg.author.username);
+ embed.setDescription(`[gif](${msg.author.displayAvatarURL({ format: 'gif', size: 2048 })})`);
+ embed.setImage(msg.author.displayAvatarURL({ format: 'gif', size: 2048 }));
+ } else {
+ embed.setAuthor(msg.author.username);
+ embed.setDescription(`[png](${msg.author.displayAvatarURL({ format: 'png', size: 2048 })}) | [jpeg](${user.displayAvatarURL({ format: 'jpeg', size: 2048 })}) | [webp](${user.displayAvatarURL({ format: 'webp', size: 2048 })})`);
+ embed.setImage(msg.author.displayAvatarURL({ format: 'png', size: 2048 }));
+ }
+ return msg.channel.send(embed);
+ } else {
+ let format = user.displayAvatarURL({ dynamic: true }).substr(user.displayAvatarURL({ dynamic: true}).length - 3);
+ if (format == 'gif') {
+ embed.setAuthor(user.username);
+ embed.setDescription(`[gif](${user.displayAvatarURL({ format: 'gif', size: 2048 })})`);
+ embed.setImage(user.displayAvatarURL({ format: 'gif', size: 2048 }));
+ } else {
+ embed.setAuthor(user.username);
+ embed.setDescription(`[png](${user.displayAvatarURL({ format: 'png', size: 2048 })}) | [jpeg](${user.displayAvatarURL({ format: 'jpeg', size: 2048 })}) | [webp](${user.displayAvatarURL({ format: 'webp', size: 2048 })})`);
+ embed.setImage(user.displayAvatarURL({ format: 'png', size: 2048 }));
+ }
+ return msg.channel.send(embed);
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/server/Poll.ts b/server/src/commands/server/Poll.ts
new file mode 100644
index 0000000..54b617f
--- /dev/null
+++ b/server/src/commands/server/Poll.ts
@@ -0,0 +1,42 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class PollServer extends Command {
+ public constructor() {
+ super('poll', {
+ aliases: ['poll', 'vote'],
+ category: 'server',
+ description: {
+ content: 'Make a poll.',
+ usage: '[poll contents]',
+ examples: [
+ 'Is Ice Cream good?'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ args: [
+ {
+ id: 'poll',
+ type: 'string',
+ prompt: {
+ start: 'What is the poll about?'
+ }
+ }
+ ],
+ userPermissions: ['MANAGE_MESSAGES']
+ });
+ }
+
+ public exec(msg: Message, { poll }): Promise<Message | void> {
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setFooter('React to vote.')
+ .setDescription(poll)
+ .setTitle(`Poll created by ${msg.author.username}`);
+ return msg.channel.send(embed).then(m => {
+ m.react('✅'); m.react('❎');
+ });
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/server/RandomMember.ts b/server/src/commands/server/RandomMember.ts
new file mode 100644
index 0000000..55b5cec
--- /dev/null
+++ b/server/src/commands/server/RandomMember.ts
@@ -0,0 +1,23 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class RandomMemberServer extends Command {
+ public constructor() {
+ super('randommember', {
+ aliases: ['randommember', 'randomuser', 'random-member', 'random-user', 'someone', '@someone'],
+ category: 'server',
+ description: {
+ content: 'Gets a random member from the current server.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ return msg.reply(`I choose ${msg.guild.members.cache.random().displayName}!`);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/server/Server.ts b/server/src/commands/server/Server.ts
new file mode 100644
index 0000000..7caf899
--- /dev/null
+++ b/server/src/commands/server/Server.ts
@@ -0,0 +1,39 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class ServerServer extends Command {
+ public constructor() {
+ super('server', {
+ aliases: ['server', 'serverinfo', 'server-info', 'serverstats', 'server-stats'],
+ category: 'server',
+ description: {
+ content: 'Gives you information about the current server.',
+ usage: '',
+ examples: [
+ ''
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild'
+ });
+ }
+
+ public exec(msg: Message): Promise<Message> {
+ const online = msg.guild.members.cache.filter(m => m.presence.status === 'online').size;
+
+ const embed = this.client.util.embed()
+ .setAuthor(`${msg.guild.name} - ${msg.guild.id}`, `${msg.guild.iconURL()}`, `https://discordapp.com/channels/${msg.guild.id}/${msg.guild.id}`)
+ .setDescription(`Heres's all the information on \`${msg.guild.name}\``)
+ .setThumbnail(`${msg.guild.iconURL()}`)
+ .addField('Owner', `\`${msg.guild.owner.user.tag}\``)
+ .addField(`Members [${msg.guild.memberCount}]`, `${online} members are online.`, true)
+ .addField(`Region`, `${msg.guild.region}`, true)
+ .addField(`Text Channels`, `${msg.guild.channels.cache.filter(c => c.type === 'text').size}`, true)
+ .addField(`Voice Channels`, `${msg.guild.channels.cache.filter(c => c.type === 'voice').size}`, true)
+ .addField('Guild Created', `${msg.guild.createdAt}`, false)
+ .addField(`${this.client.user.username} joined`, `${msg.guild.members.cache.get(this.client.user.id).joinedAt}`)
+ .setColor(colour);
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/server/User.ts b/server/src/commands/server/User.ts
new file mode 100644
index 0000000..26a24e1
--- /dev/null
+++ b/server/src/commands/server/User.ts
@@ -0,0 +1,75 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class UserServer extends Command {
+ public constructor() {
+ super('user', {
+ aliases: ['user', 'userinfo', 'user-info'],
+ category: 'bot',
+ description: {
+ content: 'Grabs information on a specified user.',
+ usage: '[user]',
+ examples: [
+ '@sin#1337'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ args: [
+ {
+ id: 'user',
+ type: 'user',
+ prompt: {
+ optional: true
+ }
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { user }): Promise<Message> {
+ if (!user) user == msg.author;
+
+ const member = msg.guild.member(user);
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setAuthor(`${user.tag} (${user.id})`, user.displayAvatarURL())
+ .addField('Highest rank HEX colour', member ? member.displayHexColor : 'No rank colour', true)
+ .addField('Joined guild at', member ? member.joinedAt : 'Not in this guild', true)
+ .addField('Date when account was created', user.createdAt, true)
+ .setTimestamp();
+ // embed.addField('-', '-');
+
+ // User status
+ if (user.presence.activities[0]) {
+ embed.addField('Presence', user.presence.activities[0], true);
+ if (user.presence.activities[0].details) embed.addField('Details', user.presence.activities[0].details, true);
+ if (user.presence.activities[0].state) embed.addField('State', user.presence.activities[0].state, true);
+ }
+ // Bot status
+ if (user.bot) embed.addField('Is a bot?', '✅', true);
+
+ // Show user locale
+ if (user.locale) embed.addField('Locale Settings', user.locale, true);
+
+ // Show user platform
+ if (user.presence.clientStatus && !user.bot) {
+ // embed.addField('-', '-');
+ if (user.presence.clientStatus.mobile) embed.addField('Using Discord on', '📱 ' + user.presence.clientStatus.mobile, true);
+ if (user.presence.clientStatus.desktop) embed.addField('Using Discord on', '💻 ' + user.presence.clientStatus.desktop, true);
+ if (user.presence.clientStatus.web) embed.addField('Using Discord on', '☁️ ' + user.presence.clientStatus.web, true);
+ }
+
+ if (member) {
+ // Boosting since
+ if (member.premiumSince) embed.addField('Boosting this guild since', member.premiumSince, true);
+ // Nickname
+ if (member.nickname) embed.addField('Nickname', member.nickname, true);
+ // Roles
+ if (member.roles) embed.addField('Roles', `${member.roles.cache.array().join(', ')}`);
+ }
+
+ return msg.channel.send(embed);
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/server/Welcome.ts b/server/src/commands/server/Welcome.ts
new file mode 100644
index 0000000..6c116d9
--- /dev/null
+++ b/server/src/commands/server/Welcome.ts
@@ -0,0 +1,81 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import Welcome from '../../database/models/WelcomeModel';
+import mongoose from 'mongoose';
+import { mongoDBUri } from '../../Config';
+mongoose.connect(mongoDBUri, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true
+});
+
+export default class WelcomeServer extends Command {
+ public constructor() {
+ super('welcome', {
+ aliases: ['welcome'],
+ category: 'server',
+ description: {
+ content: 'Allows you to set, check or delete the/ a server welcome message.',
+ usage: '[type]',
+ examples: [
+ '',
+ 'set',
+ 'remove',
+ 'check'
+ ]
+ },
+ ratelimit: 3,
+ channel: 'guild',
+ args: [
+ {
+ id: 'type',
+ type: 'string',
+ prompt: {
+ start: 'Would you like to set, check or delete the welcome channel?',
+ retries: 3,
+ retry: 'Sorry, that was not a valid type.'
+ }
+ }
+ ],
+ userPermissions: ['MANAGE_GUILD']
+ });
+ }
+
+ public exec(msg: Message, { type }): Promise<Message> | any {
+ const welcome = new Welcome({
+ _id: mongoose.Types.ObjectId(),
+ username: msg.author.username,
+ userID: msg.author.id,
+ guildname: msg.guild.name,
+ guildID: msg.guild.id,
+ channelname: msg.channel,
+ channelID: msg.channel.id,
+ time: msg.createdAt
+ });
+
+ return Welcome.findOne({ guildID: msg.guild.id }, async (error, guild) => {
+ if (error) return console.error(error);
+
+ if (guild) {
+ if (type === 'remove') {
+ await Welcome.findOneAndDelete({ guildID: msg.guild.id });
+ return msg.channel.send('The current welcome channel has been unset!');
+ } else if (type === 'set') {
+ //@ts-ignore
+ return msg.channel.send(`There already is a welcome channel set! It's ${guild.channelname}`);
+ } else if (type === 'check') {
+ //@ts-ignore
+ return msg.channel.send(`The current welcome channel is ${guild.channelname}!`);
+ }
+ } else if (!guild) {
+ if (type === 'remove') {
+ return msg.channel.send('There is no current welcome channel set for this guild!');
+ } else if (type === 'set') {
+ await welcome.save().catch(err => console.error(err));
+ return msg.channel.send(`The welcome channel has been set to ${msg.channel!}`);
+ } else if (type === 'check') {
+ return msg.reply(`There is no current welcome channel set for this guild! To set one, do ${this.client.commandHandler.prefix}welcome set in the channel you want to set it in!`);
+ }
+ }
+ });
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/util/Categories.ts b/server/src/commands/util/Categories.ts
new file mode 100644
index 0000000..7b07027
--- /dev/null
+++ b/server/src/commands/util/Categories.ts
@@ -0,0 +1,155 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class CategoriesUtil extends Command {
+ public constructor() {
+ super('categories', {
+ aliases: ['categories', 'category'],
+ category: 'utility',
+ description: {
+ content: 'Displays a list of categories or lists all commands in a specified category.',
+ usage: '[command]',
+ examples: [
+ '',
+ 'ping'
+ ]
+ },
+ ratelimit: 3,
+ clientPermissions: ['EMBED_LINKS'],
+ args: [
+ {
+ id: 'category',
+ type: 'string',
+ prompt: {
+ start: 'Which category do you need help with?',
+ retry: 'Please provide a valid category.',
+ optional: true
+ },
+ match: 'rest'
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { category }): Promise<Message> {
+ if (!category) return this.execCategoryList(msg);
+
+ let categories = [];
+ for (const category of this.handler.categories.values()) {
+ if (!categories.includes(category.id)) categories.push(category.id);
+ }
+ if (categories.includes(category)) return this.execCommandList(msg, category);
+ }
+
+ public async execCommandList(message, categorii): Promise<Message> {
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .addField('Command List', [
+ `This is a list of commands in the **${categorii}** category.`,
+ `To view details for a specific command, do \`${this.client.commandHandler.prefix}help <command>\`.`
+ ]);
+
+ for (const category of this.handler.categories.values()) {
+ let title
+ if (message.channel.type != 'dm' && message.guild.id == '663964105983393793') {
+ title = {
+ general: '📝\u2000General',
+ fun: '🎉\u2000Fun',
+ minigames: '🕹\u2000Minigames (WIP)',
+ images: '🖼\u2000Images',
+ utility: '🔩\u2000Utility',
+ moderation: '⚡\u2000Moderation',
+ owner: '🛠️\u2000Owner',
+ voice: '🎧\u2000Voice',
+ bot: '🤖\u2000Bot',
+ server: '🖥\u2000Server',
+ anime: '🎀\u2000Anime',
+ animals: '🐛\u2000Animals',
+ emma: '🔥\u2000Emma',
+ nsfw: '🔞\u2000NSFW',
+ reactions: '😮\u2000Reactions'
+ }[category.id];
+ } else {
+ title = {
+ general: '📝\u2000General',
+ fun: '🎉\u2000Fun',
+ minigames: '🕹\u2000Minigames (WIP)',
+ images: '🖼\u2000Images',
+ utility: '🔩\u2000Utility',
+ moderation: '⚡\u2000Moderation',
+ owner: '🛠️\u2000Owner',
+ voice: '🎧\u2000Voice',
+ bot: '🤖\u2000Bot',
+ server: '🖥\u2000Server',
+ anime: '🎀\u2000Anime',
+ animals: '🐛\u2000Animals',
+ nsfw: '🔞\u2000NSFW',
+ reactions: '😮\u2000Reactions'
+ }[category.id];
+ }
+
+ if (title && (category.id == categorii)) embed.addField(title, `${category.map(cmd => '`' + cmd.aliases[0] + '` - ' + cmd.description.content).join('\n')}`); // .join('`\n`')
+ }
+
+ return message.channel.send({ embed });
+ }
+
+ public async execCategoryList(message): Promise<Message> {
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .addField('Category List', [
+ `To view details for a specific category, do \`${this.client.commandHandler.prefix}category <category>\`.`
+ ]);
+
+ let count;
+ for (const category of this.handler.categories.values()) {
+ count++;
+ let title
+ if (message.channel.type != 'dm' && message.guild.id == '663964105983393793') {
+ title = {
+ general: '📝\u2000General',
+ fun: '🎉\u2000Fun',
+ minigames: '🕹\u2000Minigames (WIP)',
+ images: '🖼\u2000Images',
+ utility: '🔩\u2000Utility',
+ moderation: '⚡\u2000Moderation',
+ owner: '🛠️\u2000Owner',
+ voice: '🎧\u2000Voice',
+ bot: '🤖\u2000Bot',
+ server: '🖥\u2000Server',
+ anime: '🎀\u2000Anime',
+ animals: '🐛\u2000Animals',
+ emma: '🔥\u2000Emma',
+ nsfw: '🔞\u2000NSFW',
+ reactions: '😮\u2000Reactions'
+ }[category.id];
+ } else {
+ title = {
+ general: '📝\u2000General',
+ fun: '🎉\u2000Fun',
+ minigames: '🕹\u2000Minigames (WIP)',
+ images: '🖼\u2000Images',
+ utility: '🔩\u2000Utility',
+ moderation: '⚡\u2000Moderation',
+ owner: '🛠️\u2000Owner',
+ voice: '🎧\u2000Voice',
+ bot: '🤖\u2000Bot',
+ server: '🖥\u2000Server',
+ anime: '🎀\u2000Anime',
+ animals: '🐛\u2000Animals',
+ nsfw: '🔞\u2000NSFW',
+ reactions: '😮\u2000Reactions'
+ }[category.id];
+ }
+
+ if (title)
+ if (count % 3 == 0)
+ embed.addField(`${title}`, `${category.size} commands`, false);
+ else
+ embed.addField(`${title}`, `${category.size} commands`, true);
+ }
+
+ return message.channel.send({ embed });
+ }
+} \ No newline at end of file
diff --git a/server/src/commands/util/Help.ts b/server/src/commands/util/Help.ts
new file mode 100644
index 0000000..76d2bf6
--- /dev/null
+++ b/server/src/commands/util/Help.ts
@@ -0,0 +1,100 @@
+import { Command } from 'discord-akairo';
+import { Message } from 'discord.js';
+import { colour } from '../../Config';
+
+export default class HelpUtil extends Command {
+ public constructor() {
+ super('help', {
+ aliases: ['help'],
+ category: 'utility',
+ description: {
+ content: 'List help features or get information on a specified command.',
+ usage: '[command]',
+ examples: [
+ '',
+ '8ball'
+ ]
+ },
+ ratelimit: 3,
+ clientPermissions: ['EMBED_LINKS'],
+ args: [
+ {
+ id: 'command',
+ type: 'commandAlias',
+ prompt: {
+ start: 'Which command do you need help with?',
+ retry: 'Please provide a valid command.',
+ optional: true
+ },
+ match: 'rest'
+ }
+ ]
+ });
+ }
+
+ public exec(msg: Message, { command }): Promise<void | Message> {
+ if (!command) {
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .addFields([
+ {
+ name: 'Online Command List',
+ value: '*Coming soon!*'
+ },
+ {
+ name: 'Specific Command Help',
+ value: `${this.handler.prefix}help <command>`
+ },
+ {
+ name: 'List of all public categories',
+ value: `${this.handler.prefix}categories`
+ }
+ ]);
+ return msg.channel.send({ embed });
+ }
+
+ const description = Object.assign({
+ content: 'No description available.',
+ usage: '',
+ examples: [],
+ fields: []
+ }, command.description);
+
+ const embed = this.client.util.embed()
+ .setColor(colour)
+ .setTitle(`\`${this.client.commandHandler.prefix}${command.aliases[0]} ${description.usage}\``)
+ .addField('Description', description.content);
+
+ for (const field of description.fields) embed.addField(field.name, field.value);
+
+ if (command.aliases.length > 1) {
+ embed.addField('Aliases', `\`${command.aliases.join('`, `')}\``, true);
+ }
+
+ if (command.description.examples.length >= 1) {
+ embed.addField('Examples', `\`${this.client.commandHandler.prefix}${command.aliases[0]} ${command.description.examples.join(`, ${this.client.commandHandler.prefix}${command.aliases[0]} `)}\``, true);
+ }
+
+ if (command.userPermissions) {
+ embed.addField('User permission', `\`${command.userPermissions.join('` `')}\``, true);
+ }
+
+ if (command.clientPermissions) {
+ embed.addField('Bot permission', `\`${command.clientPermissions.join('` `')}\``, true);
+ }
+
+ if (command.contentParser.flagWords.length) {
+ embed.addField('Command flags', `\`${command.contentParser.flagWords.join('` `')}\``, true);
+ }
+
+ if (command.contentParser.optionFlagWords.length) {
+ embed.addField('Command options flags', `\`${command.contentParser.optionFlagWords.join('` `')}\``, true);
+ }
+
+ if (msg.channel.type === 'dm') return msg.author.send({ embed });
+ return msg.reply('sending you a DM with information...').then(async m => {
+ await msg.author.send({ embed });
+ m.edit('I\'ve send you a DM with information!');
+ });
+ }
+} \ No newline at end of file
diff --git a/server/src/database/index.ts b/server/src/database/index.ts
new file mode 100644
index 0000000..dc10553
--- /dev/null
+++ b/server/src/database/index.ts
@@ -0,0 +1,5 @@
+import ReactionGuildModel from './models/ReactionGuildModel';
+import ReactionModel from './models/ReactionModel';
+import SettingsProvider from './structures/SettingsProvider';
+
+export { SettingsProvider }; \ No newline at end of file
diff --git a/server/src/database/models/DarlingModel.ts b/server/src/database/models/DarlingModel.ts
new file mode 100644
index 0000000..3f98037
--- /dev/null
+++ b/server/src/database/models/DarlingModel.ts
@@ -0,0 +1,10 @@
+import mongoose from 'mongoose';
+const darlingSchema = new mongoose.Schema({
+ _id: mongoose.Schema.Types.ObjectId,
+ username: String,
+ userID: String,
+ guildname: String,
+ guildID: String,
+ time: String
+});
+export = mongoose.model('Darling', darlingSchema); \ No newline at end of file
diff --git a/server/src/database/models/FanArtModel.ts b/server/src/database/models/FanArtModel.ts
new file mode 100644
index 0000000..c2d983e
--- /dev/null
+++ b/server/src/database/models/FanArtModel.ts
@@ -0,0 +1,12 @@
+import mongoose from 'mongoose';
+const fanArtSchema = new mongoose.Schema({
+ _id: mongoose.Schema.Types.ObjectId,
+ username: String,
+ userID: String,
+ guildname: String,
+ guildID: String,
+ channelname: String,
+ channelID: String,
+ time: String
+});
+export = mongoose.model('FanArt', fanArtSchema); \ No newline at end of file
diff --git a/server/src/database/models/GoodbyeModel.ts b/server/src/database/models/GoodbyeModel.ts
new file mode 100644
index 0000000..04cc7c4
--- /dev/null
+++ b/server/src/database/models/GoodbyeModel.ts
@@ -0,0 +1,12 @@
+import mongoose from 'mongoose';
+const goodbyeSchema = new mongoose.Schema({
+ _id: mongoose.Schema.Types.ObjectId,
+ username: String,
+ userID: String,
+ guildname: String,
+ guildID: String,
+ channelname: String,
+ channelID: String,
+ time: String
+});
+export = mongoose.model('Goodbye', goodbyeSchema); \ No newline at end of file
diff --git a/server/src/database/models/ReactionGuildModel.ts b/server/src/database/models/ReactionGuildModel.ts
new file mode 100644
index 0000000..6389b56
--- /dev/null
+++ b/server/src/database/models/ReactionGuildModel.ts
@@ -0,0 +1,16 @@
+import { Document, Schema, model } from 'mongoose';
+
+export interface Guild extends Document {
+ id: string;
+ prefix: string;
+ premium: boolean;
+ expiresAt: Date;
+}
+
+const Guild: Schema = new Schema({
+ id: String,
+ prefix: String,
+ premium: Boolean,
+ expiresAt: Date
+}, { strict: false });
+export default model<Guild>('Guild', Guild); \ No newline at end of file
diff --git a/server/src/database/models/ReactionModel.ts b/server/src/database/models/ReactionModel.ts
new file mode 100644
index 0000000..509dadd
--- /dev/null
+++ b/server/src/database/models/ReactionModel.ts
@@ -0,0 +1,36 @@
+import { Document, Schema, model } from 'mongoose';
+
+export interface Reaction extends Document {
+ guildID: string;
+ messageID: string;
+ channelID: string;
+ userID: string;
+ id: string;
+ emoji: string;
+ emojiType: string;
+ roleID: string;
+ uses: number;
+ expiresAt?: Date;
+ type: number;
+ active: boolean;
+}
+
+const Reaction: Schema = new Schema({
+ guildID: String,
+ messageID: String,
+ channelID: String,
+ userID: String,
+ id: String,
+ emoji: String,
+ emojiType: String,
+ roleID: String,
+ uses: Number,
+ expiresAt: Date,
+ type: Number,
+ active: {
+ type: Boolean,
+ default: true,
+ },
+}, { strict: false });
+
+export default model<Reaction>('Reaction', Reaction);
diff --git a/server/src/database/models/WelcomeModel.ts b/server/src/database/models/WelcomeModel.ts
new file mode 100644
index 0000000..2a26a6f
--- /dev/null
+++ b/server/src/database/models/WelcomeModel.ts
@@ -0,0 +1,12 @@
+import mongoose from 'mongoose';
+const welcomeSchema = new mongoose.Schema({
+ _id: mongoose.Schema.Types.ObjectId,
+ username: String,
+ userID: String,
+ guildname: String,
+ guildID: String,
+ channelname: String,
+ channelID: String,
+ time: String
+});
+export = mongoose.model('Welcome', welcomeSchema); \ No newline at end of file
diff --git a/server/src/database/structures/SettingsProvider.ts b/server/src/database/structures/SettingsProvider.ts
new file mode 100644
index 0000000..0a2325a
--- /dev/null
+++ b/server/src/database/structures/SettingsProvider.ts
@@ -0,0 +1,212 @@
+import { Collection } from 'discord.js';
+import { connect, Model, connection, Connection } from 'mongoose';
+import { Logger } from 'winston';
+import ReactionModel, { Reaction } from '../models/ReactionModel';
+import GuildModel, { Guild } from '../models/ReactionGuildModel';
+import { MONGO_EVENTS } from '../utils/Constants'
+import BotClient from '../../client/BotClient';
+import { mongoDBUri } from '../../Config';
+
+let i = 0;
+
+/**
+ * The key, model and cached collection of a database model.
+ * @interface
+ */
+interface Combo {
+ key: string;
+ model: Model<any>;
+ cache: Collection<string, any>;
+}
+
+/**
+ * The Settings Provider that handles all database reads and rights.
+ * @private
+ */
+export default class SettingsProvider {
+ protected readonly client: BotClient;
+
+ protected readonly guilds: Collection<string, Guild> = new Collection();
+ protected readonly reactions: Collection<string, Reaction> = new Collection();
+
+ protected readonly GuildModel = GuildModel;
+ protected readonly ReactionModel = ReactionModel;
+
+ /**
+ *
+ * @param {GiveawayClient} client - The extended Akairo Client
+ */
+ public constructor(client: BotClient) {
+ this.client = client;
+ }
+
+ /**
+ * Retuns all the collection caches.
+ * @returns {Object}
+ */
+ public get cache() {
+ return {
+ guilds: this.guilds,
+ reactions: this.reactions,
+ };
+ }
+
+ /**
+ * Returns the database combos
+ * @returns {Combo[]}
+ */
+ public get combos(): Combo[] {
+ return [
+ {
+ key: 'guild',
+ model: this.GuildModel,
+ cache: this.guilds,
+ },
+ {
+ key: 'reaction',
+ model: this.ReactionModel,
+ cache: this.reactions,
+ },
+ ];
+ }
+
+ /**
+ * Creates a new database document with the provided collection name and data.
+ * @param {string} type - The collection name
+ * @param {object} data - The data for the new document
+ * @returns {Docuement}
+ */
+ public async new(type: 'guild', data: Partial<Guild>): Promise<Guild>;
+ public async new(type: 'reaction', data: Partial<Reaction>): Promise<Reaction>;
+ public async new(type: string, data: object): Promise<object> {
+ const combo = this.combos.find(c => c.key === type);
+ if (combo) {
+ const document = new combo.model(data);
+ await document.save();
+ this.client.logger.data(`[DATABASE] Made new ${combo.model.modelName} document with ID of ${document._id}.`);
+ combo.cache.set(document.id, document);
+ return document;
+ }
+ throw Error(`"${type}" is not a valid model key.`);
+ }
+
+ /**
+ * Updates the a database document's data.
+ * @param {Types} type - The collection name
+ * @param {object} key - The search paramaters for the document
+ * @param {object} data - The data you wish to overwrite in the update
+ * @returns {Promise<Faction | Guild | null>}
+ */
+ public async set(type: 'guild', key: Partial<Guild>, data: Partial<Guild>): Promise<Guild | null>;
+ public async set(type: 'reaction', key: Partial<Reaction>, data: Partial<Reaction>): Promise<Reaction | null>;
+ public async set(type: string, key: object, data: object): Promise<object | null> {
+ const combo = this.combos.find(c => c.key === type);
+ if (combo) {
+ const document = await combo.model.findOneAndUpdate(key, { $set: data }, { new: true });
+ if (document) {
+ this.client.logger.verbose(`[DATABASE] Edited ${combo.model.modelName} document with ID of ${document._id}.`);
+ combo.cache.set(document.id, document);
+ return document;
+ }
+ return null;
+ }
+ throw Error(`"${type}" is not a valid model key.`);
+ }
+
+ /**
+ * Removes a database document.
+ * @param {Types} type - The collection name
+ * @param {object} data - The search paramaters for the document
+ * @returns {Promise<Faction | Guild | null>>} The document that was removed, if any.
+ */
+ public async remove(type: 'guild', data: Partial<Guild>): Promise<Guild | null>;
+ public async remove(type: 'user', data: Partial<Reaction>): Promise<Reaction | null>;
+ public async remove(type: string, data: object): Promise<object | null> {
+ const combo = this.combos.find(c => c.key === type);
+ if (combo) {
+ const document = await combo.model.findOneAndRemove(data);
+ if (document) {
+ this.client.logger.verbose(`[DATABASE] Edited ${combo.model.modelName} document with ID of ${document._id}.`);
+ combo.cache.delete(document.id);
+ return document;
+ }
+ return null;
+ }
+ throw Error(`"${type}" is not a valid model key.`);
+ }
+
+ /**
+ * Caching all database documents.
+ * @returns {number} The amount of documents cached total.
+ * @private
+ */
+ private async _cacheAll(): Promise<number> {
+ for (const combo of this.combos) await this._cache(combo);
+ return i;
+ }
+
+ /**
+ * Caching each collection's documents.
+ * @param {Combo} combo - The combo name
+ * @returns {number} The amount of documents cached from that collection.
+ * @private
+ */
+ private async _cache(combo: Combo): Promise<any> {
+ const items = await combo.model.find();
+ for (const i of items) combo.cache.set(i.id, i);
+ this.client.logger.verbose(
+ `[DATABASE]: Cached ${items.length.toLocaleString('en-US')} items from ${combo.model.modelName}.`,
+ );
+ return (i += items.length);
+ }
+
+ /**
+ * Connect to the database
+ * @param {string} url - the mongodb uri
+ * @returns {Promise<number | Logger>} Returns a
+ */
+ private async _connect(url: string | undefined): Promise<Logger | number> {
+ if (url) {
+ const start = Date.now();
+ try {
+ await connect(url, {
+ useCreateIndex: true,
+ useNewUrlParser: true,
+ useFindAndModify: false,
+ useUnifiedTopology: true,
+ });
+ } catch (err) {
+ this.client.logger.error(`[DATABASE] Error when connecting to MongoDB:\n${err.stack}`);
+ process.exit(1);
+ }
+ return this.client.logger.verbose(`[DATABASE] Connected to MongoDB in ${Date.now() - start}ms.`);
+ }
+ this.client.logger.error('[DATABASE] No MongoDB url provided!');
+ return process.exit(1);
+ }
+
+ /**
+ * Adds all the listeners to the mongo connection.
+ * @param connection - The mongoose connection
+ * @returns {void}
+ * @private
+ */
+ private _addListeners(connection: Connection): void {
+ for (const [event, msg] of Object.entries(MONGO_EVENTS)) {
+ connection.on(event, () => this.client.logger.data(`[DATABASE]: ${msg}`));
+ }
+ }
+
+ /**
+ * Starts the Settings Provider
+ * @returns {SettingsProvider}
+ */
+ public async init(): Promise<this> {
+ this._addListeners(connection);
+ await this._connect(mongoDBUri);
+ this.client.logger.verbose(`[DATABASE]: Now caching ${this.combos.length} schema documents.`);
+ await this._cacheAll();
+ this.client.logger.info(`[DATABASE] [LAUNCHED] Successfully connected and cached ${i} documents.`);
+ return this;
+ }
+}
diff --git a/server/src/database/utils/Constants.ts b/server/src/database/utils/Constants.ts
new file mode 100644
index 0000000..911a28a
--- /dev/null
+++ b/server/src/database/utils/Constants.ts
@@ -0,0 +1,8 @@
+export const MONGO_EVENTS = {
+ connecting: 'Connecting to MongoDB...',
+ connected: 'Successfully connected to MongoDB.',
+ disconnecting: 'Disconnecting from MongoDB...',
+ disconnected: 'Disconnected from MongoDB...',
+ close: 'MongoDB connection closed.',
+ reconnected: 'Successfully reconnected to MongoDB.',
+} as { [key: string]: string };
diff --git a/server/src/inhibitors/sendMessages.ts b/server/src/inhibitors/sendMessages.ts
new file mode 100644
index 0000000..51e1fe3
--- /dev/null
+++ b/server/src/inhibitors/sendMessages.ts
@@ -0,0 +1,18 @@
+import { Inhibitor } from 'discord-akairo';
+import { Message, TextChannel } from 'discord.js';
+
+export default class SendMessagesInhibtor extends Inhibitor {
+ public constructor() {
+ super('sendMessages', {
+ reason: 'sendMessages',
+ });
+ }
+
+ // @ts-ignore
+ public exec(msg: Message): boolean {
+ if (!msg.guild) return false;
+ if (msg.channel instanceof TextChannel) {
+ return !msg.channel.permissionsFor(this.client.user!)!.has('SEND_MESSAGES');
+ }
+ }
+}
diff --git a/server/src/json/8ball.json b/server/src/json/8ball.json
new file mode 100644
index 0000000..b8c3856
--- /dev/null
+++ b/server/src/json/8ball.json
@@ -0,0 +1,37 @@
+{
+ "standard": [
+ "yes~ uwu",
+ "no.",
+ "yes!",
+ "no!",
+ "what, no.",
+ "yes.",
+ "maybe.",
+ "perhaps.",
+ "try again.",
+ "I\"m not sure."
+ ],
+ "ditf": [
+ "Maybe, darling.",
+ "Certainly not, darling.",
+ "I hope so, darling.",
+ "Not in our wildest dreams, darling.",
+ "There is a good chance, darling.",
+ "Quite likely, darling.",
+ "I think so, darling.",
+ "I hope not, darling.",
+ "I hope so, darling.",
+ "Never!",
+ "Ahaha! Really?!? XD",
+ "Hell, yes.",
+ "Hell to the no.",
+ "The future is bleak, darling",
+ "The future is uncertain, darling",
+ "I would rather not say, darling",
+ "Who cares?",
+ "Possibly, darling",
+ "Never, ever, ever... ever.",
+ "There is a small chance, darling.",
+ "Yes, darling!"
+ ]
+} \ No newline at end of file
diff --git a/server/src/listeners/client/ReadyListener.ts b/server/src/listeners/client/ReadyListener.ts
new file mode 100644
index 0000000..706e223
--- /dev/null
+++ b/server/src/listeners/client/ReadyListener.ts
@@ -0,0 +1,59 @@
+import { Listener } from 'discord-akairo';
+import API from '../../API/API';
+import { ActivityType } from 'discord.js';
+import { Guild } from 'discord.js';
+import { setInterval } from 'timers';
+
+export interface ReactionStatus {
+ text: string;
+ type: ActivityType;
+}
+
+export default class ReadyListener extends Listener {
+ public constructor() {
+ super('ready', {
+ emitter: 'client',
+ event: 'ready',
+ category: 'client'
+ });
+ }
+
+ public exec(): void {
+ console.log(`${this.client.user.tag} is now online and ready!`);
+
+ /* const activities: ReactionStatus[] = [
+ {
+ text: 'uwu$help',
+ type: 'WATCHING'
+ }
+ ]
+
+ const statuses = this.infinite(activities);
+
+ setInterval(() => {
+ const status = statuses.next() as IteratorResult<ReactionStatus>;
+ this.client.user!.setActivity(status.value.text, { type: status.value.type });
+ }, 300000);
+
+ setInterval(() => this._clearPresences(), 9e5); */
+
+ new API(this.client).start();
+ }
+
+ /* private _clearPresences(): void {
+ const i = this.client.guilds.cache.reduce((acc: number, g: Guild): number => {
+ acc += g.presences.cache.size;
+ g.presences.cache.clear();
+ return acc;
+ }, 0);
+ this.client.emit('debug', `[PRESENCES]: Cleared ${i} presences in ${this.client.guilds.cache.size} guilds.`);
+ }
+
+ public *infinite(arr: ReactionStatus[]) {
+ let i = 0;
+ while (true) {
+ yield arr[i];
+ i = (i + 1) % arr.length;
+ }
+ } */
+} \ No newline at end of file
diff --git a/server/src/listeners/client/channelDelete.ts b/server/src/listeners/client/channelDelete.ts
new file mode 100644
index 0000000..f8c45db
--- /dev/null
+++ b/server/src/listeners/client/channelDelete.ts
@@ -0,0 +1,21 @@
+import { Listener } from 'discord-akairo';
+import { GuildChannel } from 'discord.js';
+
+export default class ChannelDeleteListener extends Listener {
+ public constructor() {
+ super('channelDelete', {
+ emitter: 'client',
+ event: 'channelDelete',
+ category: 'client',
+ });
+ }
+
+ public exec(channel: GuildChannel): void {
+ if (!channel.guild) return;
+ const existing = this.client.settings.cache.reactions.filter(r => r.channelID === channel.id);
+ if (!existing.size) return;
+ for (const c of existing.values()) {
+ this.client.settings.set('reaction', { id: c.id }, { active: false });
+ }
+ }
+}
diff --git a/server/src/listeners/client/debug.ts b/server/src/listeners/client/debug.ts
new file mode 100644
index 0000000..d23314b
--- /dev/null
+++ b/server/src/listeners/client/debug.ts
@@ -0,0 +1,15 @@
+/* import { Listener } from 'discord-akairo';
+
+export default class DebugListener extends Listener {
+ public constructor() {
+ super('debug', {
+ emitter: 'client',
+ event: 'debug',
+ category: 'client',
+ });
+ }
+
+ public exec(event: any): void {
+ this.client.logger.info(`[DEBUG]: ${event}`);
+ }
+} */ \ No newline at end of file
diff --git a/server/src/listeners/client/emojiDelete.ts b/server/src/listeners/client/emojiDelete.ts
new file mode 100644
index 0000000..012d324
--- /dev/null
+++ b/server/src/listeners/client/emojiDelete.ts
@@ -0,0 +1,20 @@
+import { Listener } from 'discord-akairo';
+import { GuildEmoji } from 'discord.js';
+
+export default class EmojiDeleteListener extends Listener {
+ public constructor() {
+ super('emojiDelete', {
+ emitter: 'client',
+ event: 'emojiDelete',
+ category: 'client',
+ });
+ }
+
+ public exec(emoji: GuildEmoji): void {
+ const existing = this.client.settings.cache.reactions.filter(r => r.emoji === emoji.id && r.emojiType === 'custom');
+ if (!existing.size) return;
+ for (const c of existing.values()) {
+ this.client.settings.set('reaction', { id: c.id }, { active: false });
+ }
+ }
+}
diff --git a/server/src/listeners/client/guildCreate.ts b/server/src/listeners/client/guildCreate.ts
new file mode 100644
index 0000000..f649e2c
--- /dev/null
+++ b/server/src/listeners/client/guildCreate.ts
@@ -0,0 +1,23 @@
+import { Listener } from 'discord-akairo';
+import { Guild } from 'discord.js';
+
+export default class GuildCreateListener extends Listener {
+ public constructor() {
+ super('guildCreate', {
+ emitter: 'client',
+ event: 'guildCreate',
+ category: 'client',
+ });
+ }
+
+ public async exec(guild: Guild): Promise<void> {
+ const existing = this.client.settings.cache.guilds.get(guild.id);
+ if (!existing) {
+ this.client.settings.new('guild', {
+ id: guild.id,
+ premium: false,
+ prefix: process.env.PREFIX || 'r!',
+ });
+ }
+ }
+}
diff --git a/server/src/listeners/client/messageDelete.ts b/server/src/listeners/client/messageDelete.ts
new file mode 100644
index 0000000..63d8e56
--- /dev/null
+++ b/server/src/listeners/client/messageDelete.ts
@@ -0,0 +1,20 @@
+import { Listener } from 'discord-akairo';
+import { Message } from 'discord.js';
+
+export default class MessageDeleteListener extends Listener {
+ public constructor() {
+ super('messageDelete', {
+ emitter: 'client',
+ event: 'messageDelete',
+ category: 'client',
+ });
+ }
+
+ public exec(msg: Message): void {
+ const existing = this.client.settings.cache.reactions.filter(r => r.messageID === msg.id);
+ if (!existing.size) return;
+ for (const c of existing.values()) {
+ this.client.settings.set('reaction', { id: c.id }, { active: false });
+ }
+ }
+}
diff --git a/server/src/listeners/client/messageReactionAdd.ts b/server/src/listeners/client/messageReactionAdd.ts
new file mode 100644
index 0000000..1ef2ea9
--- /dev/null
+++ b/server/src/listeners/client/messageReactionAdd.ts
@@ -0,0 +1,65 @@
+import { Listener } from 'discord-akairo';
+import { User, MessageReaction, Permissions } from 'discord.js';
+import { stripIndents } from 'common-tags';
+
+export default class ReactionAddListener extends Listener {
+ public queue: Set<string> = new Set();
+
+ public constructor() {
+ super('messageReactionAdd', {
+ emitter: 'client',
+ event: 'messageReactionAdd',
+ category: 'client',
+ });
+ }
+
+ public async exec(reaction: MessageReaction, user: User): Promise<boolean | void> {
+ let msg = reaction.message;
+ if (msg.partial) msg = await msg.fetch();
+
+ // ignore a message reaction that isn't a guild
+ if (!msg.guild) return;
+
+ const key = `${reaction.emoji.toString()}:${user.id}`;
+ if (this.queue.has(key)) return;
+ this.queue.add(key);
+
+ // fetch our ME because it can be uncached
+ if (!msg.guild.me) await msg.guild.members.fetch(this.client.user?.id!);
+
+ // get all of our message reactions with the message ID of our message. If none, return.
+ const messages = this.client.settings.cache.reactions.filter(r => r.messageID === msg.id);
+ if (!messages.size) return this.queue.delete(key);
+
+ const rr = messages.find(r => [reaction.emoji.name, reaction.emoji.id].includes(r.emoji));
+ if (!rr || !rr.active) return this.queue.delete(key);
+
+ // fetch the role store because it may be uncached
+ const role = await msg.guild.roles.fetch(rr.roleID).catch(() => undefined);
+ if (!role) return this.queue.delete(key);
+
+ // check if we have permissions to manage roles
+ if (!msg.guild.me?.permissions.has(Permissions.FLAGS.MANAGE_ROLES)) return this.queue.delete(key);
+
+ // check if we have the permissions to apply that specific role
+ if (role.comparePositionTo(msg.guild.me.roles.highest) >= 0) return this.queue.delete(key);
+
+ const member = await msg.guild.members.fetch(user).catch(() => undefined);
+ if (!member) return this.queue.delete(key);
+
+ try {
+ await member.roles.add(role);
+ await member.send(stripIndents`
+ You've been given the **${role.name}** role in ${msg.guild.name}.
+ Please Note: You must wait 5 seconds before you can un-react to have me remove ${role.name}.
+ `);
+ } catch (err) {
+ this.client.logger.info(`[ADDROLE ERROR]: ${err}.`);
+ }
+
+ // remove the user from the queue system in 2500 seconds so they can't spam reactions
+ setTimeout(() => {
+ this.queue.delete(key);
+ }, 2500);
+ }
+}
diff --git a/server/src/listeners/client/messageReactionRemove.ts b/server/src/listeners/client/messageReactionRemove.ts
new file mode 100644
index 0000000..7a1e8f7
--- /dev/null
+++ b/server/src/listeners/client/messageReactionRemove.ts
@@ -0,0 +1,55 @@
+import { Listener } from 'discord-akairo';
+import { User, MessageReaction, Permissions } from 'discord.js';
+import { stripIndents } from 'common-tags';
+
+export default class MessageReactionRemove extends Listener {
+ public queue: Set<string> = new Set();
+
+ public constructor() {
+ super('messageReactionRemove', {
+ emitter: 'client',
+ event: 'messageReactionRemove',
+ category: 'client',
+ });
+ }
+
+ public async exec(reaction: MessageReaction, user: User): Promise<boolean | void> {
+ let msg = reaction.message;
+ if (msg.partial) msg = await msg.fetch();
+
+ // ignore a message reaction that isn't a guild
+ if (!msg.guild) return;
+
+ // fetch our ME because it can be uncached
+ if (!msg.guild.me || msg.guild.me.partial) await msg.guild.members.fetch(this.client.user?.id!);
+
+ // get all of our message reactions with the message ID of our message. If none, return.
+ const messages = this.client.settings.cache.reactions.filter(r => r.messageID === msg.id);
+ if (!messages || !messages.size) return;
+
+ const rr = messages.find(r => [reaction.emoji.name, reaction.emoji.id].includes(r.emoji));
+ if (!rr || !rr.active) return;
+
+ // fetch the role store because it may be uncached
+ const role = await msg.guild.roles.fetch(rr.roleID).catch(() => undefined);
+ if (!role) return;
+
+ // check if we have permissions to manage roles
+ if (!msg.guild.me!.permissions.has(Permissions.FLAGS.MANAGE_ROLES)) return;
+
+ // check if we have the permissions to apply that specific role
+ if (role.comparePositionTo(msg.guild.me!.roles.highest) >= 0) return;
+
+ const member = await msg.guild.members.fetch(user).catch(() => undefined);
+ if (!member || !member.roles.cache.has(role.id)) return;
+
+ try {
+ await member.roles.remove(role);
+ await member.send(stripIndents`
+ The **${role.name}** role has been removed from you in ${msg.guild.name}.
+
+ Please Note: You must wait 5 seconds before you can re-react to have **${role.name}** reinstated.
+ `);
+ } catch {}
+ }
+}
diff --git a/server/src/listeners/client/roleDelete.ts b/server/src/listeners/client/roleDelete.ts
new file mode 100644
index 0000000..fd4acf9
--- /dev/null
+++ b/server/src/listeners/client/roleDelete.ts
@@ -0,0 +1,17 @@
+import { Listener } from 'discord-akairo';
+import { Role } from 'discord.js';
+
+export default class RoleDelete extends Listener {
+ public constructor() {
+ super('roleDelete', {
+ emitter: 'client',
+ event: 'roleDelete',
+ category: 'client',
+ });
+ }
+
+ public exec(role: Role): void {
+ const existing = this.client.settings.cache.reactions.filter(r => r.roleID === role.id);
+ for (const { _id } of existing.values()) this.client.settings.set('reaction', { _id }, { active: false });
+ }
+}
diff --git a/server/src/structures/Interfaces.ts b/server/src/structures/Interfaces.ts
new file mode 100644
index 0000000..4ee09f4
--- /dev/null
+++ b/server/src/structures/Interfaces.ts
@@ -0,0 +1,16 @@
+export interface APIUser {
+ id: string;
+ username: string;
+ discriminator: string;
+ avatar: string;
+ guilds: APIGuildMin[];
+ admin: boolean;
+}
+
+export interface APIGuildMin {
+ id: string;
+ name: string;
+ icon: string;
+ admin: boolean;
+ invited: boolean;
+} \ No newline at end of file
diff --git a/server/src/structures/OAuth2.ts b/server/src/structures/OAuth2.ts
new file mode 100644
index 0000000..379cf4c
--- /dev/null
+++ b/server/src/structures/OAuth2.ts
@@ -0,0 +1,62 @@
+import { Request } from 'express';
+import { AkairoClient } from 'discord-akairo';
+import { Guild } from 'discord.js';
+import fetch from 'node-fetch';
+import { owners } from '../Config';
+import { APIUser, APIGuildMin } from './Interfaces';
+
+export default class OAuth2 {
+ protected client: AkairoClient;
+ protected guilds: object;
+
+ public constructor(client: AkairoClient) {
+ this.client = client;
+ this.guilds = new Object();
+ }
+
+ public async resolveInformation(req: Request): Promise<APIUser | null> {
+ if (!req.session.token) return null;
+
+ const userReq = await fetch('https://discord.com/api/users/@me', {
+ headers: {
+ 'Authorization': `Bearer ${req.session.token}`
+ }
+ });
+
+ const user = await userReq.json();
+ if (!user.id) return null;
+
+ if (!this.guilds[user.id]) {
+ const guildsReq = await fetch('https://discord.com/api/users/@me/guilds', {
+ headers: {
+ 'Authorization': `Bearer ${req.session.token}`
+ }
+ });
+
+ const guildsRes = await guildsReq.json();
+
+ this.guilds[user.id] = guildsRes;
+ setTimeout(() => {
+ delete this.guilds[user.id];
+ }, 3e5);
+ }
+
+ return {
+ id: user.id,
+ username: user.username,
+ discriminator: user.discriminator,
+ avatar: user.avatar,
+ guilds: this.guilds[user.id].map((guild): APIGuildMin => {
+ const g: Guild = this.client.guilds.cache.get(guild.id);
+ return {
+ id: guild.id,
+ name: guild.name,
+ icon: guild.icon,
+ admin: g ? g.members.cache.get(user.id).permissions.has('MANAGE_GUILD') : guild.owner,
+ invited: g ? true : false
+ }
+ }),
+ admin: owners.includes(user.id)
+ }
+ }
+} \ No newline at end of file
diff --git a/server/src/utils/Logger.ts b/server/src/utils/Logger.ts
new file mode 100644
index 0000000..6b87806
--- /dev/null
+++ b/server/src/utils/Logger.ts
@@ -0,0 +1,44 @@
+import { createLogger, transports, format, addColors } from 'winston';
+
+const loggerLevels = {
+ levels: {
+ error: 0,
+ debug: 1,
+ warn: 2,
+ data: 3,
+ info: 4,
+ verbose: 5,
+ silly: 6,
+ custom: 7,
+ },
+ colors: {
+ error: 'red',
+ debug: 'blue',
+ warn: 'yellow',
+ data: 'grey',
+ info: 'green',
+ verbose: 'cyan',
+ silly: 'magenta',
+ custom: 'yellow',
+ },
+};
+
+addColors(loggerLevels.colors);
+
+export const logger = createLogger({
+ levels: loggerLevels.levels,
+ format: format.combine(
+ format.colorize({ level: true }),
+ format.errors({ stack: true }),
+ format.splat(),
+ format.timestamp({ format: 'MM/DD/YYYY HH:mm:ss' }),
+ format.printf((data: any) => {
+ const { timestamp, level, message, ...rest } = data;
+ return `[${timestamp}] ${level}: ${message}${
+ Object.keys(rest).length ? `\n${JSON.stringify(rest, null, 2)}` : ''
+ }`;
+ }),
+ ),
+ transports: new transports.Console(),
+ level: 'custom',
+});
diff --git a/server/src/utils/Utils.ts b/server/src/utils/Utils.ts
new file mode 100644
index 0000000..454840e
--- /dev/null
+++ b/server/src/utils/Utils.ts
@@ -0,0 +1,5 @@
+export default class Util {
+ static shorten(text: string, maxLen = 2000) {
+ return text.length > maxLen ? `${text.substr(0, maxLen - 3)}...` : text;
+ }
+} \ No newline at end of file