diff --git a/src/class/Client.ts b/src/class/Client.ts index 7e12f5b..589ff0f 100644 --- a/src/class/Client.ts +++ b/src/class/Client.ts @@ -1,24 +1,47 @@ import eris from 'eris'; +import mongoose from 'mongoose'; import { promises as fs } from 'fs'; import { Collection, Command, Util } from '.'; +import { Moderation, ModerationInterface } from '../models'; export default class Client extends eris.Client { - public config: { token: string, prefix: string, guildID: string }; + public config: { token: string, prefix: string, guildID: string, mongoDB: string }; public commands: Collection; + public intervals: Collection; + public util: Util; + public db: { moderation: mongoose.Model }; + // eslint-disable-next-line @typescript-eslint/no-useless-constructor constructor(token: string, options?: eris.ClientOptions) { super(token, options); this.commands = new Collection(); + this.intervals = new Collection(); + this.db = { moderation: Moderation }; + } + + public async loadDatabase() { + await mongoose.connect(this.config.mongoDB, { useNewUrlParser: true, useUnifiedTopology: true }); } public loadPlugins() { this.util = new Util(this); } + public async loadIntervals() { + const intervalFiles = await fs.readdir(`${__dirname}/../intervals`); + intervalFiles.forEach((file) => { + const intervalName = file.split('.')[0]; + if (file === 'index.js') return; + const interval: NodeJS.Timeout = (require(`${__dirname}/../intervals/${file}`).default)(this); + this.intervals.add(intervalName, interval); + this.util.signale.success(`Successfully loaded interval: ${intervalName}`); + }); + } + public async loadEvents() { const evtFiles = await fs.readdir(`${__dirname}/../events`); evtFiles.forEach((file) => { diff --git a/src/class/Moderation.ts b/src/class/Moderation.ts new file mode 100644 index 0000000..565543f --- /dev/null +++ b/src/class/Moderation.ts @@ -0,0 +1,108 @@ +/* eslint-disable no-bitwise */ +import { Member } from 'eris'; +import { v4 as uuid } from 'uuid'; +import moment, { unitOfTime } from 'moment'; +import { Client, RichEmbed } from '.'; +import { Moderation as ModerationModel, ModerationInterface } from '../models'; +import { moderation as channels } from '../configs/channels.json'; + +export default class Moderation { + public client: Client; + + public logChannels: { + modlogs: string + }; + + constructor(client: Client) { + this.client = client; + this.logChannels.modlogs = channels.modlogs; + } + + public checkPermissions(member: Member, moderator: Member): boolean { + if (member.id === moderator.id) return false; + if (member.roles.some((r) => ['662163685439045632', '455972169449734144', '453689940140883988'].includes(r))) return false; + const bit = member.permission.allow; + if ((bit | 8) === bit) return false; + if ((bit | 20) === bit) return false; + return true; + } + + /** + * Converts some sort of time based duration to milliseconds based on length. + * @param time The time, examples: 2h, 1m, 1w + */ + public convertTimeDurationToMilliseconds(time: string): number { + const lockLength = time.match(/[a-z]+|[^a-z]+/gi); + const length = Number(lockLength[0]); + const unit = lockLength[1] as unitOfTime.Base; + return moment.duration(length, unit).asMilliseconds(); + } + + public async ban(member: Member, moderator: Member, duration: number, reason?: string): Promise { + await member.ban(7, reason); + const logID = uuid(); + const mod = new ModerationModel({ + userID: member.id, + logID, + moderatorID: moderator.id, + reason: reason ?? null, + type: 5, + date: new Date(), + }); + const now: number = Date.now(); + let date: Date; + let processed = true; + if (duration > 0) { + date = new Date(now + duration); + processed = false; + } else date = null; + const expiration = { date, processed }; + mod.expiration = expiration; + + const embed = new RichEmbed(); + embed.setTitle(`Case ${logID} | Ban`); + embed.setAuthor(member.user.username, member.user.avatarURL); + embed.setThumbnail(member.user.avatarURL); + embed.addField('User', `<@${member.id}>`, true); + embed.addField('Moderator', `<@${moderator.id}>`, true); + if (reason) { + embed.addField('Reason', reason, true); + } + if (date) { + embed.addField('Expiration', moment(date).calendar(), true); + } + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + this.client.createMessage(this.logChannels.modlogs, { embed }); + return mod.save(); + } + + public async unban(userID: string, moderator: Member, reason?: string): Promise { + this.client.unbanGuildMember(this.client.config.guildID, userID, reason); + const user = await this.client.getRESTUser(userID); + if (!user) throw new Error('Cannot get user.'); + const logID = uuid(); + const mod = new ModerationModel({ + userID, + logID, + moderatorID: moderator.id, + reason: reason ?? null, + type: 4, + date: new Date(), + }); + + const embed = new RichEmbed(); + embed.setTitle(`Case ${logID} | Unban`); + embed.setAuthor(user.username, user.avatarURL); + embed.setThumbnail(user.avatarURL); + embed.addField('User', `<@${user.id}>`, true); + embed.addField('Moderator', `<@${moderator.id}>`, true); + if (reason) { + embed.addField('Reason', reason, true); + } + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + this.client.createMessage(this.logChannels.modlogs, { embed }); + return mod.save(); + } +} diff --git a/src/class/Util.ts b/src/class/Util.ts index a53eb30..bcd39cb 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -1,15 +1,18 @@ import signale from 'signale'; import { Member, Message, Guild, PrivateChannel, GroupChannel, Role, AnyGuildChannel } from 'eris'; -import { Client, Command, RichEmbed } from '.'; +import { Client, Command, Moderation, RichEmbed } from '.'; import { statusMessages as emotes } from '../configs/emotes.json'; export default class Util { public client: Client; + public moderation: Moderation; + public signale: signale.Signale; constructor(client: Client) { this.client = client; + this.moderation = new Moderation(this.client); this.signale = signale; this.signale.config({ displayDate: true, @@ -49,7 +52,7 @@ export default class Util { public resolveGuildChannel(query: string, { channels }: Guild): AnyGuildChannel | undefined { let queries = query.split(' ').slice(0, 10).join(' '); - const nchannels = channels.map(c => c).sort((a: AnyGuildChannel, b: AnyGuildChannel) => a.type - b.type); + const nchannels = channels.map((c) => c).sort((a: AnyGuildChannel, b: AnyGuildChannel) => a.type - b.type); let channel = nchannels.find((c) => (c.id === queries || c.name === queries || c.name.toLowerCase() === queries.toLowerCase() || c.name.toLowerCase().startsWith(queries.toLowerCase()))); if (!channel && queries.split(' ').length > 0) { while (!channel && queries.split(' ').length > 1) { @@ -84,7 +87,9 @@ export default class Util { queries = queries.split(' ').slice(0, queries.length - 1).join(' '); // eslint-disable-next-line no-loop-func member = members.find((m) => m.mention.replace('!', '') === queries.replace('!', '') || `${m.username}#${m.discriminator}` === query || m.username === queries || m.id === queries || m.nick === queries) // Exact match for mention, username+discrim, username and user ID + // eslint-disable-next-line no-loop-func || members.find((m) => `${m.username.toLowerCase()}#${m.discriminator}` === queries.toLowerCase() || m.username.toLowerCase() === queries.toLowerCase() || (m.nick && m.nick.toLowerCase() === queries.toLowerCase())) // Case insensitive match for username+discrim, username + // eslint-disable-next-line no-loop-func || members.find((m) => m.username.toLowerCase().startsWith(queries.toLowerCase()) || (m.nick && m.nick.toLowerCase().startsWith(queries.toLowerCase()))); } } @@ -119,4 +124,23 @@ export default class Util { this.signale.error(err); } } + + public splitString(string: string, length: number): string[] { + if (!string) return []; + // eslint-disable-next-line no-param-reassign + if (Array.isArray(string)) string = string.join('\n'); + if (string.length <= length) return [string]; + const arrayString: string[] = []; + let str: string = ''; + let pos: number; + while (string.length > 0) { + pos = string.length > length ? string.lastIndexOf('\n', length) : string.length; + if (pos > length) pos = length; + str = string.substr(0, pos); + // eslint-disable-next-line no-param-reassign + string = string.substr(pos); + arrayString.push(str); + } + return arrayString; + } } diff --git a/src/class/index.ts b/src/class/index.ts index 5565632..d1cdba4 100644 --- a/src/class/index.ts +++ b/src/class/index.ts @@ -1,5 +1,6 @@ export { default as Client } from './Client'; export { default as Collection } from './Collection'; export { default as Command } from './Command'; +export { default as Moderation } from './Moderation'; export { default as RichEmbed } from './RichEmbed'; export { default as Util } from './Util';