mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
@@ -22,6 +22,7 @@
|
||||
".dev/**",
|
||||
"docs/_site/**",
|
||||
"docs/api/**",
|
||||
"docs/auriondocs/**",
|
||||
"node_modules/**",
|
||||
"**/bin/**",
|
||||
"**/obj/**",
|
||||
|
||||
@@ -4,6 +4,7 @@ Release history for EchoHub.
|
||||
|
||||
## Releases
|
||||
|
||||
- [v0.2.17](v0.2.17.md) - Server Version Reporting & IRC Multi-Line Fix
|
||||
- [v0.2.16](v0.2.16.md) - Periodic Server-Stats Report, Upload & Moderation Logging & Quieter Connection Logs
|
||||
- [v0.2.15](v0.2.15.md) - Invite Codes, Data Export & Deletion, /me, /banner, Replies, Open Images In Browser & IRC Image Links
|
||||
- [v0.2.14](v0.2.14.md) - Clipboard Image & Multi-File Paste, E2E Room Unlock Fixes, Encrypted Key Cache & IRC Gateway Polish
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
- name: Overview
|
||||
href: index.md
|
||||
- name: v0.2.17
|
||||
href: v0.2.17.md
|
||||
- name: v0.2.16
|
||||
href: v0.2.16.md
|
||||
- name: v0.2.15
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# v0.2.17
|
||||
|
||||
## New Features
|
||||
|
||||
- **Server version reporting** — `GET /api/server/info` now returns the server version. The client fetches it on connect and shows a warning dialog when the server and client versions don't match, with the option to continue or disconnect.
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **IRC multi-line messages** — sending a message with line breaks from the TUI now correctly delivers each line as a separate PRIVMSG to IRC clients. Previously newlines were embedded raw in the IRC frame, so IRC clients silently dropped everything after the first line — most visibly, a link on its own line disappeared while its embed preview still showed.
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>0.2.16</Version>
|
||||
<Version>0.2.17</Version>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1373,6 +1373,22 @@ public sealed class AppOrchestrator : IDisposable
|
||||
_mainWindow.FocusInput();
|
||||
FetchAndUpdateOnlineUsers();
|
||||
});
|
||||
|
||||
if (result.ServerInfo is not null && !string.IsNullOrEmpty(result.ServerInfo.Version)
|
||||
&& result.ServerInfo.Version != UpdateChecker.CurrentVersion)
|
||||
{
|
||||
InvokeUI(() =>
|
||||
{
|
||||
var choice = MessageBox.Query(_app, "Version Mismatch",
|
||||
$"Server version {result.ServerInfo.Version} does not match client version {UpdateChecker.CurrentVersion}.\n\n" +
|
||||
"Some features may not work correctly. Consider updating both to the same version.",
|
||||
"Continue", "Disconnect");
|
||||
|
||||
if (choice == 1)
|
||||
HandleDisconnect();
|
||||
});
|
||||
}
|
||||
|
||||
SaveServerToConfig(dialogResult);
|
||||
}, "Connection failed", "Connect");
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace EchoHub.Client.Services;
|
||||
internal record ConnectResult(
|
||||
LoginResponse Login,
|
||||
List<ChannelDto> Channels,
|
||||
Dictionary<string, List<MessageDto>> Histories);
|
||||
Dictionary<string, List<MessageDto>> Histories,
|
||||
ServerStatusDto? ServerInfo = null);
|
||||
|
||||
/// <summary>
|
||||
/// Owns connection lifecycle, authentication, SignalR event wiring, and channel tracking.
|
||||
@@ -67,6 +68,17 @@ internal sealed class ConnectionManager : IAsyncDisposable
|
||||
|
||||
try
|
||||
{
|
||||
onStatus("Fetching server info...");
|
||||
ServerStatusDto? serverInfo = null;
|
||||
try
|
||||
{
|
||||
serverInfo = await _apiClient.GetServerInfoAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Failed to fetch server info");
|
||||
}
|
||||
|
||||
onStatus("Authenticating...");
|
||||
|
||||
LoginResponse loginResponse;
|
||||
@@ -165,7 +177,7 @@ internal sealed class ConnectionManager : IAsyncDisposable
|
||||
}
|
||||
|
||||
onStatus("Connected");
|
||||
return new ConnectResult(loginResponse, channels, histories);
|
||||
return new ConnectResult(loginResponse, channels, histories, serverInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ public record ServerStatusDto(
|
||||
string? Description,
|
||||
int OnlineUsers,
|
||||
int TotalChannels,
|
||||
string RegistrationMode = "open");
|
||||
string RegistrationMode = "open",
|
||||
string Version = "0.0.0");
|
||||
|
||||
public record EncryptionKeyResponse(string Key);
|
||||
|
||||
@@ -138,34 +138,41 @@ public static class IrcMessageFormatter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Split a message into chunks of approximately maxBytes (UTF-8), at word boundaries.
|
||||
/// Split a message into chunks of approximately maxBytes (UTF-8), at line and word boundaries.
|
||||
/// </summary>
|
||||
public static List<string> SplitMessage(string content, int maxBytes)
|
||||
{
|
||||
if (Encoding.UTF8.GetByteCount(content) <= maxBytes)
|
||||
return [content];
|
||||
|
||||
var chunks = new List<string>();
|
||||
var current = new StringBuilder();
|
||||
var currentBytes = 0;
|
||||
|
||||
foreach (var word in content.Split(' '))
|
||||
foreach (var line in content.Split('\n'))
|
||||
{
|
||||
var wordBytes = Encoding.UTF8.GetByteCount(word) + 1; // +1 for space
|
||||
|
||||
if (currentBytes + wordBytes > maxBytes && current.Length > 0)
|
||||
if (Encoding.UTF8.GetByteCount(line) <= maxBytes)
|
||||
{
|
||||
chunks.Add(current.ToString().TrimEnd());
|
||||
current.Clear();
|
||||
currentBytes = 0;
|
||||
chunks.Add(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
current.Append(word).Append(' ');
|
||||
currentBytes += wordBytes;
|
||||
}
|
||||
var current = new StringBuilder();
|
||||
var currentBytes = 0;
|
||||
|
||||
if (current.Length > 0)
|
||||
chunks.Add(current.ToString().TrimEnd());
|
||||
foreach (var word in line.Split(' '))
|
||||
{
|
||||
var wordBytes = Encoding.UTF8.GetByteCount(word) + 1; // +1 for space
|
||||
|
||||
if (currentBytes + wordBytes > maxBytes && current.Length > 0)
|
||||
{
|
||||
chunks.Add(current.ToString().TrimEnd());
|
||||
current.Clear();
|
||||
currentBytes = 0;
|
||||
}
|
||||
|
||||
current.Append(word).Append(' ');
|
||||
currentBytes += wordBytes;
|
||||
}
|
||||
|
||||
if (current.Length > 0)
|
||||
chunks.Add(current.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using EchoHub.Core.DTOs;
|
||||
using EchoHub.Core.Models;
|
||||
@@ -38,12 +39,15 @@ public class ServerController : ControllerBase
|
||||
_ => "open",
|
||||
};
|
||||
|
||||
var version = typeof(ServerController).Assembly.GetName().Version?.ToString(3) ?? "0.0.0";
|
||||
|
||||
var status = new ServerStatusDto(
|
||||
_config["Server:Name"] ?? "EchoHub Server",
|
||||
_config["Server:Description"],
|
||||
userCount,
|
||||
channelCount,
|
||||
registrationMode);
|
||||
registrationMode,
|
||||
version);
|
||||
|
||||
return Ok(status);
|
||||
}
|
||||
|
||||
@@ -242,6 +242,36 @@ public class IrcMessageFormatterTests
|
||||
Assert.Equal("", chunks[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatMessage_UrlOnOwnLine_EmitsUrlAsItsOwnPrivmsg()
|
||||
{
|
||||
// Regression: multi-line content used to go out as ONE line with a raw \n
|
||||
// embedded. IRC clients drop everything after the newline as a malformed
|
||||
// frame, so a URL on its own line silently vanished while the embed lines
|
||||
// (separate, valid PRIVMSGs) still rendered.
|
||||
var embeds = new List<EmbedDto>
|
||||
{
|
||||
new("Example Site", "Page Title", "Description", null, "https://example.com")
|
||||
};
|
||||
var msg = CreateTextMessage("check this out\nhttps://example.com", embeds: embeds);
|
||||
var lines = IrcMessageFormatter.FormatMessage(msg);
|
||||
|
||||
Assert.Contains(lines, l => l.EndsWith("PRIVMSG #general :check this out"));
|
||||
Assert.Contains(lines, l => l.EndsWith("PRIVMSG #general :https://example.com"));
|
||||
Assert.All(lines, l => Assert.DoesNotContain('\n', l));
|
||||
Assert.All(lines, l => Assert.DoesNotContain('\r', l));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_MultiLineContent_SplitsOnNewlines()
|
||||
{
|
||||
var chunks = IrcMessageFormatter.SplitMessage("line one\nhttps://example.com/page", 400);
|
||||
|
||||
Assert.Equal(2, chunks.Count);
|
||||
Assert.Equal("line one", chunks[0]);
|
||||
Assert.Equal("https://example.com/page", chunks[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SplitMessage_UnicodeContent_CountsUtf8Bytes()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user