From c9ff8815af45ffaa2d6b58d37bfc9bfb0ae766c1 Mon Sep 17 00:00:00 2001 From: vycdev2 Date: Mon, 17 Aug 2026 05:14:53 +0000 Subject: [PATCH] fix: chunk application emoji listings --- Modules/EmojisModule.cs | 68 ++++++++++++++++++++++++++++- Morpheus.Tests/EmojisModuleTests.cs | 34 +++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/Modules/EmojisModule.cs b/Modules/EmojisModule.cs index 2c27a24..7448a61 100644 --- a/Modules/EmojisModule.cs +++ b/Modules/EmojisModule.cs @@ -10,6 +10,7 @@ using System.Collections.Concurrent; using System.Globalization; using System.IO.Compression; +using System.Text; namespace Morpheus.Modules; @@ -161,8 +162,71 @@ public async Task ListEmojis() return; } - string emojiList = string.Join("\n", emotes.Select(e => e.Name + " - " + e.ToString())); - await ReplyAsync($"**Custom Emojis:**\n{emojiList}"); + IReadOnlyList messages = BuildEmojiListMessages( + emotes.Select(e => e.Name + " - " + e.ToString())); + + foreach (string message in messages) + await ReplyAsync(message); + } + + internal static IReadOnlyList BuildEmojiListMessages(IEnumerable emojiEntries) + { + const string header = "**Custom Emojis:**"; + const int maxMessageLength = 2000; + var messages = new List(); + var chunk = new StringBuilder(header); + + void Flush() + { + if (chunk.Length == header.Length) + return; + + messages.Add(chunk.ToString()); + chunk.Clear().Append(header); + } + + foreach (string entry in emojiEntries) + { + if (chunk.Length > header.Length + && chunk.Length + 1 + entry.Length > maxMessageLength) + { + Flush(); + } + + int offset = 0; + while (offset < entry.Length) + { + int available = maxMessageLength - chunk.Length - 1; + if (available <= 0) + { + Flush(); + continue; + } + + int take = Math.Min(available, entry.Length - offset); + if (offset + take < entry.Length + && char.IsHighSurrogate(entry[offset + take - 1]) + && char.IsLowSurrogate(entry[offset + take])) + { + take--; + } + + if (take == 0) + { + Flush(); + continue; + } + + chunk.Append('\n').Append(entry.AsSpan(offset, take)); + offset += take; + + if (offset < entry.Length) + Flush(); + } + } + + Flush(); + return messages; } [Name("Download Emojis")] diff --git a/Morpheus.Tests/EmojisModuleTests.cs b/Morpheus.Tests/EmojisModuleTests.cs index bc625f0..d028df2 100644 --- a/Morpheus.Tests/EmojisModuleTests.cs +++ b/Morpheus.Tests/EmojisModuleTests.cs @@ -101,6 +101,40 @@ public void TryGetEmojiPage_HandlesEmptyAndOutOfRangePages( Assert.Equal(expectedTotalPages, totalPages); } + [Fact] + public void BuildEmojiListMessages_SplitsEntriesWithinDiscordLimit() + { + string[] entries = [.. Enumerable.Range(1, 100) + .Select(index => $"emoji-{index:D3} - <:emoji_{index:D3}:123456789012345678>")]; + + IReadOnlyList messages = EmojisModule.BuildEmojiListMessages(entries); + + Assert.True(messages.Count > 1); + Assert.All(messages, message => Assert.InRange(message.Length, 1, 2000)); + Assert.All(messages, message => Assert.StartsWith("**Custom Emojis:**\n", message)); + Assert.Equal( + entries, + messages.SelectMany(message => message.Split('\n').Skip(1))); + } + + [Fact] + public void BuildEmojiListMessages_SplitsOversizedEntriesWithoutBreakingUnicode() + { + const string header = "**Custom Emojis:**\n"; + string entry = new string('x', 1980) + "😀" + new string('y', 100); + + IReadOnlyList messages = EmojisModule.BuildEmojiListMessages([entry]); + string[] payloads = [.. messages.Select(message => message[header.Length..])]; + + Assert.All(messages, message => Assert.InRange(message.Length, 1, 2000)); + Assert.Equal(entry, string.Concat(payloads)); + Assert.All(payloads, payload => + { + Assert.False(char.IsHighSurrogate(payload[^1])); + Assert.False(char.IsLowSurrogate(payload[0])); + }); + } + [Fact] public async Task ImportSessionStore_AppliesConcurrentPageUpdatesWithoutLosingChanges() {