diff --git a/Makefile b/Makefile index 6be4d86..5553d82 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,25 @@ -# Builds TypeScript & Go - -check_certificate_files := $(wildcard src/go/checkCertificate/*.go) -check_certificate_signatures_files := $(wildcard src/go/checkCertSignatures/*.go) -storage_files := $(wildcard src/go/storage/*.go) - -all: check_certificate check_cert_signatures storage typescript - -check_certificate: - HOME=/root go build -v -ldflags="-s -w" -o dist/bin/checkCertificate ${check_certificate_files} - @chmod 740 dist/bin/checkCertificate - file dist/bin/checkCertificate - -check_cert_signatures: - HOME=/root go build -v -ldflags="-s -w" -o dist/bin/checkCertSignatures ${check_certificate_signatures_files} - @chmod 740 dist/bin/checkCertSignatures - file dist/bin/checkCertSignatures - -storage: - HOME=/root go build -v -ldflags="-s -w" -o dist/bin/storage ${storage_files} - @chmod 740 dist/bin/storage - file dist/bin/storage - -typescript: - tsc -p ./tsconfig.json +# Builds TypeScript & Go + +check_certificate_files := $(wildcard src/go/checkCertificate/*.go) +check_certificate_signatures_files := $(wildcard src/go/checkCertSignatures/*.go) +storage_files := $(wildcard src/go/storage/*.go) + +all: check_certificate check_cert_signatures storage typescript + +check_certificate: + HOME=/root go build -ldflags="-s -w" -o dist/bin/checkCertificate ${check_certificate_files} + @chmod 740 dist/bin/checkCertificate + file dist/bin/checkCertificate + +check_cert_signatures: + HOME=/root go build -ldflags="-s -w" -o dist/bin/checkCertSignatures ${check_certificate_signatures_files} + @chmod 740 dist/bin/checkCertSignatures + file dist/bin/checkCertSignatures + +storage: + HOME=/root go build -ldflags="-s -w" -o dist/bin/storage ${storage_files} + @chmod 740 dist/bin/storage + file dist/bin/storage + +typescript: + tsc -p ./tsconfig.json diff --git a/src/class/Collection.ts b/src/class/Collection.ts index 71e8a07..12b5b02 100644 --- a/src/class/Collection.ts +++ b/src/class/Collection.ts @@ -1,155 +1,153 @@ -/** - * Hold a bunch of something - */ -export default class Collection extends Map { - baseObject: any - - /** - * Creates an instance of Collection - */ - constructor(iterable: any[]|object = null) { - if (iterable && iterable instanceof Array) { - // @ts-ignore - super(iterable); - } else if (iterable && iterable instanceof Object) { - // @ts-ignore - super(Object.entries(iterable)); - } else { - super(); - } - } - - /** - * Map to array - * ```js - * [value, value, value] - * ``` - */ - toArray(): V[] { - return [...this.values()]; - } - - /** - * Map to object - * ```js - * { key: value, key: value, key: value } - * ``` - */ - toObject(): object { - const obj: object = {}; - for (const [key, value] of this.entries()) { - obj[key] = value; - } - return obj; - } - - /** - * Add an object - * - * If baseObject, add only if instance of baseObject - * - * If no baseObject, add - * @param key The key of the object - * @param value The object data - * @param replace Whether to replace an existing object with the same key - * @return The existing or newly created object - */ - add(key: string, value: V, replace: boolean = false): V { - if (this.has(key) && !replace) { - return this.get(key); - } - if (this.baseObject && !(value instanceof this.baseObject)) return null; - - this.set(key, value); - return value; - } - - /** - * Return the first object to make the function evaluate true - * @param func A function that takes an object and returns something - * @return The first matching object, or `null` if no match - */ - find(func: Function): V { - for (const item of this.values()) { - if (func(item)) return item; - } - return null; - } - - /** - * Return an array with the results of applying the given function to each element - * @param callbackfn A function that takes an object and returns something - */ - map(callbackfn: (value?: V, index?: number, array?: V[]) => U): U[] { - const arr = []; - for (const item of this.values()) { - arr.push(callbackfn(item)); - } - return arr; - } - - /** - * 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[] { - const arr = []; - for (const item of this.values()) { - if (func(item)) { - arr.push(item); - } - } - return arr; - } - - /** - * 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) { - for (const item of this.values()) { - if (func(item)) { - return true; - } - } - return false; - } - - /** - * Update an object - * @param key The key of the object - * @param value The updated object data - */ - update(key: string, value: V) { - return this.add(key, value, true); - } - - /** - * Remove an object - * @param key The key of the object - * @returns The removed object, or `null` if nothing was removed - */ - remove(key: string): V { - const item = this.get(key); - if (!item) { - return null; - } - this.delete(key); - return item; - } - - /** - * Get a random object from the Collection - * @returns The random object or `null` if empty - */ - random(): V { - if (!this.size) { - return null; - } - return Array.from(this.values())[Math.floor(Math.random() * this.size)]; - } - - toString() { - return `[Collection<${this.baseObject.name}>]`; - } -} +/** + * Hold a bunch of something + */ +export default class Collection extends Map { + baseObject: any + + /** + * Creates an instance of Collection + */ + constructor(iterable: any[]|object = null) { + if (iterable && iterable instanceof Array) { + super(iterable); + } else if (iterable && iterable instanceof Object) { + super(Object.entries(iterable)); + } else { + super(); + } + } + + /** + * Map to array + * ```js + * [value, value, value] + * ``` + */ + toArray(): V[] { + return [...this.values()]; + } + + /** + * Map to object + * ```js + * { key: value, key: value, key: value } + * ``` + */ + toObject(): object { + const obj: object = {}; + for (const [key, value] of this.entries()) { + obj[key] = value; + } + return obj; + } + + /** + * Add an object + * + * If baseObject, add only if instance of baseObject + * + * If no baseObject, add + * @param key The key of the object + * @param value The object data + * @param replace Whether to replace an existing object with the same key + * @return The existing or newly created object + */ + add(key: string, value: V, replace: boolean = false): V { + if (this.has(key) && !replace) { + return this.get(key); + } + if (this.baseObject && !(value instanceof this.baseObject)) return null; + + this.set(key, value); + return value; + } + + /** + * Return the first object to make the function evaluate true + * @param func A function that takes an object and returns something + * @return The first matching object, or `null` if no match + */ + find(func: Function): V { + for (const item of this.values()) { + if (func(item)) return item; + } + return null; + } + + /** + * Return an array with the results of applying the given function to each element + * @param callbackfn A function that takes an object and returns something + */ + map(callbackfn: (value?: V, index?: number, array?: V[]) => U): U[] { + const arr = []; + for (const item of this.values()) { + arr.push(callbackfn(item)); + } + return arr; + } + + /** + * 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[] { + const arr = []; + for (const item of this.values()) { + if (func(item)) { + arr.push(item); + } + } + return arr; + } + + /** + * 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) { + for (const item of this.values()) { + if (func(item)) { + return true; + } + } + return false; + } + + /** + * Update an object + * @param key The key of the object + * @param value The updated object data + */ + update(key: string, value: V) { + return this.add(key, value, true); + } + + /** + * Remove an object + * @param key The key of the object + * @returns The removed object, or `null` if nothing was removed + */ + remove(key: string): V { + const item = this.get(key); + if (!item) { + return null; + } + this.delete(key); + return item; + } + + /** + * Get a random object from the Collection + * @returns The random object or `null` if empty + */ + random(): V { + if (!this.size) { + return null; + } + return Array.from(this.values())[Math.floor(Math.random() * this.size)]; + } + + toString() { + return `[Collection<${this.baseObject.name}>]`; + } +} diff --git a/src/class/RichEmbed.ts b/src/class/RichEmbed.ts index 49d798d..678cccd 100644 --- a/src/class/RichEmbed.ts +++ b/src/class/RichEmbed.ts @@ -1,176 +1,176 @@ -/* eslint-disable no-param-reassign */ - -export default class RichEmbed { - title?: string - - type?: 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 } - - provider?: { name?: string, url?: string} - - author?: { name?: string, url?: string, proxy_icon_url?: string, icon_url?: string} - - 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}, - } = {}) { - /* - let types: { - 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} - }; - */ - this.title = data.title; - this.description = data.description; - this.url = data.url; - this.color = data.color; - this.author = data.author; - this.timestamp = data.timestamp; - this.fields = data.fields || []; - this.thumbnail = data.thumbnail; - this.image = data.image; - this.footer = data.footer; - } - - /** - * Sets the title of this embed. - */ - setTitle(title: string) { - if (typeof title !== 'string') throw new TypeError('RichEmbed titles must be a string.'); - if (title.length > 256) throw new RangeError('RichEmbed titles may not exceed 256 characters.'); - this.title = title; - return this; - } - - /** - * Sets the description of this embed. - */ - setDescription(description: string) { - if (typeof description !== 'string') throw new TypeError('RichEmbed descriptions must be a string.'); - if (description.length > 2048) throw new RangeError('RichEmbed descriptions may not exceed 2048 characters.'); - this.description = description; - return this; - } - - /** - * Sets the URL of this embed. - */ - 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; - return this; - } - - /** - * Sets the color of this embed. - */ - setColor(color: string | number) { - if (typeof color === 'string' || typeof color === 'number') { - if (typeof color === 'string') { - const regex = /[^a-f0-9]/gi; - color = color.replace(/#/g, ''); - if (regex.test(color)) throw new RangeError('Hexadecimal colours must not contain characters other than 0-9 and a-f.'); - color = parseInt(color, 16); - } else if (color < 0 || color > 16777215) throw new RangeError('Base 10 colours must not be less than 0 or greater than 16777215.'); - this.color = color; - return this; - } - throw new TypeError('RichEmbed colours must be hexadecimal as string or number.'); - } - - /** - * Sets the author of this embed. - */ - setAuthor(name: string, icon_url?: string, url?: string) { - if (typeof name !== 'string') throw new TypeError('RichEmbed Author names must be a string.'); - if (url && typeof url !== 'string') throw new TypeError('RichEmbed Author URLs must be a string.'); - if (icon_url && typeof icon_url !== 'string') throw new TypeError('RichEmbed Author icons must be a string.'); - this.author = { name, icon_url, url }; - return this; - } - - /** - * Sets the timestamp of this embed. - */ - setTimestamp(timestamp = new Date()) { - // eslint-disable-next-line no-restricted-globals - if (isNaN(timestamp.getTime())) throw new TypeError('Expecting ISO8601 (Date constructor)'); - this.timestamp = timestamp; - return this; - } - - /** - * Adds a field to the embed (max 25). - */ - addField(name: string, value: string, inline = false) { - if (typeof name !== 'string') throw new TypeError('RichEmbed Field names must be a string.'); - if (typeof value !== 'string') throw new TypeError('RichEmbed Field values must be a string.'); - if (typeof inline !== 'boolean') throw new TypeError('RichEmbed Field inlines must be a boolean.'); - if (this.fields.length >= 25) throw new RangeError('RichEmbeds may not exceed 25 fields.'); - if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.'); - if (!/\S/.test(name)) throw new RangeError('RichEmbed field names may not be empty.'); - if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.'); - if (!/\S/.test(value)) throw new RangeError('RichEmbed field values may not be empty.'); - this.fields.push({ name, value, inline }); - return this; - } - - /** - * Convenience function for `.addField('\u200B', '\u200B', inline)`. - */ - addBlankField(inline = false) { - return this.addField('\u200B', '\u200B', inline); - } - - /** - * Set the thumbnail of this embed. - */ - setThumbnail(url: string) { - if (typeof url !== 'string') throw new TypeError('RichEmbed Thumbnail URLs must be a string.'); - this.thumbnail = { url }; - return this; - } - - /** - * Set the image of this embed. - */ - setImage(url: string) { - if (typeof url !== 'string') throw new TypeError('RichEmbed Image URLs must be a string.'); - if (!url.startsWith('http://') || !url.startsWith('https://')) url = `https://${url}`; - this.image = { url }; - return this; - } - - /** - * Sets the footer of this embed. - */ - setFooter(text: string, icon_url?: string) { - if (typeof text !== 'string') throw new TypeError('RichEmbed Footers must be a string.'); - if (icon_url && typeof icon_url !== 'string') throw new TypeError('RichEmbed Footer icon URLs must be a string.'); - if (text.length > 2048) throw new RangeError('RichEmbed footer text may not exceed 2048 characters.'); - this.footer = { text, icon_url }; - return this; - } -} +/* eslint-disable no-param-reassign */ + +export default class RichEmbed { + title?: string + + type?: 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 } + + provider?: { name: string, url?: string} + + author?: { name: string, url?: string, proxy_icon_url?: string, icon_url?: string} + + 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}, + } = {}) { + /* + let types: { + 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} + }; + */ + this.title = data.title; + this.description = data.description; + this.url = data.url; + this.color = data.color; + this.author = data.author; + this.timestamp = data.timestamp; + this.fields = data.fields || []; + this.thumbnail = data.thumbnail; + this.image = data.image; + this.footer = data.footer; + } + + /** + * Sets the title of this embed. + */ + setTitle(title: string) { + if (typeof title !== 'string') throw new TypeError('RichEmbed titles must be a string.'); + if (title.length > 256) throw new RangeError('RichEmbed titles may not exceed 256 characters.'); + this.title = title; + return this; + } + + /** + * Sets the description of this embed. + */ + setDescription(description: string) { + if (typeof description !== 'string') throw new TypeError('RichEmbed descriptions must be a string.'); + if (description.length > 2048) throw new RangeError('RichEmbed descriptions may not exceed 2048 characters.'); + this.description = description; + return this; + } + + /** + * Sets the URL of this embed. + */ + 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; + return this; + } + + /** + * Sets the color of this embed. + */ + setColor(color: string | number) { + if (typeof color === 'string' || typeof color === 'number') { + if (typeof color === 'string') { + const regex = /[^a-f0-9]/gi; + color = color.replace(/#/g, ''); + if (regex.test(color)) throw new RangeError('Hexadecimal colours must not contain characters other than 0-9 and a-f.'); + color = parseInt(color, 16); + } else if (color < 0 || color > 16777215) throw new RangeError('Base 10 colours must not be less than 0 or greater than 16777215.'); + this.color = color; + return this; + } + throw new TypeError('RichEmbed colours must be hexadecimal as string or number.'); + } + + /** + * Sets the author of this embed. + */ + setAuthor(name: string, icon_url?: string, url?: string) { + if (typeof name !== 'string') throw new TypeError('RichEmbed Author names must be a string.'); + if (url && typeof url !== 'string') throw new TypeError('RichEmbed Author URLs must be a string.'); + if (icon_url && typeof icon_url !== 'string') throw new TypeError('RichEmbed Author icons must be a string.'); + this.author = { name, icon_url, url }; + return this; + } + + /** + * Sets the timestamp of this embed. + */ + setTimestamp(timestamp = new Date()) { + // eslint-disable-next-line no-restricted-globals + if (isNaN(timestamp.getTime())) throw new TypeError('Expecting ISO8601 (Date constructor)'); + this.timestamp = timestamp; + return this; + } + + /** + * Adds a field to the embed (max 25). + */ + addField(name: string, value: string, inline = false) { + if (typeof name !== 'string') throw new TypeError('RichEmbed Field names must be a string.'); + if (typeof value !== 'string') throw new TypeError('RichEmbed Field values must be a string.'); + if (typeof inline !== 'boolean') throw new TypeError('RichEmbed Field inlines must be a boolean.'); + if (this.fields.length >= 25) throw new RangeError('RichEmbeds may not exceed 25 fields.'); + if (name.length > 256) throw new RangeError('RichEmbed field names may not exceed 256 characters.'); + if (!/\S/.test(name)) throw new RangeError('RichEmbed field names may not be empty.'); + if (value.length > 1024) throw new RangeError('RichEmbed field values may not exceed 1024 characters.'); + if (!/\S/.test(value)) throw new RangeError('RichEmbed field values may not be empty.'); + this.fields.push({ name, value, inline }); + return this; + } + + /** + * Convenience function for `.addField('\u200B', '\u200B', inline)`. + */ + addBlankField(inline = false) { + return this.addField('\u200B', '\u200B', inline); + } + + /** + * Set the thumbnail of this embed. + */ + setThumbnail(url: string) { + if (typeof url !== 'string') throw new TypeError('RichEmbed Thumbnail URLs must be a string.'); + this.thumbnail = { url }; + return this; + } + + /** + * Set the image of this embed. + */ + setImage(url: string) { + if (typeof url !== 'string') throw new TypeError('RichEmbed Image URLs must be a string.'); + if (!url.startsWith('http://') || !url.startsWith('https://')) url = `https://${url}`; + this.image = { url }; + return this; + } + + /** + * Sets the footer of this embed. + */ + setFooter(text: string, icon_url?: string) { + if (typeof text !== 'string') throw new TypeError('RichEmbed Footers must be a string.'); + if (icon_url && typeof icon_url !== 'string') throw new TypeError('RichEmbed Footer icon URLs must be a string.'); + if (text.length > 2048) throw new RangeError('RichEmbed footer text may not exceed 2048 characters.'); + this.footer = { text, icon_url }; + return this; + } +} diff --git a/src/class/Util.ts b/src/class/Util.ts index 0bbf3c5..f37ba33 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -1,253 +1,257 @@ -/* eslint-disable no-param-reassign */ -import { promisify } from 'util'; -import childProcess from 'child_process'; -import nodemailer from 'nodemailer'; -import { Message, PrivateChannel, GroupChannel, Member, User } from 'eris'; -import uuid from 'uuid/v4'; -import moment from 'moment'; -import fs from 'fs'; -import os from 'os'; -import { Client } from '..'; -import { Command, RichEmbed } from '.'; -import { ModerationInterface, AccountInterface } from '../models'; - -export default class Util { - public client: Client; - - public transport: nodemailer.Transporter; - - constructor(client: Client) { - this.client = client; - this.transport = nodemailer.createTransport({ - host: 'staff.libraryofcode.org', - auth: { user: 'support', pass: this.client.config.emailPass }, - }); - } - - /** - * Executes a terminal command async. - * @param command The command to execute - * @param options childProcess.ExecOptions - */ - public async exec(command: string, options: childProcess.ExecOptions = {}): Promise { - const ex = promisify(childProcess.exec); - let result: string; - try { - const res = await ex(command, options); - result = `${res.stdout}${res.stderr}`; - } catch (err) { - return Promise.reject(new Error(`Command failed: ${err.cmd}\n${err.stderr}${err.stdout}`)); - } - return result; - } - - /** - * Resolves a command - * @param query Command input - * @param message Only used to check for errors - */ - public resolveCommand(query: string | string[], message?: Message): Promise<{cmd: Command, args: string[] }> { - try { - let resolvedCommand: Command; - if (typeof query === 'string') query = query.split(' '); - const commands = this.client.commands.toArray(); - resolvedCommand = commands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase())); - - if (!resolvedCommand) return Promise.resolve(null); - query.shift(); - while (resolvedCommand.subcommands.size && query.length) { - const subCommands = resolvedCommand.subcommands.toArray(); - const found = subCommands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase())); - if (!found) break; - resolvedCommand = found; - query.shift(); - } - return Promise.resolve({ cmd: resolvedCommand, args: query }); - } catch (error) { - if (message) this.handleError(error, message); - else this.handleError(error); - return Promise.reject(error); - } - } - - public async handleError(error: Error, message?: Message, command?: Command): Promise { - try { - this.client.signale.error(error); - const info = { content: `\`\`\`js\n${error.stack}\n\`\`\``, embed: null }; - if (message) { - const embed = new RichEmbed(); - embed.setColor('FF0000'); - embed.setAuthor(`Error caused by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); - embed.setTitle('Message content'); - embed.setDescription(message.content); - embed.addField('User', `${message.author.mention} (\`${message.author.id}\`)`, true); - embed.addField('Channel', message.channel.mention, true); - let guild: string; - if (message.channel instanceof PrivateChannel || message.channel instanceof GroupChannel) guild = '@me'; - else guild = message.channel.guild.id; - embed.addField('Message link', `[Click here](https://discordapp.com/channels/${guild}/${message.channel.id}/${message.id})`, true); - embed.setTimestamp(new Date(message.timestamp)); - info.embed = embed; - } - await this.client.createMessage('595788220764127272', info); - const msg = message.content.slice(this.client.config.prefix.length).trim().split(/ +/g); - if (command) this.resolveCommand(msg).then((c) => { c.cmd.enabled = false; }); - if (message) message.channel.createMessage(`***${this.client.stores.emojis.error} An unexpected error has occured - please contact a member of the Engineering Team.${command ? ' This command has been disabled.' : ''}***`); - } catch (err) { - this.client.signale.error(err); - } - } - - public splitFields(fields: { name: string, value: string, inline?: boolean }[]): { name: string, value: string, inline?: boolean }[][] { - let index = 0; - const array: {name: string, value: string, inline?: boolean}[][] = [[]]; - while (fields.length) { - if (array[index].length >= 25) { index += 1; array[index] = []; } - array[index].push(fields[0]); fields.shift(); - } - return array; - } - - public splitString(string: string, length: number): string[] { - if (!string) return []; - 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); - string = string.substr(pos); - arrayString.push(str); - } - return arrayString; - } - - - public async createHash(password: string): Promise { - const hashed = await this.exec(`mkpasswd -m sha-512 "${password}"`); - return hashed; - } - - public isValidEmail(email: string): boolean { - const checkAt = email.indexOf('@'); - if (checkAt < 1) return false; - const checkDomain = email.indexOf('.', checkAt + 2); - if (checkDomain < checkAt) return false; - return true; - } - - public randomPassword(): string { - let tempPass = ''; const passChars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; - while (tempPass.length < 5) { tempPass += passChars[Math.floor(Math.random() * passChars.length)]; } - return tempPass; - } - - public async createAccount(hash: string, etcPasswd: string, username: string, userID: string, emailAddress: string, moderatorID: string): Promise { - await this.exec(`useradd -m -p ${hash} -c ${etcPasswd} -s /bin/zsh ${username}`); - await this.exec(`chage -d0 ${username}`); - - const account = new this.client.db.Account({ - username, userID, emailAddress, createdBy: moderatorID, createdAt: new Date(), locked: false, ssInit: false, homepath: `/home/${username}`, - }); - return account.save(); - } - - public async deleteAccount(username: string): Promise { - const account = await this.client.db.Account.findOne({ username }); - if (!account) throw new Error('Account not found'); - this.exec(`lock ${username}`); - const tasks = [ - this.exec(`deluser ${username} --remove-home --backup-to /management/Archives && rm -rf -R ${account.homepath}`), - this.client.db.Account.deleteOne({ username }), - ]; - this.client.removeGuildMemberRole('446067825673633794', account.userID, '546457886440685578', 'Cloud Account Deleted').catch(); - // @ts-ignore - await Promise.all(tasks); - } - - public async messageCollector(message: Message, question: string, timeout: number, shouldDelete = false, choices: string[] = null, filter = (msg: Message): boolean|void => {}): Promise { - const msg = await message.channel.createMessage(question); - return new Promise((res, rej) => { - setTimeout(() => { if (shouldDelete) msg.delete().catch(); rej(new Error('Did not supply a valid input in time')); }, timeout); - this.client.on('messageCreate', (Msg) => { - if (filter(Msg) === false) return; - const verif = choices ? choices.includes(Msg.content) : Msg.content; - if (verif) { if (shouldDelete) msg.delete().catch(); res(Msg); } - }); - }); - } - - /** - * @param type `0` - Create - * - * `1` - Warn - * - * `2` - Lock - * - * `3` - Unlock - * - * `4` - Delete - */ - public async createModerationLog(user: string, moderator: Member|User, type: number, reason?: string, duration?: number): Promise { - const moderatorID = moderator.id; - const account = await this.client.db.Account.findOne({ $or: [{ username: user }, { userID: user }] }); - if (!account) return Promise.reject(new Error(`Account ${user} not found`)); - const { username, userID } = account; - const logInput: { username: string, userID: string, logID: string, moderatorID: string, reason?: string, type: number, date: Date, expiration?: { date: Date, processed: boolean }} = { - username, userID, logID: uuid(), moderatorID, type, date: new Date(), - }; - - const now: number = Date.now(); - let date: Date; - let processed = true; - if (reason) logInput.reason = reason; - if (type === 2) { - if (duration) { - date = new Date(now + duration); - processed = false; - } else date = null; - } - - const expiration = { date, processed }; - - logInput.expiration = expiration; - const log = new this.client.db.Moderation(logInput); - await log.save(); - - let embedTitle: string; - let color: string; - let archType: string; - switch (type) { - default: archType = 'Staff'; embedTitle = 'Cloud Account | Generic'; color = '0892e1'; break; - case 0: archType = 'Administrator'; embedTitle = 'Cloud Account | Create'; color = '00ff00'; break; - case 1: archType = 'Staff'; embedTitle = 'Account Warning | Warn'; color = 'ffff00'; break; - case 2: archType = 'Moderator'; embedTitle = 'Account Infraction | Lock'; color = 'ff6600'; break; - case 3: archType = 'Moderator'; embedTitle = 'Account Reclaim | Unlock'; color = '0099ff'; break; - case 4: archType = 'Administrator'; embedTitle = 'Cloud Account | Delete'; color = 'ff0000'; break; - } - const embed = new RichEmbed() - .setTitle(embedTitle) - .setColor(color) - .addField('User', `${username} | <@${userID}>`, true) - .addField(archType, moderatorID === this.client.user.id ? 'SYSTEM' : `<@${moderatorID}>`, true) - .setFooter(this.client.user.username, this.client.user.avatarURL) - .setTimestamp(); - if (reason) embed.addField('Reason', reason || 'Not specified'); - if (type === 2) embed.addField('Lock Expiration', `${date ? moment(date).format('dddd, MMMM Do YYYY, h:mm:ss A') : 'Indefinitely'}`); - // @ts-ignore - this.client.createMessage('580950455581147146', { embed }); this.client.getDMChannel(userID).then((channel) => channel.createMessage({ embed })).catch(); - - return Promise.resolve(log); - } - - public getAcctHash(userpath: string) { - try { - return fs.readFileSync(`${userpath}/.securesign/auth`).toString(); - } catch (error) { - return null; - } - } -} +/* eslint-disable no-param-reassign */ +import { promisify } from 'util'; +import childProcess from 'child_process'; +import nodemailer from 'nodemailer'; +import { Message, PrivateChannel, GroupChannel, Member, User } from 'eris'; +import uuid from 'uuid/v4'; +import moment from 'moment'; +import fs from 'fs'; +import os from 'os'; +import { Client } from '..'; +import { Command, RichEmbed } from '.'; +import { ModerationInterface, AccountInterface } from '../models'; + +export default class Util { + public client: Client; + + public transport: nodemailer.Transporter; + + constructor(client: Client) { + this.client = client; + this.transport = nodemailer.createTransport({ + host: 'staff.libraryofcode.org', + auth: { user: 'support', pass: this.client.config.emailPass }, + }); + } + + /** + * Executes a terminal command async. + * @param command The command to execute + * @param options childProcess.ExecOptions + */ + public async exec(command: string, options: childProcess.ExecOptions = {}): Promise { + const ex = promisify(childProcess.exec); + let result: string; + try { + const res = await ex(command, options); + result = `${res.stdout}${res.stderr}`; + } catch (err) { + return Promise.reject(new Error(`Command failed: ${err.cmd}\n${err.stderr}${err.stdout}`)); + } + return result; + } + + /** + * Resolves a command + * @param query Command input + * @param message Only used to check for errors + */ + public resolveCommand(query: string | string[], message?: Message): Promise<{cmd: Command, args: string[] }> { + try { + let resolvedCommand: Command; + if (typeof query === 'string') query = query.split(' '); + const commands = this.client.commands.toArray(); + resolvedCommand = commands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase())); + + if (!resolvedCommand) return Promise.resolve(null); + query.shift(); + while (resolvedCommand.subcommands.size && query.length) { + const subCommands = resolvedCommand.subcommands.toArray(); + const found = subCommands.find((c) => c.name === query[0].toLowerCase() || c.aliases.includes(query[0].toLowerCase())); + if (!found) break; + resolvedCommand = found; + query.shift(); + } + return Promise.resolve({ cmd: resolvedCommand, args: query }); + } catch (error) { + if (message) this.handleError(error, message); + else this.handleError(error); + return Promise.reject(error); + } + } + + public async handleError(error: Error, message?: Message, command?: Command): Promise { + try { + this.client.signale.error(error); + const info = { content: `\`\`\`js\n${error.stack}\n\`\`\``, embed: null }; + if (message) { + const embed = new RichEmbed(); + embed.setColor('FF0000'); + embed.setAuthor(`Error caused by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + embed.setTitle('Message content'); + embed.setDescription(message.content); + embed.addField('User', `${message.author.mention} (\`${message.author.id}\`)`, true); + embed.addField('Channel', message.channel.mention, true); + let guild: string; + if (message.channel instanceof PrivateChannel || message.channel instanceof GroupChannel) guild = '@me'; + else guild = message.channel.guild.id; + embed.addField('Message link', `[Click here](https://discordapp.com/channels/${guild}/${message.channel.id}/${message.id})`, true); + embed.setTimestamp(new Date(message.timestamp)); + info.embed = embed; + } + await this.client.createMessage('595788220764127272', info); + const msg = message.content.slice(this.client.config.prefix.length).trim().split(/ +/g); + if (command) this.resolveCommand(msg).then((c) => { c.cmd.enabled = false; }); + if (message) message.channel.createMessage(`***${this.client.stores.emojis.error} An unexpected error has occured - please contact a member of the Engineering Team.${command ? ' This command has been disabled.' : ''}***`); + } catch (err) { + this.client.signale.error(err); + } + } + + public splitFields(fields: { name: string, value: string, inline?: boolean }[]): { name: string, value: string, inline?: boolean }[][] { + let index = 0; + const array: {name: string, value: string, inline?: boolean}[][] = [[]]; + while (fields.length) { + if (array[index].length >= 25) { index += 1; array[index] = []; } + array[index].push(fields[0]); fields.shift(); + } + return array; + } + + public splitString(string: string, length: number): string[] { + if (!string) return []; + 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); + string = string.substr(pos); + arrayString.push(str); + } + return arrayString; + } + + + public async createHash(password: string): Promise { + const hashed = await this.exec(`mkpasswd -m sha-512 "${password}"`); + return hashed; + } + + public isValidEmail(email: string): boolean { + const checkAt = email.indexOf('@'); + if (checkAt < 1) return false; + const checkDomain = email.indexOf('.', checkAt + 2); + if (checkDomain < checkAt) return false; + return true; + } + + public randomPassword(): string { + let tempPass = ''; const passChars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; + while (tempPass.length < 5) { tempPass += passChars[Math.floor(Math.random() * passChars.length)]; } + return tempPass; + } + + public async createAccount(hash: string, etcPasswd: string, username: string, userID: string, emailAddress: string, moderatorID: string): Promise { + await this.exec(`useradd -m -p ${hash} -c ${etcPasswd} -s /bin/zsh ${username}`); + await this.exec(`chage -d0 ${username}`); + + const account = new this.client.db.Account({ + username, userID, emailAddress, createdBy: moderatorID, createdAt: new Date(), locked: false, ssInit: false, homepath: `/home/${username}`, + }); + return account.save(); + } + + public async deleteAccount(username: string): Promise { + const account = await this.client.db.Account.findOne({ username }); + if (!account) throw new Error('Account not found'); + this.exec(`lock ${username}`); + const tasks = [ + this.exec(`deluser ${username} --remove-home --backup-to /management/Archives && rm -rf -R ${account.homepath}`), + this.client.db.Account.deleteOne({ username }), + ]; + this.client.removeGuildMemberRole('446067825673633794', account.userID, '546457886440685578', 'Cloud Account Deleted').catch(); + // @ts-ignore + await Promise.all(tasks); + } + + public async messageCollector(message: Message, question: string, timeout: number, shouldDelete = false, choices: string[] = null, filter = (msg: Message): boolean|void => {}): Promise { + const msg = await message.channel.createMessage(question); + return new Promise((res, rej) => { + const func = (Msg: Message) => { + if (filter(Msg) === false) return; + const verif = choices ? choices.includes(Msg.content) : Msg.content; + if (verif) { if (shouldDelete) msg.delete().catch(); res(Msg); } + }; + + setTimeout(() => { + if (shouldDelete) msg.delete().catch(); rej(new Error('Did not supply a valid input in time')); + this.client.removeListener('messageCreate', func); + }, timeout); + this.client.on('messageCreate', func); + }); + } + + /** + * @param type `0` - Create + * + * `1` - Warn + * + * `2` - Lock + * + * `3` - Unlock + * + * `4` - Delete + */ + public async createModerationLog(user: string, moderator: Member|User, type: number, reason?: string, duration?: number): Promise { + const moderatorID = moderator.id; + const account = await this.client.db.Account.findOne({ $or: [{ username: user }, { userID: user }] }); + if (!account) return Promise.reject(new Error(`Account ${user} not found`)); + const { username, userID } = account; + const logInput: { username: string, userID: string, logID: string, moderatorID: string, reason?: string, type: number, date: Date, expiration?: { date: Date, processed: boolean }} = { + username, userID, logID: uuid(), moderatorID, type, date: new Date(), + }; + + const now: number = Date.now(); + let date: Date; + let processed = true; + if (reason) logInput.reason = reason; + if (type === 2) { + if (duration) { + date = new Date(now + duration); + processed = false; + } else date = null; + } + + const expiration = { date, processed }; + + logInput.expiration = expiration; + const log = new this.client.db.Moderation(logInput); + await log.save(); + + let embedTitle: string; + let color: string; + let archType: string; + switch (type) { + default: archType = 'Staff'; embedTitle = 'Cloud Account | Generic'; color = '0892e1'; break; + case 0: archType = 'Administrator'; embedTitle = 'Cloud Account | Create'; color = '00ff00'; break; + case 1: archType = 'Staff'; embedTitle = 'Account Warning | Warn'; color = 'ffff00'; break; + case 2: archType = 'Moderator'; embedTitle = 'Account Infraction | Lock'; color = 'ff6600'; break; + case 3: archType = 'Moderator'; embedTitle = 'Account Reclaim | Unlock'; color = '0099ff'; break; + case 4: archType = 'Administrator'; embedTitle = 'Cloud Account | Delete'; color = 'ff0000'; break; + } + const embed = new RichEmbed() + .setTitle(embedTitle) + .setColor(color) + .addField('User', `${username} | <@${userID}>`, true) + .addField(archType, moderatorID === this.client.user.id ? 'SYSTEM' : `<@${moderatorID}>`, true) + .setFooter(this.client.user.username, this.client.user.avatarURL) + .setTimestamp(); + if (reason) embed.addField('Reason', reason || 'Not specified'); + if (type === 2) embed.addField('Lock Expiration', `${date ? moment(date).format('dddd, MMMM Do YYYY, h:mm:ss A') : 'Indefinitely'}`); + this.client.createMessage('580950455581147146', { embed }); this.client.getDMChannel(userID).then((channel) => channel.createMessage({ embed })).catch(); + + return Promise.resolve(log); + } + + public getAcctHash(userpath: string) { + try { + return fs.readFileSync(`${userpath}/.securesign/auth`).toString(); + } catch (error) { + return null; + } + } +} diff --git a/src/commands/cwg_create.ts b/src/commands/cwg_create.ts index 7ac1ee0..fa738af 100644 --- a/src/commands/cwg_create.ts +++ b/src/commands/cwg_create.ts @@ -1,147 +1,227 @@ -import fs from 'fs-extra'; -import axios from 'axios'; -import x509 from '@ghaiklor/x509'; -import { Message } from 'eris'; -import { AccountInterface } from '../models'; -import { Command, RichEmbed } from '../class'; -import { Client } from '..'; - -export default class CWG_Create extends Command { - constructor(client: Client) { - super(client); - this.name = 'create'; - this.description = 'Bind a domain to the CWG'; - this.usage = `${this.client.config.prefix}cwg create [User ID | Username] [Domain] [Port] `; - this.permissions = { roles: ['525441307037007902'] }; - this.aliases = ['bind']; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - /* - args[0] should be the user's ID OR account username; required - args[1] should be the domain; required - args[2] should be the port; required - args[3] should be the path to the x509 certificate; not required - args[4] should be the path to the x509 key; not required - */ - try { - if (!args[2]) return this.client.commands.get('help').run(message, ['cwg', this.name]); - const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Binding domain...***`); - const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }] }); - if (!account) return edit.edit(`${this.client.stores.emojis.error} Cannot locate account, please try again.`); - if (args[3] && !args[4]) return edit.edit(`${this.client.stores.emojis.error} x509 Certificate key required`); - let certs: { cert?: string, key?: string }; if (args[4]) certs = { cert: args[3], key: args[4] }; - if (await this.client.db.Domain.exists({ domain: args[1] })) return edit.edit(`***${this.client.stores.emojis.error} This domain already exists.***`); - if (await this.client.db.Domain.exists({ port: Number(args[2]) })) { - // await edit.edit(`***${this.client.stores.emojis.error} This port is already binded to a domain. Do you wish to continue? (y/n)***`); - let answer: Message; - try { - answer = await this.client.util.messageCollector(message, - `***${this.client.stores.emojis.error} This port is already binded to a domain. Do you wish to continue? (y/n)***`, - 30000, true, ['y', 'n'], (msg) => msg.author.id === message.author.id && msg.channel.id === message.channel.id); - } catch (error) { - return edit.edit(`***${this.client.stores.emojis.error} Bind request cancelled***`); - } - if (answer.content === 'n') return edit.edit(`***${this.client.stores.emojis.error} Bind request cancelled***`); - } - const domain = await this.createDomain(account, args[1], Number(args[2]), certs); - const embed = new RichEmbed(); - embed.setTitle('Domain Creation'); - embed.setColor(3066993); - embed.addField('Account Username', account.username, true); - embed.addField('Account ID', account.id, true); - embed.addField('Engineer', `<@${message.author.id}>`, true); - embed.addField('Domain', domain.domain, true); - embed.addField('Port', String(domain.port), true); - const cert = x509.parseCert(await fs.readFile(domain.x509.cert, { encoding: 'utf8' })); - embed.addField('Certificate Issuer', cert.issuer.organizationName, true); - embed.addField('Certificate Subject', cert.subject.commonName, true); - embed.setFooter(this.client.user.username, this.client.user.avatarURL); - embed.setTimestamp(new Date(message.timestamp)); - message.delete(); - await this.client.util.exec('systemctl reload nginx'); - edit.edit(`***${this.client.stores.emojis.success} Successfully binded ${domain.domain} to port ${domain.port} for ${account.userID}.***`); - // @ts-ignore - this.client.createMessage('580950455581147146', { embed }); - // @ts-ignore - this.client.getDMChannel(account.userID).then((r) => r.createMessage({ embed })); - await this.client.util.transport.sendMail({ - to: account.emailAddress, - from: 'Library of Code sp-us | Support Team ', - subject: 'Your domain has been binded', - html: ` -

