Merge branch 'dev'

pull/29/head
Matthew 2020-07-13 00:02:05 -04:00
commit cdb074b2bf
No known key found for this signature in database
GPG Key ID: 210AF32ADE3B5C4B
12 changed files with 131 additions and 51 deletions

View File

@ -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);
}
});
}
}

View File

@ -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<MemberInterface>, Moderation: mongoose.Model<ModerationInterface>, PagerNumber: mongoose.Model<PagerNumberInterface>, Rank: mongoose.Model<RankInterface>, Redirect: mongoose.Model<RedirectInterface>, local: { muted: LocalStorage } };
public db: { File: mongoose.Model<FileInterface>, Member: mongoose.Model<MemberInterface>, Moderation: mongoose.Model<ModerationInterface>, PagerNumber: mongoose.Model<PagerNumberInterface>, Rank: mongoose.Model<RankInterface>, Redirect: mongoose.Model<RedirectInterface>, local: { muted: LocalStorage } };
constructor(token: string, options?: eris.ClientOptions) {
super(token, options);
this.commands = new Collection<Command>();
this.events = new Collection<Event>();
this.intervals = new Collection<NodeJS.Timeout>();
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() {

View File

@ -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();
}

View File

@ -189,7 +189,7 @@ export default class Moderation {
return mod.save();
}
public async kick(user: Member|User, moderator: Member, reason?: string): Promise<ModerationInterface> {
public async kick(user: Member | User, moderator: Member, reason?: string): Promise<ModerationInterface> {
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');

View File

@ -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 `<RichEmbed>.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.');

View File

@ -131,6 +131,14 @@ export default class Util {
return array;
}
public splitArray<T>(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;

View File

@ -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';

View File

@ -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] });

View File

@ -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 <channel> [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 = <TextChannel> 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 = `<h3>Library of Code sp-us</h3><strong>Channel:</strong> ${chan.name} (${chan.id})<br><strong>Generated by:</strong> ${message.author.username}#${message.author.discriminator}<br><strong>Generated at:</strong> ${new Date().toLocaleString('en-us')}<br><br>`;
for (const msg of messages) {
html += `(<i>${new Date(msg.timestamp).toLocaleString('en-us')}</i>) [<strong>${msg.author.username}#${msg.author.discriminator} - ${msg.author.id}</strong>]: ${msg.cleanContent}<br>`;
}
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);
}
}
}

View File

@ -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"
},

21
src/models/File.ts Normal file
View File

@ -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<FileInterface>('File', File);

View File

@ -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';