diff --git a/package.json b/package.json index 81eb46a..0fbd1f2 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "axios": "^0.19.2", - "eris": "^0.11.2", + "eris": "abalabahaha/eris#dev", "moment": "^2.24.0", "mongoose": "^5.9.9", "signale": "^1.4.0", diff --git a/src/class/Collection.ts b/src/class/Collection.ts index 80120b4..044e12c 100644 --- a/src/class/Collection.ts +++ b/src/class/Collection.ts @@ -2,12 +2,12 @@ * Hold a bunch of something */ export default class Collection extends Map { - baseObject: any + baseObject: new (...args: any[]) => V; /** * Creates an instance of Collection */ - constructor(iterable: any[]|object = null) { + constructor(iterable: Iterable<[string, V]>|object = null) { if (iterable && iterable instanceof Array) { super(iterable); } else if (iterable && iterable instanceof Object) { @@ -33,8 +33,8 @@ export default class Collection extends Map { * { key: value, key: value, key: value } * ``` */ - toObject(): object { - const obj: object = {}; + toObject(): { [key: string]: V } { + const obj: { [key: string]: V } = {}; for (const [key, value] of this.entries()) { obj[key] = value; } @@ -90,7 +90,7 @@ export default class Collection extends Map { * Return all the objects that make the function evaluate true * @param func A function that takes an object and returns true if it matches */ - filter(func: Function): V[] { + filter(func: (value: V) => boolean): V[] { const arr = []; for (const item of this.values()) { if (func(item)) { @@ -104,7 +104,7 @@ export default class Collection extends Map { * Test if at least one element passes the test implemented by the provided function. Returns true if yes, or false if not. * @param func A function that takes an object and returns true if it matches */ - some(func: Function) { + some(func: (value: V) => boolean) { for (const item of this.values()) { if (func(item)) { return true; diff --git a/src/class/Command.ts b/src/class/Command.ts index 9a8fc92..2fa7faa 100644 --- a/src/class/Command.ts +++ b/src/class/Command.ts @@ -1,4 +1,4 @@ -import { Member, Message } from 'eris'; +import { Member, Message, TextableChannel } from 'eris'; import { Client } from '.'; export default class Command { @@ -68,4 +68,16 @@ export default class Command { return false; } } + + public error(channel: TextableChannel, text: string): Promise { + return channel.createMessage(`***${this.client.util.emojis.ERROR} ${text}***`); + } + + public success(channel: TextableChannel, text: string): Promise { + return channel.createMessage(`***${this.client.util.emojis.SUCCESS} ${text}***`); + } + + public loading(channel: TextableChannel, text: string): Promise { + return channel.createMessage(`***${this.client.util.emojis.LOADING} ${text}***`); + } } diff --git a/src/class/Moderation.ts b/src/class/Moderation.ts index 13ba195..71fcb22 100644 --- a/src/class/Moderation.ts +++ b/src/class/Moderation.ts @@ -41,13 +41,14 @@ export default class Moderation { } public async ban(user: User, moderator: Member, duration: number, reason?: string): Promise { + if (reason && reason.length > 512) throw new Error('Ban reason cannot be longer than 512 characters'); await this.client.guilds.get(this.client.config.guildID).banMember(user.id, 7, reason); const logID = uuid(); const mod = new ModerationModel({ userID: user.id, logID, moderatorID: moderator.id, - reason: reason ?? null, + reason: reason || null, type: 5, date: new Date(), }); @@ -89,7 +90,7 @@ export default class Moderation { userID, logID, moderatorID: moderator.id, - reason: reason ?? null, + reason: reason || null, type: 4, date: new Date(), }); diff --git a/src/class/RichEmbed.ts b/src/class/RichEmbed.ts index 4f95f3b..307f9c5 100644 --- a/src/class/RichEmbed.ts +++ b/src/class/RichEmbed.ts @@ -1,6 +1,20 @@ /* eslint-disable no-param-reassign */ -export default class RichEmbed { +export interface EmbedData { + title?: string + description?: string + url?: string + timestamp?: Date + color?: number + footer?: { text: string, icon_url?: string, proxy_icon_url?: string} + image?: { url: string, proxy_url?: string, height?: number, width?: number } + thumbnail?: { url: string, proxy_url?: string, height?: number, width?: number } + video?: { url: string, height?: number, width?: number } + author?: { name: string, url?: string, proxy_icon_url?: string, icon_url?: string} + fields?: {name: string, value: string, inline?: boolean}[] +} + +export default class RichEmbed implements EmbedData { title?: string type?: string @@ -17,7 +31,7 @@ export default class RichEmbed { image?: { url: string, proxy_url?: string, height?: number, width?: number } - thumbnail?: { url?: string, proxy_url?: string, height?: number, width?: number } + thumbnail?: { url: string, proxy_url?: string, height?: number, width?: number } video?: { url: string, height?: number, width?: number } @@ -27,12 +41,7 @@ export default class RichEmbed { fields?: {name: string, value: string, inline?: boolean}[] - constructor(data: { - title?: string, type?: string, description?: string, url?: string, timestamp?: Date, color?: number, fields?: {name: string, value: string, inline?: boolean}[] - footer?: { text: string, icon_url?: string, proxy_icon_url?: string}, image?: { url: string, proxy_url?: string, height?: number, width?: number }, - thumbnail?: { url: string, proxy_url?: string, height?: number, width?: number }, video?: { url: string, height?: number, width?: number }, - provider?: { name: string, url?: string}, author?: { name: string, url?: string, proxy_icon_url?: string, icon_url?: string}, - } = {}) { + constructor(data: EmbedData = {}) { /* let types: { title?: string, type?: string, description?: string, url?: string, timestamp?: Date, color?: number, fields?: {name: string, value: string, inline?: boolean}[] @@ -79,7 +88,7 @@ export default class RichEmbed { setURL(url: string) { if (typeof url !== 'string') throw new TypeError('RichEmbed URLs must be a string.'); if (!url.startsWith('http://') || !url.startsWith('https://')) url = `https://${url}`; - this.url = url; + this.url = encodeURI(url); return this; } diff --git a/src/class/Util.ts b/src/class/Util.ts index 1c6d3e9..f31a4ca 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-bitwise */ import signale from 'signale'; import { Member, Message, Guild, PrivateChannel, GroupChannel, Role, AnyGuildChannel } from 'eris'; import { Client, Command, Moderation, RichEmbed } from '.'; @@ -50,19 +51,23 @@ export default class Util { } } - public resolveGuildChannel(query: string, { channels }: Guild): AnyGuildChannel | undefined { - const nchannels = channels.map((c) => c).sort((a: AnyGuildChannel, b: AnyGuildChannel) => a.type - b.type); - return nchannels.find((c) => (c.id === query || c.name === query || c.name.toLowerCase() === query.toLowerCase() || c.name.toLowerCase().startsWith(query.toLowerCase()))); + public resolveGuildChannel(query: string, { channels }: Guild, categories = false): AnyGuildChannel | undefined { + const ch: AnyGuildChannel[] = channels.filter((c) => (!categories ? c.type !== 4 : true)); + return ch.find((c) => c.id === query.replace(/[<#>]/g, '') || c.name === query) + || ch.find((c) => c.name.toLowerCase() === query.toLowerCase()) + || ch.find((c) => c.name.toLowerCase().startsWith(query.toLowerCase())); } public resolveRole(query: string, { roles }: Guild): Role | undefined { - return roles.find((r) => r.id === query || r.name === query || r.name.toLowerCase() === query.toLowerCase() || r.name.toLowerCase().startsWith(query.toLowerCase())); + return roles.find((r) => r.id === query.replace(/[<@&>]/g, '') || r.name === query) + || roles.find((r) => r.name.toLowerCase() === query.toLowerCase()) + || roles.find((r) => r.name.toLowerCase().startsWith(query.toLowerCase())); } public resolveMember(query: string, { members }: Guild): Member | undefined { - return members.find((m) => m.mention.replace('!', '') === query.replace('!', '') || `${m.username}#${m.discriminator}` === query || m.username === query || m.id === query || m.nick === query) // Exact match for mention, username+discrim, username and user ID - || members.find((m) => `${m.username.toLowerCase()}#${m.discriminator}` === query.toLowerCase() || m.username.toLowerCase() === query.toLowerCase() || (m.nick && m.nick.toLowerCase() === query.toLowerCase())) // Case insensitive match for username+discrim, username - || members.find((m) => m.username.toLowerCase().startsWith(query.toLowerCase()) || (m.nick && m.nick.toLowerCase().startsWith(query.toLowerCase()))); + return members.find((m) => `${m.username}#${m.discriminator}` === query || m.username === query || m.id === query.replace(/[<@!>]/g, '') || m.nick === query) // Exact match for mention, username+discrim, username and user ID + || members.find((m) => `${m.username.toLowerCase()}#${m.discriminator}` === query.toLowerCase() || m.username.toLowerCase() === query.toLowerCase() || (m.nick && m.nick.toLowerCase() === query.toLowerCase())) // Case insensitive match for username+discrim, username + || members.find((m) => m.username.toLowerCase().startsWith(query.toLowerCase()) || (m.nick && m.nick.toLowerCase().startsWith(query.toLowerCase()))); } public async handleError(error: Error, message?: Message, command?: Command, disable?: boolean): Promise { @@ -85,10 +90,10 @@ export default class Util { info.embed = embed; } await this.client.createMessage('595788220764127272', info); - const msg = message.content.slice(this.client.config.prefix.length).trim().split(/ +/g); + const msg = message ? message.content.slice(this.client.config.prefix.length).trim().split(/ +/g) : []; // eslint-disable-next-line no-param-reassign if (command && disable) this.resolveCommand(msg).then((c) => { c.cmd.enabled = false; }); - if (message) message.channel.createMessage(`***${this.emojis.ERROR} An unexpected error has occured - please contact a Faculty Marshal.${command ? ' This command has been disabled.' : ''}***`); + if (message) message.channel.createMessage(`***${this.emojis.ERROR} An unexpected error has occured - please contact a Faculty Marshal.${command && disable ? ' This command has been disabled.' : ''}***`); } catch (err) { this.signale.error(err); } @@ -112,4 +117,13 @@ export default class Util { } return arrayString; } + + public decimalToHex(int: number): string { + const red = (int && 0x0000ff) << 16; + const green = int && 0x00ff00; + const blue = (int && 0xff0000) >>> 16; + const number = red | green | blue; + const asHex = number.toString(16); + return '#000000'.substring(0, 7 - asHex.length) + asHex; + } } diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 4562e1d..d6977f7 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -1,5 +1,5 @@ import moment, { unitOfTime } from 'moment'; -import { Message, User } from 'eris'; +import { Message, User, PrivateChannel, GroupChannel } from 'eris'; import { Client, Command } from '../class'; export default class Ban extends Command { @@ -15,24 +15,20 @@ export default class Ban extends Command { public async run(message: Message, args: string[]) { try { - // @ts-ignore - const member = this.client.util.resolveMember(args[0], message.channel.guild); + const member = this.client.util.resolveMember(args[0], message.member.guild); let user: User; if (!member) { try { user = await this.client.getRESTUser(args[0]); } catch { - return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Cannot find user.***`); + return this.error(message.channel, 'Cannot find user.'); } } try { await this.client.guilds.get(this.client.config.guildID).getBan(args[0]); - return message.channel.createMessage(`***${this.client.util.emojis.ERROR} This user is already banned.***`); - } catch { - // eslint-disable-next-line no-unused-expressions - undefined; - } - if (member && !this.client.util.moderation.checkPermissions(member, message.member)) return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Permission denied.***`); + return this.error(message.channel, 'This user is already banned.'); + } catch {} // eslint-disable-line no-empty + if (member && !this.client.util.moderation.checkPermissions(member, message.member)) return this.error(message.channel, 'Permission Denied.'); message.delete(); let momentMilliseconds: number; @@ -43,9 +39,10 @@ export default class Ban extends Command { const unit = lockLength[1] as unitOfTime.Base; momentMilliseconds = moment.duration(length, unit).asMilliseconds(); reason = momentMilliseconds ? args.slice(2).join(' ') : args.slice(1).join(' '); + if (reason.length > 512) return this.error(message.channel, 'Ban reasons cannot be longer than 512 characters.'); } await this.client.util.moderation.ban(user, message.member, momentMilliseconds, reason); - return message.channel.createMessage(`***${this.client.util.emojis.SUCCESS} ${user.username}#${user.id} has been banned.***`); + return this.success(message.channel, `${user.username}#${user.id} has been banned.`); } catch (err) { return this.client.util.handleError(err, message, this, false); } diff --git a/src/commands/eval.ts b/src/commands/eval.ts index 6e78ba1..f62fc7a 100644 --- a/src/commands/eval.ts +++ b/src/commands/eval.ts @@ -54,9 +54,9 @@ export default class Eval extends Command { if (display[5]) { try { const { data } = await axios.post('https://snippets.cloud.libraryofcode.org/documents', display.join('')); - return message.channel.createMessage(`${this.client.util.emojis.SUCCESS} Your evaluation evaled can be found on https://snippets.cloud.libraryofcode.org/${data.key}`); + return this.success(message.channel, `Your evaluation evaled can be found on https://snippets.cloud.libraryofcode.org/${data.key}`); } catch (error) { - return message.channel.createMessage(`${this.client.util.emojis.ERROR} ${error}`); + return this.error(message.channel, `${error}`); } } diff --git a/src/commands/game.ts b/src/commands/game.ts index a8f82a3..f1d18d2 100644 --- a/src/commands/game.ts +++ b/src/commands/game.ts @@ -1,5 +1,5 @@ /* eslint-disable prefer-destructuring */ -import { Activity, Member, Message } from 'eris'; +import { Activity, Member, Message, PrivateChannel, GroupChannel } from 'eris'; import { Client, Command, RichEmbed } from '../class'; export default class Game extends Command { @@ -19,10 +19,9 @@ export default class Game extends Command { let member: Member; if (!args[0]) member = message.member; else { - // @ts-ignore - member = this.client.util.resolveMember(message, args[0], message.channel.guild); + member = this.client.util.resolveMember(args.join(' '), message.member.guild); if (!member) { - return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Member not found.***`); + return this.error(message.channel, 'Member not found.'); } } if (member.activities.length <= 0) return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Cannot find a game for this member.***`); @@ -35,7 +34,7 @@ export default class Game extends Command { mainStatus = member.activities[0]; } embed.setAuthor(member.user.username, member.user.avatarURL); - if (mainStatus?.name === 'Spotify') { + if (mainStatus.type === 4) { embed.setTitle('Spotify'); embed.setColor('#1ed760'); embed.addField('Song', mainStatus.details, true); @@ -51,7 +50,7 @@ export default class Game extends Command { embed.setFooter(`Listening to Spotify | ${this.client.user.username}`, 'https://media.discordapp.net/attachments/358674161566220288/496894273304920064/2000px-Spotify_logo_without_text.png'); embed.setTimestamp(); } else { - return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Only Spotify games are supported at this time.***`); + return this.error(message.channel, 'Only Spotify games are supported at this time.'); } return message.channel.createMessage({ embed }); } catch (err) { diff --git a/src/commands/roleinfo.ts b/src/commands/roleinfo.ts index 673c705..574a7d0 100644 --- a/src/commands/roleinfo.ts +++ b/src/commands/roleinfo.ts @@ -14,19 +14,13 @@ export default class Roleinfo extends Command { public async run(message: Message, args: string[]) { try { - if (!args[0]) return message.channel.createMessage(`***${this.client.util.emojis.ERROR} You need to specifiy a role ID or a role name.***`); + if (!args[0]) return this.error(message.channel, 'You need to specifiy a role ID or a role name.'); - // @ts-ignore - let role: Role = message.channel.guild.roles.find((r: Role) => r.id === args[0]); + let role: Role = message.member.guild.roles.find((r: Role) => r.id === args[0]); if (!role) { // if it's a role name - // @ts-ignore - role = message.channel.guild.roles.find((r: Role) => r.name.toLowerCase().includes(args.join(' ').toLowerCase())); + role = message.member.guild.roles.find((r: Role) => r.name.toLowerCase().includes(args.join(' ').toLowerCase())); } - if (!role) return this.client.createMessage(message.channel.id, `***${this.client.util.emojis.ERROR} Could not find role.***`); - - const ms = role.createdAt; - const date = new Date(ms).toLocaleDateString('en-us'); - const time = new Date(ms).toLocaleTimeString('en-us'); + if (!role) return this.error(message.channel, 'Could not find role.'); const perms = role.permissions; const permsArray: string[] = []; @@ -45,11 +39,11 @@ export default class Roleinfo extends Command { embed.setDescription(`<@&${role.id}> ID: \`${role.id}\``); embed.setColor(role.color); embed.addField('Name', role.name, true); - embed.addField('Color', `#${role.color.toString(16)}`, true); - embed.addField('Hoisted', role.hoist.toString(), true); - embed.addField('Position', role.position.toString(), true); - embed.addField('Creation Date', `${date} ${time}`, true); - embed.addField('Mentionnable', role.mentionable.toString(), true); + embed.addField('Color', role.color ? this.client.util.decimalToHex(role.color) : 'None', true); + embed.addField('Hoisted', role.hoist ? 'Yes' : 'No', true); + embed.addField('Position', role.position ? 'Yes' : 'No', true); + embed.addField('Creation Date', new Date(role.createdAt).toLocaleString(), true); + embed.addField('Mentionable', role.mentionable ? 'Yes' : 'No', true); embed.setFooter(this.client.user.username, this.client.user.avatarURL); embed.setTimestamp(); diff --git a/src/commands/unban.ts b/src/commands/unban.ts index 34efd89..025758c 100644 --- a/src/commands/unban.ts +++ b/src/commands/unban.ts @@ -14,29 +14,21 @@ export default class Unban extends Command { public async run(message: Message, args: string[]) { try { - // @ts-ignore let user: User; try { user = await this.client.getRESTUser(args[0]); } catch { - return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Could find find user.***`); - } - try { - if (await this.client.getRESTGuildMember(this.client.config.guildID, args[0])) return message.channel.createMessage(`***${this.client.util.emojis.ERROR} This member exists in the server.***`); - } catch { - // eslint-disable-next-line no-unused-expressions - undefined; + return this.error(message.channel, 'Could find find user.'); } try { await this.client.guilds.get(this.client.config.guildID).getBan(args[0]); } catch { - return message.channel.createMessage(`***${this.client.util.emojis.ERROR} This user is not banned.***`); + return this.error(message.channel, 'This user is not banned.'); } - if (!user) return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Unable to locate user.***`); message.delete(); await this.client.util.moderation.unban(user.id, message.member, args.slice(1).join(' ')); - return message.channel.createMessage(`***${this.client.util.emojis.SUCCESS} ${user.username}#${user.discriminator} has been unbanned.***`); + return this.success(message.channel, `${user.username}#${user.discriminator} has been unbanned.`); } catch (err) { return this.client.util.handleError(err, message, this, false); } diff --git a/src/commands/whois.ts b/src/commands/whois.ts index 4c65a3e..e5be07b 100644 --- a/src/commands/whois.ts +++ b/src/commands/whois.ts @@ -1,6 +1,6 @@ /* eslint-disable no-bitwise */ import moment from 'moment'; -import { Message, Member } from 'eris'; +import { Message, Member, PrivateChannel, GroupChannel } from 'eris'; import { Client, Command, RichEmbed } from '../class'; import acknowledgements from '../configs/acknowledgements.json'; import { whois as emotes } from '../configs/emotes.json'; @@ -21,11 +21,11 @@ export default class Whois extends Command { let member: Member; if (!args[0]) member = message.member; else { - // @ts-ignore - member = this.client.util.resolveMember(args.join(' '), message.channel.guild); - if (!member) { - return message.channel.createMessage(`***${this.client.util.emojis.ERROR} Member not found.***`); - } + member = this.client.util.resolveMember(args.join(' '), message.member.guild); + } + + if (!member) { + return this.error(message.channel, 'Member not found.'); } const embed = new RichEmbed(); embed.setAuthor(`${member.user.username}#${member.user.discriminator}`, member.user.avatarURL); @@ -58,18 +58,15 @@ export default class Whois extends Command { } description += `\n<@${member.id}>`; embed.setDescription(description); - // @ts-ignore - for (const role of member.roles.map((r) => message.channel.guild.roles.get(r)).sort((a, b) => b.position - a.position)) { - if (role.color !== 0) { - embed.setColor(role.color); - break; - } - } + + const roles = member.roles.map((r) => message.member.guild.roles.get(r)).sort((a, b) => b.position - a.position); + + const { color } = roles.find((r) => r.color); + embed.setColor(color); embed.addField('Status', `${member.status[0].toUpperCase()}${member.status.slice(1)}`, true); embed.addField('Joined At', `${moment(new Date(message.member.joinedAt)).format('dddd, MMMM Do YYYY, h:mm:ss A')} ET`, true); embed.addField('Created At', `${moment(new Date(message.author.createdAt)).format('dddd, MMMM Do YYYY, h:mm:ss A')} ET`, true); - // @ts-ignore - embed.addField(`Roles [${member.roles.length}]`, member.roles.map((r) => message.channel.guild.roles.get(r)).sort((a, b) => b.position - a.position).map((r) => `<@&${r.id}>`).join(', ')); + embed.addField(`Roles [${roles.length}]`, roles.map((r) => `<@&${r.id}>`).join(', ')); const permissions: string[] = []; const serverAcknowledgements: string[] = []; const bit = member.permission.allow; @@ -101,7 +98,6 @@ export default class Whois extends Command { } public resolveStaffInformation(id: string) { - const ack = acknowledgements.find((m) => m.id === id); - return ack; + return acknowledgements.find((m) => m.id === id); } } diff --git a/src/events/ready.ts b/src/events/ready.ts index 142f42b..908e189 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -16,8 +16,8 @@ export default class { this.client.util.handleError(err); process.exit(1); }); - process.on('unhandledRejection', (err) => { - this.client.util.handleError(new Error(err.toString())); + process.on('unhandledRejection', (err: Error) => { + this.client.util.handleError(err); }); } } diff --git a/tsconfig.json b/tsconfig.json index 251319b..fc5e133 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "ES2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */