mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 08:36:11 +02:00
feat: Implement server directory registration and user count tracking
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user