From 60aff76ebef3a4a52058fa450cc3d75b18c3084f Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 5 Aug 2020 23:57:09 +0300 Subject: [PATCH] !source: don't show source of messages you don't have access to; allow mods to use the command by default --- backend/src/commandTypes.ts | 68 ++++++++++++++++++- backend/src/data/getChannelIdFromMessageId.ts | 17 +++++ backend/src/plugins/Utility/UtilityPlugin.ts | 2 +- .../src/plugins/Utility/commands/SourceCmd.ts | 35 +++++++--- backend/src/utils/canReadChannel.ts | 10 +++ backend/src/utils/hasPermissions.ts | 17 +++++ 6 files changed, 137 insertions(+), 12 deletions(-) create mode 100644 backend/src/data/getChannelIdFromMessageId.ts create mode 100644 backend/src/utils/canReadChannel.ts create mode 100644 backend/src/utils/hasPermissions.ts diff --git a/backend/src/commandTypes.ts b/backend/src/commandTypes.ts index 9341120b..518dd157 100644 --- a/backend/src/commandTypes.ts +++ b/backend/src/commandTypes.ts @@ -1,7 +1,24 @@ -import { convertDelayStringToMS, disableCodeBlocks, resolveMember, resolveUser, UnknownUser } from "./utils"; -import { GuildChannel, Member, User } from "eris"; +import { + convertDelayStringToMS, + disableCodeBlocks, + disableInlineCode, + isSnowflake, + resolveMember, + resolveUser, + UnknownUser, +} from "./utils"; +import { GuildChannel, Member, TextChannel, User } from "eris"; import { baseTypeConverters, baseTypeHelpers, CommandContext, TypeConversionError } from "knub"; import { createTypeHelper } from "knub-command-manager"; +import { getChannelIdFromMessageId } from "./data/getChannelIdFromMessageId"; + +export interface MessageTarget { + channel: TextChannel; + messageId: string; +} + +const channelAndMessageIdRegex = /^(\d+)[\-\/](\d+)$/; +const messageLinkRegex = /^https:\/\/(?:\w+\.)?discord(?:app)?\.com\/channels\/\d+\/(\d+)\/(\d+)$/i; export const commandTypes = { ...baseTypeConverters, @@ -42,6 +59,52 @@ export const commandTypes = { } return result; }, + + async messageTarget(value: string, context: CommandContext) { + value = String(value).trim(); + + const result = await (async () => { + if (isSnowflake(value)) { + const channelId = await getChannelIdFromMessageId(value); + if (!channelId) { + throw new TypeConversionError(`Could not find channel for message ID \`${disableInlineCode(value)}\``); + } + + return { + channelId, + messageId: value, + }; + } + + const channelAndMessageIdMatch = value.match(channelAndMessageIdRegex); + if (channelAndMessageIdMatch) { + return { + channelId: channelAndMessageIdMatch[1], + messageId: channelAndMessageIdMatch[2], + }; + } + + const messageLinkMatch = value.match(messageLinkRegex); + if (messageLinkMatch) { + return { + channelId: messageLinkMatch[1], + messageId: messageLinkMatch[2], + }; + } + + throw new TypeConversionError(`Invalid message ID \`${disableInlineCode(value)}\``); + })(); + + const channel = context.pluginData.guild.channels.get(result.channelId); + if (!channel || !(channel instanceof TextChannel)) { + throw new TypeConversionError(`Invalid channel ID \`${disableInlineCode(result.channelId)}\``); + } + + return { + channel, + messageId: result.messageId, + }; + }, }; export const commandTypeHelpers = { @@ -51,4 +114,5 @@ export const commandTypeHelpers = { resolvedUser: createTypeHelper>(commandTypes.resolvedUser), resolvedUserLoose: createTypeHelper>(commandTypes.resolvedUserLoose), resolvedMember: createTypeHelper>(commandTypes.resolvedMember), + messageTarget: createTypeHelper>(commandTypes.messageTarget), }; diff --git a/backend/src/data/getChannelIdFromMessageId.ts b/backend/src/data/getChannelIdFromMessageId.ts new file mode 100644 index 00000000..64fc3b85 --- /dev/null +++ b/backend/src/data/getChannelIdFromMessageId.ts @@ -0,0 +1,17 @@ +import { SavedMessage } from "./entities/SavedMessage"; +import { Repository, getRepository } from "typeorm"; + +let repository: Repository; + +export async function getChannelIdFromMessageId(messageId: string): Promise { + if (!repository) { + repository = getRepository(SavedMessage); + } + + const savedMessage = await repository.findOne(messageId); + if (savedMessage) { + return savedMessage.channel_id; + } + + return null; +} diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index 628871d4..3b909040 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -71,6 +71,7 @@ const defaultOptions: PluginOptions = { can_context: true, can_jumbo: true, can_avatar: true, + can_source: true, }, }, { @@ -78,7 +79,6 @@ const defaultOptions: PluginOptions = { config: { can_reload_guild: true, can_ping: true, - can_source: true, can_about: true, }, }, diff --git a/backend/src/plugins/Utility/commands/SourceCmd.ts b/backend/src/plugins/Utility/commands/SourceCmd.ts index 414a9984..2a4dc4c1 100644 --- a/backend/src/plugins/Utility/commands/SourceCmd.ts +++ b/backend/src/plugins/Utility/commands/SourceCmd.ts @@ -1,8 +1,11 @@ import { utilityCmd } from "../types"; import { commandTypeHelpers as ct } from "../../../commandTypes"; import { errorMessage } from "../../../utils"; -import { getBaseUrl } from "../../../pluginUtils"; +import { getBaseUrl, sendErrorMessage } from "../../../pluginUtils"; import moment from "moment-timezone"; +import { Constants, TextChannel } from "eris"; +import { hasPermissions } from "../../../utils/hasPermissions"; +import { canReadChannel } from "../../../utils/canReadChannel"; export const SourceCmd = utilityCmd({ trigger: "source", @@ -11,22 +14,36 @@ export const SourceCmd = utilityCmd({ permission: "can_source", signature: { - messageId: ct.string(), + message: ct.messageTarget(), }, - async run({ message: msg, args, pluginData }) { - const savedMessage = await pluginData.state.savedMessages.find(args.messageId); - if (!savedMessage) { - msg.channel.createMessage(errorMessage("Unknown message")); + async run({ message: cmdMessage, args, pluginData }) { + if (!canReadChannel(args.message.channel, cmdMessage.member.id)) { + sendErrorMessage(pluginData, cmdMessage.channel, "Unknown message"); return; } - const source = - (savedMessage.data.content || "") + "\n\nSource:\n\n" + JSON.stringify(savedMessage.data); + const message = await pluginData.client + .getMessage(args.message.channel.id, args.message.messageId) + .catch(() => null); + if (!message) { + sendErrorMessage(pluginData, cmdMessage.channel, "Unknown message"); + return; + } + + const textSource = message.content || ""; + const fullSource = JSON.stringify({ + id: message.id, + content: message.content, + attachments: message.attachments, + embeds: message.embeds, + }); + + const source = `${textSource}\n\nSource:\n\n${fullSource}`; const archiveId = await pluginData.state.archives.create(source, moment().add(1, "hour")); const baseUrl = getBaseUrl(pluginData); const url = pluginData.state.archives.getUrl(baseUrl, archiveId); - msg.channel.createMessage(`Message source: ${url}`); + cmdMessage.channel.createMessage(`Message source: ${url}`); }, }); diff --git a/backend/src/utils/canReadChannel.ts b/backend/src/utils/canReadChannel.ts new file mode 100644 index 00000000..18feacb1 --- /dev/null +++ b/backend/src/utils/canReadChannel.ts @@ -0,0 +1,10 @@ +import { Constants, GuildChannel } from "eris"; +import { hasPermissions } from "./hasPermissions"; + +export function canReadChannel(channel: GuildChannel, memberId: string) { + const channelPermissions = channel.permissionsOf(memberId); + return hasPermissions(channelPermissions, [ + Constants.Permissions.readMessages, + Constants.Permissions.readMessageHistory, + ]); +} diff --git a/backend/src/utils/hasPermissions.ts b/backend/src/utils/hasPermissions.ts new file mode 100644 index 00000000..03e86d1e --- /dev/null +++ b/backend/src/utils/hasPermissions.ts @@ -0,0 +1,17 @@ +import { Constants, Permission } from "eris"; + +export function hasPermissions(channelPermissions: Permission, permissions: number | number[]) { + if (Boolean(channelPermissions.allow & Constants.Permissions.administrator)) { + return true; + } + + if (!Array.isArray(permissions)) { + permissions = [permissions]; + } + + for (const permission of permissions) { + if (!(channelPermissions.allow & permission)) return false; + } + + return true; +}