diff --git a/Manager.js b/Manager.js index 3f7430a..9a7623f 100644 --- a/Manager.js +++ b/Manager.js @@ -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,8 +25,9 @@ class Manager extends EventEmitter { _handleMessage(shard, message) { - if(message._logger) return this.logger._handleMessage(shard, message); - if(message._webhook) return undefined; //todo + if (message._mEval) return this.eval(shard, message); + if (message._logger) return this.logger._handleMessage(shard, message); + if (message._webhook) return undefined; //todo } async build() { @@ -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; \ No newline at end of file diff --git a/middleware/Shard.js b/middleware/Shard.js index 3c6d794..50e721a 100644 --- a/middleware/Shard.js +++ b/middleware/Shard.js @@ -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); diff --git a/structure/client/DiscordClient.js b/structure/client/DiscordClient.js index cc6b251..5b8ccd3 100644 --- a/structure/client/DiscordClient.js +++ b/structure/client/DiscordClient.js @@ -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,9 +72,41 @@ 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); + } get defaultConfig() { diff --git a/structure/client/Resolver.js b/structure/client/Resolver.js index e6f023c..9be273e 100644 --- a/structure/client/Resolver.js +++ b/structure/client/Resolver.js @@ -1,4 +1,5 @@ -const timestring = require('timestring'); +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} [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 || 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 diff --git a/structure/client/components/commands/administration/Grant.js b/structure/client/components/commands/administration/Grant.js index 3e58e41..b420446 100644 --- a/structure/client/components/commands/administration/Grant.js +++ b/structure/client/components/commands/administration/Grant.js @@ -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' })}` : '' }), { diff --git a/structure/client/components/commands/administration/Import.js b/structure/client/components/commands/administration/Import.js index 8659f69..1699645 100644 --- a/structure/client/components/commands/administration/Import.js +++ b/structure/client/components/commands/administration/Import.js @@ -7,6 +7,7 @@ class ImportCommand extends Command { super(client, { name: 'import', module: 'administration', + tags: ['log', 'logs', 'logging'], memberPermissions: ['ADMINISTRATOR'], archivable: false, restricted: true diff --git a/structure/client/components/commands/administration/Permissions.js b/structure/client/components/commands/administration/Permissions.js index 423fffa..164fcf7 100644 --- a/structure/client/components/commands/administration/Permissions.js +++ b/structure/client/components/commands/administration/Permissions.js @@ -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: "", + 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,110 +43,149 @@ class PermissionsCommand extends Command { async execute(message, { params, args }) { const permissions = await message.guild.permissions(); - if(args.raw) { + + if(args.export) { await this._displayRaw(message, permissions); return undefined; } - if(params.length === 0) { - await this._showPermissions(message, Boolean(args.user)); - return undefined; - } - if(params[0] === 'list') { await this._listAvailablePermissions(message); return undefined; - } + } + const parameters = params.join(' '); let resolveable = await this._parseResolveable(message, parameters); - 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; - } + if(resolveable) { + if(resolveable.user) resolveable = resolveable.user; - if(!permission) { - await message.respond(message.format('C_PERMISSIONS_NOTFOUND'), { emoji: 'failure' }); - return undefined; - } - - const embed = { - author: { - name: `${resolveable?.user?.tag || resolveable?.tag || resolveable?.name || parameters}'s Permissions`, - icon_url: resolveable.displayAvatarURL ? resolveable.displayAvatarURL() : message.guild.iconURL() - }, - 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: [] - }; - - let update = false; - for(const [channelId, perms] of Object.entries(permission.channels)) { - const channel = await this.client.resolver.resolveChannel(channelId, true, message.guild); - if(!channel) { - delete permission.channels[channelId]; - update = true; - continue; - } else { - if(embed.fields.length === 25) { - embed.description += `\n${message.format('C_PERMISSIONS_MAXFIELDS')}`; - break; + 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; + } + + if(!permission) { + await message.respond(message.format('C_PERMISSIONS_NOTFOUND'), { emoji: 'failure' }); + return undefined; + } + + const embed = { + author: { + name: `${resolveable?.user?.tag || resolveable?.tag || resolveable?.name || parameters}'s Permissions`, + 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: [] + }; + + let update = false; + for(const [channelId, perms] of Object.entries(permission.channels)) { + const channel = await this.client.resolver.resolveChannel(channelId, true, message.guild); + if(!channel) { + delete permission.channels[channelId]; + update = true; + continue; + } else { + if(embed.fields.length === 25) { + embed.description += `\n${message.format('C_PERMISSIONS_MAXFIELDS')}`; + break; + } + embed.fields.push({ + name: `#${channel.name}`, + value: this._displayNames(perms).map((p) => `\`${p}\``).join(', ') + }); } - embed.fields.push({ - name: `#${channel.name}`, - value: this._displayNames(perms).map((p) => `\`${p}\``).join(', ') - }); } + if(update) { + delete permissions._id; + try { + await this.client.storageManager.mongodb.permissions.updateOne( + { guildId: message.guild.id }, + permissions + ); + } catch(error) { + this.client.logger.error(`Error removing channel permissions to ${message.guild.id}:\n${error.stack || error}`); + } + } + + return message.embed(embed); } - if(update) { - delete permissions._id; - try { - await this.client.storageManager.mongodb.permissions.updateOne( - { guildId: message.guild.id }, - permissions - ); - } catch(error) { - this.client.logger.error(`Error removing channel permissions to ${message.guild.id}:\n${error.stack || error}`); + + //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; } } - return message.embed(embed); - + 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 - async _showPermissions(message, user = false) { 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` + } + }; + + for(const item of items) { + item.resolveable = await this._parseResolveable(message, item.id); + item.permissions = this._displayNames(item.global); + } + + 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); + } }; - 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; - } - 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; - } + 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 `\`${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; + + } + } diff --git a/structure/client/components/commands/administration/Revoke.js b/structure/client/components/commands/administration/Revoke.js index d5f1c3c..1e2afb9 100644 --- a/structure/client/components/commands/administration/Revoke.js +++ b/structure/client/components/commands/administration/Revoke.js @@ -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' }); diff --git a/structure/client/components/commands/developer/Evaluate.js b/structure/client/components/commands/developer/Evaluate.js index 54853f2..8f0281c 100644 --- a/structure/client/components/commands/developer/Evaluate.js +++ b/structure/client/components/commands/developer/Evaluate.js @@ -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 diff --git a/structure/client/components/commands/developer/Stats.js b/structure/client/components/commands/developer/Stats.js new file mode 100644 index 0000000..316117d --- /dev/null +++ b/structure/client/components/commands/developer/Stats.js @@ -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; \ No newline at end of file diff --git a/structure/client/components/commands/information/User.js b/structure/client/components/commands/information/User.js index e8dd71a..88141f6 100644 --- a/structure/client/components/commands/information/User.js +++ b/structure/client/components/commands/information/User.js @@ -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,66 +101,83 @@ 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: [ - { - name: message.format('C_USER_DATA_NAME'), - value: message.format('C_USER_DATA', { - id: user.id, - 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 - } - ], + fields: [], footer: { - text: `ID: ${user.id}` + 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' + }) + }; + + if(badges.length > 0) { + userField.value += `\n${message.format('C_USER_BADGES', { + badges: badges.join(' ') + })}`; + } + + response.fields.push(userField); + if (member) { - - response.fields.push({ + 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(', ') - }); } diff --git a/structure/client/components/commands/moderation/History.js b/structure/client/components/commands/moderation/History.js new file mode 100644 index 0000000..543405b --- /dev/null +++ b/structure/client/components/commands/moderation/History.js @@ -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: '', + type: 'DATE', + types: ['FLAG'], + required: true + }, + { + name: 'after', //Search for moderation actions after x + usage: '', + 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 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; \ No newline at end of file diff --git a/structure/client/components/commands/moderation/Prune.js b/structure/client/components/commands/moderation/Prune.js index 34f764e..03e3449 100644 --- a/structure/client/components/commands/moderation/Prune.js +++ b/structure/client/components/commands/moderation/Prune.js @@ -200,8 +200,7 @@ class PruneCommand extends Command { .handleInfraction(Prune, message, { targets: parsed, data: { - amount: int, - message: message.id + amount: int }, reason }); diff --git a/structure/client/components/commands/utility/Avatar.js b/structure/client/components/commands/utility/Avatar.js index c9c8553..6a159cc 100644 --- a/structure/client/components/commands/utility/Avatar.js +++ b/structure/client/components/commands/utility/Avatar.js @@ -46,15 +46,19 @@ class AvatarCommand extends Command { message.respond(message.format('C_AVATAR_FORMATERROR'), { emoji: 'failure' }); return undefined; } + - - - 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'}` } }); diff --git a/structure/client/components/observers/CommandHandler.js b/structure/client/components/observers/CommandHandler.js index c477906..59d5b99 100644 --- a/structure/client/components/observers/CommandHandler.js +++ b/structure/client/components/observers/CommandHandler.js @@ -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 }; } }; diff --git a/structure/client/components/observers/GuildLogging.js b/structure/client/components/observers/GuildLogging.js index 9ea6aea..2184242 100644 --- a/structure/client/components/observers/GuildLogging.js +++ b/structure/client/components/observers/GuildLogging.js @@ -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)], diff --git a/structure/extensions/GuildMember.js b/structure/extensions/GuildMember.js index 8dec77c..afe399d 100644 --- a/structure/extensions/GuildMember.js +++ b/structure/extensions/GuildMember.js @@ -38,6 +38,10 @@ const GuildMember = Structures.extend('GuildMember', (GuildMember) => { return Date.now()-this._cached; } + get display() { + return this.user.tag; + } + } diff --git a/structure/extensions/Message.js b/structure/extensions/Message.js index c12f727..02d2c41 100644 --- a/structure/extensions/Message.js +++ b/structure/extensions/Message.js @@ -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; } diff --git a/structure/extensions/Role.js b/structure/extensions/Role.js new file mode 100644 index 0000000..b36fcd8 --- /dev/null +++ b/structure/extensions/Role.js @@ -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; \ No newline at end of file diff --git a/structure/extensions/TextChannel.js b/structure/extensions/TextChannel.js new file mode 100644 index 0000000..e953ab3 --- /dev/null +++ b/structure/extensions/TextChannel.js @@ -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; \ No newline at end of file diff --git a/structure/extensions/User.js b/structure/extensions/User.js index 12b73ec..0d9da51 100644 --- a/structure/extensions/User.js +++ b/structure/extensions/User.js @@ -68,6 +68,10 @@ const User = Structures.extend('User', (User) => { return this.client._options.bot.prefix; } + get display() { + return this.tag; + } + } return ExtendedUser; diff --git a/structure/extensions/index.js b/structure/extensions/index.js index bbaf634..df3690f 100644 --- a/structure/extensions/index.js +++ b/structure/extensions/index.js @@ -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') }; \ No newline at end of file diff --git a/structure/interfaces/Argument.js b/structure/interfaces/Argument.js index a2964ac..658b28a 100644 --- a/structure/interfaces/Argument.js +++ b/structure/interfaces/Argument.js @@ -14,7 +14,8 @@ const Constants = { 'CHANNEL', 'TEXTCHANNEL', 'VOICECHANNEL', - 'TIME' + 'TIME', + 'DATE' ], ArgumentTypes: [ 'FLAG', diff --git a/structure/interfaces/Command.js b/structure/interfaces/Command.js index 98a15ee..fa0e622 100644 --- a/structure/interfaces/Command.js +++ b/structure/interfaces/Command.js @@ -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') }); } diff --git a/structure/language/languages/en_us/arguments/en_us_administrator.lang b/structure/language/languages/en_us/arguments/en_us_administrator.lang index cd7ca6e..de49fcb 100644 --- a/structure/language/languages/en_us/arguments/en_us_administrator.lang +++ b/structure/language/languages/en_us/arguments/en_us_administrator.lang @@ -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. \ No newline at end of file +[A_EXPORT_PERMISSIONS_DESCRIPTION] +Export a JSON file of all of the permissions in the server. \ No newline at end of file diff --git a/structure/language/languages/en_us/arguments/en_us_moderation.lang b/structure/language/languages/en_us/arguments/en_us_moderation.lang index 2b12b13..d7b00d3 100644 --- a/structure/language/languages/en_us/arguments/en_us_moderation.lang +++ b/structure/language/languages/en_us/arguments/en_us_moderation.lang @@ -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] diff --git a/structure/language/languages/en_us/arguments/en_us_utility.lang b/structure/language/languages/en_us/arguments/en_us_utility.lang index de8b79c..0db5da8 100644 --- a/structure/language/languages/en_us/arguments/en_us_utility.lang +++ b/structure/language/languages/en_us/arguments/en_us_utility.lang @@ -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. \ No newline at end of file +Export a JSON file of all of the settings in the server. \ No newline at end of file diff --git a/structure/language/languages/en_us/commands/en_us_administration.lang b/structure/language/languages/en_us/commands/en_us_administration.lang index 3acdca3..1aaea0e 100644 --- a/structure/language/languages/en_us/commands/en_us_administration.lang +++ b/structure/language/languages/en_us/commands/en_us_administration.lang @@ -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. diff --git a/structure/language/languages/en_us/commands/en_us_developer.lang b/structure/language/languages/en_us/commands/en_us_developer.lang index 23553f1..80fd1f4 100644 --- a/structure/language/languages/en_us/commands/en_us_developer.lang +++ b/structure/language/languages/en_us/commands/en_us_developer.lang @@ -2,4 +2,33 @@ Evaluates javascript code. [C_RELOAD_DESCRIPTION] -Reloads components and language files. \ No newline at end of file +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 \ No newline at end of file diff --git a/structure/language/languages/en_us/commands/en_us_information.lang b/structure/language/languages/en_us/commands/en_us_information.lang index fd7d7bb..e2f09fa 100644 --- a/structure/language/languages/en_us/commands/en_us_information.lang +++ b/structure/language/languages/en_us/commands/en_us_information.lang @@ -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 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) => { diff --git a/util/emojis.json b/util/emojis.json index 111bd8d..cbebe7f 100644 --- a/util/emojis.json +++ b/util/emojis.json @@ -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>" } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3f40c0d..29c05d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"