diff --git a/KfChatDotNetBot/ChatBot.cs b/KfChatDotNetBot/ChatBot.cs index 0165a75..c1b82a4 100644 --- a/KfChatDotNetBot/ChatBot.cs +++ b/KfChatDotNetBot/ChatBot.cs @@ -285,7 +285,7 @@ public class ChatBot return messageTracker; } - if (Encoding.UTF8.GetByteCount(messageTracker.Message) > lengthLimit && lengthLimitBehavior != LengthLimitBehavior.DoNothing) + if (messageTracker.Message.Utf8LengthBytes() > lengthLimit && lengthLimitBehavior != LengthLimitBehavior.DoNothing) { if (lengthLimitBehavior == LengthLimitBehavior.RefuseToSend) { @@ -296,7 +296,8 @@ public class ChatBot } if (lengthLimitBehavior == LengthLimitBehavior.TruncateNicely) { - messageTracker.Message = messageTracker.Message.Truncate(lengthLimit); + // '…' is 3 bytes so we have to make room for it + messageTracker.Message = messageTracker.Message.TruncateBytes(lengthLimit - 3).TrimEnd() + "…"; } if (lengthLimitBehavior == LengthLimitBehavior.TruncateExactly) @@ -304,7 +305,7 @@ public class ChatBot // ReSharper disable once ReplaceSubstringWithRangeIndexer // The range indexer is a fucking piece of shit that does not work. // TrimEnd in case you end up truncating on a space (happened during testing) as Sneedchat will trim it - messageTracker.Message = messageTracker.Message.Substring(0, lengthLimit).TrimEnd(); + messageTracker.Message = messageTracker.Message.TruncateBytes(lengthLimit).TrimEnd(); } } @@ -316,7 +317,7 @@ public class ChatBot } public SentMessageTrackerModel SendChatMessage(string message, bool bypassSeshDetect = false, - LengthLimitBehavior lengthLimitBehavior = LengthLimitBehavior.TruncateNicely, int lengthLimit = 500) + LengthLimitBehavior lengthLimitBehavior = LengthLimitBehavior.TruncateNicely, int lengthLimit = 1023) { return SendChatMessageAsync(message, bypassSeshDetect, lengthLimitBehavior, lengthLimit).Result; } diff --git a/KfChatDotNetBot/Commands/AdminCommands.cs b/KfChatDotNetBot/Commands/AdminCommands.cs index 991c450..daae646 100644 --- a/KfChatDotNetBot/Commands/AdminCommands.cs +++ b/KfChatDotNetBot/Commands/AdminCommands.cs @@ -123,7 +123,7 @@ public class GmKasinoListCommand : ICommand result += $"[br]{i}: {image}"; } - await botInstance.SendChatMessagesAsync(result.SplitMessage().ToList(), true); + await botInstance.SendChatMessagesAsync(result.FancySplitMessage(), true); } } @@ -213,7 +213,7 @@ public class GnKasinoListCommand : ICommand result += $"[br]{i}: {image}"; } - await botInstance.SendChatMessagesAsync(result.SplitMessage().ToList(), true); + await botInstance.SendChatMessagesAsync(result.FancySplitMessage(), true); } } diff --git a/KfChatDotNetBot/Extensions.cs b/KfChatDotNetBot/Extensions.cs index 27b507b..1b99dab 100644 --- a/KfChatDotNetBot/Extensions.cs +++ b/KfChatDotNetBot/Extensions.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Text; +using System.Text.RegularExpressions; namespace KfChatDotNetBot; @@ -44,4 +45,80 @@ public static class Extensions } } + /// + /// Split messages to x number of bytes while avoiding splitting mid-word where possible + /// + /// String that should get split + /// Length limit, no part should be > than the number of bytes specified + /// Limit for how many parts to return (returns first n elements). Set to 0 to disable. + /// + public static List FancySplitMessage(this string s, int partLengthBytes = 1023, int partLimit = 5) + { + var output = new List(); + var part = string.Empty; + foreach (var word in s.Split(' ')) + { + if (word.Utf8LengthBytes() > partLengthBytes) + { + // Add the part already in memory if there is one + if (part != string.Empty) + { + output.Add(part.TrimEnd()); + part = string.Empty; + } + // Breaks into chunks of x size which will break really long URLs etc. but no other way really + output.AddRange(word.ChunkBytes(partLengthBytes)); + continue; + } + + if (part.Utf8LengthBytes() + word.Utf8LengthBytes() > partLengthBytes) + { + // TrimEnd() to remove trailing spaces + output.Add(part.TrimEnd()); + part = word + " "; + continue; + } + + part += word + " "; + } + + // Add on whatever remains + if (part != string.Empty) + { + output.Add(part.TrimEnd()); + } + + if (partLimit != 0 && output.Count > partLimit) + { + return output.Take(partLimit).ToList(); + } + + return output; + } + + public static int Utf8LengthBytes(this string s) + { + return Encoding.UTF8.GetByteCount(s); + } + + public static IEnumerable ChunkBytes(this string input, int bytesPerChunk) + { + var bytes = Encoding.UTF8.GetBytes(input); + + for (var i = 0; i < bytes.Length; i += bytesPerChunk) + { + var chunkSize = Math.Min(bytesPerChunk, bytes.Length - i); + yield return Encoding.UTF8.GetString(bytes, i, chunkSize); + } + } + + + public static string TruncateBytes(this string s, int limitBytes) + { + return Encoding.UTF8.GetString( + Encoding.UTF8.GetBytes(s) + .Take(limitBytes) + .ToArray() + ).TrimEnd(); + } } \ No newline at end of file