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),
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)
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"))
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
var tags = new List<(string Key, string? Value)>();
@@ -52,13 +66,13 @@ public class IrcBroadcaster : IChatBroadcaster
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
{
foreach (var line in baseLines)
foreach (var line in lines)
await conn.SendAsync(tagPrefix + line);
}
}
@@ -72,10 +86,32 @@ public class IrcBroadcaster : IChatBroadcaster
var batchRef = $"ml{Guid.NewGuid().ToString("N")[..8]}";
var ircChannel = $"#{channelName}";
var sharedTags = tagPrefix.Length > 0
? tagPrefix.TrimEnd() + ";" + IrcMessage.BuildTagPrefix(("batch", batchRef)).TrimEnd()
: IrcMessage.BuildTagPrefix(("batch", batchRef)).TrimEnd();
var linePrefix = $"@{sharedTags} ";
// Per draft/multiline spec: msgid and +reply go on the BATCH start line
// only; per-message tags (time, batch) go on individual lines.
// Parse the pre-built tagPrefix to split batch-level from line-level tags.
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
var firstLine = lines[0];
@@ -83,11 +119,10 @@ public class IrcBroadcaster : IChatBroadcaster
? firstLine[1..firstLine.IndexOf(' ')]
: _gateway.Options.ServerName;
var batchTags = tagPrefix.Length > 0 ? tagPrefix : "";
await conn.SendAsync($"{batchTags}:{senderPrefix} BATCH +{batchRef} draft/multiline {ircChannel}");
foreach (var line in lines)
await conn.SendAsync($"{linePrefix}{line}");
await conn.SendAsync($"{lineTags}{line}");
await conn.SendAsync($"BATCH -{batchRef}");
}
+27 -4
View File
@@ -346,7 +346,16 @@ public sealed class IrcCommandHandler
var channelName = IrcToEchoHubChannel(target);
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).
var content = string.Join("\n", batch.Lines);
await _chatService.SendMessageAsync(
_conn.UserId!.Value, _conn.Nickname!, batch.Target, content, _conn.ConnectionId);
var error = await _chatService.SendMessageAsync(
_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 ──────────────────────────────────────────────────────
@@ -789,8 +804,16 @@ public sealed class IrcCommandHandler
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(
_conn.UserId!.Value, _conn.Nickname!, channelName, content, _conn.ConnectionId);
_conn.UserId!.Value, _conn.Nickname!, channelName, content, _conn.ConnectionId, replyTo);
if (error is not null)
{
@@ -10,4 +10,5 @@ public sealed class MultilineBatchContext(string referenceTag, string target)
public string Target { get; } = target;
public List<string> Lines { get; } = [];
public bool UsesConcat { get; set; }
public Guid? ReplyToMessageId { get; set; }
}