mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: implement newline sanitization in chat messages to prevent excessive newlines
This commit is contained in:
@@ -231,11 +231,11 @@ public class ChatListSource : IListDataSource
|
|||||||
attr = new Attribute(attr.Foreground, mentionBg.Value);
|
attr = new Attribute(attr.Foreground, mentionBg.Value);
|
||||||
listView.SetAttribute(attr);
|
listView.SetAttribute(attr);
|
||||||
|
|
||||||
foreach (var ch in segment.Text)
|
foreach (var rune in segment.Text.EnumerateRunes())
|
||||||
{
|
{
|
||||||
if (charPos >= viewportX && drawnChars < width)
|
if (charPos >= viewportX && drawnChars < width)
|
||||||
{
|
{
|
||||||
listView.AddRune(new Rune(ch));
|
listView.AddRune(rune);
|
||||||
drawnChars++;
|
drawnChars++;
|
||||||
}
|
}
|
||||||
charPos++;
|
charPos++;
|
||||||
@@ -324,9 +324,9 @@ public class ChannelListSource : IListDataSource
|
|||||||
if (selected)
|
if (selected)
|
||||||
{
|
{
|
||||||
listView.SetAttribute(focusAttr);
|
listView.SetAttribute(focusAttr);
|
||||||
foreach (var ch in (prefix + channelText + badge))
|
foreach (var rune in (prefix + channelText + badge).EnumerateRunes())
|
||||||
{
|
{
|
||||||
if (drawnChars < width) { listView.AddRune(new Rune(ch)); drawnChars++; }
|
if (drawnChars < width) { listView.AddRune(rune); drawnChars++; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -334,26 +334,26 @@ public class ChannelListSource : IListDataSource
|
|||||||
// Prefix
|
// Prefix
|
||||||
var prefixAttr = isActive ? ActiveAttr : NormalAttr;
|
var prefixAttr = isActive ? ActiveAttr : NormalAttr;
|
||||||
listView.SetAttribute(prefixAttr);
|
listView.SetAttribute(prefixAttr);
|
||||||
foreach (var ch in prefix)
|
foreach (var rune in prefix.EnumerateRunes())
|
||||||
{
|
{
|
||||||
if (drawnChars < width) { listView.AddRune(new Rune(ch)); drawnChars++; }
|
if (drawnChars < width) { listView.AddRune(rune); drawnChars++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channel name
|
// Channel name
|
||||||
var nameAttr = isActive ? ActiveAttr : hasUnread ? UnreadAttr : NormalAttr;
|
var nameAttr = isActive ? ActiveAttr : hasUnread ? UnreadAttr : NormalAttr;
|
||||||
listView.SetAttribute(nameAttr);
|
listView.SetAttribute(nameAttr);
|
||||||
foreach (var ch in channelText)
|
foreach (var rune in channelText.EnumerateRunes())
|
||||||
{
|
{
|
||||||
if (drawnChars < width) { listView.AddRune(new Rune(ch)); drawnChars++; }
|
if (drawnChars < width) { listView.AddRune(rune); drawnChars++; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unread badge
|
// Unread badge
|
||||||
if (hasUnread)
|
if (hasUnread)
|
||||||
{
|
{
|
||||||
listView.SetAttribute(BadgeAttr);
|
listView.SetAttribute(BadgeAttr);
|
||||||
foreach (var ch in badge)
|
foreach (var rune in badge.EnumerateRunes())
|
||||||
{
|
{
|
||||||
if (drawnChars < width) { listView.AddRune(new Rune(ch)); drawnChars++; }
|
if (drawnChars < width) { listView.AddRune(rune); drawnChars++; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,24 +403,25 @@ public class UserListSource : IListDataSource
|
|||||||
var (text, nameColor) = _users[item];
|
var (text, nameColor) = _users[item];
|
||||||
var normalAttr = listView.GetAttributeForRole(selected ? VisualRole.Focus : VisualRole.Normal);
|
var normalAttr = listView.GetAttributeForRole(selected ? VisualRole.Focus : VisualRole.Normal);
|
||||||
|
|
||||||
|
// Convert to runes for safe surrogate handling
|
||||||
|
var runes = text.EnumerateRunes().ToArray();
|
||||||
|
|
||||||
// Find where the name starts (after status icon + space + optional role badge)
|
// Find where the name starts (after status icon + space + optional role badge)
|
||||||
// Format: "● ★Username" or "● Username"
|
// Format: "● ★Username" or "● Username"
|
||||||
int nameStart = 0;
|
int nameStart = 0;
|
||||||
int i = 0;
|
while (nameStart < runes.Length && !Rune.IsLetterOrDigit(runes[nameStart]) && runes[nameStart].Value != '_')
|
||||||
// Skip status icon
|
nameStart++;
|
||||||
while (i < text.Length && !char.IsLetterOrDigit(text[i]) && text[i] != '_') i++;
|
|
||||||
nameStart = i;
|
|
||||||
|
|
||||||
int drawnChars = 0;
|
int drawnChars = 0;
|
||||||
|
|
||||||
// Draw prefix (status icon + role badge) in normal color
|
// Draw prefix (status icon + role badge) in normal color
|
||||||
var prefixAttr = normalAttr;
|
var prefixAttr = normalAttr;
|
||||||
for (int c = 0; c < nameStart && c < text.Length; c++)
|
for (int c = 0; c < nameStart && c < runes.Length; c++)
|
||||||
{
|
{
|
||||||
if (drawnChars < width)
|
if (drawnChars < width)
|
||||||
{
|
{
|
||||||
listView.SetAttribute(prefixAttr);
|
listView.SetAttribute(prefixAttr);
|
||||||
listView.AddRune(new Rune(text[c]));
|
listView.AddRune(runes[c]);
|
||||||
drawnChars++;
|
drawnChars++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -428,12 +429,12 @@ public class UserListSource : IListDataSource
|
|||||||
// Draw name in nickname color
|
// Draw name in nickname color
|
||||||
var userAttr = nameColor ?? normalAttr;
|
var userAttr = nameColor ?? normalAttr;
|
||||||
if (selected) userAttr = normalAttr; // use focus attr when selected
|
if (selected) userAttr = normalAttr; // use focus attr when selected
|
||||||
for (int c = nameStart; c < text.Length; c++)
|
for (int c = nameStart; c < runes.Length; c++)
|
||||||
{
|
{
|
||||||
if (drawnChars < width)
|
if (drawnChars < width)
|
||||||
{
|
{
|
||||||
listView.SetAttribute(userAttr);
|
listView.SetAttribute(userAttr);
|
||||||
listView.AddRune(new Rune(text[c]));
|
listView.AddRune(runes[c]);
|
||||||
drawnChars++;
|
drawnChars++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ public static class HubConstants
|
|||||||
public const int MaxMessageLength = 2000;
|
public const int MaxMessageLength = 2000;
|
||||||
public const int MaxFileSizeBytes = 10 * 1024 * 1024; // 10 MB
|
public const int MaxFileSizeBytes = 10 * 1024 * 1024; // 10 MB
|
||||||
public const int MaxAvatarSizeBytes = 2 * 1024 * 1024; // 2 MB
|
public const int MaxAvatarSizeBytes = 2 * 1024 * 1024; // 2 MB
|
||||||
|
public const int MaxMessageNewlines = 30;
|
||||||
|
public const int MaxConsecutiveNewlines = 2;
|
||||||
public const int AsciiArtWidth = 80;
|
public const int AsciiArtWidth = 80;
|
||||||
public const int AsciiArtHeight = 40;
|
public const int AsciiArtHeight = 40;
|
||||||
public const int AsciiArtHeightHalfBlock = 80;
|
public const int AsciiArtHeightHalfBlock = 80;
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ public class ChatService : IChatService
|
|||||||
if (content.Length > HubConstants.MaxMessageLength)
|
if (content.Length > HubConstants.MaxMessageLength)
|
||||||
return $"Message exceeds maximum length of {HubConstants.MaxMessageLength} characters.";
|
return $"Message exceeds maximum length of {HubConstants.MaxMessageLength} characters.";
|
||||||
|
|
||||||
|
// Sanitize excessive newlines
|
||||||
|
content = SanitizeNewlines(content);
|
||||||
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<EchoHubDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<EchoHubDbContext>();
|
||||||
|
|
||||||
@@ -343,6 +346,28 @@ public class ChatService : IChatService
|
|||||||
return (user.Id, user.Username);
|
return (user.Id, user.Username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collapse consecutive newlines and cap total line count to prevent newline spam.
|
||||||
|
/// </summary>
|
||||||
|
private static string SanitizeNewlines(string content)
|
||||||
|
{
|
||||||
|
// Normalize \r\n → \n
|
||||||
|
content = content.Replace("\r\n", "\n").Replace('\r', '\n');
|
||||||
|
|
||||||
|
// Collapse runs of >MaxConsecutiveNewlines into MaxConsecutiveNewlines
|
||||||
|
var maxRun = new string('\n', HubConstants.MaxConsecutiveNewlines + 1);
|
||||||
|
var replacement = new string('\n', HubConstants.MaxConsecutiveNewlines);
|
||||||
|
while (content.Contains(maxRun))
|
||||||
|
content = content.Replace(maxRun, replacement);
|
||||||
|
|
||||||
|
// Cap total newlines
|
||||||
|
var lines = content.Split('\n');
|
||||||
|
if (lines.Length > HubConstants.MaxMessageNewlines)
|
||||||
|
content = string.Join('\n', lines.Take(HubConstants.MaxMessageNewlines));
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<List<MessageDto>> GetChannelHistoryInternalAsync(EchoHubDbContext db, string channelName, int count)
|
private static async Task<List<MessageDto>> GetChannelHistoryInternalAsync(EchoHubDbContext db, string channelName, int count)
|
||||||
{
|
{
|
||||||
var channel = await db.Channels.FirstOrDefaultAsync(c => c.Name == channelName);
|
var channel = await db.Channels.FirstOrDefaultAsync(c => c.Name == channelName);
|
||||||
|
|||||||
Reference in New Issue
Block a user