feat: Implement server directory registration and user count tracking

This commit is contained in:
HueByte
2026-02-19 06:05:31 +01:00
parent b45b4c3139
commit a273ff6bea
5 changed files with 150 additions and 1 deletions
+1
View File
@@ -12,6 +12,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.3" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup> </ItemGroup>
+1
View File
@@ -100,6 +100,7 @@ while (true)
builder.Services.AddSingleton<PresenceTracker>(); builder.Services.AddSingleton<PresenceTracker>();
builder.Services.AddSingleton<ImageToAsciiService>(); builder.Services.AddSingleton<ImageToAsciiService>();
builder.Services.AddSingleton<FileStorageService>(); builder.Services.AddSingleton<FileStorageService>();
builder.Services.AddHostedService<ServerDirectoryService>();
builder.Services.AddHttpClient("ImageDownload", client => builder.Services.AddHttpClient("ImageDownload", client =>
{ {
client.Timeout = TimeSpan.FromSeconds(15); client.Timeout = TimeSpan.FromSeconds(15);
@@ -110,4 +110,9 @@ public class PresenceTracker
{ {
return _userConnections.TryGetValue(username, out var connections) && connections.Count > 0; return _userConnections.TryGetValue(username, out var connections) && connections.Count > 0;
} }
public int GetOnlineUserCount()
{
return _userConnections.Count;
}
} }
@@ -0,0 +1,139 @@
using Microsoft.AspNetCore.SignalR.Client;
namespace EchoHub.Server.Services;
public sealed class ServerDirectoryService(
IConfiguration configuration,
PresenceTracker presenceTracker,
ILogger<ServerDirectoryService> logger) : BackgroundService
{
private const string DirectoryHubUrl = "https://echohub.voidcube.cloud/hubs/servers";
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(30);
private HubConnection? _connection;
private int _lastReportedUserCount = -1;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var isPublic = configuration.GetValue<bool>("Server:PublicServer");
if (!isPublic)
{
logger.LogInformation("PublicServer is disabled — not registering with directory");
return;
}
var host = configuration["Server:PublicHost"];
var port = configuration.GetValue<int>("Server:PublicPort");
if (string.IsNullOrWhiteSpace(host))
{
logger.LogWarning("PublicServer is enabled but Server:PublicHost is not set — skipping directory registration");
return;
}
if (port <= 0)
{
logger.LogWarning("PublicServer is enabled but Server:PublicPort is invalid — skipping directory registration");
return;
}
var serverName = configuration["Server:Name"] ?? "EchoHub Server";
var description = configuration["Server:Description"];
_connection = new HubConnectionBuilder()
.WithUrl(DirectoryHubUrl)
.WithAutomaticReconnect()
.Build();
_connection.Reconnected += async _ =>
{
logger.LogInformation("Reconnected to directory — re-registering server");
await RegisterAsync(serverName, description, host, port);
};
_connection.Closed += ex =>
{
if (ex is not null)
logger.LogWarning(ex, "Directory connection closed with error");
return Task.CompletedTask;
};
// Initial connection with retry
while (!stoppingToken.IsCancellationRequested)
{
try
{
await _connection.StartAsync(stoppingToken);
logger.LogInformation("Connected to directory at {Url}", DirectoryHubUrl);
break;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to connect to directory — retrying in 30s");
await Task.Delay(UpdateInterval, stoppingToken);
}
}
if (stoppingToken.IsCancellationRequested)
return;
// Register on first connect
await RegisterAsync(serverName, description, host, port);
// Poll user count and send updates
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(UpdateInterval, stoppingToken);
if (_connection.State != HubConnectionState.Connected)
continue;
var currentCount = presenceTracker.GetOnlineUserCount();
if (currentCount == _lastReportedUserCount)
continue;
try
{
await _connection.InvokeAsync("UpdateUserCount", currentCount, stoppingToken);
_lastReportedUserCount = currentCount;
logger.LogDebug("Updated directory user count to {Count}", currentCount);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to update user count on directory");
}
}
}
private async Task RegisterAsync(string name, string? description, string host, int port)
{
if (_connection?.State != HubConnectionState.Connected)
return;
try
{
var userCount = presenceTracker.GetOnlineUserCount();
var dto = new RegisterServerDto(name, description, host, port, userCount);
await _connection.InvokeAsync("RegisterServer", dto);
_lastReportedUserCount = userCount;
logger.LogInformation("Registered with directory as {Name} at {Host}:{Port}", name, host, port);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to register with directory");
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
if (_connection is not null)
{
await _connection.DisposeAsync();
_connection = null;
}
await base.StopAsync(cancellationToken);
}
}
internal record RegisterServerDto(string Name, string? Description, string Host, int Port, int UserCount);
+4 -1
View File
@@ -10,7 +10,10 @@
}, },
"Server": { "Server": {
"Name": "My EchoHub Server", "Name": "My EchoHub Server",
"Description": "A self-hosted EchoHub chat server" "Description": "A self-hosted EchoHub chat server",
"PublicServer": false,
"PublicHost": "",
"PublicPort": 5000
}, },
"Serilog": { "Serilog": {
"MinimumLevel": { "MinimumLevel": {