mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
@@ -22,6 +22,7 @@
|
|||||||
".dev/**",
|
".dev/**",
|
||||||
"docs/_site/**",
|
"docs/_site/**",
|
||||||
"docs/api/**",
|
"docs/api/**",
|
||||||
|
"docs/auriondocs/**",
|
||||||
"node_modules/**",
|
"node_modules/**",
|
||||||
"**/bin/**",
|
"**/bin/**",
|
||||||
"**/obj/**",
|
"**/obj/**",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Release history for EchoHub.
|
|||||||
|
|
||||||
## Releases
|
## 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.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.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
|
- [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
|
- name: Overview
|
||||||
href: index.md
|
href: index.md
|
||||||
|
- name: v0.2.17
|
||||||
|
href: v0.2.17.md
|
||||||
- name: v0.2.16
|
- name: v0.2.16
|
||||||
href: v0.2.16.md
|
href: v0.2.16.md
|
||||||
- name: v0.2.15
|
- 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>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>0.2.16</Version>
|
<Version>0.2.17</Version>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -1373,6 +1373,22 @@ public sealed class AppOrchestrator : IDisposable
|
|||||||
_mainWindow.FocusInput();
|
_mainWindow.FocusInput();
|
||||||
FetchAndUpdateOnlineUsers();
|
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);
|
SaveServerToConfig(dialogResult);
|
||||||
}, "Connection failed", "Connect");
|
}, "Connection failed", "Connect");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ namespace EchoHub.Client.Services;
|
|||||||
internal record ConnectResult(
|
internal record ConnectResult(
|
||||||
LoginResponse Login,
|
LoginResponse Login,
|
||||||
List<ChannelDto> Channels,
|
List<ChannelDto> Channels,
|
||||||
Dictionary<string, List<MessageDto>> Histories);
|
Dictionary<string, List<MessageDto>> Histories,
|
||||||
|
ServerStatusDto? ServerInfo = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Owns connection lifecycle, authentication, SignalR event wiring, and channel tracking.
|
/// Owns connection lifecycle, authentication, SignalR event wiring, and channel tracking.
|
||||||
@@ -67,6 +68,17 @@ internal sealed class ConnectionManager : IAsyncDisposable
|
|||||||
|
|
||||||
try
|
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...");
|
onStatus("Authenticating...");
|
||||||
|
|
||||||
LoginResponse loginResponse;
|
LoginResponse loginResponse;
|
||||||
@@ -165,7 +177,7 @@ internal sealed class ConnectionManager : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
onStatus("Connected");
|
onStatus("Connected");
|
||||||
return new ConnectResult(loginResponse, channels, histories);
|
return new ConnectResult(loginResponse, channels, histories, serverInfo);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public record ServerStatusDto(
|
|||||||
string? Description,
|
string? Description,
|
||||||
int OnlineUsers,
|
int OnlineUsers,
|
||||||
int TotalChannels,
|
int TotalChannels,
|
||||||
string RegistrationMode = "open");
|
string RegistrationMode = "open",
|
||||||
|
string Version = "0.0.0");
|
||||||
|
|
||||||
public record EncryptionKeyResponse(string Key);
|
public record EncryptionKeyResponse(string Key);
|
||||||
|
|||||||
@@ -138,18 +138,24 @@ public static class IrcMessageFormatter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public static List<string> SplitMessage(string content, int maxBytes)
|
public static List<string> SplitMessage(string content, int maxBytes)
|
||||||
{
|
{
|
||||||
if (Encoding.UTF8.GetByteCount(content) <= maxBytes)
|
|
||||||
return [content];
|
|
||||||
|
|
||||||
var chunks = new List<string>();
|
var chunks = new List<string>();
|
||||||
|
|
||||||
|
foreach (var line in content.Split('\n'))
|
||||||
|
{
|
||||||
|
if (Encoding.UTF8.GetByteCount(line) <= maxBytes)
|
||||||
|
{
|
||||||
|
chunks.Add(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var current = new StringBuilder();
|
var current = new StringBuilder();
|
||||||
var currentBytes = 0;
|
var currentBytes = 0;
|
||||||
|
|
||||||
foreach (var word in content.Split(' '))
|
foreach (var word in line.Split(' '))
|
||||||
{
|
{
|
||||||
var wordBytes = Encoding.UTF8.GetByteCount(word) + 1; // +1 for space
|
var wordBytes = Encoding.UTF8.GetByteCount(word) + 1; // +1 for space
|
||||||
|
|
||||||
@@ -166,6 +172,7 @@ public static class IrcMessageFormatter
|
|||||||
|
|
||||||
if (current.Length > 0)
|
if (current.Length > 0)
|
||||||
chunks.Add(current.ToString().TrimEnd());
|
chunks.Add(current.ToString().TrimEnd());
|
||||||
|
}
|
||||||
|
|
||||||
return chunks;
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Reflection;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using EchoHub.Core.DTOs;
|
using EchoHub.Core.DTOs;
|
||||||
using EchoHub.Core.Models;
|
using EchoHub.Core.Models;
|
||||||
@@ -38,12 +39,15 @@ public class ServerController : ControllerBase
|
|||||||
_ => "open",
|
_ => "open",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var version = typeof(ServerController).Assembly.GetName().Version?.ToString(3) ?? "0.0.0";
|
||||||
|
|
||||||
var status = new ServerStatusDto(
|
var status = new ServerStatusDto(
|
||||||
_config["Server:Name"] ?? "EchoHub Server",
|
_config["Server:Name"] ?? "EchoHub Server",
|
||||||
_config["Server:Description"],
|
_config["Server:Description"],
|
||||||
userCount,
|
userCount,
|
||||||
channelCount,
|
channelCount,
|
||||||
registrationMode);
|
registrationMode,
|
||||||
|
version);
|
||||||
|
|
||||||
return Ok(status);
|
return Ok(status);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,6 +242,36 @@ public class IrcMessageFormatterTests
|
|||||||
Assert.Equal("", chunks[0]);
|
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]
|
[Fact]
|
||||||
public void SplitMessage_UnicodeContent_CountsUtf8Bytes()
|
public void SplitMessage_UnicodeContent_CountsUtf8Bytes()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user