diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index b3279390..f17f64a0 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -8,15 +8,14 @@ import { Supporters } from "../../data/Supporters"; import { ServerCmd } from "./commands/ServerCmd"; import { RolesCmd } from "./commands/RolesCmd"; import { LevelCmd } from "./commands/LevelCmd"; +import { SearchCmd } from "./commands/SearchCmd"; +import { BanSearchCmd } from "./commands/BanSearchCmd"; +import { InfoCmd } from "./commands/InfoCmd"; export const UtilityPlugin = zeppelinPlugin()("utility", { configSchema: ConfigSchema, - commands: [ - LevelCmd, - RolesCmd, - ServerCmd, - ], + commands: [BanSearchCmd, InfoCmd, LevelCmd, RolesCmd, SearchCmd, ServerCmd], onLoad({ state, guild }) { state.logs = new GuildLogs(guild.id); @@ -26,5 +25,5 @@ export const UtilityPlugin = zeppelinPlugin()("utility", { state.supporters = new Supporters(); state.lastReload = Date.now(); - } + }, }); diff --git a/backend/src/plugins/Utility/commands/BanSearchCmd.ts b/backend/src/plugins/Utility/commands/BanSearchCmd.ts index d3e8328d..71bba571 100644 --- a/backend/src/plugins/Utility/commands/BanSearchCmd.ts +++ b/backend/src/plugins/Utility/commands/BanSearchCmd.ts @@ -1,6 +1,6 @@ import { utilityCmd } from "../types"; import { baseTypeHelpers as t } from "knub"; -import { archiveSearch, displaySearch, SearchType } from "./search"; +import { archiveSearch, displaySearch, SearchType } from "../search"; // Separate from BanSearchCmd to avoid a circular reference from ./search.ts export const banSearchSignature = { diff --git a/backend/src/plugins/Utility/commands/InfoCmd.ts b/backend/src/plugins/Utility/commands/InfoCmd.ts new file mode 100644 index 00000000..fc075d44 --- /dev/null +++ b/backend/src/plugins/Utility/commands/InfoCmd.ts @@ -0,0 +1,143 @@ +import { utilityCmd } from "../types"; +import { baseTypeHelpers as t } from "knub"; +import { customArgumentHelpers as ct } from "../../../customArgumentTypes"; +import { embedPadding, resolveMember, trimLines, UnknownUser } from "../../../utils"; +import { EmbedOptions, GuildTextableChannel } from "eris"; +import moment from "moment-timezone"; +import humanizeDuration from "humanize-duration"; +import { CaseTypes } from "../../../data/CaseTypes"; + +export const InfoCmd = utilityCmd({ + trigger: "info", + description: "Show basic information about a user", + usage: "!info 106391128718245888", + permission: "can_info", + + signature: { + user: ct.resolvedUserLoose({ required: false }), + + compact: t.switchOption({ shortcut: "c" }), + }, + + async run({ message: msg, args, pluginData }) { + const user = args.user || msg.author; + + let member; + if (!(user instanceof UnknownUser)) { + member = await resolveMember(pluginData.client, (msg.channel as GuildTextableChannel).guild, user.id); + } + + const embed: EmbedOptions = { + fields: [], + }; + + if (user && !(user instanceof UnknownUser)) { + const createdAt = moment(user.createdAt); + const accountAge = humanizeDuration(moment().valueOf() - user.createdAt, { + largest: 2, + round: true, + }); + + embed.title = `${user.username}#${user.discriminator}`; + embed.thumbnail = { url: user.avatarURL }; + + if (args.compact) { + embed.fields.push({ + name: "User information", + value: trimLines(` + Profile: <@!${user.id}> + Created: **${accountAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})** + `), + }); + if (member) { + const joinedAt = moment(member.joinedAt); + const joinAge = humanizeDuration(moment().valueOf() - member.joinedAt, { + largest: 2, + round: true, + }); + embed.fields[0].value += `\nJoined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})**`; + } else { + embed.fields.push({ + name: "!! USER IS NOT ON THE SERVER !!", + value: embedPadding, + }); + } + msg.channel.createMessage({ embed }); + return; + } else { + embed.fields.push({ + name: "User information", + value: + trimLines(` + ID: **${user.id}** + Profile: <@!${user.id}> + Created: **${accountAge} ago (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")})** + `) + embedPadding, + }); + } + } else { + embed.title = `Unknown user`; + } + + if (member) { + const joinedAt = moment(member.joinedAt); + const joinAge = humanizeDuration(moment().valueOf() - member.joinedAt, { + largest: 2, + round: true, + }); + const roles = member.roles.map(id => pluginData.guild.roles.get(id)).filter(r => !!r); + + embed.fields.push({ + name: "Member information", + value: + trimLines(` + Joined: **${joinAge} ago (${joinedAt.format("YYYY-MM-DD[T]HH:mm:ss")})** + ${roles.length > 0 ? "Roles: " + roles.map(r => r.name).join(", ") : ""} + `) + embedPadding, + }); + + const voiceChannel = member.voiceState.channelID + ? pluginData.guild.channels.get(member.voiceState.channelID) + : null; + if (voiceChannel || member.voiceState.mute || member.voiceState.deaf) { + embed.fields.push({ + name: "Voice information", + value: + trimLines(` + ${voiceChannel ? `Current voice channel: **${voiceChannel ? voiceChannel.name : "None"}**` : ""} + ${member.voiceState.mute ? "Server voice muted: **Yes**" : ""} + ${member.voiceState.deaf ? "Server voice deafened: **Yes**" : ""} + `) + embedPadding, + }); + } + } else { + embed.fields.push({ + name: "!! USER IS NOT ON THE SERVER !!", + value: embedPadding, + }); + } + const cases = (await pluginData.state.cases.getByUserId(user.id)).filter(c => !c.is_hidden); + + if (cases.length > 0) { + cases.sort((a, b) => { + return a.created_at < b.created_at ? 1 : -1; + }); + + const caseSummary = cases.slice(0, 3).map(c => { + return `${CaseTypes[c.type]} (#${c.case_number})`; + }); + + const summaryText = cases.length > 3 ? "Last 3 cases" : "Summary"; + + embed.fields.push({ + name: "Cases", + value: trimLines(` + Total cases: **${cases.length}** + ${summaryText}: ${caseSummary.join(", ")} + `), + }); + } + + msg.channel.createMessage({ embed }); + }, +}); diff --git a/backend/src/plugins/Utility/commands/SearchCmd.ts b/backend/src/plugins/Utility/commands/SearchCmd.ts index c415f3b7..784910d3 100644 --- a/backend/src/plugins/Utility/commands/SearchCmd.ts +++ b/backend/src/plugins/Utility/commands/SearchCmd.ts @@ -1,6 +1,6 @@ import { utilityCmd } from "../types"; import { baseTypeHelpers as t } from "knub"; -import { archiveSearch, displaySearch, SearchType } from "./search"; +import { archiveSearch, displaySearch, SearchType } from "../search"; // Separate from SearchCmd to avoid a circular reference from ./search.ts export const searchCmdSignature = { diff --git a/backend/src/plugins/Utility/commands/ServerCmd.ts b/backend/src/plugins/Utility/commands/ServerCmd.ts index 24f20904..2be14495 100644 --- a/backend/src/plugins/Utility/commands/ServerCmd.ts +++ b/backend/src/plugins/Utility/commands/ServerCmd.ts @@ -1,4 +1,4 @@ -import { CategoryChannel, EmbedOptions, TextChannel, VoiceChannel } from "eris"; +import { CategoryChannel, EmbedOptions, RESTChannelInvite, TextChannel, VoiceChannel } from "eris"; import moment from "moment-timezone"; import { embedPadding, formatNumber, memoize, MINUTES, trimLines } from "../../../utils"; import { utilityCmd } from "../types"; @@ -10,56 +10,56 @@ export const ServerCmd = utilityCmd({ usage: "!server", permission: "can_server", - async run({ message }) { + async run({ message, pluginData }) { + const { guild } = pluginData; + const embed: EmbedOptions = { fields: [], color: parseInt("6b80cf", 16), }; - embed.thumbnail = { url: this.guild.iconURL }; + embed.thumbnail = { url: guild.iconURL }; - const createdAt = moment(this.guild.createdAt); - const serverAge = humanizeDuration(moment().valueOf() - this.guild.createdAt, { + const createdAt = moment(guild.createdAt); + const serverAge = humanizeDuration(moment().valueOf() - guild.createdAt, { largest: 2, round: true, }); - const owner = this.bot.users.get(this.guild.ownerID); + const owner = pluginData.client.users.get(guild.ownerID); const ownerName = owner ? `${owner.username}#${owner.discriminator}` : "Unknown#0000"; embed.fields.push({ - name: `Server information - ${this.guild.name}`, + name: `Server information - ${guild.name}`, value: trimLines(` Created: **${serverAge} ago** (${createdAt.format("YYYY-MM-DD[T]HH:mm:ss")}) - Owner: **${ownerName}** (${this.guild.ownerID}) - Voice region: **${this.guild.region}** - ${this.guild.features.length > 0 ? "Features: " + this.guild.features.join(", ") : ""} + Owner: **${ownerName}** (${guild.ownerID}) + Voice region: **${guild.region}** + ${guild.features.length > 0 ? "Features: " + guild.features.join(", ") : ""} `) + embedPadding, }); const restGuild = await memoize( - () => this.bot.getRESTGuild(this.guildId), - `getRESTGuild_${this.guildId}`, + () => pluginData.client.getRESTGuild(guild.id), + `getRESTGuild_${guild.id}`, 10 * MINUTES, ); // For servers with a vanity URL, we can use the numbers from the invite for online count // (which is nowadays usually more accurate for large servers) - const invite = this.guild.vanityURL - ? await memoize( - () => this.bot.getInvite(this.guild.vanityURL, true), - `getInvite_${this.guild.vanityURL}`, - 10 * MINUTES, - ) + const invite = guild.vanityURL + ? ((await memoize( + () => pluginData.client.getInvite(guild.vanityURL, true), + `getInvite_${guild.vanityURL}`, + 10 * MINUTES, + )) as RESTChannelInvite) : null; - const totalMembers = invite ? invite.memberCount : this.guild.memberCount; + const totalMembers = invite?.memberCount || guild.memberCount; - const onlineMemberCount = invite - ? invite.presenceCount - : this.guild.members.filter(m => m.status !== "offline").length; - const offlineMemberCount = this.guild.memberCount - onlineMemberCount; + const onlineMemberCount = invite ? invite.presenceCount : guild.members.filter(m => m.status !== "offline").length; + const offlineMemberCount = guild.memberCount - onlineMemberCount; let memberCountTotalLines = `Total: **${formatNumber(totalMembers)}**`; if (restGuild.maxMembers) { @@ -81,10 +81,10 @@ export const ServerCmd = utilityCmd({ `), }); - const totalChannels = this.guild.channels.size; - const categories = this.guild.channels.filter(channel => channel instanceof CategoryChannel); - const textChannels = this.guild.channels.filter(channel => channel instanceof TextChannel); - const voiceChannels = this.guild.channels.filter(channel => channel instanceof VoiceChannel); + const totalChannels = guild.channels.size; + const categories = guild.channels.filter(channel => channel instanceof CategoryChannel); + const textChannels = guild.channels.filter(channel => channel instanceof TextChannel); + const voiceChannels = guild.channels.filter(channel => channel instanceof VoiceChannel); embed.fields.push({ name: "Channels", @@ -104,16 +104,16 @@ export const ServerCmd = utilityCmd({ 1: 100, 2: 150, 3: 250, - }[this.guild.premiumTier] || 50; + }[guild.premiumTier] || 50; embed.fields.push({ name: "Other stats", inline: true, value: trimLines(` - Roles: **${this.guild.roles.size}** / 250 - Emojis: **${this.guild.emojis.length}** / ${maxEmojis} - Boosts: **${this.guild.premiumSubscriptionCount ?? 0}** (level ${this.guild.premiumTier}) + Roles: **${guild.roles.size}** / 250 + Emojis: **${guild.emojis.length}** / ${maxEmojis} + Boosts: **${guild.premiumSubscriptionCount ?? 0}** (level ${guild.premiumTier}) `) + embedPadding, }); diff --git a/backend/src/plugins/Utility/commands/search.ts b/backend/src/plugins/Utility/search.ts similarity index 97% rename from backend/src/plugins/Utility/commands/search.ts rename to backend/src/plugins/Utility/search.ts index f57db35f..992f4a4b 100644 --- a/backend/src/plugins/Utility/commands/search.ts +++ b/backend/src/plugins/Utility/search.ts @@ -2,14 +2,14 @@ import { Member, Message, User } from "eris"; import moment from "moment-timezone"; import escapeStringRegexp from "escape-string-regexp"; import safeRegex from "safe-regex"; -import { isFullMessage, MINUTES, multiSorter, noop, sorter, trimLines } from "../../../utils"; -import { sendErrorMessage } from "../../../pluginUtils"; +import { isFullMessage, MINUTES, multiSorter, noop, sorter, trimLines } from "../../utils"; +import { sendErrorMessage } from "../../pluginUtils"; import { PluginData } from "knub"; import { ArgsFromSignatureOrArray } from "knub/dist/commands/commandUtils"; -import { searchCmdSignature } from "./SearchCmd"; -import { banSearchSignature } from "./BanSearchCmd"; -import { UtilityPluginType } from "../types"; -import { refreshMembersIfNeeded } from "../refreshMembers"; +import { searchCmdSignature } from "./commands/SearchCmd"; +import { banSearchSignature } from "./commands/BanSearchCmd"; +import { UtilityPluginType } from "./types"; +import { refreshMembersIfNeeded } from "./refreshMembers"; const SEARCH_RESULTS_PER_PAGE = 15; const SEARCH_ID_RESULTS_PER_PAGE = 50;