Library of Code sp-us | Cloud Services

-

Hello, this is an email informing you that a new domain under your account has been binded. - Information is below.

- Domain: ${domain.domain}
- Port: ${domain.port}
- Certificate Issuer: ${cert.issuer.organizationName}
- Certificate Subject: ${cert.subject.commonName}
- Responsible Engineer: ${message.author.username}#${message.author.discriminator}

- - If you have any questions about additional setup, you can reply to this email or send a message in #cloud-support in our Discord server.
- - Library of Code sp-us | Support Team - `, - }); - if (!domain.domain.includes('cloud.libraryofcode.org')) { - const content = `__**DNS Record Setup**__\nYou recently a binded a custom domain to your Library of Code sp-us Account. You'll have to update your DNS records. We've provided the records below.\n\n\`${domain.domain} IN CNAME cloud.libraryofcode.org AUTO/500\`\nThis basically means you need to make a CNAME record with the key/host of ${domain.domain} and the value/point to cloud.libraryofcode.org. If you have any questions, don't hesitate to ask us.`; - this.client.getDMChannel(account.userID).then((r) => r.createMessage(content)); - } - return domain; - } catch (err) { - await fs.unlink(`/etc/nginx/sites-available/${args[1]}`); - await fs.unlink(`/etc/nginx/sites-enabled/${args[1]}`); - await this.client.db.Domain.deleteMany({ domain: args[1] }); - return this.client.util.handleError(err, message, this); - } - } - - /** - * This function binds a domain to a port on the CWG. - * @param account The account of the user. - * @param subdomain The domain to use. `mydomain.cloud.libraryofcode.org` - * @param port The port to use, must be between 1024 and 65535. - * @param x509 The paths to the certificate and key files. Must be already existant. - * @example await CWG.createDomain('mydomain.cloud.libraryofcode.org', 6781); - */ - public async createDomain(account: AccountInterface, domain: string, port: number, x509Certificate: { cert?: string, key?: string } = { cert: '/etc/nginx/ssl/cloud-org.chain.crt', key: '/etc/nginx/ssl/cloud-org.key.pem' }) { - try { - if (port <= 1024 || port >= 65535) throw new RangeError(`Port range must be between 1024 and 65535, received ${port}.`); - if (await this.client.db.Domain.exists({ domain })) throw new Error(`Domain ${domain} already exists in the database.`); - if (!await this.client.db.Account.exists({ userID: account.userID })) throw new Error(`Cannot find account ${account.userID}.`); - await fs.access(x509Certificate.cert, fs.constants.R_OK); - await fs.access(x509Certificate.key, fs.constants.R_OK); - let cfg = await fs.readFile('/var/CloudServices/dist/static/nginx.conf', { encoding: 'utf8' }); - cfg = cfg.replace(/\[DOMAIN]/g, domain); - cfg = cfg.replace(/\[PORT]/g, String(port)); - cfg = cfg.replace(/\[CERTIFICATE]/g, x509Certificate.cert); - cfg = cfg.replace(/\[KEY]/g, x509Certificate.key); - await fs.writeFile(`/etc/nginx/sites-available/${domain}`, cfg, { encoding: 'utf8' }); - await fs.symlink(`/etc/nginx/sites-available/${domain}`, `/etc/nginx/sites-enabled/${domain}`); - const entry = new this.client.db.Domain({ - account, - domain, - port, - x509: x509Certificate, - enabled: true, - }); - if (domain.includes('cloud.libraryofcode.org')) { - const dmn = domain.split('.'); - await axios({ - method: 'post', - url: 'https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records', - headers: { Authorization: `Bearer ${this.client.config.cloudflare}`, 'Content-Type': 'application/json' }, - data: JSON.stringify({ type: 'CNAME', name: `${dmn[0]}.${dmn[1]}`, content: 'cloud.libraryofcode.org', proxied: false }), - }); - } - return entry.save(); - } catch (error) { - await fs.unlink(`/etc/nginx/sites-enabled/${domain}`); - await fs.unlink(`/etc/nginx/sites-available/${domain}`); - await this.client.db.Domain.deleteMany({ domain }); - throw error; - } - } -} +import fs, { writeFile, unlink } from 'fs-extra'; +import axios from 'axios'; +import { Message } from 'eris'; +import { AccountInterface } from '../models'; +import { Command, RichEmbed } from '../class'; +import { Client } from '..'; +import { parseCertificate } from '../functions'; + +export default class CWG_Create extends Command { + public urlRegex: RegExp; + + constructor(client: Client) { + super(client); + this.name = 'create'; + this.description = 'Bind a domain to the CWG'; + this.usage = `${this.client.config.prefix}cwg create [User ID | Username] [Domain] [Port] || Use snippets raw URL`; + this.permissions = { roles: ['525441307037007902'] }; + this.aliases = ['bind']; + this.enabled = true; + this.urlRegex = /^[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=]+$/; + } + + public async run(message: Message, args: string[]) { + /* + args[0] should be the user's ID OR account username; required + args[1] should be the domain; required + args[2] should be the port; required + args[3] should be the path to the x509 certificate; not required + args[4] should be the path to the x509 key; not required + */ + try { + if (!args[2]) return this.client.commands.get('help').run(message, ['cwg', this.name]); + + let certs: { cert: string, key: string }; + + if (!this.urlRegex.test(args[1])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid URL***`); + if (Number(args[2]) < 1024 || Number(args[2]) > 65535) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Port must be greater than 1024 and less than 65535***`); + if (!args[1].endsWith('.cloud.libraryofcode.org') && !args[4]) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Certificate Chain and Private Key are required for custom domains***`); + + const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }] }); + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} Cannot locate account, please try again.`); + + if (await this.client.db.Domain.exists({ domain: args[1] })) return message.channel.createMessage(`${this.client.stores.emojis.error} ***This domain already exists***`); + + if (await this.client.db.Domain.exists({ port: Number(args[2]) })) { + let answer: Message; + try { + answer = await this.client.util.messageCollector( + message, + `***${this.client.stores.emojis.error} ***This port is already binded to a domain. Do you wish to continue? (y/n)***`, + 30000, true, ['y', 'n'], (msg) => msg.author.id === message.author.id && msg.channel.id === message.channel.id, + ); + } catch (error) { + return message.channel.createMessage(`${this.client.stores.emojis.error} ***Bind request cancelled***`); + } + if (answer.content === 'n') return message.channel.createMessage(`${this.client.stores.emojis.error} ***Bind request cancelled***`); + } + + const edit = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Binding domain...***`); + + if (!args[1].endsWith('.cloud.libraryofcode.org')) { + const urls = args.slice(3, 5); + if (urls.some((l) => !l.includes('snippets.cloud.libraryofcode.org/raw/'))) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid snippets URL***`); + + const tasks = urls.map((l) => axios({ method: 'GET', url: l })); + const response = await Promise.all(tasks); + const certAndPrivateKey: string[] = response.map((r) => r.data); + + if (!this.isValidCertificateChain(certAndPrivateKey[0])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid Certificate Chain***`); + if (!this.isValidPrivateKey(certAndPrivateKey[1])) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid Private Key***`); + + certs = { cert: certAndPrivateKey[0], key: certAndPrivateKey[1] }; + } + + const domain = await this.createDomain(account, args[1], Number(args[2]), certs); + + const tasks = [message.delete(), this.client.util.exec('systemctl reload')]; + // @ts-ignore + await Promise.all(tasks); + + const embed = new RichEmbed() + .setTitle('Domain Creation') + .setColor(3066993) + .addField('Account Username', account.username, true) + .addField('Account ID', account.id, true) + .addField('Engineer', `<@${message.author.id}>`, true) + .addField('Domain', domain.domain, true) + .addField('Port', String(domain.port), true); + + const cert = await parseCertificate(this.client, domain.x509.cert); + + embed.addField('Certificate Issuer', cert.issuer.organizationName, true) + .addField('Certificate Subject', cert.subject.commonName, true) + .setFooter(this.client.user.username, this.client.user.avatarURL) + .setTimestamp(new Date(message.timestamp)); + + const completed = [ + edit.edit(`***${this.client.stores.emojis.success} Successfully binded ${domain.domain} to port ${domain.port} for ${account.userID}.***`), + this.client.createMessage('580950455581147146', { embed }), + this.client.getDMChannel(account.userID).then((r) => r.createMessage({ embed })), + this.client.util.transport.sendMail({ + to: account.emailAddress, + from: 'Library of Code sp-us | Support Team ', + subject: 'Your domain has been binded', + html: ` +

