diff options
Diffstat (limited to 'server/src')
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 |