yes
This commit is contained in:
root 2020-08-14 10:15:23 -04:00
commit a146997534
35 changed files with 945 additions and 178 deletions

View File

@ -1,4 +1,5 @@
const { EventEmitter } = require('events');
const { inspect } = require('util');
const ShardManager = require('./middleware/ShardManager.js');
const Logger = require('./middleware/logger/Logger.js');
@ -13,6 +14,7 @@ class Manager extends EventEmitter {
this.logger = new Logger(this);
this._built = false;
this.readyAt = null;
this.shardManager.on('message', this._handleMessage.bind(this));
// this.on('built', () => {
@ -23,6 +25,7 @@ class Manager extends EventEmitter {
_handleMessage(shard, message) {
if (message._mEval) return this.eval(shard, message);
if (message._logger) return this.logger._handleMessage(shard, message);
if (message._webhook) return undefined; //todo
}
@ -33,9 +36,46 @@ class Manager extends EventEmitter {
this._built = true;
this.emit('built');
this.readyAt = Date.now();
}
/**
*
*
* @param {*} shard The shard from which the eval came and to which it will be returned
* @param {*} script The script to be executed
* @memberof Manager
* @private
*/
async eval(shard, { script }) {
this.logger.write('info', `Incoming manager eval from shard ${shard.id}:\n${script}`);
let result = null,
error = null;
try {
// eslint-disable-next-line no-eval
result = eval(script);
if (result instanceof Promise) result = await result;
//if(typeof result !== 'string') result = inspect(result);
} catch (err) {
error = err.stack || err;
}
return shard.send({
_evalResult: true,
script,
result,
error
});
}
get uptime() {
return this.readyAt !== null ? Date.now() - this.readyAt : 0;
}
}
module.exports = Manager;

View File

@ -138,7 +138,7 @@ class Shard extends EventEmitter {
if(!message || message._eval !== script) return;
child.removeListener('message', listener);
this._evals.delete(script);
if(!message._error) resolve(message._result); else reject(new Error(message._error));
if(!message._error) resolve(message._result); else reject(new Error(message._error.stack));
};
child.on('message', listener);

View File

@ -13,7 +13,7 @@ const RateLimiter = require('./RateLimiter.js');
const StorageManager = require('../storage/StorageManager.js');
const ModerationManager = require('../moderation/ModerationManager.js');
const { Guild, GuildMember, User, Message } = require('../../structure/extensions/'); //eslint-disable-line no-unused-vars
const { Guild, GuildMember, User, Message, TextChannel, Role } = require('../../structure/extensions/'); //eslint-disable-line no-unused-vars
const { Command, Observer, Inhibitor, Setting } = require('../../structure/interfaces/');
const { DefaultGuild } = require('../../util/defaults/');
@ -40,6 +40,8 @@ class DiscordClient extends Client {
//TODO: Default config for users and guilds.
this._defaultConfig = null;
this._evals = new Map();
process.on('message', this._handleMessage.bind(this));
}
@ -70,8 +72,40 @@ class DiscordClient extends Client {
}
async resolveUsers() {
return this.resolver.resolveUsers(...arguments);
}
async resolveUser() {
return this.resolver.resolveUser(...arguments);
}
async _handleMessage(message) {
//Handle misc. messages.
if (message._evalResult) this.evalResult(message);
}
async managerEval(script) {
return new Promise((resolve, reject) => {
this._evals.set(script, { resolve, reject });
process.send({ _mEval: true, script });
});
}
evalResult({ script, result, error }) {
const promise = this._evals.get(script);
if (result) promise.resolve(result);
else promise.reject(error);
this._evals.delete(script);
}

View File

@ -1,4 +1,5 @@
const timestring = require('timestring');
const moment = require('moment');
class Resolver {
@ -15,6 +16,7 @@ class Resolver {
* @returns
* @memberof Resolver
*/
resolveComponent(arg, strict = true, type = 'any') {
const string = arg.toLowerCase();
@ -27,6 +29,32 @@ class Resolver {
}
resolveCases(str = '', max = 0) {
const cases = [];
const matches = str.match(/(\d{1,6})(?:\.\.\.?|-)(\d{1,6})?/iu);
if(matches) {
const [ , first, second ] = matches;
let difference = Math.abs((second ? second : max) - parseInt(first));
if(difference+parseInt(first) > max) difference = max;
new Array(difference+1).fill(0)
.forEach((item, i) => {
const number = i+parseInt(first);
if(number <= max) cases.push(i+parseInt(first));
});
} else {
const split = str.split(' ');
for(const string of split) {
const number = parseInt(string);
if(number <= max && !cases.includes(number)) cases.push(number);
}
}
return cases;
}
components(str = '', type, exact = true) {
const string = str.toLowerCase();
@ -360,6 +388,7 @@ class Resolver {
* @param {array<string>} [resolveables=[]] an array of channel resolveables (name, id)
* @param {guild} guild the guild in which to look for channels
* @param {boolean} [strict=false] whether or not partial names are resolved
* @param {function} [filter=()] filter the resolving channels
* @returns {array<GuildChannel> || false} an array of guild channels or false if none were resolved
* @memberof Resolver
*/
@ -484,6 +513,20 @@ class Resolver {
return time;
}
resolveDate(string) {
let date = null;
const matches = string.match(/([0-9]{4}(?:\/|-)[0-9]{2}(?:\/|-)[0-9]{2})/gimu); //YYYY-MM-DD is REQUIRED.
if(matches && matches.length > 0) {
try {
const string = matches[0].replace(/\//giu, '-');
date = moment(string);
} catch(error) {
return null;
}
}
return date;
}
async infinite(args = [], resolvers = [], strict, guild) {
let parsed = [], //eslint-disable-line prefer-const

View File

@ -1,6 +1,6 @@
const { Command } = require('../../../../interfaces/');
const { GuildMember } = require('../../../../extensions/');
const { Emojis } = require('../../../../../util/');
const { Emojis, Util } = require('../../../../../util/');
class GrantCommand extends Command {
@ -131,7 +131,7 @@ class GrantCommand extends Command {
return message.respond(message.format('C_GRANT_SUCCESS', {
targets: parsed.map((p) => p instanceof GuildMember ? `**${p.user.tag}**` : `**${p.name}**`).join(' '),
targets: parsed.map((p) => p instanceof GuildMember ? `**${Util.escapeMarkdown(p.user.tag)}**` : `**${Util.escapeMarkdown(p.name)}**`).join(' '),
permissions: parsedPermissions.map((p) => `\`${p}\``).join(', '),
channel: args.channel ? ` ${message.format('C_GRANT_SUCCESSCHANNELS', { channels: args.channel.value.map((c) => `**#${c.name}**`).join(', '), plural: args.channel.value.length === 1 ? '' : 's' })}` : ''
}), {

View File

@ -7,6 +7,7 @@ class ImportCommand extends Command {
super(client, {
name: 'import',
module: 'administration',
tags: ['log', 'logs', 'logging'],
memberPermissions: ['ADMINISTRATOR'],
archivable: false,
restricted: true

View File

@ -1,8 +1,13 @@
const { Role, MessageAttachment } = require('discord.js');
const { Command } = require('../../../../interfaces/');
const { Util, Emojis } = require('../../../../../util/');
const { stripIndents } = require('common-tags');
const Constants = {
pageSize: 9
};
class PermissionsCommand extends Command {
constructor(client) {
@ -10,7 +15,7 @@ class PermissionsCommand extends Command {
super(client, {
name: 'permissions',
module: 'administration',
usage: "<list|role-name|user-name>",
usage: "['list'|role|user]",
aliases: [
'perms',
'permission',
@ -23,15 +28,7 @@ class PermissionsCommand extends Command {
],
arguments: [
{
name: 'user',
aliases: ['users'],
type: 'BOOLEAN',
types: ['VERBAL', 'FLAG'],
default: true
},
{
name: 'raw',
aliases: ['json'],
name: 'export',
type: 'BOOLEAN',
types: ['VERBAL', 'FLAG'],
default: true
@ -46,13 +43,9 @@ class PermissionsCommand extends Command {
async execute(message, { params, args }) {
const permissions = await message.guild.permissions();
if(args.raw) {
await this._displayRaw(message, permissions);
return undefined;
}
if(params.length === 0) {
await this._showPermissions(message, Boolean(args.user));
if(args.export) {
await this._displayRaw(message, permissions);
return undefined;
}
@ -60,11 +53,15 @@ class PermissionsCommand extends Command {
await this._listAvailablePermissions(message);
return undefined;
}
const parameters = params.join(' ');
let resolveable = await this._parseResolveable(message, parameters);
if(resolveable) {
if(resolveable.user) resolveable = resolveable.user;
const type = resolveable.tag ? 'user' : 'role';
const permission = permissions[resolveable?.id || parameters];
if(resolveable && !permission) {
await message.respond(message.format('C_PERMISSIONS_PERMISSIONSNOTFOUND', { resolveable: resolveable.tag || resolveable.name, type, they: type === 'user' ? 'they ' : '' }), { emoji: 'failure' });
return undefined;
@ -78,7 +75,7 @@ class PermissionsCommand extends Command {
const embed = {
author: {
name: `${resolveable?.user?.tag || resolveable?.tag || resolveable?.name || parameters}'s Permissions`,
icon_url: resolveable.displayAvatarURL ? resolveable.displayAvatarURL() : message.guild.iconURL()
icon_url: resolveable.displayAvatarURL ? resolveable.displayAvatarURL() : message.guild.iconURL() //eslint-disable-line camelcase
},
description: `${message.format('C_PERMISSIONS_GLOBAL', { permissions: permission.global.length > 0 ? this._displayNames(permission.global).map((p) => `\`${p}\``).join(', ') : "`N/A`" })} ${Object.values(permission.channels).length > 0 ? message.format('C_PERMISSIONS_GLOBALALT') : ''}`,
fields: []
@ -115,41 +112,80 @@ class PermissionsCommand extends Command {
}
return message.embed(embed);
}
async _showPermissions(message, user = false) {
//End of displaying user/role permissions.
const _permissions = [];
for(const [key, value] of Object.entries(permissions)) {
if(value?.global?.length === 0 && Object.keys(value?.channels).length === 0) {
await this._deletePermission(message.guild, key);
continue;
}
if(!Number.isNaN(parseInt(key))) _permissions.push({ ...value, id: key });
}
let currentPage = 1;
if(parameters.length > 0) {
const number = parseInt(parameters[0]);
if(!Number.isNaN(number) && number > 1) {
currentPage = number;
}
}
const size = _permissions.length;
if(size === 0) {
return message.respond(message.format('C_PERMISSIONS_NOPERMISSIONS'), {
emoji: 'failure'
});
}
let { items, page, maxPage } = Util.paginate(_permissions, currentPage, Constants.pageSize); //eslint-disable-line prefer-const
const embed = {
author: {
name: message.format('C_PERMISSIONS_SHOWTITLE', { user }, true),
name: `Guild Permissions`,
icon_url: message.guild.iconURL() //eslint-disable-line camelcase
},
description: message.format('C_PERMISSIONS_SHOWDESCRIPTION', { resolve: user ? 'user' : 'role' }),
fields: []
fields: [],
footer: {
text: `• Page ${page}/${maxPage} | ${size} Results`
}
};
const permissions = message.guild._permissions;
for(const [id, value] of Object.entries(permissions)) {
if(id === '_id' || id === 'guildId') continue;
const item = await this.client.resolver[user ? 'resolveUser' : 'resolveRole'](id, true, message.guild); //dont kill me
if(item instanceof Role && user
|| !user && !(item instanceof Role)
|| !item) continue;
if(embed.fields.length === 25) {
embed.description += `\n${message.format('C_PERMISSIONS_MAXFIELDS')}`;
break;
for(const item of items) {
item.resolveable = await this._parseResolveable(message, item.id);
item.permissions = this._displayNames(item.global);
}
const name = item?.user?.tag || item?.tag || item?.name || id; //please dont kill me again
const channels = Object.values(value.channels).length;
if(channels === 0 && value.global.length === 0) {
embed.description += `\n\n${message.format('C_PERMISSIONS_NOPERMISSIONS')}`;
break;
const display = (items) => {
items = items.sort((a, b) => b.permissions.length - a.permissions.length);
for(const item of items) {
const field = {
name: `${item.resolveable instanceof Role ? Emojis.role : Emojis.member} ${Util.escapeMarkdown(item.resolveable.display)}`,
value: item.permissions.map((n) => `\`${n}\``).join('\n'),
inline: true
};
const channels = Object.keys(item.channels).length;
if(channels > 0) {
field.value += `\n\`..${channels} channel${channels === 1 ? '' : 's'}\``;
}
embed.fields.push(field);
}
};
display(items.filter((i) => i.resolveable instanceof Role));
display(items.filter((i) => !(i.resolveable instanceof Role)));
const empty = items.length % 3;
for(let i = 0; i<empty; i++) {
embed.fields.push({
name,
value: stripIndents`${this._displayNames(value.global).map((n) => `\`${n}\``).join('\n')}
${channels > 0 ? `\`..${channels} channel${channels === 1 ? '' : 's'}\`` : ''}`
name: '\u200b',
value: '\u200b',
inline: true
});
}
@ -162,16 +198,18 @@ class PermissionsCommand extends Command {
const components = this.client.registry.components.filter((c) => c.type === 'command' || c.type === 'module' && c.components.some((c) => c.type === 'command'))
.sort((a, b) => a - b);
return await message.respond(message.format('C_PERMISSIONS_LIST', { permissions: components.map((c) => `\`${c.resolveable}\``).join(', ') }), { emoji: 'success' });
return message.respond(message.format('C_PERMISSIONS_LIST', {
permissions: components.map((c) => `\`${c.resolveable}\``).join(', ')
}), { emoji: 'success' });
}
async _parseResolveable(message, resolveable) {
let parsed = await this.client.resolver.resolveRole(resolveable, false, message.guild);
let parsed = await this.client.resolver.resolveRole(resolveable, true, message.guild);
if(!parsed) {
parsed = await this.client.resolver.resolveMember(resolveable, false, message.guild);
parsed = await this.client.resolver.resolveMember(resolveable, true, message.guild);
if(!parsed) {
parsed = await this.client.resolver.resolveUser(resolveable, false);
parsed = await this.client.resolver.resolveUser(resolveable, true);
if(!parsed) return null;
}
}
@ -183,7 +221,10 @@ class PermissionsCommand extends Command {
const string = JSON.stringify(permissions);
const attachment = new MessageAttachment(Buffer.from(string), "permissions.json");
return await message.respond(message.format('C_PERMISSIONS_JSON'), { emoji: 'success', attachments: [ attachment ] });
return message.respond(message.format('C_PERMISSIONS_JSON'), {
emoji: 'success',
files: [ attachment ]
});
}
@ -199,7 +240,7 @@ class PermissionsCommand extends Command {
temp.push(component.resolveable);
}
}
temp.length === module.components.filter((c) => c.type === 'command').size
temp.length === module.components.filter((c) => c.type === 'command').size //eslint-disable-line no-unused-expressions
? names.push(module.resolveable)
: names = names.concat(temp);
temp = [];
@ -209,6 +250,25 @@ class PermissionsCommand extends Command {
}
async _deletePermission(guild, key) {
const permissions = guild._permissions;
delete permissions[key];
delete permissions._id;
try {
await this.client.storageManager.mongodb.permissions.updateOne(
{ guildId: guild.id },
permissions
);
} catch(error) {
return false;
}
return true;
}
}

View File

@ -1,6 +1,7 @@
const { User, GuildMember } = require('discord.js');
const { Command } = require('../../../../interfaces/');
const { Util } = require('../../../../../util/');
class RevokeCommand extends Command {
@ -122,7 +123,7 @@ class RevokeCommand extends Command {
};
return message.respond(message.format('C_REVOKE_SUCCESS', {
targets: parsed.map((p) => `**${name(p)}**`).join(' '),
targets: parsed.map((p) => `**${Util.escapeMarkdown(name(p))}**`).join(' '),
permissions: parsedPermissions.map((p) => `\`${p}\``).join(', '),
channel: args.channel ? ` ${message.format('C_REVOKE_SUCCESSCHANNELS', { channels: args.channel.value.map((c) => `**#${c.name}**`).join(', '), plural: args.channel.value.length === 1 ? '' : 's' })}` : ''
}), { emoji: 'success' });

View File

@ -1,5 +1,7 @@
const { inspect } = require('util');
const { username } = require('os').userInfo();
// eslint-disable-next-line no-unused-vars
const moment = require('moment');
let _storage = null; //eslint-disable-line
@ -42,7 +44,7 @@ class Evaluate extends Command {
async execute(message, { params, args }) {
params = params.join(' ');
const { guild, author, member } = message; //eslint-disable-line no-unused-vars
const { guild, author, member, client } = message; //eslint-disable-line no-unused-vars
try {
let evaled = eval(params); //eslint-disable-line no-eval

View File

@ -0,0 +1,109 @@
const { Command } = require('../../../../interfaces/');
class StatsCommand extends Command {
constructor(client) {
super(client, {
name: 'about',
module: 'developer',
aliases: [
'stats',
'info'
],
usage: '',
restricted: true,
arguments: [
{
name: 'log',
type: 'BOOLEAN',
types: ['FLAG'],
description: 'Logs the output in the console.'
}
],
showUsage: false
});
}
async execute(message, { params, args }) {
// TODO:
// Add some stuff that only shows when a dev runs the command, for instance amount of cached messages etc
const { guild } = message;
const { shard } = this.client;
const evalFunc = (thisArg) => {
return {
users: thisArg.users.cache.size,
guilds: thisArg.guilds.cache.size,
channels: thisArg.channels.cache.size,
memory: Math.floor(process.memoryUsage().heapUsed / 1024 / 1024),
uptime: thisArg.uptime
};
};
const mEvalFunc = (thisArg) => {
return {
memory: Math.floor(process.memoryUsage().heapUsed / 1024 / 1024),
uptime: Date.now() - thisArg.readyAt
};
};
const shardResults = await shard.broadcastEval(evalFunc).catch((error) => this.client.logger.error(error));
const managerResult = await this.client.managerEval(`(${mEvalFunc})(this)`).catch((error) => this.client.logger.error(error));
const descValues = {
devs: await this.client.resolveUsers(this.client._options.bot.owners).then((owners) => {
return owners.map((o) => o.tag).join(', ');
}),
uptime: this.client.resolver.timeAgo(Math.floor(managerResult.uptime/1000)),
memory: managerResult.memory,
shards: shard.count
};
const shardValues = {
cachedUsers: this.client.users.cache.size,
guilds: this.client.guilds.cache.size,
channels: this.client.channels.cache.size,
uptime: this.client.resolver.timeAgo(Math.floor(this.client.uptime/1000)),
memory: Math.floor(process.memoryUsage().heapUsed / 1024 / 1024)
};
const totalValues = shardResults.reduce((acc, curr) => {
Object.entries(curr).forEach(([key, val]) => {
if (!acc[key]) acc[key] = 0;
acc[key] += val;
});
return acc;
}, {});
totalValues.uptime = this.client.resolver.timeAgo(Math.floor(totalValues.uptime / 1000));
const embed = {
title: message.format('C_STATS_TITLE', {
client: this.client.user.tag,
version: require('../../../../../package.json').version
}),
description: message.format('C_STATS_DESC', descValues),
fields: [
{
name: message.format('C_STATS_CURRENT_SHARD', { shard: guild.shardID }),
value: message.format('C_STATS_CURRENT_SHARDS_VALUE', shardValues),
inline: true
},
{
name: message.format('C_STATS_TOTAL', { shards: shard.count }),
value: message.format('C_STATS_TOTAL_VALUE', totalValues),
inline: true
}
]
};
message.embed(embed);
}
}
module.exports = StatsCommand;

View File

@ -1,6 +1,21 @@
const { Command } = require('../../../../interfaces');
const similarity = require('similarity');
const { Util } = require('../../../../../util');
const { Util, Emojis } = require('../../../../../util');
const Constants = {
Badges: {
DISCORD_EMPLOYEE: Emojis['discord-staff'],
DISCORD_PARTNER: Emojis['discord-partner'],
HYPESQUAD_EVENTS: Emojis['hypesquad-events'],
BUGHUNTER_LEVEL_1: Emojis['bughunter'], //eslint-disable-line dot-notation
BUGHUNTER_LEVEL_2: Emojis['bughunter-gold'],
HOUSE_BRAVERY: Emojis['hypesquad-bravery'],
HOUSE_BRILLIANCE: Emojis['hypesquad-brilliance'],
HOUSE_BALANCE: Emojis['hypesquad-balance'],
EARLY_SUPPORTER: Emojis['early-supporter'],
VERIFIED_DEVELOPER: Emojis['bot-developer']
}
};
class UserCommand extends Command {
@ -86,67 +101,84 @@ class UserCommand extends Command {
if (!user) return message.formattedRespond('C_USER_404');
} else user = message.author;
const member = await message.guild.members.fetch(user.id).catch();
const member = await message.guild.members.fetch(user.id).catch((error) => {}); //eslint-disable-line
const { activities } = user.presence;
const flags = user.flags || await user.fetchFlags();
const badges = flags.toArray().filter((f) => Constants.Badges[f])
.map((f) => Constants.Badges[f]);
response = {
title: `**${user.tag}**`,
author: {
name: Util.escapeMarkdown(user.tag),
icon_url: user.displayAvatarURL() //eslint-disable-line camelcase
},
description: response,
thumbnail: {
url: user.avatarURL() || user.defaultAvatarURL
},
fields: [
{
fields: [],
footer: {
text: `• User ID: ${user.id}`
}
};
const userField = {
name: message.format('C_USER_DATA_NAME'),
value: message.format('C_USER_DATA', {
id: user.id,
bot: user.bot ? ` ${Emojis.bot}` : '',
created: user.createdAt.toDateString(),
status: user.presence.status,
// eslint-disable-next-line no-nested-ternary
activity: activities.length > 0 ? activities[0].type === 'CUSTOM_STATUS' ? `${activities[0].name}: ${activities[0].state || 'emoji'}` : activities[0].name : 'Nothing',
globalActivity: user.lastMessage ? user.lastMessage.createdAt.toDateString() : 'N/A'
}),
inline: true
}
],
footer: {
text: `ID: ${user.id}`
}
})
};
if (member) {
if(badges.length > 0) {
userField.value += `\n${message.format('C_USER_BADGES', {
badges: badges.join(' ')
})}`;
}
response.fields.push({
response.fields.push(userField);
if (member) {
const memberField = {
name: message.format('C_USER_MEMBER_NAME'),
value: message.format('C_USER_MEMBER', {
nickname: member.nickname ? member.nickname : 'N/A',
joined: member.joinedAt ? member.joinedAt.toDateString() : 'N/A',
serverActivity: member.lastMessage ? member.lastMessage.createdAt.toDateString() : 'N/A'
}),
inline: true
});
})
};
const roles = member.roles.cache.filter((r) => r.name !== '@everyone').sort((a, b) => b.rawPosition - a.rawPosition);
let counter = 0;
if (roles.size) response.fields.push({
name: message.format('C_USER_ROLES_TITLE'),
value: roles.map((r) => {
const str = `<@&${r.id}>`;
counter += str.length;
return counter <= 950 ? str : '';
}).join(' ')
});
const maxRoles = 30;
if(roles.size > 0) {
memberField.value += `\n${message.format('C_USER_MEMBER_ROLES', {
roles: roles.size > maxRoles ? `${roles.slice(0, maxRoles).map((r) => `<@&${r.id}>`).join(' ')} \`...${maxRoles-roles.size} more roles\`` : roles.map((r) => `<@&${r.id}>`).join(' ')
})}`;
}
response.fields.push(memberField);
// let counter = 0;
// if (roles.size) response.fields.push({
// name: message.format('C_USER_ROLES_TITLE'),
// value: roles.map((r) => {
// const str = `<@&${r.id}>`;
// counter += str.length;
// return counter <= 950 ? str : '';
// }).join(' ')
// });
const highestColouredRole = member.roles.cache.filter((role) => role.color !== 0).sort((a, b) => b.rawPosition - a.rawPosition).first();
if (highestColouredRole) response.color = highestColouredRole.color;
}
const flags = user.flags || await user.fetchFlags();
if (flags.bitfield) response.fields.push({
name: message.format('C_USER_FLAGS'),
value: flags.toArray().join(', ')
});
}
return message.embed(response);

View File

@ -0,0 +1,270 @@
const { Command } = require('../../../../interfaces/');
const { MessageAttachment } = require('discord.js');
const { stripIndents } = require('common-tags');
const moment = require('moment');
const { UploadLimit } = require('../../../../../util/Constants.js');
const { Util } = require('../../../../../util/');
const Constants = {
PageSize: 5,
MaxCharacters: 256,
MaxCharactersVerbose: 128, //Displays more information in the field, decreases characters.
Types: {
'NOTE': ['note', 'notes'],
'WARN': ['warn', 'warning', 'warns', 'warnings'],
'MUTE': ['mute', 'mutes', 'tempmute', 'tempmutes'],
'UNMUTE': ['unmute', 'unmutes', 'untempmute', 'untempmutes'],
'KICK': ['kick', 'kicks'],
'SOFTBAN': ['softban', 'softbans'],
'BAN': ['ban', 'bans', 'hardban', 'hardbans'],
'UNBAN': ['unban', 'unbans', 'unhardban', 'unhardbans'],
'VCMUTE': ['vcmute', 'vcmutes', 'vctempmute', 'vctempmutes'],
'VCUNMUTE': ['vcunmute', 'vcunmutes', 'vctempunmute', 'vctempunmutes'],
'VCKICK': ['vckick', 'vckicks'],
'VCBAN': ['vcban', 'vcbans'],
'VCUNBAN': ['vcunban', 'vcunbans'],
'PRUNE': ['prune', 'prunes', 'purge', 'purges'],
'SLOWMODE': ['slowmode', 'slowmodes'],
'ADDROLE': ['addrole', 'addroles', 'roleadd', 'roleadds'],
'REMOVEROLE': ['removerole', 'removeroles', 'roleremove', 'roleremoves'],
'NICKNAME': ['nickname', 'nicknames', 'dehoist', 'dehoists'],
'LOCKDOWN': ['lockdown', 'lockdowns'],
'UNLOCKDOWN': ['unlockdown', 'unlockdowns']
}
};
class HistoryCommand extends Command {
constructor(client) {
super(client, {
name: 'history',
module: 'moderation',
usage: "[user..|channel..]",
aliases: [
'moderation'
],
memberPermissions: ['MANAGE_MESSAGES'],
guildOnly: true,
arguments: [
{
name: 'before', //Search for moderation actions before x
usage: '<date>',
type: 'DATE',
types: ['FLAG'],
required: true
},
{
name: 'after', //Search for moderation actions after x
usage: '<date>',
type: 'DATE',
types: ['FLAG'],
required: true
},
{
name: 'oldest',
aliases: ['old'],
type: 'BOOLEAN',
types: ['FLAG'],
default: true
},
{
name: 'type',
aliases: ['types'],
type: 'STRING',
types: ['FLAG'],
infinite: true,
required: true
},
{
name: 'pagesize',
type: 'INTEGER',
types: ['FLAG'],
required: true,
default: 10,
min: 1,
max: 10
},
{
name: 'verbose', //Shows IDs for users/channels.
type: 'BOOLEAN',
types: ['FLAG'],
default: true
},
{
name: 'export', //Export moderation actions in a JSON.
type: 'BOOLEAN',
types: ['FLAG'],
default: true
},
{
name: 'private', //Send moderation history in DMs.
type: 'BOOLEAN',
types: ['FLAG'],
default: true
} //filter, exclude, verbose (NO PAGESIZE)
],
throttling: {
usages: 2,
duration: 10
}
});
this.client = client;
}
async execute(message, { params, args }) {
if(args.export) return this._exportLogs(message, Boolean(args.private));
const query = {
guild: message.guild.id
};
const { parsed, parameters } = await this.client.resolver.infinite(params, [
this.client.resolver.resolveMember.bind(this.client.resolver),
this.client.resolver.resolveUser.bind(this.client.resolver),
this.client.resolver.resolveChannel.bind(this.client.resolver)
], true, message.guild, (c) => c.type === 'text');
if(parsed.length > 0) query.target = { $in: parsed.map((p) => p.id) }; //Add resolved ids to the query.
if(args.before || args.after) {
query.timestamp = {};
if(args.before) query.timestamp.$lt = args.before.value.valueOf(); //Add before timestamps to the query.
if(args.after) query.timestamp.$gt = args.after.value.valueOf(); //Add after timestamps to the query.
}
if(args.type) {
const filter = [];
for(const value of args.type.value) {
for(const [ type, matches ] of Object.entries(Constants.Types)) {
if(matches.includes(value.toLowerCase())) {
filter.push(type);
}
}
}
query.type = { $in: filter };
}
const pageSize = args.pagesize ? args.pagesize.value : Constants.PageSize;
let page = 1;
if(parameters.length > 0) {
const number = parseInt(parameters[0]);
if(!Number.isNaN(number) && number > 1) {
page = number;
}
}
const collectionSize = await this.client.storageManager.mongodb.infractions.count(query);
if(collectionSize === 0) {
return message.respond(message.format('C_HISTORY_NORESULTS'), {
emoji: 'failure'
});
}
const maxPage = Math.ceil(collectionSize/pageSize);
if(page > maxPage) page = maxPage;
const infractions = await this.client.storageManager.mongodb.db.collection('infractions').find(query)
.sort({ timestamp: args.oldest ? 1 : -1 })
.skip((page-1)*pageSize).limit(pageSize)
.toArray();
const embed = {
author: {
name: 'Infraction History',
icon_url: message.guild.iconURL() //eslint-disable-line camelcase
},
fields: [],
footer: {
text: `• Page ${page}/${maxPage} | ${collectionSize} Results`
}
};
let long = false;
const handleReason = (text) => {
const MaxCharacters = Constants[args.verbose ? 'MaxCharactersVerbose' : 'MaxCharacters'];
text = Util.escapeMarkdown(text);
if(text.length > MaxCharacters) {
text = `${text.substring(0, MaxCharacters-3)}...`;
long = true;
}
text = text.replace(/\\n/giu, ' ');
return text;
};
for(let i = 0; i<infractions.length; i++) {
const infraction = infractions[i];
let target = null;
if(infraction.targetType === 'user') {
target = await this.client.resolver.resolveUser(infraction.target);
} else {
target = await this.client.resolver.resolveChannel(infraction.target, true, message.guild);
}
const executor = await this.client.resolver.resolveUser(infraction.executor);
let string = stripIndents`**Target:** ${Util.escapeMarkdown(target.display)}${args.verbose ? ` (${target.id})` : ''}
**Moderator:** ${executor ? `${Util.escapeMarkdown(executor.tag)}${args.verbose ? ` (${infraction.executor})` : ''}` : infraction.executor}`;
if(infraction.duration) string += `\n**Duration:** ${Util.duration(infraction.duration)}`;
if(infraction.points) string += `\n**Points:** ${infraction.points}`;
string += `\n**Reason:** \`${handleReason(infraction.reason)}\``;
if(i !== infractions.length-1) string += `\n\u200b`; //Space out cases (as long as its not at the end)
embed.fields.push({
name: `__**${infraction.type} \`[case-${infraction.case}]\`** *(${moment(infraction.timestamp).fromNow()})*__`,
value: string
});
}
if(long) embed.footer.text += ` • To see the full reason, use the ${message.guild.prefix}case command.`;
const type = message.format('C_HISTORY_SUCCESSTYPE', { old: Boolean(args.oldest) }, true);
message.respond(message.format('C_HISTORY_SUCCESS', {
targets: parsed.length > 0 ? message.format('C_HISTORY_SUCCESSTARGETS', {
plural: parsed.length === 1 ? '' : 's',
targets: parsed.map((p) => `**${Util.escapeMarkdown(p.display)}**`).join(' ')
}) : '',
type
}), {
emoji: 'success',
embed,
dm: Boolean(args.private)
});
}
async _exportLogs(message, priv = false) {
const logs = await this.client.storageManager.mongodb.infractions.find({ guild: message.guild.id });
const string = JSON.stringify(logs);
const attachment = new MessageAttachment(Buffer.from(string), `${message.guild.id}-moderation.json`);
const bytes = Buffer.byteLength(attachment.attachment);
const premium = priv ? '0' : message.guild.premiumTier;
if(bytes > UploadLimit[premium]*1024*1024) {
message.respond(message.format('C_HISTORY_FAILEXPORT', {
amount: bytes,
max: UploadLimit[premium]*1024*1024
}));
}
message.respond(message.format('C_HISTORY_SUCCESSEXPORT'), {
files: [ attachment ],
emoji: 'success',
dm: priv
});
}
}
module.exports = HistoryCommand;

View File

@ -200,8 +200,7 @@ class PruneCommand extends Command {
.handleInfraction(Prune, message, {
targets: parsed,
data: {
amount: int,
message: message.id
amount: int
},
reason
});

View File

@ -48,13 +48,17 @@ class AvatarCommand extends Command {
}
return await message.embed({
return message.embed({
author: {
name: user.tag
name: user.tag,
icon_url: user.displayAvatarURL()
},
description: `[**Link to Image**](${avatar})`,
image: {
url: avatar
},
footer: {
text: `• Format: .${args.format?.value || 'webp'} | Size: ${args.size?.value || '128'}`
}
});

View File

@ -488,6 +488,11 @@ class CommandHandler extends Observer {
const time = await this.client.resolver.resolveTime(str);
if(!time) return { error: true };
return { error: false, value: time };
},
DATE: async(str) => {
const date = await this.client.resolver.resolveDate(str);
if(!date) return { error: true };
return { error: false, value: date };
}
};

View File

@ -34,7 +34,6 @@ class GuildLogger extends Observer {
});
this.hooks = [
// ['message', this.storeAttachment.bind(this)], //Attachment logging
['messageDelete', this.messageDelete.bind(this)],
['messageDeleteBulk', this.messageDeleteBulk.bind(this)],
['messageUpdate', this.messageEdit.bind(this)],

View File

@ -38,6 +38,10 @@ const GuildMember = Structures.extend('GuildMember', (GuildMember) => {
return Date.now()-this._cached;
}
get display() {
return this.user.tag;
}
}

View File

@ -94,7 +94,7 @@ const Message = Structures.extend('Message', (Message) => {
}
async respond(str, opts = { files: [], embed: null }) {
async respond(str, opts = { files: [], embed: null, dm: false }) {
if(typeof str === 'string') {
if(opts.emoji) {
@ -105,7 +105,7 @@ const Message = Structures.extend('Message', (Message) => {
if(opts.reply) str = `<@!${this.author.id}> ${str}`;
}
this._pending = await this.channel.send(str, { files: opts.files, embed: opts.embed });
this._pending = await this[opts.dm ? 'author' : 'channel'].send(str, { files: opts.files, embed: opts.embed });
return this._pending;
}

View File

@ -0,0 +1,17 @@
const { Structures } = require('discord.js');
const Role = Structures.extend('Role', (Role) => {
class ExtendedRole extends Role {
get display() {
return this.name;
}
}
return ExtendedRole;
});
module.exports = Role;

View File

@ -0,0 +1,17 @@
const { Structures } = require('discord.js');
const TextChannel = Structures.extend('TextChannel', (TextChannel) => {
class ExtendedTextChannel extends TextChannel {
get display() {
return `#${this.name}`;
}
}
return ExtendedTextChannel;
});
module.exports = TextChannel;

View File

@ -68,6 +68,10 @@ const User = Structures.extend('User', (User) => {
return this.client._options.bot.prefix;
}
get display() {
return this.tag;
}
}
return ExtendedUser;

View File

@ -2,5 +2,7 @@ module.exports = {
Message: require('./Message.js'),
Guild: require('./Guild.js'),
GuildMember: require('./GuildMember.js'),
User: require('./User.js')
User: require('./User.js'),
TextChannel: require('./TextChannel.js'),
Role: require('./Role.js')
};

View File

@ -14,7 +14,8 @@ const Constants = {
'CHANNEL',
'TEXTCHANNEL',
'VOICECHANNEL',
'TIME'
'TIME',
'DATE'
],
ArgumentTypes: [
'FLAG',

View File

@ -75,7 +75,7 @@ class Command extends Component {
if (this.arguments.length && verbose) {
fields.push({
name: `${message.format('GENERAL_ARGUMENTS')}`,
value: this.arguments.map((a) => `\`${a.types.length === 1 && a.types.includes('FLAG') ? '--' : ''}${a.name}${a.usage ? ` ${a.usage}` : ''}\`: ${message.format(`A_${a.name.toUpperCase()}_${this.name.toUpperCase()}_DESCRIPTION`)}`)
value: this.arguments.map((a) => `\`${a.types.length === 1 && a.types.includes('FLAG') ? '--' : ''}${a.name}${a.usage ? ` ${a.usage}` : ''}\`: ${message.format(`A_${a.name.toUpperCase()}_${this.name.toUpperCase()}_DESCRIPTION`)}`).join('\n')
});
}

View File

@ -1,5 +1,5 @@
//Command Arguments
//Grant Command
[A_CHANNEL_GRANT_DESCRIPTION]
Specify channels to grant specific permissions to.
@ -8,8 +8,5 @@ Specify channels to grant specific permissions to.
Specify channels to revoke permissions from.
//Permissions Command
[A_USER_PERMISSIONS_DESCRIPTION]
Enable viewing all user's permissions.
[A_RAW_PERMISSIONS_DESCRIPTION]
Upload a raw JSON file of all of the permissions in the guild.
[A_EXPORT_PERMISSIONS_DESCRIPTION]
Export a JSON file of all of the permissions in the server.

View File

@ -100,7 +100,10 @@ Filter messages sent by specified users.
[A_BOTS_PRUNE_DESCRIPTION]
Filter messages sent by bots.
[A_TEXT_PRUNE_DESCRIPTION]
[A_HUMANS_PRUNE_DESCRIPTION]
Filter messages sent by non-bots.
[A_CONTAINS_PRUNE_DESCRIPTION]
Filter messages containing specified text.
[A_STARTSWITH_PRUNE_DESCRIPTION]
@ -109,6 +112,15 @@ Filter messages starting with specified text.
[A_ENDSWITH_PRUNE_DESCRIPTION]
Filter messages ending with specified text.
[A_TEXT_PRUNE_DESCRIPTION]
Filter messages containing text.
[A_INVITES_PRUNE_DESCRIPTION]
Filter messages containing discord invites.
[A_LINKS_PRUNE_DESCRIPTION]
Filter messages containing links.
[A_EMOJIS_PRUNE_DESCRIPTION]
Filter messages containing emojis.
@ -122,20 +134,33 @@ Filter messages containing images.
Filter messages containing any attachment.
[A_AFTER_PRUNE_DESCRIPTION]
Filter messages after a specified message ID.
Filter messages after a specified message.
[A_BEFORE_PRUNE_DESCRIPTION]
Filter messages before a specified message ID.
Filter messages before a specified message.
[A_AND_PRUNE_DESCRIPTION]
Use a logical AND for all checks.
Use a logical AND for checks.
[A_NOT_PRUNE_DESCRIPTION]
Use a logical NOT for all checks.
Use a logical NOT for checks.
[A_SILENT_PRUNE_DESCRIPTION]
Deletes the command message and the execution message.
//History Arguments
[A_BEFORE_HISTORY_DESCRIPTION]
[A_AFTER_HISTORY_DESCRIPTION]
[A_TYPE_HISTORY_DESCRIPTION]
[A_OLDEST_HISTORY_DESCRIPTION]
Sort history by oldest.
[A_VERBOSE_HISTORY_DESCRIPTION]
Display user IDs.
//Settings Arguments
//Mute Setting
[A_CREATE_MUTE_SETTINGS]

View File

@ -5,4 +5,4 @@ View or edit user-only settings.
View all guild, user, or restricted settings.
[A_RAW_SETTINGS]
Upload a raw JSON file of all of the settings for your user or guild.
Export a JSON file of all of the settings in the server.

View File

@ -85,6 +85,10 @@ Channel-specific permissions are listed below.
[C_PERMISSIONS_PERMISSIONSNOTFOUND]
Found {type} **{resolveable}** but {they}had no permissions.
[C_PERMISSIONS_NOPERMISSIONS]
Your server has no granted permissions to any roles or users.
If you would like to grant permissions, use the command `{prefix}grant` for more information.
//Disable Command
[C_DISABLE_DESCRIPTION]
Disable commands in your server to prevent usage.

View File

@ -3,3 +3,32 @@ Evaluates javascript code.
[C_RELOAD_DESCRIPTION]
Reloads components and language files.
[C_STATS_TITLE]
Statistics for {client} ({version}).
[C_STATS_DESC]
**Developers:** {devs}
**Manager Uptime:** {uptime}
**Managed Shards:** {shards}
**Manager Memory Consumption:** {memory}MB
[C_STATS_CURRENT_SHARD]
__**Current shard**__ [{shard}]
[C_STATS_CURRENT_SHARDS_VALUE]
**Cached users:** {cachedUsers}
**Guilds:** {guilds}
**Channels:** {channels}
**Uptime:** {uptime}
**Memory Consumption:** {memory}MB
[C_STATS_TOTAL]
__**All shards**__
[C_STATS_TOTAL_VALUE]
**Cached users:** {users}
**Guilds:** {guilds}
**Channels:** {channels}
**Average Uptime:** {uptime}
**Total Memory Consumption:** {memory}MB

View File

@ -133,8 +133,7 @@ __Members__
__Channels__
[C_GUILD_CHANNELS]
**Total:**
{totalChannels}/500
**Total:** {totalChannels}/500
{emoji_category-channel} **Categories:** {cat}
{emoji_text-channel} **Text:** {tc}
{emoji_voice-channel} **Voice:** {vc}
@ -175,12 +174,15 @@ Search for users or view user information.
__User Data__
[C_USER_DATA]
**User:** <@{id}>
**User:** <@{id}>{bot}
**Account created:** {created}
**Status:** {status}
**Activity:** {activity}
**Last global activity:** {globalActivity}
[C_USER_BADGES]
**Badges:** {badges}
[C_USER_MEMBER_NAME]
__Member Data__
@ -189,6 +191,9 @@ __Member Data__
**Server join date:** {joined}
**Last server activity:** {serverActivity}
[C_USER_MEMBER_ROLES]
**Roles:** {roles}
[C_USER_SEARCH_TITLE]
Search result for: `{key}`
@ -207,9 +212,6 @@ To search server members with similar names use `{prefix}user search <arguments.
[C_USER_ROLES_TITLE]
__Member Roles__
[C_USER_FLAGS]
__User Badges__
//Commands Command
[C_COMMANDS_DESCRIPTION]

View File

@ -331,3 +331,37 @@ I don't have permission to manage roles
[C_REMOVEROLE_ROLEHIERARCHY]
the provided role(s) have a higher position than yours
//History Command
[C_HISTORY_DESCRIPTION]
Display moderation history for the server or for certain users.
[C_HISTORY_ERROR]
I had issues finding moderation history in your server.
[C_HISTORY_NORESULTS]
There are no results for that search query.
[C_HISTORY_SUCCESSTYPE]
switch({old}) {
case true:
'oldest';
break;
case false:
'newest';
break;
}
[C_HISTORY_SUCCESS]
Fetched the {type} moderation cases{targets}.
[C_HISTORY_SUCCESSTARGETS]
for target{plural}: {targets}
[C_HISTORY_SUCCESSEXPORT]
Attached the JSON-formatted moderation file in the file below.
You may want to format this file for your viewing pleasure.
[C_HISTORY_FAILEXPORT]
Unable to upload JSON file, the file is too large to upload here. **\`[amount/max]\`**
If you absolutely need this, contact a bot developer in our support server.

6
util/Constants.js Normal file
View File

@ -0,0 +1,6 @@
exports.UploadLimit = {
'0': 8,
'1': 8,
'2': 50,
'3': 100
};

View File

@ -11,6 +11,19 @@ class Util {
throw new Error("Class may not be instantiated.");
}
static paginate(items, page = 1, pageLength = 10) {
const maxPage = Math.ceil(items.length / pageLength);
if(page < 1) page = 1;
if(page > maxPage) page = maxPage;
const startIndex = (page - 1) * pageLength;
return {
items: items.length > pageLength ? items.slice(startIndex, startIndex + pageLength) : items,
page,
maxPage,
pageLength
};
}
static downloadAsBuffer(source) {
return new Promise((resolve, reject) => {
fetch(source).then((res) => {

View File

@ -9,7 +9,8 @@
"voice-channel": "<:voicechannel:716414422662512762>",
"online": "<:online:741718263058268220>",
"offline": "<:offline:741718263188422787>",
"member": "<:members:741721081261588589>",
"member": "<:member:743722505826730034>",
"members": "<:members:741721081261588589>",
"store-channel": "<:store:741727831955865610>",
"invite": "<:invite:741721080846221425>",
"owner": "<:owner:741721640789999669>",
@ -27,5 +28,17 @@
"category-channel": "<:category:741731053818871901>",
"gif": "<:gif:741729824267305064>",
"book": "📕",
"role": "<:role:743563678292639794>"
"role": "<:role:743563678292639794>",
"discord-staff": "<:discord_staff:743733186043052093>",
"discord-partner": "<:discord_partner:743733185984331858>",
"hypesquad-events": "<:hypesquad_events:743733934545829960>",
"bughunter": "<:bughunter:743733185598324837>",
"bughunter-gold": "<:bughunter_gold:743734254415904818>",
"hypesquad-bravery": "<:house_bravery:743733185698988093>",
"hypesquad-brilliance": "<:house_brilliance:743733185778810917>",
"hypesquad-balance": "<:house_balance:743733185913028618> ",
"early-supporter": "<:early_supporter:743733185485078550>",
"bot-developer": "<:bot_developer:743734629793660969>",
"discord-nitro": "<:discord_nitro:743733186252767282>",
"bot": "<:bot:743733185531347055>"
}

View File

@ -960,7 +960,7 @@ director@1.2.7:
discord.js@discordjs/discord.js:
version "12.2.0"
resolved "https://codeload.github.com/discordjs/discord.js/tar.gz/153a030c1fc04fd2a144108680dbf7bb1d5b9cc9"
resolved "https://codeload.github.com/discordjs/discord.js/tar.gz/fb1dd6b53aee68722b057f9a460eb618c61dd1c6"
dependencies:
"@discordjs/collection" "^0.1.5"
"@discordjs/form-data" "^3.0.1"