Library of Code sp-us | Cloud Services

+

Hello, this is an email informing you that a new domain under your account has been binded. + Information is below.

+ Domain: ${domain.domain}
+ Port: ${domain.port}
+ Certificate Issuer: ${cert.issuer.organizationName}
+ Certificate Subject: ${cert.subject.commonName}
+ Responsible Engineer: ${message.author.username}#${message.author.discriminator}

+ + If you have any questions about additional setup, you can reply to this email or send a message in #cloud-support in our Discord server.
+ + Library of Code sp-us | Support Team + `, + }), + ]; + + if (!domain.domain.includes('cloud.libraryofcode.org')) { + const content = `__**DNS Record Setup**__\nYou recently a binded a custom domain to your Library of Code sp-us Account. You'll have to update your DNS records. We've provided the records below.\n\n\`${domain.domain} IN CNAME cloud.libraryofcode.org AUTO/500\`\nThis basically means you need to make a CNAME record with the key/host of ${domain.domain} and the value/point to cloud.libraryofcode.org. If you have any questions, don't hesitate to ask us.`; + completed.push(this.client.getDMChannel(account.userID).then((r) => r.createMessage(content))); + } + + return Promise.all(completed); + } catch (err) { + await fs.unlink(`/etc/nginx/sites-available/${args[1]}`); + await fs.unlink(`/etc/nginx/sites-enabled/${args[1]}`); + await this.client.db.Domain.deleteMany({ domain: args[1] }); + return this.client.util.handleError(err, message, this); + } + } + + /** + * This function binds a domain to a port on the CWG. + * @param account The account of the user. + * @param subdomain The domain to use. `mydomain.cloud.libraryofcode.org` + * @param port The port to use, must be between 1024 and 65535. + * @param x509Certificate The contents the certificate and key files. + * @example await CWG.createDomain(account, 'mydomain.cloud.libraryofcode.org', 6781); + */ + public async createDomain(account: AccountInterface, domain: string, port: number, x509Certificate: { cert?: string, key?: string }) { + try { + if (port <= 1024 || port >= 65535) throw new RangeError(`Port range must be between 1024 and 65535, received ${port}.`); + if (await this.client.db.Domain.exists({ domain })) throw new Error(`Domain ${domain} already exists in the database.`); + if (!await this.client.db.Account.exists({ userID: account.userID })) throw new Error(`Cannot find account ${account.userID}.`); + let x509: { cert: string, key: string }; + if (x509Certificate) { + x509 = await this.createCertAndPrivateKey(domain, x509Certificate.cert, x509Certificate.key); + } + let cfg = await fs.readFile('/var/CloudServices/dist/static/nginx.conf', { encoding: 'utf8' }); + cfg = cfg.replace(/\[DOMAIN]/g, domain); + cfg = cfg.replace(/\[PORT]/g, String(port)); + cfg = cfg.replace(/\[CERTIFICATE]/g, x509.cert); + cfg = cfg.replace(/\[KEY]/g, x509.key); + await fs.writeFile(`/etc/nginx/sites-available/${domain}`, cfg, { encoding: 'utf8' }); + await fs.symlink(`/etc/nginx/sites-available/${domain}`, `/etc/nginx/sites-enabled/${domain}`); + const entry = new this.client.db.Domain({ + account, + domain, + port, + x509, + enabled: true, + }); + if (domain.includes('cloud.libraryofcode.org')) { + const dmn = domain.split('.'); + await axios({ + method: 'post', + url: 'https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records', + headers: { Authorization: `Bearer ${this.client.config.cloudflare}`, 'Content-Type': 'application/json' }, + data: JSON.stringify({ type: 'CNAME', name: `${dmn[0]}.${dmn[1]}`, content: 'cloud.libraryofcode.org', proxied: false }), + }); + } + return entry.save(); + } catch (error) { + await fs.unlink(`/etc/nginx/sites-enabled/${domain}`); + await fs.unlink(`/etc/nginx/sites-available/${domain}`); + await this.client.db.Domain.deleteMany({ domain }); + throw error; + } + } + + public async createCertAndPrivateKey(domain: string, certChain: string, privateKey: string) { + if (!this.isValidCertificateChain(certChain)) throw new Error('Invalid Certificate Chain'); + if (!this.isValidPrivateKey(privateKey)) throw new Error('Invalid Private Key'); + const path = `/var/CloudServices/temp/${domain}`; + const temp = [writeFile(`${path}.chain.crt`, certChain), writeFile(`${path}.key.pem`, privateKey)]; + const removeFiles = [unlink(`${path}.chain.crt`), unlink(`${path}.key.pem`)]; + await Promise.all(temp); + if (!this.isMatchingPair(`${path}.chain.crt`, `${path}.key.pem`)) { + await Promise.all(removeFiles); + throw new Error('Certificate and Private Key do not match'); + } + + const tasks = [writeFile(`/etc/nginx/ssl/${domain}.chain.crt`, certChain), writeFile(`/etc/nginx/ssl/${domain}.key.pem`, privateKey)]; + await Promise.all(tasks); + return { cert: `/etc/nginx/ssl/${domain}.chain.crt`, key: `/etc/nginx/ssl/${domain}.key.pem` }; + } + + public checkOccurance(text: string, query: string) { + return (text.match(new RegExp(query, 'g')) || []).length; + } + + public isValidCertificateChain(cert: string) { + if (!cert.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN CERTIFICATE-----')) return false; + if (!cert.replace(/^\s+|\s+$/g, '').endsWith('-----END CERTIFICATE-----')) return false; + if (this.checkOccurance(cert.replace(/^\s+|\s+$/g, ''), '-----BEGIN CERTIFICATE-----') !== 2) return false; + if (this.checkOccurance(cert.replace(/^\s+|\s+$/g, ''), '-----END CERTIFICATE-----') !== 2) return false; + return true; + } + + public isValidPrivateKey(key: string) { + if (!key.replace(/^\s+|\s+$/g, '').startsWith('-----BEGIN PRIVATE KEY-----')) return false; + if (!key.replace(/^\s+|\s+$/g, '').endsWith('-----END PRIVATE KEY-----')) return false; + if (this.checkOccurance(key.replace(/^\s+|\s+$/g, ''), '-----BEGIN PRIVATE KEY-----') !== 1) return false; + if (this.checkOccurance(key.replace(/^\s+|\s+$/g, ''), '-----END PRIVATE KEY-----') !== 1) return false; + return true; + } + + public async isMatchingPair(cert: string, privateKey: string) { + const result: string = await this.client.util.exec(`${__dirname}/../bin/checkCertSignatures ${cert} ${privateKey}`); + const { ok }: { ok: boolean } = JSON.parse(result); + return ok; + } +} diff --git a/src/commands/cwg_data.ts b/src/commands/cwg_data.ts index 60dad9a..354fc07 100644 --- a/src/commands/cwg_data.ts +++ b/src/commands/cwg_data.ts @@ -1,58 +1,56 @@ -import fs from 'fs'; -import moment from 'moment'; -import x509 from '@ghaiklor/x509'; -import { createPaginationEmbed } from 'eris-pagination'; -import { Message } from 'eris'; -import { Command, RichEmbed } from '../class'; -import { Client } from '..'; - -export default class CWG_Data extends Command { - constructor(client: Client) { - super(client); - this.name = 'data'; - this.description = 'Check CWG data'; - this.usage = `${this.client.config.prefix}cwg data [Domain | Port]`; - this.permissions = { roles: ['446104438969466890'] }; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - if (!args[0]) return this.client.commands.get('help').run(message, ['cwg', this.name]); - const dom = await this.client.db.Domain.find({ $or: [{ domain: args[0] }, { port: Number(args[0]) || '' }] }); - if (!dom.length) { - if (!Number.isNaN(Number(args[0]))) { - try { - await this.client.util.exec(`fuser ${args[0]}/tcp`); - return message.channel.createMessage(`***${this.client.stores.emojis.error} The port you provided is being used by a system process.***`); - } catch (error) { - return message.channel.createMessage(`***${this.client.stores.emojis.error} The domain or port you provided could not be found.***`); - } - } - return message.channel.createMessage(`***${this.client.stores.emojis.error} The domain or port you provided could not be found.***`); - } - const embeds = dom.map((domain) => { - const cert = fs.readFileSync(domain.x509.cert, { encoding: 'utf8' }); - const embed = new RichEmbed(); - embed.setTitle('Domain Information'); - embed.addField('Account Username', domain.account.username, true); - embed.addField('Account ID', domain.account.userID, true); - embed.addField('Domain', domain.domain, true); - embed.addField('Port', String(domain.port), true); - embed.addField('Certificate Issuer', x509.getIssuer(cert).organizationName, true); - embed.addField('Certificate Subject', x509.getSubject(cert).commonName, true); - embed.addField('Certificate Expiration Date', moment(x509.parseCert(cert).notAfter).format('dddd, MMMM Do YYYY, h:mm:ss A'), true); - embed.setFooter(this.client.user.username, this.client.user.avatarURL); - embed.setTimestamp(); - return embed; - }); - this.client.signale.log(embeds); - // @ts-ignore - if (embeds.length === 1) return message.channel.createMessage({ embed: embeds[0] }); - // @ts-ignore - return createPaginationEmbed(message, this.client, embeds, {}); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import fs from 'fs'; +import moment from 'moment'; +import x509 from '@ghaiklor/x509'; +import { createPaginationEmbed } from 'eris-pagination'; +import { Message } from 'eris'; +import { Command, RichEmbed } from '../class'; +import { Client } from '..'; + +export default class CWG_Data extends Command { + constructor(client: Client) { + super(client); + this.name = 'data'; + this.description = 'Check CWG data'; + this.usage = `${this.client.config.prefix}cwg data [Domain | Port]`; + this.permissions = { roles: ['446104438969466890'] }; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, ['cwg', this.name]); + const dom = await this.client.db.Domain.find({ $or: [{ domain: args[0] }, { port: Number(args[0]) || '' }] }); + if (!dom.length) { + if (!Number.isNaN(Number(args[0]))) { + try { + await this.client.util.exec(`fuser ${args[0]}/tcp`); + return message.channel.createMessage(`***${this.client.stores.emojis.error} The port you provided is being used by a system process.***`); + } catch (error) { + return message.channel.createMessage(`***${this.client.stores.emojis.error} The domain or port you provided could not be found.***`); + } + } + return message.channel.createMessage(`***${this.client.stores.emojis.error} The domain or port you provided could not be found.***`); + } + const embeds = dom.map((domain) => { + const cert = fs.readFileSync(domain.x509.cert, { encoding: 'utf8' }); + const embed = new RichEmbed(); + embed.setTitle('Domain Information'); + embed.addField('Account Username', domain.account.username, true); + embed.addField('Account ID', domain.account.userID, true); + embed.addField('Domain', domain.domain, true); + embed.addField('Port', String(domain.port), true); + embed.addField('Certificate Issuer', x509.getIssuer(cert).organizationName, true); + embed.addField('Certificate Subject', x509.getSubject(cert).commonName, true); + embed.addField('Certificate Expiration Date', moment(x509.parseCert(cert).notAfter).format('dddd, MMMM Do YYYY, h:mm:ss A'), true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + return embed; + }); + this.client.signale.log(embeds); + if (embeds.length === 1) return message.channel.createMessage({ embed: embeds[0] }); + return createPaginationEmbed(message, this.client, embeds, {}); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/cwg_delete.ts b/src/commands/cwg_delete.ts index cf58e16..f142d10 100644 --- a/src/commands/cwg_delete.ts +++ b/src/commands/cwg_delete.ts @@ -1,63 +1,61 @@ -import fs from 'fs-extra'; -import axios from 'axios'; -import { Message } from 'eris'; -import { Command, RichEmbed } from '../class'; -import { Client } from '..'; - -export default class CWG_Delete extends Command { - constructor(client: Client) { - super(client); - this.name = 'delete'; - this.description = 'Unbind a domain to the CWG'; - this.usage = `${this.client.config.prefix}cwg delete [Domain | Port]`; - this.permissions = { roles: ['525441307037007902'] }; - this.aliases = ['unbind']; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - if (!args[0]) return this.client.commands.get('help').run(message, ['cwg', this.name]); - const domain = await this.client.db.Domain.findOne({ $or: [{ domain: args[0] }, { port: Number(args[0]) || '' }] }); - if (!domain) return message.channel.createMessage(`***${this.client.stores.emojis.error} The domain or port you provided could not be found.***`); - const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Deleting domain...***`); - const embed = new RichEmbed(); - embed.setTitle('Domain Deletion'); - embed.addField('Account Username', domain.account.username, true); - embed.addField('Account ID', domain.account.userID, true); - embed.addField('Domain', domain.domain, true); - embed.addField('Port', String(domain.port), true); - embed.setFooter(this.client.user.username, this.client.user.avatarURL); - embed.setTimestamp(); - if (domain.domain.includes('cloud.libraryofcode.org')) { - const resultID = await axios({ - method: 'get', - url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records?name=${domain.domain}`, - headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, - }); - this.client.signale.debug(resultID.data); - if (resultID.data.result[0]) { - const recordID = resultID.data.result[0].id; - await axios({ - method: 'delete', - url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records/${recordID}`, - headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, - }); - } - } - try { - await fs.unlink(`/etc/nginx/sites-enabled/${domain.domain}`); - await fs.unlink(`/etc/nginx/sites-available/${domain.domain}`); - } catch (e) { this.client.signale.error(e); } - await this.client.db.Domain.deleteOne({ domain: domain.domain }); - await this.client.util.exec('systemctl reload nginx'); - edit.edit(`***${this.client.stores.emojis.success} Domain ${domain.domain} with port ${domain.port} has been successfully deleted.***`); - // @ts-ignore - this.client.createMessage('580950455581147146', { embed }); - // @ts-ignore - return this.client.getDMChannel(domain.account.userID).then((channel) => channel.createMessage({ embed })).catch(() => {}); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import fs from 'fs-extra'; +import axios from 'axios'; +import { Message } from 'eris'; +import { Command, RichEmbed } from '../class'; +import { Client } from '..'; + +export default class CWG_Delete extends Command { + constructor(client: Client) { + super(client); + this.name = 'delete'; + this.description = 'Unbind a domain to the CWG'; + this.usage = `${this.client.config.prefix}cwg delete [Domain | Port]`; + this.permissions = { roles: ['525441307037007902'] }; + this.aliases = ['unbind']; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, ['cwg', this.name]); + const domain = await this.client.db.Domain.findOne({ $or: [{ domain: args[0] }, { port: Number(args[0]) || '' }] }); + if (!domain) return message.channel.createMessage(`***${this.client.stores.emojis.error} The domain or port you provided could not be found.***`); + const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Deleting domain...***`); + const embed = new RichEmbed(); + embed.setTitle('Domain Deletion'); + embed.addField('Account Username', domain.account.username, true); + embed.addField('Account ID', domain.account.userID, true); + embed.addField('Domain', domain.domain, true); + embed.addField('Port', String(domain.port), true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + if (domain.domain.includes('cloud.libraryofcode.org')) { + const resultID = await axios({ + method: 'get', + url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records?name=${domain.domain}`, + headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, + }); + this.client.signale.debug(resultID.data); + if (resultID.data.result[0]) { + const recordID = resultID.data.result[0].id; + await axios({ + method: 'delete', + url: `https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records/${recordID}`, + headers: { Authorization: `Bearer ${this.client.config.cloudflare}` }, + }); + } + } + try { + await fs.unlink(`/etc/nginx/sites-enabled/${domain.domain}`); + await fs.unlink(`/etc/nginx/sites-available/${domain.domain}`); + } catch (e) { this.client.signale.error(e); } + await this.client.db.Domain.deleteOne({ domain: domain.domain }); + await this.client.util.exec('systemctl reload nginx'); + edit.edit(`***${this.client.stores.emojis.success} Domain ${domain.domain} with port ${domain.port} has been successfully deleted.***`); + this.client.createMessage('580950455581147146', { embed }); + return this.client.getDMChannel(domain.account.userID).then((channel) => channel.createMessage({ embed })).catch(() => {}); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/disk.ts b/src/commands/disk.ts index b1ac0d6..b53b45c 100644 --- a/src/commands/disk.ts +++ b/src/commands/disk.ts @@ -1,45 +1,43 @@ -import { Message } from 'eris'; -import moment from 'moment'; -import { Client } from '..'; -import { RichEmbed, Command } from '../class'; -import { dataConversion } from '../functions'; -// eslint-disable-next-line import/no-unresolved -import 'moment-precise-range-plugin'; - -export default class Disk extends Command { - constructor(client: Client) { - super(client); - this.name = 'disk'; - this.description = 'Checks the used disk space by a user'; - this.usage = `${this.client.config.prefix}disk [Username/User ID/Email]`; - this.permissions = { roles: ['446104438969466890'] }; - this.enabled = false; - } - - async run(message: Message, args: string[]) { - try { - if (!args[0]) return this.client.commands.get('help').run(message, [this.name]); - const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }, { emailAddress: args[0] }] }); - if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found***`); - if (account.root || args[0].includes('./')) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Permission denied***`); - const diskReply = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Fetching total disk size may up to 10 minutes. This message will edit when the disk size has been located.***`); - const start = Date.now(); - const result = await this.client.util.exec(`du -s ${account.homepath}`); - const end = Date.now(); - // @ts-ignore - const totalTime: string = moment.preciseDiff(start, end); - const embed = new RichEmbed(); - embed.setTitle('Disk Usage'); - embed.setColor('ff0000'); - embed.setDescription(result.split(/ +/g)[1]); - embed.addField('Result', dataConversion(Number(result.split(/ +/g)[0])), true); - embed.addField('Time taken', totalTime, true); - embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); - embed.setTimestamp(); - // @ts-ignore - return diskReply.edit({ content: '', embed }); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import { Message } from 'eris'; +import moment from 'moment'; +import { Client } from '..'; +import { RichEmbed, Command } from '../class'; +import { dataConversion } from '../functions'; +// eslint-disable-next-line import/no-unresolved +import 'moment-precise-range-plugin'; + +export default class Disk extends Command { + constructor(client: Client) { + super(client); + this.name = 'disk'; + this.description = 'Checks the used disk space by a user'; + this.usage = `${this.client.config.prefix}disk [Username/User ID/Email]`; + this.permissions = { roles: ['446104438969466890'] }; + this.enabled = false; + } + + async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, [this.name]); + const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }, { emailAddress: args[0] }] }); + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found***`); + if (account.root || args[0].includes('./')) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Permission denied***`); + const diskReply = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Fetching total disk size may up to 10 minutes. This message will edit when the disk size has been located.***`); + const start = Date.now(); + const result = await this.client.util.exec(`du -s ${account.homepath}`); + const end = Date.now(); + const totalTime: string = moment.preciseDiff(start, end); + const embed = new RichEmbed(); + embed.setTitle('Disk Usage'); + embed.setColor('ff0000'); + embed.setDescription(result.split(/ +/g)[1]); + embed.addField('Result', dataConversion(Number(result.split(/ +/g)[0])), true); + embed.addField('Time taken', totalTime, true); + embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + embed.setTimestamp(); + return diskReply.edit({ content: '', embed }); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/help.ts b/src/commands/help.ts index 6ad81aa..ac915e1 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,70 +1,67 @@ -import { Message } from 'eris'; -import { createPaginationEmbed } from 'eris-pagination'; -import { Client } from '..'; -import { Command, RichEmbed } from '../class'; - -export default class Help extends Command { - constructor(client: Client) { - super(client); - this.name = 'help'; - this.description = 'Display a list of commands'; - this.usage = `${this.client.config.prefix}help | ${this.client.config.prefix}help ping`; - this.aliases = ['commands']; - this.enabled = true; - } - - // eslint-disable-next-line consistent-return - public async run(message: Message, args?: string[]) { - try { - if (!args[0]) { - const cmdList: Command[] = []; - this.client.commands.forEach((c) => cmdList.push(c)); - const commands = this.client.commands.map((c) => { - const aliases = c.aliases.map((alias) => `${this.client.config.prefix}${alias}`).join(', '); - const perms: string[] = []; - let allowedRoles = c.permissions && c.permissions.roles && c.permissions.roles.map((r) => `<@&${r}>`).join(', '); - if (allowedRoles) { allowedRoles = `**Roles:** ${allowedRoles}`; perms.push(allowedRoles); } - let allowedUsers = c.permissions && c.permissions.users && c.permissions.users.map((u) => `<@${u}>`).join(', '); - if (allowedUsers) { allowedUsers = `**Users:** ${allowedUsers}`; perms.push(allowedUsers); } - const displayedPerms = perms.length ? `**Permissions:**\n${perms.join('\n')}` : ''; - return { name: `${this.client.config.prefix}${c.name}`, value: `**Description:** ${c.description}\n**Aliases:** ${aliases}\n**Usage:** ${c.usage}\n${displayedPerms}`, inline: false }; - }); - - const splitCommands = this.client.util.splitFields(commands); - const cmdPages: RichEmbed[] = []; - splitCommands.forEach((splitCmd) => { - const embed = new RichEmbed(); - embed.setTimestamp(); embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); - embed.setAuthor(`${this.client.user.username}#${this.client.user.discriminator}`, this.client.user.avatarURL); - embed.setDescription(`Command list for ${this.client.user.username}`); - splitCmd.forEach((c) => embed.addField(c.name, c.value, c.inline)); - return cmdPages.push(embed); - }); - // @ts-ignore - if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] }); - // @ts-ignore - return createPaginationEmbed(message, this.client, cmdPages); - } - const resolved = await this.client.util.resolveCommand(args, message); - if (!resolved) return message.channel.createMessage(`${this.client.stores.emojis.error} **Command not found!**`); - const { cmd } = resolved; - const perms: string[] = []; - let allowedRoles = cmd.permissions && cmd.permissions.roles && cmd.permissions.roles.map((r) => `<@&${r}>`).join(', '); - if (allowedRoles) { allowedRoles = `**Roles:** ${allowedRoles}`; perms.push(allowedRoles); } - let allowedUsers = cmd.permissions && cmd.permissions.users && cmd.permissions.users.map((u) => `<@${u}>`).join(', '); - if (allowedUsers) { allowedUsers = `**Users:** ${allowedUsers}`; perms.push(allowedUsers); } - const displayedPerms = perms.length ? `\n**Permissions:**\n${perms.join('\n')}` : ''; - const aliases = cmd.aliases.length ? `\n**Aliases:** ${cmd.aliases.map((alias) => `${this.client.config.prefix}${cmd.parentName ? `${cmd.parentName} ` : ''}${alias}`).join(', ')}` : ''; - const subcommands = cmd.subcommands.size ? `\n**Subcommands:** ${cmd.subcommands.map((s) => `${cmd.name} ${s.name}`).join(', ')}` : ''; - const embed = new RichEmbed(); - embed.setTimestamp(); embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); - embed.setTitle(`${this.client.config.prefix}${cmd.parentName ? `${cmd.parentName}${cmd.name}` : cmd.name}`); embed.setAuthor(`${this.client.user.username}#${this.client.user.discriminator}`, this.client.user.avatarURL); - const description = `**Description**: ${cmd.description}\n**Usage:** ${cmd.usage}${aliases}${displayedPerms}${subcommands}`; - embed.setDescription(description); - // @ts-ignore - message.channel.createMessage({ embed }); - } catch (error) { - this.client.util.handleError(error, message, this); - } - } -} +import { Message } from 'eris'; +import { createPaginationEmbed } from 'eris-pagination'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; + +export default class Help extends Command { + constructor(client: Client) { + super(client); + this.name = 'help'; + this.description = 'Display a list of commands'; + this.usage = `${this.client.config.prefix}help | ${this.client.config.prefix}help ping`; + this.aliases = ['commands']; + this.enabled = true; + } + + // eslint-disable-next-line consistent-return + public async run(message: Message, args?: string[]) { + try { + if (!args[0]) { + const cmdList: Command[] = []; + this.client.commands.forEach((c) => cmdList.push(c)); + const commands = this.client.commands.map((c) => { + const aliases = c.aliases.map((alias) => `${this.client.config.prefix}${alias}`).join(', '); + const perms: string[] = []; + let allowedRoles = c.permissions && c.permissions.roles && c.permissions.roles.map((r) => `<@&${r}>`).join(', '); + if (allowedRoles) { allowedRoles = `**Roles:** ${allowedRoles}`; perms.push(allowedRoles); } + let allowedUsers = c.permissions && c.permissions.users && c.permissions.users.map((u) => `<@${u}>`).join(', '); + if (allowedUsers) { allowedUsers = `**Users:** ${allowedUsers}`; perms.push(allowedUsers); } + const displayedPerms = perms.length ? `**Permissions:**\n${perms.join('\n')}` : ''; + return { name: `${this.client.config.prefix}${c.name}`, value: `**Description:** ${c.description}\n**Aliases:** ${aliases}\n**Usage:** ${c.usage}\n${displayedPerms}`, inline: false }; + }); + + const splitCommands = this.client.util.splitFields(commands); + const cmdPages: RichEmbed[] = []; + splitCommands.forEach((splitCmd) => { + const embed = new RichEmbed(); + embed.setTimestamp(); embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + embed.setAuthor(`${this.client.user.username}#${this.client.user.discriminator}`, this.client.user.avatarURL); + embed.setDescription(`Command list for ${this.client.user.username}`); + splitCmd.forEach((c) => embed.addField(c.name, c.value, c.inline)); + return cmdPages.push(embed); + }); + if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] }); + return createPaginationEmbed(message, this.client, cmdPages); + } + const resolved = await this.client.util.resolveCommand(args, message); + if (!resolved) return message.channel.createMessage(`${this.client.stores.emojis.error} **Command not found!**`); + const { cmd } = resolved; + const perms: string[] = []; + let allowedRoles = cmd.permissions && cmd.permissions.roles && cmd.permissions.roles.map((r) => `<@&${r}>`).join(', '); + if (allowedRoles) { allowedRoles = `**Roles:** ${allowedRoles}`; perms.push(allowedRoles); } + let allowedUsers = cmd.permissions && cmd.permissions.users && cmd.permissions.users.map((u) => `<@${u}>`).join(', '); + if (allowedUsers) { allowedUsers = `**Users:** ${allowedUsers}`; perms.push(allowedUsers); } + const displayedPerms = perms.length ? `\n**Permissions:**\n${perms.join('\n')}` : ''; + const aliases = cmd.aliases.length ? `\n**Aliases:** ${cmd.aliases.map((alias) => `${this.client.config.prefix}${cmd.parentName ? `${cmd.parentName} ` : ''}${alias}`).join(', ')}` : ''; + const subcommands = cmd.subcommands.size ? `\n**Subcommands:** ${cmd.subcommands.map((s) => `${cmd.name} ${s.name}`).join(', ')}` : ''; + const embed = new RichEmbed(); + embed.setTimestamp(); embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + embed.setTitle(`${this.client.config.prefix}${cmd.parentName ? `${cmd.parentName}${cmd.name}` : cmd.name}`); embed.setAuthor(`${this.client.user.username}#${this.client.user.discriminator}`, this.client.user.avatarURL); + const description = `**Description**: ${cmd.description}\n**Usage:** ${cmd.usage}${aliases}${displayedPerms}${subcommands}`; + embed.setDescription(description); + message.channel.createMessage({ embed }); + } catch (error) { + this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/lock.ts b/src/commands/lock.ts index b85ec33..245013a 100644 --- a/src/commands/lock.ts +++ b/src/commands/lock.ts @@ -1,54 +1,55 @@ -import moment from 'moment'; -import { Message } from 'eris'; -import { Client } from '..'; -import { Command } from '../class'; - -export default class Lock extends Command { - constructor(client: Client) { - super(client); - this.name = 'lock'; - this.description = 'Locks an account.'; - this.permissions = { roles: ['455972169449734144', '662163685439045632'] }; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { // eslint-disable-line - try { - if (!args.length) return this.client.commands.get('help').run(message, [this.name]); - const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0].replace(/[<@!>]/gi, '') }] }); - if (!account) return message.channel.createMessage(`***${this.client.stores.emojis.error} Cannot find user.***`); - if (account.locked) return message.channel.createMessage(`***${this.client.stores.emojis.error} This account is already locked.***`); - const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Locking account...***`); - if (account.username === 'matthew' || account.root) return edit.edit(`***${this.client.stores.emojis.error} Permission denied.***`); - await this.client.util.exec(`lock ${account.username}`); - await account.updateOne({ locked: true }); - - const expiry = new Date(); - const lockLength = args[1].match(/[a-z]+|[^a-z]+/gi); - // @ts-ignore - const momentMilliseconds = moment.duration(Number(lockLength[0]), lockLength[1]).asMilliseconds(); - const reason = momentMilliseconds ? args.slice(2).join(' ') : args.slice(1).join(' '); - - await this.client.util.createModerationLog(account.userID, message.member, 2, reason, momentMilliseconds); - edit.edit(`***${this.client.stores.emojis.success} Account ${account.username} has been locked by Moderator ${message.author.username}#${message.author.discriminator}.***`); - message.delete(); - - this.client.util.transport.sendMail({ - to: account.emailAddress, - from: 'Library of Code sp-us | Cloud Services ', - subject: 'Your account has been locked', - html: ` -

Library of Code | Cloud Services

-

Your Cloud Account has been locked until ${momentMilliseconds ? moment(expiry).calendar() : 'indefinitely'} under the EULA.

-

Reason: ${momentMilliseconds ? args.slice(2).join(' ') : args.slice(1).join(' ')}

-

Supervisor: ${message.author.username}

-

Expiration: ${momentMilliseconds ? moment(expiry).format('dddd, MMMM Do YYYY, h:mm:ss A') : 'N/A'}

- - Library of Code sp-us | Support Team - `, - }); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import moment, { unitOfTime } from 'moment'; +import { Message } from 'eris'; +import { Client } from '..'; +import { Command } from '../class'; + +export default class Lock extends Command { + constructor(client: Client) { + super(client); + this.name = 'lock'; + this.description = 'Locks an account.'; + this.permissions = { roles: ['455972169449734144', '662163685439045632'] }; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { // eslint-disable-line + try { + if (!args.length) return this.client.commands.get('help').run(message, [this.name]); + const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0].replace(/[<@!>]/gi, '') }] }); + if (!account) return message.channel.createMessage(`***${this.client.stores.emojis.error} Cannot find user.***`); + if (account.locked) return message.channel.createMessage(`***${this.client.stores.emojis.error} This account is already locked.***`); + const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Locking account...***`); + if (account.username === 'matthew' || account.root) return edit.edit(`***${this.client.stores.emojis.error} Permission denied.***`); + await this.client.util.exec(`lock ${account.username}`); + await account.updateOne({ locked: true }); + + const expiry = new Date(); + const lockLength = args[1].match(/[a-z]+|[^a-z]+/gi); + const length = Number(lockLength[0]); + const unit = lockLength[1] as unitOfTime.Base; + const momentMilliseconds = moment.duration(length, unit).asMilliseconds(); + const reason = momentMilliseconds ? args.slice(2).join(' ') : args.slice(1).join(' '); + + await this.client.util.createModerationLog(account.userID, message.member, 2, reason, momentMilliseconds); + edit.edit(`***${this.client.stores.emojis.success} Account ${account.username} has been locked by Moderator ${message.author.username}#${message.author.discriminator}.***`); + message.delete(); + + this.client.util.transport.sendMail({ + to: account.emailAddress, + from: 'Library of Code sp-us | Cloud Services ', + subject: 'Your account has been locked', + html: ` +

Library of Code | Cloud Services

+

Your Cloud Account has been locked until ${momentMilliseconds ? moment(expiry).calendar() : 'indefinitely'} under the EULA.

+

Reason: ${momentMilliseconds ? args.slice(2).join(' ') : args.slice(1).join(' ')}

+

Supervisor: ${message.author.username}

+

Expiration: ${momentMilliseconds ? moment(expiry).format('dddd, MMMM Do YYYY, h:mm:ss A') : 'N/A'}

+ + Library of Code sp-us | Support Team + `, + }); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/modlogs.ts b/src/commands/modlogs.ts index dc23c45..fd974ed 100644 --- a/src/commands/modlogs.ts +++ b/src/commands/modlogs.ts @@ -1,68 +1,65 @@ -import { Message } from 'eris'; -// eslint-disable-next-line import/no-unresolved -import { createPaginationEmbed } from 'eris-pagination'; -import { Client } from '..'; -import { Command, RichEmbed } from '../class'; - -export default class Modlogs extends Command { - constructor(client: Client) { - super(client); - this.name = 'modlogs'; - this.description = 'Check a user\'s Cloud Modlogs'; - this.aliases = ['infractions', 'modlog']; - this.enabled = true; - this.permissions = { roles: ['446104438969466890'] }; - } - - public async run(message: Message, args: string[]) { - try { - if (!args.length) return this.client.commands.get('help').run(message, [this.name]); - const msg: Message = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Locating modlogs...***`); - const query = await this.client.db.Moderation.find({ $or: [{ username: args.join(' ') }, { userID: args.filter((a) => a)[0].replace(/[<@!>]/g, '') }] }); - if (!query.length) return msg.edit(`***${this.client.stores.emojis.error} Cannot locate modlogs for ${args.join(' ')}***`); - - // @ts-ignore - const formatted = query.sort((a, b) => a.date - b.date).map((log) => { - const { username, moderatorID, reason, type, date, logID } = log; - let name: string; - switch (type) { - default: name = 'Generic'; break; - case 0: name = 'Create'; break; - case 1: name = 'Warn'; break; - case 2: name = 'Lock'; break; - case 3: name = 'Unlock'; break; - case 4: name = 'Delete'; break; - } - const value = `**ID:** ${logID}\n**Account name:** ${username}\n**Moderator:** <@${moderatorID}>\n**Reason:** ${reason || 'Not supplied'}\n**Date:** ${date.toLocaleString('en-us')} EST`; - const inline = true; - return { name, value, inline }; - }); - const users = [...new Set(query.map((log) => log.userID))].map((u) => `<@${u}>`); - - const logs = this.client.util.splitFields(formatted); - - const embeds = logs.map((l) => { - const embed = new RichEmbed(); - embed.setDescription(`List of Cloud moderation logs for ${users.join(', ')}`); - embed.setAuthor('Library of Code | Cloud Services', this.client.user.avatarURL, 'https://libraryofcode.org/'); - embed.setTitle('Cloud Modlogs/Infractions'); - embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); - l.forEach((f) => embed.addField(f.name, f.value, f.inline)); - embed.setTimestamp(); - embed.setColor(3447003); - return embed; - }); - - if (embeds.length === 1) { - // @ts-ignore - msg.edit({ content: '', embed: embeds[0] }); - } else { - // @ts-ignore - createPaginationEmbed(message, this.client, embeds, {}, msg); - } - return msg; - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import { Message } from 'eris'; +// eslint-disable-next-line import/no-unresolved +import { createPaginationEmbed } from 'eris-pagination'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; + +export default class Modlogs extends Command { + constructor(client: Client) { + super(client); + this.name = 'modlogs'; + this.description = 'Check a user\'s Cloud Modlogs'; + this.aliases = ['infractions', 'modlog']; + this.enabled = true; + this.permissions = { roles: ['446104438969466890'] }; + } + + public async run(message: Message, args: string[]) { + try { + if (!args.length) return this.client.commands.get('help').run(message, [this.name]); + const msg: Message = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Locating modlogs...***`); + const query = await this.client.db.Moderation.find({ $or: [{ username: args.join(' ') }, { userID: args.filter((a) => a)[0].replace(/[<@!>]/g, '') }] }); + if (!query.length) return msg.edit(`***${this.client.stores.emojis.error} Cannot locate modlogs for ${args.join(' ')}***`); + + const formatted = query.sort((a, b) => a.date.getTime() - b.date.getTime()).map((log) => { + const { username, moderatorID, reason, type, date, logID } = log; + let name: string; + switch (type) { + default: name = 'Generic'; break; + case 0: name = 'Create'; break; + case 1: name = 'Warn'; break; + case 2: name = 'Lock'; break; + case 3: name = 'Unlock'; break; + case 4: name = 'Delete'; break; + } + const value = `**ID:** ${logID}\n**Account name:** ${username}\n**Moderator:** <@${moderatorID}>\n**Reason:** ${reason || 'Not supplied'}\n**Date:** ${date.toLocaleString('en-us')} EST`; + const inline = true; + return { name, value, inline }; + }); + const users = [...new Set(query.map((log) => log.userID))].map((u) => `<@${u}>`); + + const logs = this.client.util.splitFields(formatted); + + const embeds = logs.map((l) => { + const embed = new RichEmbed(); + embed.setDescription(`List of Cloud moderation logs for ${users.join(', ')}`); + embed.setAuthor('Library of Code | Cloud Services', this.client.user.avatarURL, 'https://libraryofcode.org/'); + embed.setTitle('Cloud Modlogs/Infractions'); + embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + l.forEach((f) => embed.addField(f.name, f.value, f.inline)); + embed.setTimestamp(); + embed.setColor(3447003); + return embed; + }); + + if (embeds.length === 1) { + msg.edit({ content: '', embed: embeds[0] }); + } else { + createPaginationEmbed(message, this.client, embeds, {}, msg); + } + return msg; + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/notify.ts b/src/commands/notify.ts index ff3ff1e..c7b4cfd 100644 --- a/src/commands/notify.ts +++ b/src/commands/notify.ts @@ -1,53 +1,51 @@ -/* eslint-disable consistent-return */ -import { Message } from 'eris'; -import { Client } from '..'; -import { Command, RichEmbed } from '../class'; - -export default class Notify extends Command { - constructor(client: Client) { - super(client); - this.name = 'notify'; - this.description = 'Sends a notification to a user.'; - this.usage = `${this.client.config.prefix}notify [username | user ID]`; - this.permissions = { roles: ['446104438969466890'] }; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - if (!args.length) return this.client.commands.get('help').run(message, [this.name]); - const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Sending notification...***`); - const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0].replace(/[<@!>]/gi, '') }] }); - if (!account) return edit.edit(`***${this.client.stores.emojis.error} Cannot find user.***`); - const embed = new RichEmbed() - .setTitle('Cloud Account | Notification') - .setDescription(args.slice(1).join(' ')) - .addField('Moderator', `<@${message.author.id}>`, true) - .setFooter(this.client.user.username, this.client.user.avatarURL) - .setTimestamp(); - this.client.getDMChannel(account.userID).then((channel) => { - // @ts-ignore - channel.createMessage({ embed }); - }); - embed.addField('User', `${account.username} | <@${account.userID}>`, true); - // @ts-ignore - this.client.createMessage('580950455581147146', { embed }); - this.client.util.transport.sendMail({ - to: account.emailAddress, - from: 'Library of Code sp-us | Cloud Services ', - subject: 'Notification', - html: ` -

Library of Code sp-us | Cloud Services

-

${args.slice(1).join(' ')}

-

Moderator: ${message.author.username}

- - Library of Code sp-us | Support Team - `, - }); - message.delete(); - edit.edit(`***${this.client.stores.emojis.success} Send notification to ${account.username}.***`); - } catch (error) { - await this.client.util.handleError(error, message, this); - } - } -} +/* eslint-disable consistent-return */ +import { Message } from 'eris'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; + +export default class Notify extends Command { + constructor(client: Client) { + super(client); + this.name = 'notify'; + this.description = 'Sends a notification to a user.'; + this.usage = `${this.client.config.prefix}notify [username | user ID]`; + this.permissions = { roles: ['446104438969466890'] }; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + if (!args.length) return this.client.commands.get('help').run(message, [this.name]); + const edit = await message.channel.createMessage(`***${this.client.stores.emojis.loading} Sending notification...***`); + const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0].replace(/[<@!>]/gi, '') }] }); + if (!account) return edit.edit(`***${this.client.stores.emojis.error} Cannot find user.***`); + const embed = new RichEmbed() + .setTitle('Cloud Account | Notification') + .setDescription(args.slice(1).join(' ')) + .addField('Moderator', `<@${message.author.id}>`, true) + .setFooter(this.client.user.username, this.client.user.avatarURL) + .setTimestamp(); + this.client.getDMChannel(account.userID).then((channel) => { + channel.createMessage({ embed }); + }); + embed.addField('User', `${account.username} | <@${account.userID}>`, true); + this.client.createMessage('580950455581147146', { embed }); + this.client.util.transport.sendMail({ + to: account.emailAddress, + from: 'Library of Code sp-us | Cloud Services ', + subject: 'Notification', + html: ` +

Library of Code sp-us | Cloud Services

+

${args.slice(1).join(' ')}

+

Moderator: ${message.author.username}

+ + Library of Code sp-us | Support Team + `, + }); + message.delete(); + edit.edit(`***${this.client.stores.emojis.success} Send notification to ${account.username}.***`); + } catch (error) { + await this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/parse.ts b/src/commands/parse.ts index 2bd957b..f23f682 100644 --- a/src/commands/parse.ts +++ b/src/commands/parse.ts @@ -1,70 +1,69 @@ -import fs from 'fs-extra'; -import { parseCert } from '@ghaiklor/x509'; -import { Message } from 'eris'; -import { Client } from '..'; -import { Command, RichEmbed } from '../class'; -import { parseCertificate, Certificate } from '../functions'; - -export default class Parse extends Command { - constructor(client: Client) { - super(client); - this.name = 'parse'; - this.description = 'Gets information on a user\'s x509 certificate.'; - this.usage = `${this.client.config.prefix}parse [username || user ID]`; - this.permissions = { roles: ['446104438969466890'] }; - } - - public async run(message: Message, args: string[]) { // eslint-disable-line - try { - if (!args.length) return this.client.commands.get('help').run(message, [this.name]); - const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0].replace(/[<@!>]/gi, '') }] }); - if (!account) return message.channel.createMessage(`***${this.client.stores.emojis.error} Cannot find user.***`); - let dir: string[]; - try { - dir = await fs.readdir(`${account.homepath}/Validation`); - } catch (err) { - return message.channel.createMessage(`***${this.client.stores.emojis.error} Cannot locate Validation directory.***`); - } - if (!dir.length) return message.channel.createMessage(`***${this.client.stores.emojis.error} Cannot locate certificate.***`); - let cert: Certificate; - try { - cert = await parseCertificate(this.client, `${account.homepath}/Validation/${dir[0]}`); - } catch (error) { - if (error.message.includes('panic: Certificate PEM Encode == nil')) return message.channel.createMessage(`***${this.client.stores.emojis.error} Invalid certificate.***`); - } - // const cert = parseCert(`${account.homepath}/Validation/${dir[0]}`); - const subjectCommonName = cert.subject.commonName || 'Not Specified'; - const subjectEmailAddress = cert.subject.emailAddress || 'Not Specified'; - const subjectOrganization = cert.subject.organizationName || 'Not Specified'; - const subjectOrganizationalUnit = cert.subject.organizationalUnitName || 'Not Specified'; - const subjectCountry = cert.subject.countryName || 'Not Specified'; - const issuerCommonName = cert.issuer.commonName || 'Not Specified'; - const issuerEmailAddress = cert.issuer.emailAddress || 'Not Specified'; - const issuerOrganization = cert.issuer.organizationName || 'Not Specified'; - const issuerOrganizationalUnit = cert.issuer.organizationalUnitName || 'Not Specified'; - const issuerCountry = cert.issuer.countryName || 'Not Specified'; - const user = this.client.users.get(account.userID) || await this.client.getRESTUser(account.userID); - const embed = new RichEmbed(); - embed.setTitle('Parse x509 Certificate'); - embed.setDescription(`${account.homepath}/Validation/${dir[0]} | ${account.username} <@${user.id}>`); - embed.setColor(3447003); - embed.addField('Subject', `**Common Name:** ${subjectCommonName}\n**Email Address:** ${subjectEmailAddress}\n**Organization:** ${subjectOrganization}\n**Organizational Unit:** ${subjectOrganizationalUnit}\n**Country:** ${subjectCountry}`, true); - embed.addField('Issuer', `**Common Name:** ${issuerCommonName}\n**Email Address:** ${issuerEmailAddress}\n**Organization:** ${issuerOrganization}\n**Organizational Unit:** ${issuerOrganizationalUnit}\n**Country:** ${issuerCountry}`, true); - embed.addField('Serial Number', cert.serial, true); - embed.addField('Fingerprint', cert.fingerPrint, true); - embed.addField('Signature Algorithm', cert.signatureAlgorithm, true); - embed.addField('Public Key Algorithm', cert.publicKeyAlgorithm, true); - embed.addField('Key Usage', cert.extensions.keyUsage, true); - embed.addField('Extended Key Usage', cert.extensions.extendedKeyUsage.join(', '), true); - embed.addField('Policies', cert.extensions.certificatePolicies.join(', '), true); - embed.addField('Issued On', new Date(cert.notBefore).toLocaleString('en-us'), true); - embed.addField('Expires On', new Date(cert.notAfter).toLocaleString('en-us'), true); - embed.setFooter(this.client.user.username, this.client.user.avatarURL); - embed.setTimestamp(); - // @ts-ignore - message.channel.createMessage({ embed }); - } catch (error) { - await this.client.util.handleError(error, message, this); - } - } -} +import fs from 'fs-extra'; +import { parseCert } from '@ghaiklor/x509'; +import { Message } from 'eris'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; +import { parseCertificate, Certificate } from '../functions'; + +export default class Parse extends Command { + constructor(client: Client) { + super(client); + this.name = 'parse'; + this.description = 'Gets information on a user\'s x509 certificate.'; + this.usage = `${this.client.config.prefix}parse [username || user ID]`; + this.permissions = { roles: ['446104438969466890'] }; + } + + public async run(message: Message, args: string[]) { // eslint-disable-line + try { + if (!args.length) return this.client.commands.get('help').run(message, [this.name]); + const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0].replace(/[<@!>]/gi, '') }] }); + if (!account) return message.channel.createMessage(`***${this.client.stores.emojis.error} Cannot find user.***`); + let dir: string[]; + try { + dir = await fs.readdir(`${account.homepath}/Validation`); + } catch (err) { + return message.channel.createMessage(`***${this.client.stores.emojis.error} Cannot locate Validation directory.***`); + } + if (!dir.length) return message.channel.createMessage(`***${this.client.stores.emojis.error} Cannot locate certificate.***`); + let cert: Certificate; + try { + cert = await parseCertificate(this.client, `${account.homepath}/Validation/${dir[0]}`); + } catch (error) { + if (error.message.includes('panic: Certificate PEM Encode == nil')) return message.channel.createMessage(`***${this.client.stores.emojis.error} Invalid certificate.***`); + } + // const cert = parseCert(`${account.homepath}/Validation/${dir[0]}`); + const subjectCommonName = cert.subject.commonName || 'Not Specified'; + const subjectEmailAddress = cert.subject.emailAddress || 'Not Specified'; + const subjectOrganization = cert.subject.organizationName || 'Not Specified'; + const subjectOrganizationalUnit = cert.subject.organizationalUnitName || 'Not Specified'; + const subjectCountry = cert.subject.countryName || 'Not Specified'; + const issuerCommonName = cert.issuer.commonName || 'Not Specified'; + const issuerEmailAddress = cert.issuer.emailAddress || 'Not Specified'; + const issuerOrganization = cert.issuer.organizationName || 'Not Specified'; + const issuerOrganizationalUnit = cert.issuer.organizationalUnitName || 'Not Specified'; + const issuerCountry = cert.issuer.countryName || 'Not Specified'; + const user = this.client.users.get(account.userID) || await this.client.getRESTUser(account.userID); + const embed = new RichEmbed(); + embed.setTitle('Parse x509 Certificate'); + embed.setDescription(`${account.homepath}/Validation/${dir[0]} | ${account.username} <@${user.id}>`); + embed.setColor(3447003); + embed.addField('Subject', `**Common Name:** ${subjectCommonName}\n**Email Address:** ${subjectEmailAddress}\n**Organization:** ${subjectOrganization}\n**Organizational Unit:** ${subjectOrganizationalUnit}\n**Country:** ${subjectCountry}`, true); + embed.addField('Issuer', `**Common Name:** ${issuerCommonName}\n**Email Address:** ${issuerEmailAddress}\n**Organization:** ${issuerOrganization}\n**Organizational Unit:** ${issuerOrganizationalUnit}\n**Country:** ${issuerCountry}`, true); + embed.addField('Serial Number', cert.serial, true); + embed.addField('Fingerprint', cert.fingerPrint, true); + embed.addField('Signature Algorithm', cert.signatureAlgorithm, true); + embed.addField('Public Key Algorithm', cert.publicKeyAlgorithm, true); + embed.addField('Key Usage', cert.extensions.keyUsage, true); + embed.addField('Extended Key Usage', cert.extensions.extendedKeyUsage.join(', '), true); + embed.addField('Policies', cert.extensions.certificatePolicies.join(', '), true); + embed.addField('Issued On', new Date(cert.notBefore).toLocaleString('en-us'), true); + embed.addField('Expires On', new Date(cert.notAfter).toLocaleString('en-us'), true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + message.channel.createMessage({ embed }); + } catch (error) { + await this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/parseall.ts b/src/commands/parseall.ts index 65e1531..7e1164a 100644 --- a/src/commands/parseall.ts +++ b/src/commands/parseall.ts @@ -1,75 +1,72 @@ -import { parseCert } from '@ghaiklor/x509'; -import { Message } from 'eris'; -import { readdirSync } from 'fs'; -import moment from 'moment'; -import { Client } from '..'; -import { Command, RichEmbed } from '../class'; -import { parseCertificate, Certificate } from '../functions'; - -export default class Parseall extends Command { - constructor(client: Client) { - super(client); - - this.name = 'parseall'; - this.description = 'Displays certificate validation for all accounts'; - this.usage = `${this.client.config.prefix}parseall`; - this.permissions = { roles: ['446104438969466890'] }; - this.aliases = ['checkcerts', 'verifyall', 'verifycerts']; - } - - public async run(message: Message, args: string[]) { - try { - const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Loading...***`); - const embed = new RichEmbed(); - embed.setTitle('Certificate Validation'); - embed.setAuthor(this.client.user.username, this.client.user.avatarURL); - embed.setFooter(`Requested by ${message.member.username}#${message.member.discriminator}`, message.member.avatarURL); - embed.setTimestamp(); - const search = await this.client.db.Account.find(); - - const files = search.map((acc) => { - let certfile: string; - try { certfile = readdirSync(`${acc.homepath}/Validation`)[0] } catch (error) { if (error.message.includes('no such file or directory') || error.message.includes('File doesn\'t exist.')) certfile = 'not_found.crt' } // eslint-disable-line - return `${acc.homepath}/Validation/${certfile}`; - }); - - // @ts-ignore - const parsed: ({ status: 'fulfilled', value: Certificate } | { status: 'rejected', reason: Error })[] = await Promise.allSettled(files.map((c) => parseCertificate(this.client, c))); - - const final: string[] = await Promise.all(search.map(async (a) => { - const result = parsed[search.findIndex((acc) => acc === a)]; - if (result.status === 'rejected') { - if (result.reason.message.includes('no such file or directory') || result.reason.message.includes('File doesn\'t exist.')) return `${this.client.stores.emojis.error} **${a.username}** Unable to locate certificate`; - if (result.reason.message.includes('panic: Certificate PEM Encode == nil')) return `${this.client.stores.emojis.error} **${a.username}** Invalid certificate`; - throw result.reason; - } - const { notAfter } = result.value; - // @ts-ignore - const timeObject: {years: number, months: number, days: number, hours: number, minutes: number, seconds: number, firstDateWasLater: boolean} = moment.preciseDiff(new Date(), notAfter, true); - const precise: [number, string][] = []; - // @ts-ignore - const timeArray: number[] = Object.values(timeObject).filter((v) => typeof v === 'number'); - timeArray.forEach((t) => { // eslint-disable-line - const index = timeArray.indexOf(t); - const measurements = ['yr', 'mo', 'd', 'h', 'm', 's']; - precise.push([t, measurements[index]]); - }); - const time = precise.filter((n) => n[0]).map(((v) => v.join(''))).join(', '); - - if (notAfter < new Date()) return `${this.client.stores.emojis.error} **${a.username}** Expired ${time} ago`; - return `${this.client.stores.emojis.success} **${a.username}** Expires in ${time}`; - })); - - if (final.join('\n').length < 2048) embed.setDescription(final.join('\n')); - else { - const split = this.client.util.splitString(final.join('\n'), 1024); - split.forEach((s) => embed.addField('\u200B', s)); - } - - // @ts-ignore - return await msg.edit({ content: '', embed }); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import { parseCert } from '@ghaiklor/x509'; +import { Message } from 'eris'; +import { readdirSync } from 'fs'; +import moment from 'moment'; +import 'moment-precise-range-plugin'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; +import { parseCertificate, Certificate } from '../functions'; + +export default class Parseall extends Command { + constructor(client: Client) { + super(client); + + this.name = 'parseall'; + this.description = 'Displays certificate validation for all accounts'; + this.usage = `${this.client.config.prefix}parseall`; + this.permissions = { roles: ['446104438969466890'] }; + this.aliases = ['checkcerts', 'verifyall', 'verifycerts']; + } + + public async run(message: Message, args: string[]) { + try { + const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Loading...***`); + const embed = new RichEmbed(); + embed.setTitle('Certificate Validation'); + embed.setAuthor(this.client.user.username, this.client.user.avatarURL); + embed.setFooter(`Requested by ${message.member.username}#${message.member.discriminator}`, message.member.avatarURL); + embed.setTimestamp(); + const search = await this.client.db.Account.find(); + + const files = search.map((acc) => { + let certfile: string; + try { certfile = readdirSync(`${acc.homepath}/Validation`)[0] } catch (error) { if (error.message.includes('no such file or directory') || error.message.includes('File doesn\'t exist.')) certfile = 'not_found.crt' } // eslint-disable-line + return `${acc.homepath}/Validation/${certfile}`; + }); + + const parsed = await Promise.allSettled(files.map((c) => parseCertificate(this.client, c))); + + const final: string[] = await Promise.all(search.map(async (a) => { + const result = parsed[search.findIndex((acc) => acc === a)]; + if (result.status === 'rejected') { + if (result.reason.message.includes('no such file or directory') || result.reason.message.includes('File doesn\'t exist.')) return `${this.client.stores.emojis.error} **${a.username}** Unable to locate certificate`; + if (result.reason.message.includes('panic: Certificate PEM Encode == nil')) return `${this.client.stores.emojis.error} **${a.username}** Invalid certificate`; + throw result.reason; + } + const { notAfter } = result.value; + const timeObject = moment.preciseDiff(new Date(), notAfter, true); + const precise: [number, string][] = []; + const timeArray: number[] = Object.values(timeObject).filter((v) => typeof v === 'number'); + timeArray.forEach((t) => { // eslint-disable-line + const index = timeArray.indexOf(t); + const measurements = ['yr', 'mo', 'd', 'h', 'm', 's']; + precise.push([t, measurements[index]]); + }); + const time = precise.filter((n) => n[0]).map(((v) => v.join(''))).join(', '); + + if (notAfter < new Date()) return `${this.client.stores.emojis.error} **${a.username}** Expired ${time} ago`; + return `${this.client.stores.emojis.success} **${a.username}** Expires in ${time}`; + })); + + if (final.join('\n').length < 2048) embed.setDescription(final.join('\n')); + else { + const split = this.client.util.splitString(final.join('\n'), 1024); + split.forEach((s) => embed.addField('\u200B', s)); + } + + return await msg.edit({ content: '', embed }); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/securesign_account.ts b/src/commands/securesign_account.ts index 64b4ed6..0ec1efc 100644 --- a/src/commands/securesign_account.ts +++ b/src/commands/securesign_account.ts @@ -1,47 +1,46 @@ -import { Message, PrivateChannel } from 'eris'; -import { Client } from '..'; -import { Command, RichEmbed } from '../class'; -import { AccountInterface } from '../models'; - -export default class SecureSign_Account extends Command { - constructor(client: Client) { - super(client); - this.name = 'account'; - this.description = 'Provides SecureSign account details for currently logged in user'; - this.usage = `${this.client.config.prefix}securesign account`; - this.enabled = true; - this.guildOnly = false; - } - - public async run(message: Message, args: string[]) { - try { - const user = await this.client.db.Account.findOne({ userID: message.author.id }); - if (!user || (!user.permissions.staff && !(message.channel instanceof PrivateChannel))) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Run this command in your DMs!***`); - - let account: AccountInterface; - if (!args[0] || !user.permissions.staff) account = user; - else account = await this.client.db.Account.findOne({ $or: [{ userID: args[0] }, { username: args[0] }, { emailAddress: args[0] }] }); - - if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found***`); - if (!account.hash) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not initialized***`); - const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Loading account details...***`); - - const details = await this.client.util.exec(`sudo -H -u ${account.username} bash -c 'securesign-canary account'`); - const info = details.replace(/^\s+|\s+$/g, '').replace(/\n/g, '\n**').replace(/: /g, ':** ').split('\n'); - const title = info.shift(); - const description = info.join('\n'); - const content = ''; - - const embed = new RichEmbed(); - embed.setTitle(title); - embed.setDescription(description); - embed.setAuthor(this.client.user.username, this.client.user.avatarURL); - embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); - - // @ts-ignore - return msg.edit({ content, embed }); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import { Message, PrivateChannel } from 'eris'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; +import { AccountInterface } from '../models'; + +export default class SecureSign_Account extends Command { + constructor(client: Client) { + super(client); + this.name = 'account'; + this.description = 'Provides SecureSign account details for currently logged in user'; + this.usage = `${this.client.config.prefix}securesign account`; + this.enabled = true; + this.guildOnly = false; + } + + public async run(message: Message, args: string[]) { + try { + const user = await this.client.db.Account.findOne({ userID: message.author.id }); + if (!user || (!user.permissions.staff && !(message.channel instanceof PrivateChannel))) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Run this command in your DMs!***`); + + let account: AccountInterface; + if (!args[0] || !user.permissions.staff) account = user; + else account = await this.client.db.Account.findOne({ $or: [{ userID: args[0] }, { username: args[0] }, { emailAddress: args[0] }] }); + + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found***`); + if (!account.hash) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not initialized***`); + const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Loading account details...***`); + + const details = await this.client.util.exec(`sudo -H -u ${account.username} bash -c 'securesign-canary account'`); + const info = details.replace(/^\s+|\s+$/g, '').replace(/\n/g, '\n**').replace(/: /g, ':** ').split('\n'); + const title = info.shift(); + const description = info.join('\n'); + const content = ''; + + const embed = new RichEmbed(); + embed.setTitle(title); + embed.setDescription(description); + embed.setAuthor(this.client.user.username, this.client.user.avatarURL); + embed.setFooter(`Requested by ${message.author.username}#${message.author.discriminator}`, message.author.avatarURL); + + return msg.edit({ content, embed }); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/securesign_build.ts b/src/commands/securesign_build.ts index fb28b3e..48f5d2a 100644 --- a/src/commands/securesign_build.ts +++ b/src/commands/securesign_build.ts @@ -1,36 +1,35 @@ -import { Message } from 'eris'; -import { Client } from '..'; -import { Command, RichEmbed } from '../class'; - -export default class SecureSign_Build extends Command { - constructor(client: Client) { - super(client); - this.name = 'build'; - this.description = 'Shows information about the current build of the CLI'; - this.usage = `${this.client.config.prefix}securesign build`; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Loading build information...***`); - - const build = await this.client.util.exec("sudo -H -u root bash -c 'securesign-canary build'"); - const info = build.replace(/^\s+|\s+$/g, '').replace(/\n/g, '\n**').replace(/: /g, ':** ').split('\n'); - const title = info.shift(); - const description = info.join('\n'); - const content = ''; - - const embed = new RichEmbed(); - embed.setTitle(title); - embed.setDescription(description); - embed.setAuthor(this.client.user.username, this.client.user.avatarURL); - embed.setFooter(`Requested by ${message.member.username}#${message.member.discriminator}`, message.member.avatarURL); - - // @ts-ignore - msg.edit({ content, embed }); - } catch (error) { - this.client.util.handleError(error, message, this); - } - } -} +import { Message } from 'eris'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; + +export default class SecureSign_Build extends Command { + constructor(client: Client) { + super(client); + this.name = 'build'; + this.description = 'Shows information about the current build of the CLI'; + this.usage = `${this.client.config.prefix}securesign build`; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Loading build information...***`); + + const build = await this.client.util.exec("sudo -H -u root bash -c 'securesign-canary build'"); + const info = build.replace(/^\s+|\s+$/g, '').replace(/\n/g, '\n**').replace(/: /g, ':** ').split('\n'); + const title = info.shift(); + const description = info.join('\n'); + const content = ''; + + const embed = new RichEmbed(); + embed.setTitle(title); + embed.setDescription(description); + embed.setAuthor(this.client.user.username, this.client.user.avatarURL); + embed.setFooter(`Requested by ${message.member.username}#${message.member.discriminator}`, message.member.avatarURL); + + msg.edit({ content, embed }); + } catch (error) { + this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/securesign_createcrt.ts b/src/commands/securesign_createcrt.ts index 80e5d8a..85052db 100644 --- a/src/commands/securesign_createcrt.ts +++ b/src/commands/securesign_createcrt.ts @@ -1,65 +1,64 @@ -import { Message, PrivateChannel, TextChannel } from 'eris'; -import axios from 'axios'; -import { Client } from '..'; -import { Command } from '../class'; - -export default class SecureSign_Init extends Command { - constructor(client: Client) { - super(client); - this.name = 'createcrt'; - this.description = 'Creates a new certificate'; - this.usage = `${this.client.config.prefix}securesign createcrt [-s sign] [-c class] [-m digest]\n\`sign\`: Sign type (ecc/rsa)\n\`class\`: Certificate Class (1/2/3)\n\`digest\`: SHA Digest (256/384/512)`; - this.enabled = true; - this.guildOnly = false; - } - - public async run(message: Message, args: string[]) { - try { - const account = await this.client.db.Account.findOne({ userID: message.author.id }); - if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found***`); - if (!account.hash) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not initialized***`); - - // @ts-ignore - const options: { s?: string, c?: string, m?: string } = args.length ? Object.fromEntries(` ${args.join(' ')}`.split(' -').filter((a) => a).map((a) => a.split(/ (.+)/)).filter((a) => a.length > 1)) : {}; - if (options.s && options.s.toLowerCase() !== 'ecc' && options.s.toLowerCase() !== 'rsa') return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid signing type, choose between \`ecc\` or \`rsa\``); - if (options.c && (!Number(options.c) || Number(options.c) < 1 || Number(options.c) > 3)) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid class selected, choose between Class \`1\`, \`2\` or \`3\``); - if (options.m && (!Number(options.m) || (options.m !== '256' && options.m !== '384' && options.m !== '512'))) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid SHA Digest selected, choose between \`256\`, \`384\` or \`512\``); - if (Number(options.c) === 3 && (!options.s || options.s.toLowerCase() === 'ecc')) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Class 3 ECC certificates are not supported, please use the \`-s rsa\` option instead***`); - - const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Creating certificate...***`); - if (options.s) options.s = options.s.toLowerCase(); - const hash = this.client.util.getAcctHash(account.homepath); - - // Check if they can generate certificate - try { - const { data } = await axios({ - method: 'GET', - url: 'https://api.securesign.org/account/details', - headers: { Authorization: hash, 'Content-Type': 'application/json' }, - }); - - const { total, allowed } = data.message; - if (total >= allowed) return msg.edit(`${this.client.stores.emojis.error} ***Not enough certificate allowances - please ask a member of staff to increase this limit from ${total}***`); - if (Number(options.c) > data.message.class) return msg.edit(`${this.client.stores.emojis.error} ***Class too low, you are on a class ${data.message.class} account***`); - } catch (error) { - const { code } = error.response.data; - if (code === 1001) { - await this.client.db.Account.updateOne({ userID: account.userID }, { $set: { hash: false } }); - this.client.getDMChannel(account.userID).then((channel) => channel.createMessage('Your SecureSign password has been reset - please reinitialize your SecureSign account')).catch(); - return msg.edit(`${this.client.stores.emojis.error} ***Authentication failed***`); - } - throw error; - } - - const execoptions = `${options.s ? ` -s ${options.s}` : ''}${options.c ? ` -c ${options.c}` : ''}${options.m ? ` -m ${options.m}` : ''}`; - const cmd = `sudo -H -u ${account.username} bash -c 'securesign-canary createcrt${execoptions}'`; - - const exec = await this.client.util.exec(cmd); - if (!exec.replace(/^\s+|\s+$/g, '').endsWith('Successfully wrote certificate.')) throw new Error(`Certificate generation did not complete successfully:\n${cmd}`); - - return msg.edit(`${this.client.stores.emojis.success} ***Successfully created certificate***`); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import { Message, PrivateChannel, TextChannel } from 'eris'; +import axios from 'axios'; +import { Client } from '..'; +import { Command } from '../class'; + +export default class SecureSign_Init extends Command { + constructor(client: Client) { + super(client); + this.name = 'createcrt'; + this.description = 'Creates a new certificate'; + this.usage = `${this.client.config.prefix}securesign createcrt [-s sign] [-c class] [-m digest]\n\`sign\`: Sign type (ecc/rsa)\n\`class\`: Certificate Class (1/2/3)\n\`digest\`: SHA Digest (256/384/512)`; + this.enabled = true; + this.guildOnly = false; + } + + public async run(message: Message, args: string[]) { + try { + const account = await this.client.db.Account.findOne({ userID: message.author.id }); + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found***`); + if (!account.hash) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not initialized***`); + + const options: { s?: string, c?: string, m?: string } = args.length ? Object.fromEntries(` ${args.join(' ')}`.split(' -').filter((a) => a).map((a) => a.split(/ (.+)/)).filter((a) => a.length > 1)) : {}; + if (options.s && options.s.toLowerCase() !== 'ecc' && options.s.toLowerCase() !== 'rsa') return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid signing type, choose between \`ecc\` or \`rsa\``); + if (options.c && (!Number(options.c) || Number(options.c) < 1 || Number(options.c) > 3)) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid class selected, choose between Class \`1\`, \`2\` or \`3\``); + if (options.m && (!Number(options.m) || (options.m !== '256' && options.m !== '384' && options.m !== '512'))) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Invalid SHA Digest selected, choose between \`256\`, \`384\` or \`512\``); + if (Number(options.c) === 3 && (!options.s || options.s.toLowerCase() === 'ecc')) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Class 3 ECC certificates are not supported, please use the \`-s rsa\` option instead***`); + + const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Creating certificate...***`); + if (options.s) options.s = options.s.toLowerCase(); + const hash = this.client.util.getAcctHash(account.homepath); + + // Check if they can generate certificate + try { + const { data } = await axios({ + method: 'GET', + url: 'https://api.securesign.org/account/details', + headers: { Authorization: hash, 'Content-Type': 'application/json' }, + }); + + const { total, allowed } = data.message; + if (total >= allowed) return msg.edit(`${this.client.stores.emojis.error} ***Not enough certificate allowances - please ask a member of staff to increase this limit from ${total}***`); + if (Number(options.c) > data.message.class) return msg.edit(`${this.client.stores.emojis.error} ***Class too low, you are on a class ${data.message.class} account***`); + } catch (error) { + const { code } = error.response.data; + if (code === 1001) { + await this.client.db.Account.updateOne({ userID: account.userID }, { $set: { hash: false } }); + this.client.getDMChannel(account.userID).then((channel) => channel.createMessage('Your SecureSign password has been reset - please reinitialize your SecureSign account')).catch(); + return msg.edit(`${this.client.stores.emojis.error} ***Authentication failed***`); + } + throw error; + } + + const execoptions = `${options.s ? ` -s ${options.s}` : ''}${options.c ? ` -c ${options.c}` : ''}${options.m ? ` -m ${options.m}` : ''}`; + const cmd = `sudo -H -u ${account.username} bash -c 'securesign-canary createcrt${execoptions}'`; + + const exec = await this.client.util.exec(cmd); + if (!exec.replace(/^\s+|\s+$/g, '').endsWith('Successfully wrote certificate.')) throw new Error(`Certificate generation did not complete successfully:\n${cmd}`); + + return msg.edit(`${this.client.stores.emojis.success} ***Successfully created certificate***`); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/securesign_init.ts b/src/commands/securesign_init.ts index 1ccccfa..ecaacb3 100644 --- a/src/commands/securesign_init.ts +++ b/src/commands/securesign_init.ts @@ -1,56 +1,55 @@ -import { Message, PrivateChannel, TextChannel } from 'eris'; -import axios, { AxiosResponse } from 'axios'; -import { Client } from '..'; -import { Command } from '../class'; - -export default class SecureSign_Init extends Command { - constructor(client: Client) { - super(client); - this.name = 'init'; - this.description = 'Inits configuration files and environment variables (DM only)'; - this.usage = `${this.client.config.prefix}securesign init [hash]`; - this.enabled = true; - this.guildOnly = false; - } - - public async run(message: Message, args: string[]) { - try { - if (!args[0]) return this.client.commands.get('help').run(message, ['securesign', this.name]); - if (!(message.channel instanceof PrivateChannel)) { - message.delete(); - return message.channel.createMessage(`${this.client.stores.emojis.error} ***Run this command in your DMs!***`); - } - const account = await this.client.db.Account.findOne({ userID: message.author.id }); - if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not registered***`); - if (account.locked) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Your account is locked***`); - const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Initializing account...***`); - let verify: AxiosResponse; - try { - verify = await axios({ - method: 'get', - url: 'https://api.securesign.org/account/details', - headers: { Authorization: args[0] }, - }); - } catch (error) { - const { status } = error.response; - if (status === 400 || status === 401 || status === 403 || status === 404) return msg.edit(`${this.client.stores.emojis.error} ***Credentials incorrect***`); - throw error; - } - const { id } = verify.data.message; - if (id !== message.author.id && !account.root) { - // @ts-ignore - const channel: TextChannel = this.client.guilds.get('446067825673633794').channels.get('501089664040697858'); - channel.createMessage(`**__UNAUTHORIZED ACCESS ALERT__**\n${message.author.mention} tried to initialize their account using <@${id}>'s SecureSign credentials.\nTheir account has been locked under Section 5.2 of the EULA.`); - const tasks = [this.client.util.exec(`lock ${account.username}`), account.updateOne({ locked: true }), this.client.util.createModerationLog(account.userID, this.client.user, 2, 'Violation of Section 5.2 of the EULA')]; - await Promise.all(tasks); - return msg.edit(`${this.client.stores.emojis.error} ***Credentials incorrect***`); - } - const init = await this.client.util.exec(`sudo -H -u ${account.username} bash -c 'securesign-canary init -a ${args[0]}'`); - if (!init.replace(/^\s+|\s+$/g, '').endsWith('Initialization sequence completed.')) throw new Error(`Account initialization did not complete successfully:\n${init}`); - await this.client.db.Account.updateOne({ userID: message.author.id }, { $set: { hash: true } }); - return msg.edit(`${this.client.stores.emojis.success} ***Account initialized***`); - } catch (error) { - return this.client.util.handleError(error, message, this); - } - } -} +import { Message, PrivateChannel, TextChannel } from 'eris'; +import axios, { AxiosResponse } from 'axios'; +import { Client } from '..'; +import { Command } from '../class'; + +export default class SecureSign_Init extends Command { + constructor(client: Client) { + super(client); + this.name = 'init'; + this.description = 'Inits configuration files and environment variables (DM only)'; + this.usage = `${this.client.config.prefix}securesign init [hash]`; + this.enabled = true; + this.guildOnly = false; + } + + public async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, ['securesign', this.name]); + if (!(message.channel instanceof PrivateChannel)) { + message.delete(); + return message.channel.createMessage(`${this.client.stores.emojis.error} ***Run this command in your DMs!***`); + } + const account = await this.client.db.Account.findOne({ userID: message.author.id }); + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not registered***`); + if (account.locked) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Your account is locked***`); + const msg = await message.channel.createMessage(`${this.client.stores.emojis.loading} ***Initializing account...***`); + let verify: AxiosResponse; + try { + verify = await axios({ + method: 'get', + url: 'https://api.securesign.org/account/details', + headers: { Authorization: args[0] }, + }); + } catch (error) { + const { status } = error.response; + if (status === 400 || status === 401 || status === 403 || status === 404) return msg.edit(`${this.client.stores.emojis.error} ***Credentials incorrect***`); + throw error; + } + const { id } = verify.data.message; + if (id !== message.author.id && !account.root) { + const channel = this.client.guilds.get('446067825673633794').channels.get('501089664040697858') as TextChannel; + channel.createMessage(`**__UNAUTHORIZED ACCESS ALERT__**\n${message.author.mention} tried to initialize their account using <@${id}>'s SecureSign credentials.\nTheir account has been locked under Section 5.2 of the EULA.`); + const tasks = [this.client.util.exec(`lock ${account.username}`), account.updateOne({ locked: true }), this.client.util.createModerationLog(account.userID, this.client.user, 2, 'Violation of Section 5.2 of the EULA')]; + await Promise.all(tasks); + return msg.edit(`${this.client.stores.emojis.error} ***Credentials incorrect***`); + } + const init = await this.client.util.exec(`sudo -H -u ${account.username} bash -c 'securesign-canary init -a ${args[0]}'`); + if (!init.replace(/^\s+|\s+$/g, '').endsWith('Initialization sequence completed.')) throw new Error(`Account initialization did not complete successfully:\n${init}`); + await this.client.db.Account.updateOne({ userID: message.author.id }, { $set: { hash: true } }); + return msg.edit(`${this.client.stores.emojis.success} ***Account initialized***`); + } catch (error) { + return this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/sysinfo.ts b/src/commands/sysinfo.ts index 3f880a2..8925cae 100644 --- a/src/commands/sysinfo.ts +++ b/src/commands/sysinfo.ts @@ -1,36 +1,35 @@ -import moment from 'moment'; -import { Message } from 'eris'; -import os, { totalmem } from 'os'; -import { Command, RichEmbed } from '../class'; -import { dataConversion } from '../functions'; -import { Client } from '..'; - -export default class SysInfo extends Command { - constructor(client: Client) { - super(client); - this.name = 'sysinfo'; - this.description = 'Provides system information.'; - this.enabled = true; - } - - public async run(message: Message) { - const availableMemory: string = await this.client.util.exec('free -b'); - const usedMemory = dataConversion(totalmem() - Number(availableMemory.split('\n')[1].split(' ').slice(-1)[0])); - const date = new Date(); - date.setMilliseconds(-(moment.duration(os.uptime(), 's').asMilliseconds())); - - const embed = new RichEmbed(); - embed.setTitle('System Information & Statistics'); - embed.addField('Hostname', os.hostname(), true); - embed.addField('Uptime', `${moment.duration(os.uptime(), 's').humanize()} | Last restart was on ${moment(date).format('dddd, MMMM Do YYYY, h:mm:ss A')} EST`, true); - embed.addField('CPU', `${os.cpus()[0].model} ${os.cpus()[0].speed / 1000}GHz | ${os.cpus().length} Cores | ${os.arch()}`, true); - embed.addField('Load Average (last 15 minutes)', os.loadavg()[2].toFixed(3), true); - embed.addField('Memory/RAM', `${usedMemory} / ${dataConversion(totalmem())}`, true); - embed.addField('Network Interfaces (IPv4)', os.networkInterfaces().eth0.filter((r) => r.family === 'IPv4')[0].address, true); - embed.addField('Network Interfaces (IPv6)', os.networkInterfaces().eth0.filter((r) => r.family === 'IPv6')[0].address.replace(/:/gi, '\:'), true); // eslint-disable-line - embed.setFooter(this.client.user.username, this.client.user.avatarURL); - embed.setTimestamp(); - // @ts-ignore - message.channel.createMessage({ embed }); - } -} +import moment from 'moment'; +import { Message } from 'eris'; +import os, { totalmem } from 'os'; +import { Command, RichEmbed } from '../class'; +import { dataConversion } from '../functions'; +import { Client } from '..'; + +export default class SysInfo extends Command { + constructor(client: Client) { + super(client); + this.name = 'sysinfo'; + this.description = 'Provides system information.'; + this.enabled = true; + } + + public async run(message: Message) { + const availableMemory: string = await this.client.util.exec('free -b'); + const usedMemory = dataConversion(totalmem() - Number(availableMemory.split('\n')[1].split(' ').slice(-1)[0])); + const date = new Date(); + date.setMilliseconds(-(moment.duration(os.uptime(), 's').asMilliseconds())); + + const embed = new RichEmbed(); + embed.setTitle('System Information & Statistics'); + embed.addField('Hostname', os.hostname(), true); + embed.addField('Uptime', `${moment.duration(os.uptime(), 's').humanize()} | Last restart was on ${moment(date).format('dddd, MMMM Do YYYY, h:mm:ss A')} EST`, true); + embed.addField('CPU', `${os.cpus()[0].model} ${os.cpus()[0].speed / 1000}GHz | ${os.cpus().length} Cores | ${os.arch()}`, true); + embed.addField('Load Average (last 15 minutes)', os.loadavg()[2].toFixed(3), true); + embed.addField('Memory/RAM', `${usedMemory} / ${dataConversion(totalmem())}`, true); + embed.addField('Network Interfaces (IPv4)', os.networkInterfaces().eth0.filter((r) => r.family === 'IPv4')[0].address, true); + embed.addField('Network Interfaces (IPv6)', os.networkInterfaces().eth0.filter((r) => r.family === 'IPv6')[0].address.replace(/:/gi, '\:'), true); // eslint-disable-line + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + message.channel.createMessage({ embed }); + } +} diff --git a/src/commands/whois.ts b/src/commands/whois.ts index 58082cb..07f3004 100644 --- a/src/commands/whois.ts +++ b/src/commands/whois.ts @@ -1,64 +1,63 @@ -/* eslint-disable consistent-return */ -import moment from 'moment'; -import { Message } from 'eris'; -import { Client } from '..'; -import { Command, RichEmbed } from '../class'; -import { dataConversion } from '../functions'; -import User from './whois_user'; - -export default class Whois extends Command { - constructor(client: Client) { - super(client); - this.name = 'whois'; - this.description = 'Views information for a cloud account.'; - this.aliases = ['account', 'user']; - this.usage = `${this.client.config.prefix}account [User Name | User ID | Email Address]`; - this.permissions = { roles: ['446104438969466890'] }; - this.subcmds = [User]; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - if (!args[0]) return this.client.commands.get('help').run(message, [this.name]); - const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }, { emailAddress: args[0] }] }); - if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found.***`); - const embed = new RichEmbed(); - embed.setTitle('Account Information'); - if (this.client.users.get(account.userID)) embed.setThumbnail(this.client.users.get(account.userID).avatarURL); - embed.setColor(0x36393f); - let fingerInformation: string; - const result = await this.client.util.exec(`finger ${account.username}`); - if (message.member && !message.member.roles.includes('143414786913206272')) { - fingerInformation = result.replace(/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/g, '[MASKED IP ADDRESS]'); - } else { - fingerInformation = result; - } - embed.setDescription(`${fingerInformation}\n${await this.client.util.exec(`chage -l ${account.username}`)}`); - embed.addField('Username', `${account.username} | <@${account.userID}>`, true); - embed.addField('ID', account.userID, true); - embed.addField('Email Address', account.emailAddress, true); - embed.addField('Created By', `<@${this.client.users.get(account.createdBy).id}>`, true); - embed.addField('Created At', moment(account.createdAt).format('dddd, MMMM Do YYYY, h:mm:ss A'), true); - const cpuUsage = await this.client.util.exec(`top -b -n 1 -u ${account.username} | awk 'NR>7 { sum += $9; } END { print sum; }'`); - embed.addField('CPU Usage', cpuUsage.split('\n')[0] ? `${cpuUsage.split('\n')[0]}%` : '0%', true); - embed.addField('Memory', dataConversion(Number(await this.client.util.exec(`memory ${account.username}`)) * 1000), true); - const data = await this.client.redis.get(`storage-${account.username}`) ? dataConversion(Number(await this.client.redis.get(`storage-${account.username}`))) : 'N/A'; - embed.addField('Storage', data, true); - let details = ''; - if (account.locked) details += 'This account is currently locked.\n'; - if (account.permissions.engineer) details += 'This account belongs to an Engineer.\n'; - else if (account.permissions.communityManager) details += 'This account belongs to a Community Manager.\n'; - else if (account.permissions.supervisor) details += 'This account belongs to a Supervisor.\n'; - else if (account.permissions.staff) details += 'This account belongs to a Staff member.\n'; - if (account.root) details += 'This account has root/administrative privileges.\n'; - if (details) embed.addField('Additional Details', details, true); - embed.setFooter(this.client.user.username, this.client.user.avatarURL); - embed.setTimestamp(); - // @ts-ignore - message.channel.createMessage({ embed }); - } catch (error) { - await this.client.util.handleError(error, message, this); - } - } -} +/* eslint-disable consistent-return */ +import moment from 'moment'; +import { Message } from 'eris'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; +import { dataConversion } from '../functions'; +import User from './whois_user'; + +export default class Whois extends Command { + constructor(client: Client) { + super(client); + this.name = 'whois'; + this.description = 'Views information for a cloud account.'; + this.aliases = ['account', 'user']; + this.usage = `${this.client.config.prefix}account [User Name | User ID | Email Address]`; + this.permissions = { roles: ['446104438969466890'] }; + this.subcmds = [User]; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, [this.name]); + const account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }, { emailAddress: args[0] }] }); + if (!account) return message.channel.createMessage(`${this.client.stores.emojis.error} ***Account not found.***`); + const embed = new RichEmbed(); + embed.setTitle('Account Information'); + if (this.client.users.get(account.userID)) embed.setThumbnail(this.client.users.get(account.userID).avatarURL); + embed.setColor(0x36393f); + let fingerInformation: string; + const result = await this.client.util.exec(`finger ${account.username}`); + if (message.member && !message.member.roles.includes('143414786913206272')) { + fingerInformation = result.replace(/((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/g, '[MASKED IP ADDRESS]'); + } else { + fingerInformation = result; + } + embed.setDescription(`${fingerInformation}\n${await this.client.util.exec(`chage -l ${account.username}`)}`); + embed.addField('Username', `${account.username} | <@${account.userID}>`, true); + embed.addField('ID', account.userID, true); + embed.addField('Email Address', account.emailAddress, true); + embed.addField('Created By', `<@${this.client.users.get(account.createdBy).id}>`, true); + embed.addField('Created At', moment(account.createdAt).format('dddd, MMMM Do YYYY, h:mm:ss A'), true); + const cpuUsage = await this.client.util.exec(`top -b -n 1 -u ${account.username} | awk 'NR>7 { sum += $9; } END { print sum; }'`); + embed.addField('CPU Usage', cpuUsage.split('\n')[0] ? `${cpuUsage.split('\n')[0]}%` : '0%', true); + embed.addField('Memory', dataConversion(Number(await this.client.util.exec(`memory ${account.username}`)) * 1000), true); + const data = await this.client.redis.get(`storage-${account.username}`) ? dataConversion(Number(await this.client.redis.get(`storage-${account.username}`))) : 'N/A'; + embed.addField('Storage', data, true); + let details = ''; + if (account.locked) details += 'This account is currently locked.\n'; + if (account.permissions.engineer) details += 'This account belongs to an Engineer.\n'; + else if (account.permissions.communityManager) details += 'This account belongs to a Community Manager.\n'; + else if (account.permissions.supervisor) details += 'This account belongs to a Supervisor.\n'; + else if (account.permissions.staff) details += 'This account belongs to a Staff member.\n'; + if (account.root) details += 'This account has root/administrative privileges.\n'; + if (details) embed.addField('Additional Details', details, true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + message.channel.createMessage({ embed }); + } catch (error) { + await this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/commands/whois_user.ts b/src/commands/whois_user.ts index a635a8c..cd152de 100644 --- a/src/commands/whois_user.ts +++ b/src/commands/whois_user.ts @@ -1,53 +1,52 @@ -/* eslint-disable consistent-return */ -import moment from 'moment'; -import { Message } from 'eris'; -import { Client } from '..'; -import { Command, RichEmbed } from '../class'; -import { dataConversion } from '../functions'; -import { AccountInterface } from '../models'; - -export default class Whois_User extends Command { - constructor(client: Client) { - super(client); - this.name = 'user'; - this.description = 'Gets information about your account.'; - this.usage = `${this.client.config.prefix}whois user `; - this.enabled = true; - } - - public async run(message: Message, args: string[]) { - try { - let account: AccountInterface; - if (!args[0]) account = await this.client.db.Account.findOne({ userID: message.author.id }); - else account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }] }); - if (!account) return message.channel.createMessage(`***${this.client.stores.emojis.error} You don't have an account.***`); - const embed = new RichEmbed(); - embed.setTitle('Account Information'); - if (this.client.users.get(account.userID)) embed.setThumbnail(this.client.users.get(account.userID).avatarURL); - embed.setColor(0x36393f); - embed.addField('Username', `${account.username} | <@${account.userID}>`, true); - embed.addField('ID', account.userID, true); - embed.addField('Created By', `<@${this.client.users.get(account.createdBy).id}>`, true); - embed.addField('Created At', moment(account.createdAt).format('dddd, MMMM Do YYYY, h:mm:ss A'), true); - const cpuUsage = await this.client.util.exec(`top -b -n 1 -u ${account.username} | awk 'NR>7 { sum += $9; } END { print sum; }'`); - embed.addField('CPU Usage', cpuUsage.split('\n')[0] ? `${cpuUsage.split('\n')[0]}%` : '0%', true); - embed.addField('Memory', dataConversion(Number(await this.client.util.exec(`memory ${account.username}`)) * 1000), true); - const data = await this.client.redis.get(`storage-${account.username}`) ? dataConversion(Number(await this.client.redis.get(`storage-${account.username}`))) : 'N/A'; - embed.addField('Storage', data, true); - let details = ''; - if (account.locked) details += 'This account is currently locked.\n'; - if (account.permissions.engineer) details += 'This account belongs to an Engineer.\n'; - else if (account.permissions.communityManager) details += 'This account belongs to a Community Manager.\n'; - else if (account.permissions.supervisor) details += 'This account belongs to a Supervisor.\n'; - else if (account.permissions.staff) details += 'This account belongs to a Staff member.\n'; - if (account.root) details += 'This account has root/administrative privileges.\n'; - if (details) embed.addField('Additional Details', details, true); - embed.setFooter(this.client.user.username, this.client.user.avatarURL); - embed.setTimestamp(); - // @ts-ignore - message.channel.createMessage({ embed }); - } catch (error) { - this.client.util.handleError(error, message, this); - } - } -} +/* eslint-disable consistent-return */ +import moment from 'moment'; +import { Message } from 'eris'; +import { Client } from '..'; +import { Command, RichEmbed } from '../class'; +import { dataConversion } from '../functions'; +import { AccountInterface } from '../models'; + +export default class Whois_User extends Command { + constructor(client: Client) { + super(client); + this.name = 'user'; + this.description = 'Gets information about your account.'; + this.usage = `${this.client.config.prefix}whois user `; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + let account: AccountInterface; + if (!args[0]) account = await this.client.db.Account.findOne({ userID: message.author.id }); + else account = await this.client.db.Account.findOne({ $or: [{ username: args[0] }, { userID: args[0] }] }); + if (!account) return message.channel.createMessage(`***${this.client.stores.emojis.error} You don't have an account.***`); + const embed = new RichEmbed(); + embed.setTitle('Account Information'); + if (this.client.users.get(account.userID)) embed.setThumbnail(this.client.users.get(account.userID).avatarURL); + embed.setColor(0x36393f); + embed.addField('Username', `${account.username} | <@${account.userID}>`, true); + embed.addField('ID', account.userID, true); + embed.addField('Created By', `<@${this.client.users.get(account.createdBy).id}>`, true); + embed.addField('Created At', moment(account.createdAt).format('dddd, MMMM Do YYYY, h:mm:ss A'), true); + const cpuUsage = await this.client.util.exec(`top -b -n 1 -u ${account.username} | awk 'NR>7 { sum += $9; } END { print sum; }'`); + embed.addField('CPU Usage', cpuUsage.split('\n')[0] ? `${cpuUsage.split('\n')[0]}%` : '0%', true); + embed.addField('Memory', dataConversion(Number(await this.client.util.exec(`memory ${account.username}`)) * 1000), true); + const data = await this.client.redis.get(`storage-${account.username}`) ? dataConversion(Number(await this.client.redis.get(`storage-${account.username}`))) : 'N/A'; + embed.addField('Storage', data, true); + let details = ''; + if (account.locked) details += 'This account is currently locked.\n'; + if (account.permissions.engineer) details += 'This account belongs to an Engineer.\n'; + else if (account.permissions.communityManager) details += 'This account belongs to a Community Manager.\n'; + else if (account.permissions.supervisor) details += 'This account belongs to a Supervisor.\n'; + else if (account.permissions.staff) details += 'This account belongs to a Staff member.\n'; + if (account.root) details += 'This account has root/administrative privileges.\n'; + if (details) embed.addField('Additional Details', details, true); + embed.setFooter(this.client.user.username, this.client.user.avatarURL); + embed.setTimestamp(); + message.channel.createMessage({ embed }); + } catch (error) { + this.client.util.handleError(error, message, this); + } + } +} diff --git a/src/index.ts b/src/index.ts index 4d8c716..3f299c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ -export { default as Client } from './Client'; -export { default as config } from './config.json'; -export { default as Classes } from './class'; -export { default as Commands } from './commands'; -export { default as Events } from './events'; -export { default as Models } from './models'; -export { default as Stores } from './stores'; +export { default as Client } from './Client'; +export { default as config } from './config.json'; +export * from './class'; +export * from './commands'; +export * from './events'; +export * from './models'; +export * from './stores'; diff --git a/tsconfig.json b/tsconfig.json index d97cc52..04f4976 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,64 +1,64 @@ -{ - "compilerOptions": { - /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ - "target": "ES2017", /* 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. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "declaration": false, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./dist", /* Redirect output structure to the directory. */ - "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": false, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - "resolveJsonModule": true, - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - } -} +{ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "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. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": false, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": false, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "resolveJsonModule": true, + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} diff --git a/types/eris.d.ts b/types/eris.d.ts new file mode 100644 index 0000000..e9a3c05 --- /dev/null +++ b/types/eris.d.ts @@ -0,0 +1,8 @@ +import { EmbedOptions } from 'eris'; +import RichEmbed from '../src/class/RichEmbed'; + +declare global { + namespace Eris { + type MessageContent = string | { content?: string; tts?: boolean; disableEveryone?: boolean; embed?: EmbedOptions | RichEmbed; flags?: number }; + } +} diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 0000000..04a9dd5 --- /dev/null +++ b/types/global.d.ts @@ -0,0 +1,31 @@ + +interface PromiseFulfilledResult { + status: 'fulfilled'; + value: T; +} + +interface PromiseRejectedResult { + status: 'rejected'; + reason: any; +} + +type PromiseSettledResult = PromiseFulfilledResult | PromiseRejectedResult; + +interface PromiseConstructor { + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: T): + Promise<{ -readonly [P in keyof T]: PromiseSettledResult ? U : T[P]> }>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: Iterable): Promise ? U : T>[]>; +} diff --git a/types/moment.d.ts b/types/moment.d.ts new file mode 100644 index 0000000..a90caea --- /dev/null +++ b/types/moment.d.ts @@ -0,0 +1,15 @@ +import moment from 'moment'; + +declare module 'moment' { + interface PreciseRangeValueObject extends moment.MomentObjectOutput { + firstDateWasLater: boolean; + } + + interface Moment { + preciseDiff(d2: moment.MomentInput, returnValueObject?: false): string; + preciseDiff(d2: moment.MomentInput, returnValueObject: true): PreciseRangeValueObject; + } + + function preciseDiff(d1: moment.MomentInput, d2: moment.MomentInput, returnValueObject?: false): string; + function preciseDiff(d1: moment.MomentInput, d2: moment.MomentInput, returnValueObject: true): PreciseRangeValueObject; +}