mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: add /meta command for channel metadata retrieval
- Implemented the `/meta` command to fetch and display channel metadata including room ID, topic, message count, unique user count, estimated size, and protection level. - Added `ChannelMetaDto` to encapsulate channel metadata. - Updated `ChannelsController` to handle the new `/meta` endpoint. - Introduced `UploadLimits` configuration for admin-defined upload size limits for files, images, audio, and avatars. - Enhanced error handling and user feedback for metadata retrieval. - Updated documentation to reflect changes in encryption and room metadata. - Added tests for the new functionality and upload limits.
This commit is contained in:
@@ -127,6 +127,7 @@ public sealed class AppOrchestrator : IDisposable
|
||||
_commandHandler.OnLeaveChannel += HandleCmdLeaveChannel;
|
||||
_commandHandler.OnSetTopic += HandleCmdSetTopic;
|
||||
_commandHandler.OnListUsers += HandleCmdListUsers;
|
||||
_commandHandler.OnRoomInfo += HandleCmdMeta;
|
||||
_commandHandler.OnKickUser += HandleCmdKickUser;
|
||||
_commandHandler.OnBanUser += HandleCmdBanUser;
|
||||
_commandHandler.OnUnbanUser += HandleCmdUnbanUser;
|
||||
@@ -611,6 +612,46 @@ public sealed class AppOrchestrator : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCmdMeta()
|
||||
{
|
||||
if (!_conn.IsConnected || _conn.Api is null) return;
|
||||
|
||||
var channel = _mainWindow.CurrentChannel;
|
||||
if (string.IsNullOrEmpty(channel)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var meta = await _conn.Api.GetChannelMetaAsync(channel);
|
||||
if (meta is null)
|
||||
{
|
||||
InvokeUI(() => _mainWindow.ShowError($"Channel #{channel} not found."));
|
||||
return;
|
||||
}
|
||||
|
||||
var size = meta.EstimatedSizeBytes <= 0 ? "0 B" : ChatMessageManager.FormatFileSize(meta.EstimatedSizeBytes);
|
||||
var protection = meta.IsEncrypted ? "end-to-end encrypted"
|
||||
: meta.IsProtected ? "password-protected"
|
||||
: "open";
|
||||
|
||||
InvokeUI(() =>
|
||||
{
|
||||
_messageManager.AddSystemMessage(channel, $"Room info for #{meta.Name}:");
|
||||
if (!string.IsNullOrWhiteSpace(meta.Topic))
|
||||
_messageManager.AddSystemMessage(channel, $" Topic {meta.Topic}");
|
||||
_messageManager.AddSystemMessage(channel, $" Room ID {meta.Id}");
|
||||
_messageManager.AddSystemMessage(channel, $" Created {meta.CreatedAt.ToLocalTime():g}");
|
||||
_messageManager.AddSystemMessage(channel, $" Messages {meta.MessageCount}");
|
||||
_messageManager.AddSystemMessage(channel, $" Unique users {meta.UniqueUserCount}");
|
||||
_messageManager.AddSystemMessage(channel, $" Est. size {size}");
|
||||
_messageManager.AddSystemMessage(channel, $" Protection {protection}");
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
InvokeUI(() => _mainWindow.ShowError($"Failed to fetch room info: {ex.Message}"));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCmdKickUser(string username, string? reason)
|
||||
{
|
||||
if (!_conn.IsAuthenticated) return;
|
||||
|
||||
@@ -21,6 +21,7 @@ public class CommandHandler
|
||||
public event Func<Task>? OnLeaveChannel;
|
||||
public event Func<string, Task>? OnSetTopic;
|
||||
public event Func<Task>? OnListUsers;
|
||||
public event Func<Task>? OnRoomInfo;
|
||||
public event Func<string, Task>? OnSetAvatar;
|
||||
public event Func<string, string?, Task>? OnKickUser;
|
||||
public event Func<string, string?, Task>? OnBanUser;
|
||||
@@ -62,6 +63,7 @@ public class CommandHandler
|
||||
"leave" => await HandleLeave(),
|
||||
"topic" => await HandleTopic(args),
|
||||
"users" => await HandleUsers(),
|
||||
"meta" or "info" => await HandleMeta(),
|
||||
"kick" => await HandleKick(args),
|
||||
"ban" => await HandleBan(args),
|
||||
"unban" => await HandleUnban(args),
|
||||
@@ -273,6 +275,13 @@ public class CommandHandler
|
||||
return new CommandResult(true);
|
||||
}
|
||||
|
||||
private async Task<CommandResult> HandleMeta()
|
||||
{
|
||||
if (OnRoomInfo is not null)
|
||||
await OnRoomInfo();
|
||||
return new CommandResult(true);
|
||||
}
|
||||
|
||||
private async Task<CommandResult> HandleQuit()
|
||||
{
|
||||
if (OnQuit is not null)
|
||||
@@ -403,6 +412,7 @@ public class CommandHandler
|
||||
/leave - Leave current channel
|
||||
/topic <text> - Set channel topic
|
||||
/users - List online users
|
||||
/meta - Show room info (size, messages, users, created, id)
|
||||
Moderation:
|
||||
/kick <user> [reason] - Kick a user (Mod+)
|
||||
/ban <user> [reason] - Ban a user (Admin+)
|
||||
|
||||
@@ -271,6 +271,20 @@ public sealed class ApiClient : IDisposable
|
||||
return await response.Content.ReadFromJsonAsync<ChannelCryptoDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a channel's human-facing metadata (message count, unique posters, estimated
|
||||
/// size, created date, room id) for the <c>/meta</c> command. Returns null if it doesn't exist.
|
||||
/// </summary>
|
||||
public async Task<ChannelMetaDto?> GetChannelMetaAsync(string channelName)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
using var response = await AuthenticatedGetAsync($"/api/channels/{Uri.EscapeDataString(channelName)}/meta");
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
return null;
|
||||
await EnsureSuccessAsync(response);
|
||||
return await response.Content.ReadFromJsonAsync<ChannelMetaDto>();
|
||||
}
|
||||
|
||||
public async Task<ChannelDto?> RekeyChannelAsync(string channelName, RekeyChannelRequest request)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text;
|
||||
|
||||
using AlwaysUpToDate;
|
||||
|
||||
using EchoHub.Client.UI.Dialogs;
|
||||
@@ -88,6 +90,10 @@ public sealed class UpdateChecker : IDisposable
|
||||
private async Task ApplyUpdateAsync()
|
||||
{
|
||||
_applying = true;
|
||||
|
||||
// The TUI restored the console on shutdown; make sure the block-glyph bar renders.
|
||||
try { Console.OutputEncoding = Encoding.UTF8; } catch { /* redirected/non-interactive */ }
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"Updating EchoHub to v{_pendingVersion}...");
|
||||
|
||||
@@ -106,23 +112,49 @@ public sealed class UpdateChecker : IDisposable
|
||||
await _updater.UpdateAsync(); // download → extract → restart → Environment.Exit(0)
|
||||
}
|
||||
|
||||
private const int BarWidth = 28;
|
||||
|
||||
private void OnProgressChanged(UpdateStep step, long itemsProcessed, long? totalItems, double? progressPercentage)
|
||||
{
|
||||
// Before the TUI is torn down (i.e. during a check) there is no progress surface; the
|
||||
// real work happens headless after shutdown, so report it on the console.
|
||||
// real work happens headless after shutdown, so draw a progress bar on the console.
|
||||
if (!_applying)
|
||||
return;
|
||||
|
||||
// Finish the previous step's line so each step keeps its completed bar.
|
||||
if (step != _lastStep)
|
||||
{
|
||||
Console.WriteLine();
|
||||
if (_lastStep != (UpdateStep)(-1))
|
||||
Console.WriteLine();
|
||||
_lastStep = step;
|
||||
}
|
||||
|
||||
var pct = progressPercentage ?? 0;
|
||||
Console.Write($"\r {step}: {itemsProcessed}/{totalItems ?? 0} ({pct:F0}%) ");
|
||||
var label = Humanize(step);
|
||||
|
||||
if (progressPercentage is { } percent)
|
||||
{
|
||||
var pct = (int)Math.Clamp(Math.Round(percent), 0, 100);
|
||||
var filled = pct * BarWidth / 100;
|
||||
var bar = new string('█', filled) + new string('░', BarWidth - filled); // █ / ░
|
||||
Console.Write($"\r {label,-13} [{bar}] {pct,3}% ");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Steps with no measurable total (verifying, restarting): show an indeterminate marker.
|
||||
Console.Write($"\r {label,-13} working... ");
|
||||
}
|
||||
}
|
||||
|
||||
private static string Humanize(UpdateStep step) => step switch
|
||||
{
|
||||
UpdateStep.Downloading => "Downloading",
|
||||
UpdateStep.VerifyingChecksum => "Verifying",
|
||||
UpdateStep.Extracting => "Extracting",
|
||||
UpdateStep.CleaningUp => "Cleaning up",
|
||||
UpdateStep.Restarting => "Restarting",
|
||||
_ => step.ToString(),
|
||||
};
|
||||
|
||||
private void OnUpdateStarted(string version)
|
||||
{
|
||||
Log.Information("Update started: v{Version}", version);
|
||||
|
||||
@@ -451,10 +451,17 @@ public sealed class ChatMessageManager
|
||||
|
||||
private static string FormatDateTime(DateTimeOffset timestamp)
|
||||
{
|
||||
if (timestamp.Date == DateTimeOffset.Now.Date)
|
||||
return timestamp.ToLocalTime().ToString("t");
|
||||
else
|
||||
return timestamp.ToLocalTime().ToString("g");
|
||||
// Server timestamps arrive in UTC; convert to local before deciding the calendar day,
|
||||
// otherwise a "today" message near midnight is misclassified against the local date.
|
||||
var local = timestamp.ToLocalTime();
|
||||
|
||||
// Today's messages show a compact date + short time; older messages fall back to the
|
||||
// culture's general short date/time. Both always include the date so new messages
|
||||
// are never left date-less.
|
||||
if (local.Date == DateTimeOffset.Now.Date)
|
||||
return $"{local:d} {local:t}";
|
||||
|
||||
return local.ToString("g");
|
||||
}
|
||||
|
||||
internal static string FormatFileSize(long? bytes)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
using Terminal.Gui.App;
|
||||
using Terminal.Gui.Views;
|
||||
using Terminal.Gui.ViewBase;
|
||||
|
||||
namespace EchoHub.Client.UI.Dialogs;
|
||||
|
||||
public sealed class UpdateProgressDialog
|
||||
{
|
||||
private readonly Dialog _dialog;
|
||||
private readonly ProgressBar _progressBar;
|
||||
private readonly Label _infoLabel;
|
||||
private readonly IApplication _app;
|
||||
|
||||
public UpdateProgressDialog(IApplication app, string newVersion)
|
||||
{
|
||||
_app = app;
|
||||
|
||||
_dialog = new Dialog { Title = $"Updating to {newVersion}", Width = 50, Height = 10 };
|
||||
|
||||
_infoLabel = new Label
|
||||
{
|
||||
Text = "Preparing update...",
|
||||
X = 1,
|
||||
Y = 1,
|
||||
Width = Dim.Fill(2)
|
||||
};
|
||||
|
||||
_progressBar = new ProgressBar
|
||||
{
|
||||
X = 1,
|
||||
Y = 3,
|
||||
Width = Dim.Fill(2),
|
||||
Fraction = 0f
|
||||
};
|
||||
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "Cancel",
|
||||
X = Pos.Center(),
|
||||
Y = 6
|
||||
};
|
||||
|
||||
_dialog.Add(_infoLabel, _progressBar);
|
||||
}
|
||||
|
||||
public void UpdateProgress(float fraction, string statusText)
|
||||
{
|
||||
_progressBar.Fraction = fraction;
|
||||
_infoLabel.Text = statusText;
|
||||
}
|
||||
|
||||
public void Show()
|
||||
{
|
||||
_app.Run(_dialog);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_app.RequestStop();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user