mute command, local storage class, and additional improvements

merge-requests/15/head
Matthew 2020-07-09 04:41:29 -04:00
parent c627367577
commit 9c119691a4
No known key found for this signature in database
GPG Key ID: 210AF32ADE3B5C4B
21 changed files with 356 additions and 30 deletions

1
.gitignore vendored
View File

@ -12,3 +12,4 @@ build/config.yaml
# Build/Distribution Files
build
dist
localstorage

View File

@ -1,7 +1,7 @@
import eris from 'eris';
import mongoose from 'mongoose';
import { promises as fs } from 'fs';
import { Collection, Command, Util, ServerManagement, Event } from '.';
import { Collection, Command, LocalStorage, Util, ServerManagement, Event } from '.';
import { Member, MemberInterface, Moderation, ModerationInterface, PagerNumber, PagerNumberInterface, Rank, RankInterface, Redirect, RedirectInterface } from '../models';
export default class Client extends eris.Client {
@ -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> };
public db: { Member: mongoose.Model<MemberInterface>, Moderation: mongoose.Model<ModerationInterface>, PagerNumber: mongoose.Model<PagerNumberInterface>, Rank: mongoose.Model<RankInterface>, Redirect: mongoose.Model<RedirectInterface>, local: 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 };
this.db = { Member, Moderation, PagerNumber, Rank, Redirect, local: new LocalStorage(this) };
}
public async loadDatabase() {

View File

@ -1,4 +1,4 @@
import { Member, Message, TextableChannel } from 'eris';
import { Guild, Member, Message, TextableChannel } from 'eris';
import { Client } from '.';
export default class Command {
@ -54,6 +54,10 @@ export default class Command {
this.aliases = [];
}
get mainGuild() {
return this.client.guilds.get(this.client.config.guildID);
}
public checkPermissions(member: Member): boolean {
if (member.id === '278620217221971968' || member.id === '253600545972027394') return true;
switch (this.permissions) {

120
src/class/LocalStorage.ts Normal file
View File

@ -0,0 +1,120 @@
/* eslint-disable no-constant-condition */
import { promises as fs, constants } from 'fs';
import { Client } from '.';
type JSONData = [{key: string, value: any}?];
/**
* Persistant local JSON-based storage.
* Auto-locking system to prevent corrupted data.
* @author Matthew <matthew@staff.libraryofcode.org>
*/
export default class LocalStorage {
private client: Client;
protected storagePath: string;
private locked: boolean = false;
constructor(client: Client, storagePath = `${__dirname}/../../localstorage`) {
this.client = client;
this.storagePath = storagePath;
this.init();
}
private async init() {
try {
await fs.access(`${this.storagePath}/1.json`, constants.F_OK);
} catch {
const setup = [];
await fs.writeFile(`${this.storagePath}/1.json`, JSON.stringify(setup), { encoding: 'utf8' });
}
}
/**
* Retrieves one data from the store.
* If the store has multiple entries for the same key, this function will only return the first entry.
* ```ts
* await LocalStorage.get<type>('data-key');
* ```
* @param key The key for the data entry.
*/
public async get<T>(key: string): Promise<T> {
while (true) {
if (!this.locked) break;
}
this.locked = true;
const file = await fs.readFile(`${this.storagePath}/1.json`, { encoding: 'utf8' });
this.locked = false;
const json: JSONData = JSON.parse(file);
const result = json.filter((data) => data.key === key);
if (!result[0]) return null;
return result[0].value;
}
/**
* Retrieves multiple data keys/values from the store.
* This function will return all of the values matching the key you provided exactly. Use `LocalStorage.get();` if possible.
* ```ts
* await LocalStorage.get<type>('data-key');
* @param key The key for the data entry.
*/
public async getMany<T>(key: string): Promise<{key: string, value: T}[]> {
while (true) {
if (!this.locked) break;
}
this.locked = true;
const file = await fs.readFile(`${this.storagePath}/1.json`, { encoding: 'utf8' });
this.locked = false;
const json: JSONData = JSON.parse(file);
const result = json.filter((data) => data.key === key);
if (result.length < 1) return null;
return result;
}
/**
* Sets a key/value pair and creates a new data entry.
* @param key The key for the data entry.
* @param value The value for the data entry, can be anything that is valid JSON.
* @param options.override [DEPRECATED] By default, this function will error if the key you're trying to set already exists. Set this option to true to override that setting.
* ```ts
* await LocalStorage.set('data-key', 'test');
* ```
*/
public async set(key: string, value: any): Promise<void> {
while (true) {
if (!this.locked) break;
}
this.locked = true;
const file = await fs.readFile(`${this.storagePath}/1.json`, { encoding: 'utf8' });
const json: JSONData = JSON.parse(file);
json.push({ key, value });
await fs.writeFile(`${this.storagePath}/1.json`, JSON.stringify(json), { encoding: 'utf8' });
this.locked = false;
}
/**
* Deletes the data for the specified key.
* **Warning:** This function will delete ALL matching entries.
* ```ts
* await LocalStorage.del('data-key');
* ```
* @param key The key for the data entry.
*/
public async del(key: string): Promise<void> {
while (true) {
if (!this.locked) break;
}
this.locked = true;
const file = await fs.readFile(`${this.storagePath}/1.json`, { encoding: 'utf8' });
const json: JSONData = JSON.parse(file);
const filtered = json.filter((data) => data.key !== key);
await fs.writeFile(`${this.storagePath}/1.json`, JSON.stringify(filtered), { encoding: 'utf8' });
this.locked = false;
}
}

View File

@ -111,6 +111,85 @@ export default class Moderation {
return mod.save();
}
public async mute(user: User, moderator: Member, duration: number, reason?: string): Promise<ModerationInterface> {
if (reason && reason.length > 512) throw new Error('Mute reason cannot be longer than 512 characters');
const member = await this.client.getRESTGuildMember(this.client.config.guildID, user.id);
if (!member) throw new Error('Cannot find member.');
await member.addRole('478373942638149643', `Muted by ${moderator.username}#${moderator.discriminator}`);
const logID = randomBytes(2).toString('hex');
const mod = new ModerationModel({
userID: user.id,
logID,
moderatorID: moderator.id,
reason: reason || null,
type: 2,
date: new Date(),
});
const now: number = Date.now();
let date: Date;
let processed = true;
if (duration > 0) {
date = new Date(now + duration);
processed = false;
} else date = null;
const expiration = { date, processed };
mod.expiration = expiration;
await this.client.db.local.set(`muted-${member.id}`, true);
const embed = new RichEmbed();
embed.setTitle(`Case ${logID} | Mute`);
embed.setColor('#ffff00');
embed.setAuthor(user.username, user.avatarURL);
embed.setThumbnail(user.avatarURL);
embed.addField('User', `<@${user.id}>`, true);
embed.addField('Moderator', `<@${moderator.id}>`, true);
if (reason) {
embed.addField('Reason', reason, true);
}
if (date) {
embed.addField('Expiration', moment(date).calendar(), true);
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
this.client.createMessage(this.logChannels.modlogs, { embed });
return mod.save();
}
public async unmute(userID: string, moderator: Member, reason?: string): Promise<ModerationInterface> {
const member = await this.client.getRESTGuildMember(this.client.config.guildID, userID);
if (!member) {
await this.client.db.local.del(`muted-${userID}`);
throw new Error('Member doesn\'t exist.');
}
await member.removeRole('478373942638149643');
const logID = randomBytes(2).toString('hex');
const mod = new ModerationModel({
userID,
logID,
moderatorID: moderator.id,
reason: reason || null,
type: 1,
date: new Date(),
});
await this.client.db.local.del(`muted-${member.id}`);
const embed = new RichEmbed();
embed.setTitle(`Case ${logID} | Unmute`);
embed.setColor('#1abc9c');
embed.setAuthor(member.user.username, member.user.avatarURL);
embed.setThumbnail(member.user.avatarURL);
embed.addField('User', `<@${member.user.id}>`, true);
embed.addField('Moderator', `<@${moderator.id}>`, true);
if (reason) {
embed.addField('Reason', reason, true);
}
embed.setFooter(this.client.user.username, this.client.user.avatarURL);
embed.setTimestamp();
this.client.createMessage(this.logChannels.modlogs, { embed });
return mod.save();
}
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);

View File

@ -2,6 +2,7 @@ export { default as Client } from './Client';
export { default as Collection } from './Collection';
export { default as Command } from './Command';
export { default as Event } from './Event';
export { default as LocalStorage } from './LocalStorage';
export { default as Moderation } from './Moderation';
export { default as RichEmbed } from './RichEmbed';
export { default as Route } from './Route';

View File

@ -17,7 +17,7 @@ export default class AddRank extends Command {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
if (!args[1]) return this.error(message.channel, 'Permissions are required.');
if (!args[2]) return this.error(message.channel, 'A description is required');
const role = this.client.util.resolveRole(args[0], this.client.guilds.get(this.client.config.guildID));
const role = this.client.util.resolveRole(args[0], this.mainGuild);
if (!role) return this.error(message.channel, 'The role you specified doesn\'t appear to exist.');
const check = await this.client.db.Rank.findOne({ roleID: role.id });

View File

@ -16,7 +16,7 @@ export default class Ban extends Command {
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const member = this.client.util.resolveMember(args[0], message.guild);
const member = this.client.util.resolveMember(args[0], this.mainGuild);
let user: User;
if (!member) {
try {

View File

@ -15,7 +15,7 @@ export default class DelRank extends Command {
public async run(message: Message, args: string[]) {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
const role = this.client.util.resolveRole(args[0], this.client.guilds.get(this.client.config.guildID));
const role = this.client.util.resolveRole(args[0], this.mainGuild);
if (!role) return this.error(message.channel, 'The role you specified doesn\'t appear to exist.');
const check = await this.client.db.Rank.findOne({ roleID: role.id });

View File

@ -27,7 +27,7 @@ export default class Game extends Command {
let member: Member;
if (!args[0]) member = message.member;
else {
member = this.client.util.resolveMember(args.join(' '), message.guild);
member = this.client.util.resolveMember(args.join(' '), this.mainGuild);
if (!member) {
return this.error(message.channel, 'Member not found.');
}

View File

@ -12,10 +12,12 @@ export { default as help } from './help';
export { default as info } from './info';
export { default as kick } from './kick';
export { default as listredirects } from './listredirects';
export { default as mute } from './mute';
export { default as npm } from './npm';
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 unban } from './unban';
export { default as unmute } from './unmute';
export { default as whois } from './whois';

View File

@ -18,7 +18,7 @@ export default class Kick extends Command {
let user: Member = this.client.util.resolveMember(args[0], message.guild);
if (!user) {
try {
user = await this.client.getRESTGuildMember(this.client.config.guildID, args[0]);
user = await this.client.getRESTGuildMember(this.mainGuild.id, args[0]);
} catch {
return this.error(message.channel, 'Cannot find user.');
}

45
src/commands/mute.ts Normal file
View File

@ -0,0 +1,45 @@
import moment, { unitOfTime } from 'moment';
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Mute extends Command {
constructor(client: Client) {
super(client);
this.name = 'mute';
this.description = 'Mutes a member.';
this.usage = 'mute <member> [time] [reason]';
this.permissions = 2;
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 member = this.client.util.resolveMember(args[0], this.mainGuild);
if (!member) return this.error(message.channel, 'Cannot find user.');
try {
const res1 = await this.client.db.local.get<boolean>(`muted-${member.id}`);
if (res1 || this.mainGuild.members.get(member.id).roles.includes('478373942638149643')) return this.error(message.channel, 'This user is already muted.');
} catch {} // eslint-disable-line no-empty
if (member && !this.client.util.moderation.checkPermissions(member, message.member)) return this.error(message.channel, 'Permission Denied.');
message.delete();
let momentMilliseconds: number;
let reason: string;
if (args.length > 1) {
const lockLength = args[1].match(/[a-z]+|[^a-z]+/gi);
const length = Number(lockLength[0]);
const unit = lockLength[1] as unitOfTime.Base;
momentMilliseconds = moment.duration(length, unit).asMilliseconds();
reason = momentMilliseconds ? args.slice(2).join(' ') : args.slice(1).join(' ');
if (reason.length > 512) return this.error(message.channel, 'Mute reasons cannot be longer than 512 characters.');
}
await this.client.util.moderation.mute(member.user, message.member, momentMilliseconds, reason);
return this.success(message.channel, `${member.user.username}#${member.user.discriminator} has been muted.`);
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

View File

@ -65,7 +65,7 @@ export default class Page extends Command {
}
public logPage(sender: { number: string, user?: string }, recipient: { number: string, user?: string }, type: 'discord' | 'email', code: string): void {
const chan = <TextableChannel> this.client.guilds.get(this.client.config.guildID).channels.get('722636436716781619');
const chan = <TextableChannel> this.mainGuild.channels.get('722636436716781619');
chan.createMessage(`***[${type.toUpperCase()}] \`${sender.number} (${sender.user ? sender.user : ''})\` sent a page to \`${recipient.number} (${recipient.user ? recipient.user : ''})\` with code \`${code}\`.***`);
this.client.util.signale.log(`PAGE (${type.toUpperCase()})| TO: ${recipient.number}, FROM: ${sender.number}, CODE: ${code}`);
}
@ -119,8 +119,8 @@ export default class Page extends Command {
}
for (const id of recipientEntry.discordIDs) {
const recipient = this.client.guilds.get(this.client.config.guildID).members.get(recipientEntry.individualAssignID);
const sender = this.client.guilds.get(this.client.config.guildID).members.get(senderEntry.individualAssignID);
const recipient = this.mainGuild.members.get(recipientEntry.individualAssignID);
const sender = this.mainGuild.members.get(senderEntry.individualAssignID);
const chan = await this.client.getDMChannel(id);
if (!chan) continue;
if (!recipient || !sender) {
@ -131,8 +131,8 @@ export default class Page extends Command {
chan.createMessage(`${options?.emergencyNumber ? `[SEN#${options.emergencyNumber}] ` : ''}__**Page**__\n**Recipient PN:** ${recipientNumber}\n**Sender PN:** ${senderNumber} (${sender ? `${sender.username}#${sender.discriminator}` : ''})\n**Initial Command:** https://discordapp.com/channels/${message.guild.id}/${message.channel.id}/${message.id} (<#${message.channel.id}>)\n\n**Pager Code:** ${code} (${this.local.codeDict.get(code)})${txt ? `\n**Message:** ${txt}` : ''}`);
}
for (const email of recipientEntry.emailAddresses) {
const recipient = this.client.guilds.get(this.client.config.guildID).members.get(recipientEntry.individualAssignID);
const sender = this.client.guilds.get(this.client.config.guildID).members.get(senderEntry.individualAssignID);
const recipient = this.mainGuild.members.get(recipientEntry.individualAssignID);
const sender = this.mainGuild.members.get(senderEntry.individualAssignID);
if (!recipient || !sender) {
this.logPage({ number: senderNumber, user: 'N/A' }, { number: recipientNumber, user: 'N/A' }, 'email', code);
} else {

View File

@ -1,4 +1,5 @@
import { Message, Role } from 'eris';
import { createPaginationEmbed } from 'eris-pagination';
import { Client, Command, RichEmbed } from '../class';
export default class Rank extends Command {
@ -16,9 +17,7 @@ export default class Rank extends Command {
try {
if (!args[0]) {
const roles = await this.client.db.Rank.find();
const embed = new RichEmbed();
embed.setTitle('Ranks');
embed.setDescription(`Use \`${this.client.config.prefix}rank <rank name>\` to join/leave the rank.`);
const rankArray: [{ name: string, value: string }?] = [];
for (const rank of roles.sort((a, b) => a.name.localeCompare(b.name))) {
let perms: string;
if (rank.permissions.includes('0')) {
@ -26,17 +25,26 @@ export default class Rank extends Command {
} else {
const rolesArray: Role[] = [];
rank.permissions.forEach((r) => {
rolesArray.push(this.client.guilds.get(this.client.config.guildID).roles.get(r));
rolesArray.push(this.mainGuild.roles.get(r));
});
perms = rolesArray.map((r) => message.guild.roles.get(r.id)).sort((a, b) => b.position - a.position).map((r) => `<@&${r.id}>`).join(', ');
}
let hasRank = false;
if (message.member.roles.includes(rank.roleID)) hasRank = true;
embed.addField(rank.name, `${hasRank ? '*You have this role.*\n' : ''}__Description:__ ${rank.description}\n__Permissions:__ ${perms}`);
rankArray.push({ name: rank.name, value: `${hasRank ? '*You have this role.*\n' : ''}__Description:__ ${rank.description}\n__Permissions:__ ${perms}` });
}
const ranksSplit = this.client.util.splitFields(rankArray);
const cmdPages: RichEmbed[] = [];
ranksSplit.forEach((split) => {
const embed = new RichEmbed();
embed.setTitle('Ranks');
embed.setDescription(`Use \`${this.client.config.prefix}rank <rank name>\` to join/leave the rank.`);
embed.setFooter(`Requested by: ${message.author.username}#${message.author.discriminator} | ${this.client.user.username}`, message.author.avatarURL);
embed.setTimestamp();
return message.channel.createMessage({ embed });
split.forEach((c) => embed.addField(c.name, c.value));
});
if (cmdPages.length === 1) return message.channel.createMessage({ embed: cmdPages[0] });
return createPaginationEmbed(message, cmdPages);
}
const role = this.client.util.resolveRole(args.join(' '), this.client.guilds.get(this.client.config.guildID));
if (!role) return this.error(message.channel, 'The role you specified doesn\'t exist.');

View File

@ -17,10 +17,7 @@ export default class Roleinfo extends Command {
try {
if (!args[0]) return this.client.commands.get('help').run(message, [this.name]);
let role: Role = message.guild.roles.find((r: Role) => r.id === args[0]);
if (!role) { // if it's a role name
role = message.guild.roles.find((r: Role) => r.name.toLowerCase().includes(args.join(' ').toLowerCase()));
}
const role = this.client.util.resolveRole(args[0], this.mainGuild);
if (!role) return this.error(message.channel, 'Could not find role.');
const perms = role.permissions;

34
src/commands/unmute.ts Normal file
View File

@ -0,0 +1,34 @@
import { Message } from 'eris';
import { Client, Command } from '../class';
export default class Unmute extends Command {
constructor(client: Client) {
super(client);
this.name = 'unmute';
this.description = 'Unmutes a member.';
this.usage = 'unmute <member> [reason]';
this.permissions = 2;
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 member = this.client.util.resolveMember(args[0], this.mainGuild);
if (!member) return this.error(message.channel, 'Cannot find user.');
try {
const res1 = await this.client.db.local.get<boolean>(`muted-${member.id}`);
if (!res1 || !this.mainGuild.members.get(member.id).roles.includes('478373942638149643')) return this.error(message.channel, 'This user is already unmuted.');
} catch {} // eslint-disable-line no-empty
if (member && !this.client.util.moderation.checkPermissions(member, message.member)) return this.error(message.channel, 'Permission Denied.');
message.delete();
await this.client.util.moderation.unmute(member.user.id, message.member, args.slice(1).join(' '));
return this.success(message.channel, `${member.user.username}#${member.user.discriminator} has been unmuted.`);
} catch (err) {
return this.client.util.handleError(err, message, this, false);
}
}
}

View File

@ -64,7 +64,7 @@ export default class Whois extends Command {
break;
}
}
embed.addField('Status', member.status === 'dnd' ? 'Do Not Disturb' : this.capsFirstLetter(member.status) || 'Unknown', true);
embed.addField('Status', member.status === 'dnd' ? 'Do Not Disturb' : this.capsFirstLetter(member.status) || 'Offline', true);
// const platform = member.bot && member.status !== 'offline' ? 'API/WebSocket' : Object.entries(message.member.clientStatus).filter((a) => a[1] !== 'offline').map((a) => this.capsFirstLetter(a[0])).join(', ');
// if (platform) embed.addField('Platform', platform, true);
embed.addField('Joined At', `${moment(new Date(member.joinedAt)).format('dddd, MMMM Do YYYY, h:mm:ss A')} ET`, true);

View File

@ -0,0 +1,22 @@
import { Member } from 'eris';
import { Client, Event } from '../class';
export default class GuildMemberAdd extends Event {
public client: Client;
constructor(client: Client) {
super(client);
this.event = 'guildMemberAdd';
}
public async run(member: Member) {
try {
const search = await this.client.db.local.get<boolean>(`muted-${member.user.id}`);
if (search === true) {
member.addRole('478373942638149643');
}
} catch (err) {
this.client.util.handleError(err);
}
}
}

View File

@ -11,8 +11,21 @@ export default function checkLock(client: Client): NodeJS.Timeout {
if (moderation.expiration.processed) return;
if (new Date() > moderation.expiration.date) {
await moderation.updateOne({ 'expiration.processed': true });
const moderator = client.guilds.get(client.config.guildID).members.get(moderation.moderatorID);
await client.util.moderation.unban(moderation.userID, moderator);
// const moderator = client.guilds.get(client.config.guildID).members.get(moderation.moderatorID);
const system = client.guilds.get(client.config.guildID).members.get(client.user.id);
switch (moderation.type) {
case 5:
await client.util.moderation.unban(moderation.userID, system);
break;
case 2:
console.log(await client.db.local.get<boolean>(`muted-${moderation.userID}`));
if (await client.db.local.get<boolean>(`muted-${moderation.userID}`) === true) {
await client.util.moderation.unmute(moderation.userID, system);
}
break;
default:
break;
}
client.util.signale.complete(`Released member ${moderation.userID} | Queue date at ${moderation.expiration.date.toLocaleString('en-us')}`);
}
});

View File

@ -15,7 +15,7 @@ export interface ModerationInterface extends Document {
*/
type: 0 | 1 | 2 | 3 | 4 | 5
date: Date,
expiration: {
expiration?: {
date: Date,
processed: boolean
}