diff --git a/src/api/loc.sh/routes/root.ts b/src/api/loc.sh/routes/root.ts index 110ba2e..086d1a2 100644 --- a/src/api/loc.sh/routes/root.ts +++ b/src/api/loc.sh/routes/root.ts @@ -23,5 +23,18 @@ export default class Root extends Route { return res.status(500).json({ code: this.constants.codes.SERVER_ERROR, message: this.constants.messages.SERVER_ERROR }); } }); + + this.router.get('/m/:id', async (req, res) => { + try { + const id = req.params.id.split('.')[0]; + const file = await this.server.client.db.File.findOne({ identifier: id }); + if (file.downloaded >= file.maxDownloads) return res.status(404).json({ code: this.constants.codes.NOT_FOUND, message: this.constants.messages.NOT_FOUND }); + res.contentType(file.mimeType); + res.status(200).send(file.data); + return await file.updateOne({ $inc: { downloaded: 1 } }); + } catch (err) { + return this.handleError(err, res); + } + }); } } diff --git a/src/class/Client.ts b/src/class/Client.ts index 1c83c3b..8a8eabb 100644 --- a/src/class/Client.ts +++ b/src/class/Client.ts @@ -2,7 +2,7 @@ import eris from 'eris'; import mongoose from 'mongoose'; import { promises as fs } from 'fs'; import { Collection, Command, LocalStorage, Util, ServerManagement, Event } from '.'; -import { Member, MemberInterface, Moderation, ModerationInterface, PagerNumber, PagerNumberInterface, Rank, RankInterface, Redirect, RedirectInterface } from '../models'; +import { File, FileInterface, Member, MemberInterface, Moderation, ModerationInterface, PagerNumber, PagerNumberInterface, Rank, RankInterface, Redirect, RedirectInterface } from '../models'; export default class Client extends eris.Client { public config: { token: string, prefix: string, guildID: string, mongoDB: string, emailPass: string, }; @@ -17,14 +17,14 @@ export default class Client extends eris.Client { public serverManagement: ServerManagement; - public db: { Member: mongoose.Model, Moderation: mongoose.Model, PagerNumber: mongoose.Model, Rank: mongoose.Model, Redirect: mongoose.Model, local: { muted: LocalStorage } }; + public db: { File: mongoose.Model, Member: mongoose.Model, Moderation: mongoose.Model, PagerNumber: mongoose.Model, Rank: mongoose.Model, Redirect: mongoose.Model, local: { muted: LocalStorage } }; constructor(token: string, options?: eris.ClientOptions) { super(token, options); this.commands = new Collection(); this.events = new Collection(); this.intervals = new Collection(); - this.db = { Member, Moderation, PagerNumber, Rank, Redirect, local: { muted: new LocalStorage('muted') } }; + this.db = { File, Member, Moderation, PagerNumber, Rank, Redirect, local: { muted: new LocalStorage('muted') } }; } public async loadDatabase() { diff --git a/src/class/LocalStorage.ts b/src/class/LocalStorage.ts index 4827d07..7203050 100644 --- a/src/class/LocalStorage.ts +++ b/src/class/LocalStorage.ts @@ -17,8 +17,8 @@ export default class LocalStorage { private locked: boolean = false; - constructor(dbName: string) { - this.storagePath = `${__dirname}/../../localstorage/${dbName}.json.gz`; + constructor(dbName: string, dir = `${__dirname}/../../localstorage`) { + this.storagePath = `${dir}/${dbName}.json.gz`; this.init(); } diff --git a/src/class/Moderation.ts b/src/class/Moderation.ts index 4b457e6..9b3f302 100644 --- a/src/class/Moderation.ts +++ b/src/class/Moderation.ts @@ -189,7 +189,7 @@ export default class Moderation { return mod.save(); } - public async kick(user: Member|User, moderator: Member, reason?: string): Promise { + public async kick(user: Member | User, moderator: Member, reason?: string): Promise { if (reason && reason.length > 512) throw new Error('Kick reason cannot be longer than 512 characters'); await this.client.guilds.get(this.client.config.guildID).kickMember(user.id, reason); const logID = randomBytes(2).toString('hex'); diff --git a/src/class/RichEmbed.ts b/src/class/RichEmbed.ts index b0e4b72..fda660e 100644 --- a/src/class/RichEmbed.ts +++ b/src/class/RichEmbed.ts @@ -45,7 +45,7 @@ export default class RichEmbed implements EmbedOptions { /** * Sets the title of this embed. */ - setTitle(title: string) { + public 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; @@ -55,7 +55,7 @@ export default class RichEmbed implements EmbedOptions { /** * Sets the description of this embed. */ - setDescription(description: string) { + public 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; @@ -65,7 +65,7 @@ export default class RichEmbed implements EmbedOptions { /** * Sets the URL of this embed. */ - setURL(url: string) { + public 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 = encodeURI(url); @@ -75,7 +75,7 @@ export default class RichEmbed implements EmbedOptions { /** * Sets the color of this embed. */ - setColor(color: string | number) { + public setColor(color: string | number) { if (typeof color === 'string' || typeof color === 'number') { if (typeof color === 'string') { const regex = /[^a-f0-9]/gi; @@ -92,7 +92,7 @@ export default class RichEmbed implements EmbedOptions { /** * Sets the author of this embed. */ - setAuthor(name: string, icon_url?: string, url?: string) { + public 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.'); @@ -103,7 +103,7 @@ export default class RichEmbed implements EmbedOptions { /** * Sets the timestamp of this embed. */ - setTimestamp(timestamp = new Date()) { + public setTimestamp(timestamp = new Date()) { if (Number.isNaN(timestamp.getTime())) throw new TypeError('Expecting ISO8601 (Date constructor)'); this.timestamp = timestamp; return this; @@ -112,7 +112,7 @@ export default class RichEmbed implements EmbedOptions { /** * Adds a field to the embed (max 25). */ - addField(name: string, value: string, inline = false) { + public 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.'); @@ -128,14 +128,14 @@ export default class RichEmbed implements EmbedOptions { /** * Convenience function for `.addField('\u200B', '\u200B', inline)`. */ - addBlankField(inline = false) { + public addBlankField(inline = false) { return this.addField('\u200B', '\u200B', inline); } /** * Set the thumbnail of this embed. */ - setThumbnail(url: string) { + public setThumbnail(url: string) { if (typeof url !== 'string') throw new TypeError('RichEmbed Thumbnail URLs must be a string.'); this.thumbnail = { url }; return this; @@ -144,7 +144,7 @@ export default class RichEmbed implements EmbedOptions { /** * Set the image of this embed. */ - setImage(url: string) { + public 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 }; @@ -154,7 +154,7 @@ export default class RichEmbed implements EmbedOptions { /** * Sets the footer of this embed. */ - setFooter(text: string, icon_url?: string) { + public 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.'); diff --git a/src/class/Util.ts b/src/class/Util.ts index d1a096d..184e55d 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -131,6 +131,14 @@ export default class Util { return array; } + public splitArray(array: T[], count: number) { + const finalArray: T[][] = []; + while (array.length) { + finalArray.push(array.splice(0, count)); + } + return finalArray; + } + public decimalToHex(int: number): string { const hex = int.toString(16); return '#000000'.substring(0, 7 - hex.length) + hex; diff --git a/src/commands/index.ts b/src/commands/index.ts index f31b84d..614abd0 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -19,6 +19,7 @@ export { default as page } from './page'; export { default as ping } from './ping'; export { default as rank } from './rank'; export { default as roleinfo } from './roleinfo'; +export { default as storemessages } from './storemessages'; export { default as unban } from './unban'; export { default as unmute } from './unmute'; export { default as whois } from './whois'; diff --git a/src/commands/members.ts b/src/commands/members.ts index 6261924..e0feee5 100644 --- a/src/commands/members.ts +++ b/src/commands/members.ts @@ -1,6 +1,7 @@ import { Message } from 'eris'; import { createPaginationEmbed } from 'eris-pagination'; import { Client, Command, RichEmbed } from '../class'; +import { members } from '.'; export default class extends Command { constructor(client: Client) { @@ -9,7 +10,7 @@ export default class extends Command { this.description = 'Gets a list of members in the server or members in a specific role.'; this.usage = `${this.client.config.prefix}members [role name]`; this.guildOnly = true; - this.enabled = false; + this.enabled = true; } public async run(message: Message, args: string[]) { @@ -20,19 +21,12 @@ export default class extends Command { const membersOnline = this.mainGuild.members.filter((member) => member.status === 'online'); const membersIdle = this.mainGuild.members.filter((member) => member.status === 'idle'); const membersDnd = this.mainGuild.members.filter((member) => member.status === 'dnd'); - const membersOffline = this.mainGuild.members.filter((member) => member.status === 'offline'); + const membersOffline = this.mainGuild.members.filter((member) => member.status === 'offline' || member.status === undefined); const membersBots = this.mainGuild.members.filter((member) => member.user.bot === true); const membersHuman = this.mainGuild.members.filter((member) => member.user.bot === false); embed.setTitle('Members'); - embed.addField('Total', `${this.mainGuild.members.size}`, true); - embed.addField('Humans', `${membersHuman.length}`, true); - embed.addField('Bots', `${membersBots.length}`, true); - embed.addBlankField(); - embed.addField('Online', `${membersOnline.length}`, true); - embed.addField('Idle', `${membersIdle.length}`, true); - embed.addField('Do Not Disturb', `${membersDnd.length}`, true); - embed.addField('Offline', `${membersOffline.length}`, true); + embed.setDescription(`**Total:** ${this.mainGuild.members.size}\n**Humans:** ${membersHuman.length}\n**Bots:** ${membersBots.length}\n\n**<:online:732025023547834369> Online:** ${membersOnline.length}\n**<:idle:732025087896715344> Idle:** ${membersIdle.length}\n**<:dnd:732024861853089933> Do Not Disturb:** ${membersDnd.length}\n**<:offline:732024920518688849> Offline:** ${membersOffline.length}`); embed.setFooter(this.client.user.username, this.client.user.avatarURL); embed.setTimestamp(); @@ -41,7 +35,7 @@ export default class extends Command { const role = this.client.util.resolveRole(args.join(' '), this.mainGuild); if (!role) return this.error(message.channel, 'The role you specified doesn\'t exist.'); - const membersArray: [{name: string, value: string}?] = []; + const statusArray: string[] = []; const membersOnline: string[] = []; const membersIdle: string[] = []; const membersDnd: string[] = []; @@ -49,38 +43,37 @@ export default class extends Command { for (const member of this.mainGuild.members.filter((m) => m.roles.includes(role.id)).sort((a, b) => a.username.localeCompare(b.username))) { switch (member.status) { case 'online': - membersOnline.push(`${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); + membersOnline.push(`<:online:732025023547834369> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); break; case 'idle': - membersIdle.push(`${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); + membersIdle.push(`<:idle:732025087896715344> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); break; case 'dnd': - membersDnd.push(`${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); + membersDnd.push(`<:dnd:732024861853089933> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); break; case 'offline': - membersOffline.push(`${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); + membersOffline.push(`<:offline:732024920518688849> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); break; case undefined: - membersOffline.push(`${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); + membersOffline.push(`<:offline:732024920518688849> ${member.user.username}#${member.user.discriminator} | <@${member.user.id}>`); break; default: break; } } - if (membersOnline.length > 0) membersArray.push({ name: 'Online', value: membersOnline.join('\n') }); - if (membersIdle.length > 0) membersArray.push({ name: 'Idle', value: membersIdle.join('\n') }); - if (membersDnd.length > 0) membersArray.push({ name: 'Do Not Disturb', value: membersDnd.join('\n') }); - if (membersOffline.length > 0) membersArray.push({ name: 'Offline', value: membersOffline.join('\n') }); - const membersSplit = this.client.util.splitFields(membersArray); + if (membersOnline.length > 0) statusArray.push(membersOnline.join('\n')); + if (membersIdle.length > 0) statusArray.push(membersIdle.join('\n')); + if (membersDnd.length > 0) statusArray.push(membersDnd.join('\n')); + if (membersOffline.length > 0) statusArray.push(membersOffline.join('\n')); + const statusSplit = this.client.util.splitString(statusArray.join('\n'), 2000); const cmdPages: RichEmbed[] = []; - membersSplit.forEach((split) => { + statusSplit.forEach((split) => { const embed = new RichEmbed(); embed.setTitle(`Members in ${role.name}`); - embed.setDescription(`Members in Role: ${membersOnline.length + membersIdle.length + membersDnd.length + membersOffline.length}`); + embed.setDescription(`Members in Role: ${membersOnline.length + membersIdle.length + membersDnd.length + membersOffline.length}\n\n${split}`); embed.setColor(role.color); embed.setFooter(this.client.user.username, this.client.user.avatarURL); embed.setTimestamp(); - split.forEach((c) => embed.addField(c.name, c.value)); return cmdPages.push(embed); }); if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] }); diff --git a/src/commands/storemessages.ts b/src/commands/storemessages.ts new file mode 100644 index 0000000..ef93c6b --- /dev/null +++ b/src/commands/storemessages.ts @@ -0,0 +1,52 @@ +import { randomBytes } from 'crypto'; +import { Message, TextChannel } from 'eris'; +import { Client, Command, LocalStorage } from '../class'; + +export default class StoreMessages extends Command { + constructor(client: Client) { + super(client); + this.name = 'storemessages'; + this.description = 'Fetches 1000 messages from the specified channel and stores them in a HTML file.'; + this.usage = `${this.client.config.prefix}storemessages [member ID]`; + this.aliases = ['sm']; + this.permissions = 3; + this.guildOnly = true; + 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 check = this.client.util.resolveGuildChannel(args[0], this.mainGuild, false); + if (!check || check.type !== 0) return this.error(message.channel, 'The channel you specified either doesn\'t exist or isn\'t a textable guild channel.'); + const chan = this.mainGuild.channels.get(check.id); + const loadingMessage = await this.loading(message.channel, 'Fetching messages...'); + let messages = await chan.getMessages(10000); + if (args[1]) { + messages = messages.filter((m) => m.author.id === args[1]); + } + let html = `

Library of Code sp-us

Channel: ${chan.name} (${chan.id})
Generated by: ${message.author.username}#${message.author.discriminator}
Generated at: ${new Date().toLocaleString('en-us')}

`; + for (const msg of messages) { + html += `(${new Date(msg.timestamp).toLocaleString('en-us')}) [${msg.author.username}#${msg.author.discriminator} - ${msg.author.id}]: ${msg.cleanContent}
`; + } + message.delete(); + const identifier = randomBytes(10).toString('hex'); + + const comp = await LocalStorage.compress(html); + const file = new this.client.db.File({ + name: `${chan.name}-${new Date().toLocaleString('en-us')}.html.gz`, + identifier, + mimeType: 'application/gzip', + data: comp, + downloaded: 0, + maxDownloads: 1, + }); + await file.save(); + loadingMessage.delete(); + this.client.getDMChannel(message.author.id).then((c) => c.createMessage(`https://loc.sh/m/${identifier}.html.gz`)).catch(() => this.error(message.channel, 'Could not send a DM to you.')); + return this.success(message.channel, `Fetched messages for <#${chan.id}>. Check your DMs for link to access.`); + } catch (err) { + return this.client.util.handleError(err, message, this); + } + } +} diff --git a/src/configs/acknowledgements.json b/src/configs/acknowledgements.json index 2d1813f..58db6c6 100644 --- a/src/configs/acknowledgements.json +++ b/src/configs/acknowledgements.json @@ -109,15 +109,6 @@ "github": "https://github.com/Khaazz", "bio": "I baguette for a living and eat code for breakfast." }, - { - "name": "Ryan", - "id": "186679073764802560", - "dept": "Associate", - "emailAddress": "wbdvryan@staff.libraryofcode.org", - "gitlab": "https://gitlab.libraryofcode.org/plainRyan", - "bio": "Experiment, learn, share, repeat.", - "acknowledgements": ["Contributor"] - }, { "name": "Zloth", "id": "382368885267234816", @@ -129,7 +120,7 @@ { "name": "PlayerVMachine", "id": "273999507174195203", - "dept": "Instructor & Associate", + "dept": "Instructor & Core Team", "emailAddress": "nicolas@staff.libraryofcode.org", "bio": "I write C++ to pay off my student loans" }, diff --git a/src/models/File.ts b/src/models/File.ts new file mode 100644 index 0000000..f2fa685 --- /dev/null +++ b/src/models/File.ts @@ -0,0 +1,21 @@ +import { Document, Schema, model } from 'mongoose'; + +export interface FileInterface extends Document { + name: string, + identifier: string, + mimeType: string, + data: Buffer, + downloaded: number, + maxDownloads: number, +} + +const File: Schema = new Schema({ + name: String, + identifier: String, + mimeType: String, + data: Buffer, + downloaded: Number, + maxDownloads: Number, +}); + +export default model('File', File); diff --git a/src/models/index.ts b/src/models/index.ts index c296c03..8bf8622 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,3 +1,4 @@ +export { default as File, FileInterface } from './File'; export { default as Member, MemberInterface } from './Member'; export { default as Moderation, ModerationInterface } from './Moderation'; export { default as PagerNumber, PagerNumberInterface, PagerNumberRaw } from './PagerNumber';