diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index e2ba30b0..6da212cd 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -26,7 +26,6 @@ import { ReloadGuildCmd } from "./commands/ReloadGuildCmd"; import { JumboCmd } from "./commands/JumboCmd"; import { AvatarCmd } from "./commands/AvatarCmd"; import { CleanCmd } from "./commands/CleanCmd"; -import { Message } from "eris"; import { InviteInfoCmd } from "./commands/InviteInfoCmd"; import { ChannelInfoCmd } from "./commands/ChannelInfoCmd"; import { MessageInfoCmd } from "./commands/MessageInfoCmd"; @@ -37,6 +36,8 @@ import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { VcdisconnectCmd } from "./commands/VcdisconnectCmd"; import { ModActionsPlugin } from "../ModActions/ModActionsPlugin"; import { refreshMembersIfNeeded } from "./refreshMembers"; +import { RoleInfoCmd } from "./commands/RoleInfoCmd"; +import { EmojiInfoCmd } from "./commands/EmojiInfoCmd"; const defaultOptions: PluginOptions = { config: { @@ -50,6 +51,8 @@ const defaultOptions: PluginOptions = { can_channelinfo: false, can_messageinfo: false, can_userinfo: false, + can_roleinfo: false, + can_emojiinfo: false, can_snowflake: false, can_reload_guild: false, can_nickname: false, @@ -79,6 +82,8 @@ const defaultOptions: PluginOptions = { can_channelinfo: true, can_messageinfo: true, can_userinfo: true, + can_roleinfo: true, + can_emojiinfo: true, can_snowflake: true, can_nickname: true, can_vcmove: true, @@ -138,6 +143,8 @@ export const UtilityPlugin = zeppelinGuildPlugin()("utility", MessageInfoCmd, InfoCmd, SnowflakeInfoCmd, + RoleInfoCmd, + EmojiInfoCmd, ], onLoad(pluginData) { diff --git a/backend/src/plugins/Utility/commands/EmojiInfoCmd.ts b/backend/src/plugins/Utility/commands/EmojiInfoCmd.ts new file mode 100644 index 00000000..41889509 --- /dev/null +++ b/backend/src/plugins/Utility/commands/EmojiInfoCmd.ts @@ -0,0 +1,32 @@ +import { utilityCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage } from "../../../pluginUtils"; +import { customEmojiRegex } from "../../../utils"; +import { getEmojiInfoEmbed } from "../functions/getEmojiInfoEmbed"; + +export const EmojiInfoCmd = utilityCmd({ + trigger: ["emoji", "emojiinfo"], + description: "Show information about an emoji", + usage: "!emoji 106391128718245888", + permission: "can_emojiinfo", + + signature: { + emoji: ct.string({ required: false }), + }, + + async run({ message, args, pluginData }) { + const emojiIdMatch = args.emoji.match(customEmojiRegex); + if (!emojiIdMatch?.[2]) { + sendErrorMessage(pluginData, message.channel, "Emoji not found"); + return; + } + + const embed = await getEmojiInfoEmbed(pluginData, emojiIdMatch[2]); + if (!embed) { + sendErrorMessage(pluginData, message.channel, "Emoji not found"); + return; + } + + message.channel.createMessage({ embed }); + }, +}); diff --git a/backend/src/plugins/Utility/commands/InfoCmd.ts b/backend/src/plugins/Utility/commands/InfoCmd.ts index 10fc500a..62db7c5a 100644 --- a/backend/src/plugins/Utility/commands/InfoCmd.ts +++ b/backend/src/plugins/Utility/commands/InfoCmd.ts @@ -2,16 +2,18 @@ import { utilityCmd } from "../types"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { sendErrorMessage } from "../../../pluginUtils"; import { getInviteInfoEmbed } from "../functions/getInviteInfoEmbed"; -import { isValidSnowflake, parseInviteCodeInput, resolveInvite, resolveUser } from "../../../utils"; +import { customEmojiRegex, isValidSnowflake, parseInviteCodeInput, resolveInvite, resolveUser } from "../../../utils"; import { getUserInfoEmbed } from "../functions/getUserInfoEmbed"; import { resolveMessageTarget } from "../../../utils/resolveMessageTarget"; import { canReadChannel } from "../../../utils/canReadChannel"; import { getMessageInfoEmbed } from "../functions/getMessageInfoEmbed"; import { getChannelInfoEmbed } from "../functions/getChannelInfoEmbed"; import { getServerInfoEmbed } from "../functions/getServerInfoEmbed"; -import { getChannelId } from "knub/dist/utils"; +import { getChannelId, getRoleId } from "knub/dist/utils"; import { getGuildPreview } from "../functions/getGuildPreview"; import { getSnowflakeInfoEmbed } from "../functions/getSnowflakeInfoEmbed"; +import { getRoleInfoEmbed } from "../functions/getRoleInfoEmbed"; +import { getEmojiInfoEmbed } from "../functions/getEmojiInfoEmbed"; export const InfoCmd = utilityCmd({ trigger: "info", @@ -34,59 +36,81 @@ export const InfoCmd = utilityCmd({ }); // 1. Channel - const channelId = getChannelId(value); - const channel = channelId && pluginData.guild.channels.get(channelId); - if (channel && userCfg.can_channelinfo) { - const embed = await getChannelInfoEmbed(pluginData, channelId!, message.author.id); - if (embed) { - message.channel.createMessage({ embed }); - return; + if (userCfg.can_channelinfo) { + const channelId = getChannelId(value); + const channel = channelId && pluginData.guild.channels.get(channelId); + if (channel) { + const embed = await getChannelInfoEmbed(pluginData, channelId!, message.author.id); + if (embed) { + message.channel.createMessage({ embed }); + return; + } } } // 2. Server - const guild = pluginData.client.guilds.get(value); - if (guild && userCfg.can_server) { - const embed = await getServerInfoEmbed(pluginData, value, message.author.id); - if (embed) { - message.channel.createMessage({ embed }); - return; + if (userCfg.can_server) { + const guild = pluginData.client.guilds.get(value); + if (guild) { + const embed = await getServerInfoEmbed(pluginData, value, message.author.id); + if (embed) { + message.channel.createMessage({ embed }); + return; + } } } // 3. User - const user = await resolveUser(pluginData.client, value); - if (user && userCfg.can_userinfo) { - const embed = await getUserInfoEmbed(pluginData, user.id, Boolean(args.compact), message.author.id); - if (embed) { - message.channel.createMessage({ embed }); - return; + if (userCfg.can_userinfo) { + const user = await resolveUser(pluginData.client, value); + if (user && userCfg.can_userinfo) { + const embed = await getUserInfoEmbed(pluginData, user.id, Boolean(args.compact), message.author.id); + if (embed) { + message.channel.createMessage({ embed }); + return; + } } } // 4. Message - const messageTarget = await resolveMessageTarget(pluginData, value); - if (messageTarget && userCfg.can_messageinfo) { - if (canReadChannel(messageTarget.channel, message.member)) { - const embed = await getMessageInfoEmbed( - pluginData, - messageTarget.channel.id, - messageTarget.messageId, - message.author.id, - ); - if (embed) { - message.channel.createMessage({ embed }); - return; + if (userCfg.can_messageinfo) { + const messageTarget = await resolveMessageTarget(pluginData, value); + if (messageTarget) { + if (canReadChannel(messageTarget.channel, message.member)) { + const embed = await getMessageInfoEmbed( + pluginData, + messageTarget.channel.id, + messageTarget.messageId, + message.author.id, + ); + if (embed) { + message.channel.createMessage({ embed }); + return; + } } } } // 5. Invite - const inviteCode = parseInviteCodeInput(value) ?? value; - if (inviteCode) { - const invite = await resolveInvite(pluginData.client, inviteCode, true); - if (invite && userCfg.can_inviteinfo) { - const embed = await getInviteInfoEmbed(pluginData, inviteCode); + if (userCfg.can_inviteinfo) { + const inviteCode = parseInviteCodeInput(value) ?? value; + if (inviteCode) { + const invite = await resolveInvite(pluginData.client, inviteCode, true); + if (invite) { + const embed = await getInviteInfoEmbed(pluginData, inviteCode); + if (embed) { + message.channel.createMessage({ embed }); + return; + } + } + } + } + + // 6. Server again (fallback for discovery servers) + if (userCfg.can_server) { + const serverPreview = getGuildPreview(pluginData.client, value).catch(() => null); + if (serverPreview) { + const embed = await getServerInfoEmbed(pluginData, value, message.author.id); if (embed) { message.channel.createMessage({ embed }); return; @@ -94,24 +118,37 @@ export const InfoCmd = utilityCmd({ } } - // 6. Server again (fallback for discovery servers) - const serverPreview = getGuildPreview(pluginData.client, value).catch(() => null); - if (serverPreview && userCfg.can_server) { - const embed = await getServerInfoEmbed(pluginData, value, message.author.id); - if (embed) { + // 7. Role + if (userCfg.can_roleinfo) { + const roleId = getRoleId(value); + const role = roleId && pluginData.guild.roles.get(roleId); + if (role) { + const embed = await getRoleInfoEmbed(pluginData, role, message.author.id); message.channel.createMessage({ embed }); return; } } - // 7. Arbitrary ID + // 8. Emoji + if (userCfg.can_emojiinfo) { + const emojiIdMatch = value.match(customEmojiRegex); + if (emojiIdMatch?.[2]) { + const embed = await getEmojiInfoEmbed(pluginData, emojiIdMatch[2]); + if (embed) { + message.channel.createMessage({ embed }); + return; + } + } + } + + // 9. Arbitrary ID if (isValidSnowflake(value) && userCfg.can_snowflake) { const embed = await getSnowflakeInfoEmbed(pluginData, value, true, message.author.id); message.channel.createMessage({ embed }); return; } - // 7. No can do + // 10. No can do sendErrorMessage( pluginData, message.channel, diff --git a/backend/src/plugins/Utility/commands/RoleInfoCmd.ts b/backend/src/plugins/Utility/commands/RoleInfoCmd.ts new file mode 100644 index 00000000..f374bea3 --- /dev/null +++ b/backend/src/plugins/Utility/commands/RoleInfoCmd.ts @@ -0,0 +1,20 @@ +import { utilityCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage } from "../../../pluginUtils"; +import { getRoleInfoEmbed } from "../functions/getRoleInfoEmbed"; + +export const RoleInfoCmd = utilityCmd({ + trigger: ["role", "roleinfo"], + description: "Show information about a role", + usage: "!role 106391128718245888", + permission: "can_roleinfo", + + signature: { + role: ct.role({ required: true }), + }, + + async run({ message, args, pluginData }) { + const embed = await getRoleInfoEmbed(pluginData, args.role, message.author.id); + message.channel.createMessage({ embed }); + }, +}); diff --git a/backend/src/plugins/Utility/functions/getEmojiInfoEmbed.ts b/backend/src/plugins/Utility/functions/getEmojiInfoEmbed.ts new file mode 100644 index 00000000..28ee5712 --- /dev/null +++ b/backend/src/plugins/Utility/functions/getEmojiInfoEmbed.ts @@ -0,0 +1,34 @@ +import { EmbedOptions } from "eris"; +import { GuildPluginData } from "knub"; +import { UtilityPluginType } from "../types"; +import { trimLines, preEmbedPadding, EmbedWith } from "../../../utils"; + +export async function getEmojiInfoEmbed( + pluginData: GuildPluginData, + emojiId: string, +): Promise { + const emoji = pluginData.guild.emojis.find(e => e.id === emojiId); + if (!emoji) { + return null; + } + + const embed: EmbedWith<"fields"> = { + fields: [], + }; + + embed.author = { + name: `Emoji: ${emoji.name}`, + icon_url: `https://cdn.discordapp.com/emojis/${emoji.id}.${emoji.animated ? "gif" : "png"}?v=1`, + }; + + embed.fields.push({ + name: preEmbedPadding + "Emoji information", + value: trimLines(` + Name: **${emoji.name}** + ID: \`${emoji.id}\` + Animated: **${emoji.animated ? "Yes" : "No"}** + `), + }); + + return embed; +} diff --git a/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts new file mode 100644 index 00000000..5dcfcb26 --- /dev/null +++ b/backend/src/plugins/Utility/functions/getRoleInfoEmbed.ts @@ -0,0 +1,69 @@ +import { EmbedOptions, Role } from "eris"; +import { GuildPluginData } from "knub"; +import { UtilityPluginType } from "../types"; +import { trimLines, preEmbedPadding, EmbedWith } from "../../../utils"; +import moment from "moment-timezone"; +import humanizeDuration from "humanize-duration"; +import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; + +const MENTION_ICON = "https://cdn.discordapp.com/attachments/705009450855039042/839284872152481792/mention.png"; + +export async function getRoleInfoEmbed( + pluginData: GuildPluginData, + role: Role, + requestMemberId?: string, +): Promise { + const embed: EmbedWith<"fields"> = { + fields: [], + }; + + embed.author = { + name: `Role: ${role.name}`, + icon_url: MENTION_ICON, + }; + + embed.color = role.color; + + const createdAt = moment.utc(role.createdAt, "x"); + const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); + const tzCreatedAt = requestMemberId + ? await timeAndDate.inMemberTz(requestMemberId, createdAt) + : timeAndDate.inGuildTz(createdAt); + const prettyCreatedAt = tzCreatedAt.format(timeAndDate.getDateFormat("pretty_datetime")); + const roleAge = humanizeDuration(Date.now() - role.createdAt, { + largest: 2, + round: true, + }); + + const rolePerms = Object.keys(role.permissions.json).map(p => + p + // Voice channel related permission names start with 'voice' + .replace(/^voice/i, "") + .replace(/([a-z])([A-Z])/g, "$1 $2") + .toLowerCase() + .replace(/(^\w{1})|(\s{1}\w{1})/g, l => l.toUpperCase()), + ); + + // -1 because of the @everyone role + const totalGuildRoles = pluginData.guild.roles.size - 1; + + embed.fields.push({ + name: preEmbedPadding + "Role information", + value: trimLines(` + Name: **${role.name}** + ID: \`${role.id}\` + Created: **${roleAge} ago** (\`${prettyCreatedAt}\`) + Position: **${role.position} / ${totalGuildRoles}** + Color: **#${role.color + .toString(16) + .toUpperCase() + .padStart(6, "0")}** + Mentionable: **${role.mentionable ? "Yes" : "No"}** + Hoisted: **${role.hoist ? "Yes" : "No"}** + Permissions: \`${rolePerms.length ? rolePerms.join(", ") : "None"}\` + Mention: <@&${role.id}> (\`<@&${role.id}>\`) + `), + }); + + return embed; +} diff --git a/backend/src/plugins/Utility/types.ts b/backend/src/plugins/Utility/types.ts index 8d6de6b4..2da7a559 100644 --- a/backend/src/plugins/Utility/types.ts +++ b/backend/src/plugins/Utility/types.ts @@ -18,6 +18,8 @@ export const ConfigSchema = t.type({ can_channelinfo: t.boolean, can_messageinfo: t.boolean, can_userinfo: t.boolean, + can_roleinfo: t.boolean, + can_emojiinfo: t.boolean, can_snowflake: t.boolean, can_reload_guild: t.boolean, can_nickname: t.boolean,