mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-05 23:34:09 +02:00
feat: add image handling actions and improve IRC message formatting
This commit is contained in:
@@ -1,20 +1,20 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using EchoHub.Core.Contracts;
|
||||
using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
using EchoHub.Core.Security;
|
||||
|
||||
namespace EchoHub.Server.Irc;
|
||||
|
||||
public static partial class IrcMessageFormatter
|
||||
public static class IrcMessageFormatter
|
||||
{
|
||||
private const int MaxIrcLineContentBytes = 400;
|
||||
|
||||
/// <summary>
|
||||
/// Format a MessageDto as one or more IRC PRIVMSG lines.
|
||||
/// Attachments are rendered as single link lines \u2014 the widely-supported IRC convention
|
||||
/// (clients auto-preview or open plain http(s) URLs) \u2014 never as terminal color art.
|
||||
/// <paramref name="publicBaseUrl"/> makes the links absolute so any IRC client can open them.
|
||||
/// </summary>
|
||||
public static List<string> FormatMessage(MessageDto message)
|
||||
public static List<string> FormatMessage(MessageDto message, string? publicBaseUrl = null)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
var ircChannel = $"#{message.ChannelName}";
|
||||
@@ -27,34 +27,19 @@ public static partial class IrcMessageFormatter
|
||||
lines.Add($"{prefix} PRIVMSG {ircChannel} :{chunk}");
|
||||
}
|
||||
|
||||
// One block per attachment
|
||||
// One link line per attachment
|
||||
if (message.Attachments is { Count: > 0 })
|
||||
{
|
||||
foreach (var attachment in message.Attachments)
|
||||
{
|
||||
switch (attachment.Kind)
|
||||
var url = ToAbsoluteUrl(attachment.Url, publicBaseUrl);
|
||||
var tag = attachment.Kind switch
|
||||
{
|
||||
case AttachmentKind.Image:
|
||||
lines.Add($"{prefix} PRIVMSG {ircChannel} :[Image: {attachment.FileName}] {attachment.Url}");
|
||||
if (attachment.AsciiPreview is not null && !IsCiphertext(attachment.AsciiPreview))
|
||||
{
|
||||
foreach (var line in attachment.AsciiPreview.Split('\n'))
|
||||
{
|
||||
var trimmed = line.TrimEnd('\r');
|
||||
if (trimmed.Length > 0)
|
||||
lines.Add($"{prefix} PRIVMSG {ircChannel} :{ColorTagsToAnsi(trimmed)}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case AttachmentKind.Audio:
|
||||
lines.Add($"{prefix} PRIVMSG {ircChannel} :\u266a [Audio: {attachment.FileName}] {attachment.Url}");
|
||||
break;
|
||||
|
||||
default:
|
||||
lines.Add($"{prefix} PRIVMSG {ircChannel} :[File: {attachment.FileName}] {attachment.Url}");
|
||||
break;
|
||||
}
|
||||
AttachmentKind.Image => $"[Image: {attachment.FileName}]",
|
||||
AttachmentKind.Audio => $"\u266a [Audio: {attachment.FileName}]",
|
||||
_ => $"[File: {attachment.FileName}]",
|
||||
};
|
||||
lines.Add($"{prefix} PRIVMSG {ircChannel} :{tag} {url}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,13 +54,15 @@ public static partial class IrcMessageFormatter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when a preview is still encrypted — transport ($ENC$v1$) if a broadcast path
|
||||
/// forgot to decrypt it, or E2E room ciphertext ($RC1$) the server cannot decrypt.
|
||||
/// Emitting it would flood IRC clients with a multi-KB base64 blob.
|
||||
/// Joins a relative attachment path onto the configured public base URL.
|
||||
/// Already-absolute URLs and unset base URLs pass through unchanged.
|
||||
/// </summary>
|
||||
private static bool IsCiphertext(string text) =>
|
||||
text.StartsWith(IMessageEncryptionService.CiphertextPrefix, StringComparison.Ordinal)
|
||||
|| text.StartsWith(RoomCrypto.CiphertextPrefix, StringComparison.Ordinal);
|
||||
public static string ToAbsoluteUrl(string url, string? publicBaseUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(publicBaseUrl) || Uri.IsWellFormedUriString(url, UriKind.Absolute))
|
||||
return url;
|
||||
return $"{publicBaseUrl.TrimEnd('/')}/{url.TrimStart('/')}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a link embed as IRC PRIVMSG lines (text-only, no ASCII thumbnail).
|
||||
@@ -104,35 +91,6 @@ public static partial class IrcMessageFormatter
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert printable color tags ({F:RRGGBB}, {B:RRGGBB}, {X}) to ANSI escape codes for IRC clients.
|
||||
/// Also passes through content that already uses ANSI codes unchanged.
|
||||
/// </summary>
|
||||
public static string ColorTagsToAnsi(string text)
|
||||
{
|
||||
if (!text.Contains('{'))
|
||||
return text;
|
||||
|
||||
return ColorTagRegex().Replace(text, match =>
|
||||
{
|
||||
if (match.Groups[1].Success) // {X} reset
|
||||
return "\x1b[0m";
|
||||
if (match.Groups[2].Success) // {F:RRGGBB} or {B:RRGGBB}
|
||||
{
|
||||
var hex = match.Groups[3].Value;
|
||||
var r = Convert.ToInt32(hex[..2], 16);
|
||||
var g = Convert.ToInt32(hex[2..4], 16);
|
||||
var b = Convert.ToInt32(hex[4..6], 16);
|
||||
var code = match.Groups[2].Value == "F" ? "38" : "48";
|
||||
return $"\x1b[{code};2;{r};{g};{b}m";
|
||||
}
|
||||
return match.Value;
|
||||
});
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\{(?:(X)|(?:(F|B):([0-9A-Fa-f]{6})))\}")]
|
||||
private static partial Regex ColorTagRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Split a message into chunks of approximately maxBytes (UTF-8), at word boundaries.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user