5.6 KiB
Split Messages - Design Spec
Goal
When a composed message exceeds Discord's character limit (2000, or 4000 with Nitro), automatically split it into multiple sequential messages instead of showing Discord's native "message too long" popup and blocking the send.
Built for classic (Vendetta-compat, /root/revenge-plugins) first, to validate the concept before porting to Next.
Reference
Ported from sunnniee/vendetta-plugins's SplitLargeMessages (https://github.com/sunnniee/vendetta-plugins/tree/master/plugins/SplitLargeMessages), live-verified against a connected classic client on this session's Discord build:
findByProps("MAX_MESSAGE_LENGTH")exists, withMAX_MESSAGE_LENGTH: 2000andMAX_MESSAGE_LENGTH_PREMIUM: 4000.findByProps("sendMessage", "editMessage")._sendMessageexists as a callable function.UserStore.getCurrentUser().premiumTypereflects Nitro status (2 = full Nitro).
Architecture
On onLoad:
- Locate
Constants = findByProps("MAX_MESSAGE_LENGTH")and raiseMAX_MESSAGE_LENGTH/MAX_MESSAGE_LENGTH_PREMIUMto a very large number (2 ** 30). This is what stops Discord's own composer from ever showing the native "too long" popup or blocking the send button - the check that shows that popup reads these constants directly, not anyMessageActionsfunction (confirmed live: neithersendMessagenorvalidateMessagewere called at all when a real over-limit send was attempted before this fix). - Patch
beforeonMessageActions.sendMessage. Inside the hook:- Read the real limit for this send:
UserStore.getCurrentUser()?.premiumType === 2 ? 4000 : 2000. - If
content.lengthis under that limit, return immediately (no-op, real send proceeds normally). - Otherwise, run the splitting algorithm (below) to get an ordered list of chunks.
- If splitting fails (an unsplittable token longer than the limit), clear
args[1].contentto"", show an error toast, and return - nothing sends. - Otherwise, mutate
args[1].contentto the FIRST chunk only, so the originalsendMessagecall proceeds as a normal send for chunk 1. - Fire an async loop (not awaited by the hook itself) that sends the remaining chunks in order via
MessageActions._sendMessage(channelId, { ...same invalidEmojis/validNonShortcutEmojis/tts, content: chunk }, {}), sleepingMath.max(channel.rateLimitPerUser, 1000)ms (channel slowmode, 1s floor) between each.
- If splitting fails (an unsplittable token longer than the limit), clear
- Read the real limit for this send:
On onUnload: restore Constants.MAX_MESSAGE_LENGTH = 2000 and MAX_MESSAGE_LENGTH_PREMIUM = 4000, and unpatch the hook, so nothing is left in a broken state if disabled.
Splitting algorithm
Input: content: string, maxLength: number. Output: string[] | false (false = unsplittable).
- Segment the content into an ordered list of pieces by extracting fenced code blocks (
```...```, including the fence lines and any language tag) as atomic units, with everything between/around them as plain-text pieces. A code block piece is never split internally unless it alone exceedsmaxLength(see step 3). - Plain-text pieces: split by paragraph first (newline-separated), greedily packing paragraphs into sub-chunks up to
maxLengthcharacters (matching the reference's reduce-based packing). If any resulting sub-chunk is still overmaxLength(a single paragraph longer than the limit with no newlines to split on), fall back to packing whole words (space-separated) instead, for that piece. - Code-block pieces: if the whole block (fences included) fits within
maxLength, keep it as one atomic piece. If it doesn't fit even alone, split its inner lines into groups that fit (accounting for the extra```fence overhead added to each group), and re-wrap each group with its own opening/closing fence so every resulting message still renders as a code block. - Pack: greedily accumulate the ordered pieces (text sub-chunks and code-block pieces) into final chunks up to
maxLength, same greedy strategy as the reference - never split a code-block piece across this final packing step. - Failure: if any single atomic piece from steps 2-3 is still longer than
maxLengthon its own (e.g. one word/URL/token with no break points), splitting fails - returnfalse.
Setting: storage.splitOnWords (boolean, default false) - when true, skip the paragraph-first pass for plain text and go straight to word-based splitting, matching the reference's existing toggle.
Explicitly out of scope
- Editing an existing message into an over-limit state (matches the reference - only sending is handled).
- Any "split into N messages" notification - silent by design, per user's call.
- The Next port - this spec covers classic only; Next's plugin architecture differs enough (different module system, different
MessageActionsequivalent) that it needs its own investigation once this is validated.
Testing
No automated test suite exists for this repo's plugins (matches the rest of the codebase - verification is live, against a connected devtools client). Verification plan:
- Live-verify the raised constants actually suppress the native popup for an over-2000/over-4000 character message.
- Live-verify a plain-text-only over-limit message splits and sends correctly, with all chunks arriving in order.
- Live-verify a message containing a code block that would otherwise straddle a split point instead keeps the code block intact in one message.
- Live-verify the unsplittable-token failure case (a single 5000-character no-space token) shows the error toast and sends nothing.
- Live-verify
onUnloadrestores native behavior (native popup returns for an over-limit message after disabling the plugin).