From d5ea95d9e902185077f8c579908fc67449076988 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Wed, 21 Oct 2020 21:03:33 +0300 Subject: [PATCH] Limit replies to fit within one message This applies to both the DM to be sent to the user, and the message created in the thread channel. This is because edits (via !edit) could change the amount of messages a reply takes (based on message formatting), leading to either being unable to post the full edit if it goes over the message limits, or having to edit a previous message to be 'empty' if the result of the edit would take fewer messages to post than the original reply. This also fixes an issue where !edit/!delete would not apply to more than the first message created by a reply - whether in user DMs or in the thread channel. --- src/data/Thread.js | 18 ++++++++++++++-- src/utils.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/data/Thread.js b/src/data/Thread.js index fdb789a..8cd79e8 100644 --- a/src/data/Thread.js +++ b/src/data/Thread.js @@ -223,8 +223,16 @@ class Thread { attachments: attachmentLinks, }); - // Send the reply DM const dmContent = formatters.formatStaffReplyDM(threadMessage); + const inboxContent = formatters.formatStaffReplyThreadMessage(threadMessage); + + // Because moderator replies have to be editable, we enforce them to fit within 1 message + if (! utils.messageContentIsWithinMaxLength(dmContent) || ! utils.messageContentIsWithinMaxLength(inboxContent)) { + await this.postSystemMessage("Reply is too long! Make sure your reply is under 2000 characters total, moderator name in the reply included."); + return false; + } + + // Send the reply DM let dmMessage; try { dmMessage = await this._sendDMToUser(dmContent, files); @@ -240,7 +248,6 @@ class Thread { }); // Show the reply in the inbox thread - const inboxContent = formatters.formatStaffReplyThreadMessage(threadMessage); const inboxMessage = await this._postToThreadChannel(inboxContent, files); if (inboxMessage) { await this._updateThreadMessage(threadMessage.id, { inbox_message_id: inboxMessage.id }); @@ -719,6 +726,13 @@ class Thread { const formattedThreadMessage = formatters.formatStaffReplyThreadMessage(newThreadMessage); const formattedDM = formatters.formatStaffReplyDM(newThreadMessage); + // Same restriction as in replies. Because edits could theoretically change the number of messages a reply takes, we enforce replies + // to fit within 1 message to avoid the headache and issues caused by that. + if (! utils.messageContentIsWithinMaxLength(formattedDM) || ! utils.messageContentIsWithinMaxLength(formattedThreadMessage)) { + await this.postSystemMessage("Edited reply is too long! Make sure the edit is under 2000 characters total, moderator name in the reply included."); + return false; + } + await bot.editMessage(threadMessage.dm_channel_id, threadMessage.dm_message_id, formattedDM); await bot.editMessage(this.channel_id, threadMessage.inbox_message_id, formattedThreadMessage); diff --git a/src/utils.js b/src/utils.js index d97447a..83118fe 100644 --- a/src/utils.js +++ b/src/utils.js @@ -336,6 +336,56 @@ function readMultilineConfigValue(str) { function noop() {} +// https://discord.com/developers/docs/resources/channel#create-message-params +const MAX_MESSAGE_CONTENT_LENGTH = 2000; + +// https://discord.com/developers/docs/resources/channel#embed-limits +const MAX_EMBED_CONTENT_LENGTH = 6000; + +/** + * Checks if the given message content is within Discord's message length limits. + * + * Based on testing, Discord appears to enforce length limits (at least in the client) + * the same way JavaScript does, using the UTF-16 byte count as the number of characters. + * + * @param {string|Eris.MessageContent} content + */ +function messageContentIsWithinMaxLength(content) { + if (typeof content === "string") { + content = { content }; + } + + if (content.content && content.content.length > MAX_MESSAGE_CONTENT_LENGTH) { + return false; + } + + if (content.embed) { + let embedContentLength = 0; + + if (content.embed.title) embedContentLength += content.embed.title.length; + if (content.embed.description) embedContentLength += content.embed.description.length; + if (content.embed.footer && content.embed.footer.text) { + embedContentLength += content.embed.footer.text.length; + } + if (content.embed.author && content.embed.author.name) { + embedContentLength += content.embed.author.name.length; + } + + if (content.embed.fields) { + for (const field of content.embed.fields) { + if (field.title) embedContentLength += field.name.length; + if (field.description) embedContentLength += field.value.length; + } + } + + if (embedContentLength > MAX_EMBED_CONTENT_LENGTH) { + return false; + } + } + + return true; +} + module.exports = { BotError, @@ -377,5 +427,7 @@ module.exports = { readMultilineConfigValue, + messageContentIsWithinMaxLength, + noop, };