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, };