fix: IRCv3 echo messages incorrect format

This commit is contained in:
Stone_Red
2026-07-26 14:52:22 +02:00
parent 441a97e8c9
commit 5d1934e2c6
3 changed files with 73 additions and 14 deletions
+45 -10
View File
@@ -23,7 +23,16 @@ public class IrcBroadcaster : IChatBroadcaster
Content = _encryption.Decrypt(message.Content), Content = _encryption.Decrypt(message.Content),
ReplyTo = message.ReplyTo is { } reply ? reply with { Content = _encryption.Decrypt(reply.Content) } : null, ReplyTo = message.ReplyTo is { } reply ? reply with { Content = _encryption.Decrypt(reply.Content) } : null,
}; };
var baseLines = IrcMessageFormatter.FormatMessage(decryptedMessage, _gateway.Options.PublicBaseUrl);
// Clients that support message-tags receive the +reply tag instead of
// the text reply prefix, so format without it. The sender's echo-message
// also needs the raw content to correlate with what they sent.
var hasReply = message.ReplyTo is not null;
var modernMessage = hasReply ? decryptedMessage with { ReplyTo = null } : decryptedMessage;
var legacyLines = IrcMessageFormatter.FormatMessage(decryptedMessage, _gateway.Options.PublicBaseUrl);
var modernLines = hasReply
? IrcMessageFormatter.FormatMessage(modernMessage, _gateway.Options.PublicBaseUrl)
: legacyLines;
// Compute shared tag components (same for all connections in this channel) // Compute shared tag components (same for all connections in this channel)
var serverTimeTag = message.SentAt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); var serverTimeTag = message.SentAt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
@@ -37,6 +46,11 @@ public class IrcBroadcaster : IChatBroadcaster
if (conn.ConnectionId == excludeConnectionId && !conn.HasCap("echo-message")) if (conn.ConnectionId == excludeConnectionId && !conn.HasCap("echo-message"))
continue; continue;
// Use modern lines (without reply prefix) for echo-message senders and
// for any client that gets the +reply tag via message-tags capability.
var useModern = conn.ConnectionId == excludeConnectionId || conn.HasCap("message-tags");
var lines = useModern ? modernLines : legacyLines;
// Build per-connection tags // Build per-connection tags
var tags = new List<(string Key, string? Value)>(); var tags = new List<(string Key, string? Value)>();
@@ -52,13 +66,13 @@ public class IrcBroadcaster : IChatBroadcaster
var tagPrefix = tags.Count > 0 ? IrcMessage.BuildTagPrefix([.. tags]) : ""; var tagPrefix = tags.Count > 0 ? IrcMessage.BuildTagPrefix([.. tags]) : "";
if (conn.HasCap("draft/multiline") && conn.HasCap("batch") && baseLines.Count > 1) if (conn.HasCap("draft/multiline") && conn.HasCap("batch") && lines.Count > 1)
{ {
await SendMultilineBatchAsync(conn, channelName, tagPrefix, baseLines); await SendMultilineBatchAsync(conn, channelName, tagPrefix, lines);
} }
else else
{ {
foreach (var line in baseLines) foreach (var line in lines)
await conn.SendAsync(tagPrefix + line); await conn.SendAsync(tagPrefix + line);
} }
} }
@@ -72,10 +86,32 @@ public class IrcBroadcaster : IChatBroadcaster
var batchRef = $"ml{Guid.NewGuid().ToString("N")[..8]}"; var batchRef = $"ml{Guid.NewGuid().ToString("N")[..8]}";
var ircChannel = $"#{channelName}"; var ircChannel = $"#{channelName}";
var sharedTags = tagPrefix.Length > 0 // Per draft/multiline spec: msgid and +reply go on the BATCH start line
? tagPrefix.TrimEnd() + ";" + IrcMessage.BuildTagPrefix(("batch", batchRef)).TrimEnd() // only; per-message tags (time, batch) go on individual lines.
: IrcMessage.BuildTagPrefix(("batch", batchRef)).TrimEnd(); // Parse the pre-built tagPrefix to split batch-level from line-level tags.
var linePrefix = $"@{sharedTags} "; string batchTags, lineTags;
if (tagPrefix.Length > 0 && tagPrefix.StartsWith('@'))
{
var tagBody = tagPrefix.AsSpan(1).TrimEnd(' ');
var parts = tagBody.ToString().Split(';', StringSplitOptions.RemoveEmptyEntries);
var batchParts = new List<string>();
var lineParts = new List<string>();
foreach (var part in parts)
{
if (part.StartsWith("msgid=") || part.StartsWith("+reply="))
batchParts.Add(part);
else
lineParts.Add(part);
}
batchTags = batchParts.Count > 0 ? "@" + string.Join(";", batchParts) + " " : "";
lineParts.Add("batch=" + batchRef);
lineTags = "@" + string.Join(";", lineParts) + " ";
}
else
{
batchTags = "";
lineTags = "@batch=" + batchRef + " ";
}
// Extract the sender prefix from the first line // Extract the sender prefix from the first line
var firstLine = lines[0]; var firstLine = lines[0];
@@ -83,11 +119,10 @@ public class IrcBroadcaster : IChatBroadcaster
? firstLine[1..firstLine.IndexOf(' ')] ? firstLine[1..firstLine.IndexOf(' ')]
: _gateway.Options.ServerName; : _gateway.Options.ServerName;
var batchTags = tagPrefix.Length > 0 ? tagPrefix : "";
await conn.SendAsync($"{batchTags}:{senderPrefix} BATCH +{batchRef} draft/multiline {ircChannel}"); await conn.SendAsync($"{batchTags}:{senderPrefix} BATCH +{batchRef} draft/multiline {ircChannel}");
foreach (var line in lines) foreach (var line in lines)
await conn.SendAsync($"{linePrefix}{line}"); await conn.SendAsync($"{lineTags}{line}");
await conn.SendAsync($"BATCH -{batchRef}"); await conn.SendAsync($"BATCH -{batchRef}");
} }
+27 -4
View File
@@ -346,7 +346,16 @@ public sealed class IrcCommandHandler
var channelName = IrcToEchoHubChannel(target); var channelName = IrcToEchoHubChannel(target);
if (channelName is null) return; if (channelName is null) return;
_conn.PendingMultilineBatch = new MultilineBatchContext(reference[1..], channelName); var batchCtx = new MultilineBatchContext(reference[1..], channelName);
// Capture +reply tag from the BATCH start line for reply handling
if (msg.Tags.TryGetValue("+reply", out var replyStr) &&
Guid.TryParse(replyStr, out var replyId))
{
batchCtx.ReplyToMessageId = replyId;
}
_conn.PendingMultilineBatch = batchCtx;
} }
} }
@@ -371,8 +380,14 @@ public sealed class IrcCommandHandler
// are directly concatenated (already handled during collection). // are directly concatenated (already handled during collection).
var content = string.Join("\n", batch.Lines); var content = string.Join("\n", batch.Lines);
await _chatService.SendMessageAsync( var error = await _chatService.SendMessageAsync(
_conn.UserId!.Value, _conn.Nickname!, batch.Target, content, _conn.ConnectionId); _conn.UserId!.Value, _conn.Nickname!, batch.Target, content, _conn.ConnectionId, batch.ReplyToMessageId);
if (error is not null)
{
await _conn.SendNumericAsync(ServerName, IrcNumericReply.ERR_CANNOTSENDTOCHAN,
$"#{batch.Target} :{error}");
}
} }
// ── Authentication ────────────────────────────────────────────────────── // ── Authentication ──────────────────────────────────────────────────────
@@ -789,8 +804,16 @@ public sealed class IrcCommandHandler
return; return;
} }
// Parse +reply tag for reply-to support from IRC clients
Guid? replyTo = null;
if (msg.Tags.TryGetValue("+reply", out var replyStr) &&
Guid.TryParse(replyStr, out var replyId))
{
replyTo = replyId;
}
var error = await _chatService.SendMessageAsync( var error = await _chatService.SendMessageAsync(
_conn.UserId!.Value, _conn.Nickname!, channelName, content, _conn.ConnectionId); _conn.UserId!.Value, _conn.Nickname!, channelName, content, _conn.ConnectionId, replyTo);
if (error is not null) if (error is not null)
{ {
@@ -10,4 +10,5 @@ public sealed class MultilineBatchContext(string referenceTag, string target)
public string Target { get; } = target; public string Target { get; } = target;
public List<string> Lines { get; } = []; public List<string> Lines { get; } = [];
public bool UsesConcat { get; set; } public bool UsesConcat { get; set; }
public Guid? ReplyToMessageId { get; set; }
} }