375 lines
14 KiB
C#
375 lines
14 KiB
C#
using LightlessSync.API.Dto.Group;
|
|
using LightlessSync.API.Dto.User;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.WebAPI;
|
|
using LightlessSync.WebAPI.SignalR;
|
|
using Microsoft.AspNetCore.SignalR;
|
|
using Microsoft.AspNetCore.SignalR.Client;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace LightlessSync.Services;
|
|
public class BroadcastService : IHostedService, IMediatorSubscriber
|
|
{
|
|
private readonly ILogger<BroadcastService> _logger;
|
|
private readonly ApiController _apiController;
|
|
private readonly LightlessMediator _mediator;
|
|
private readonly HubFactory _hubFactory;
|
|
private readonly LightlessConfigService _config;
|
|
private readonly DalamudUtilService _dalamudUtil;
|
|
public LightlessMediator Mediator => _mediator;
|
|
|
|
public bool IsLightFinderAvailable { get; private set; } = true;
|
|
|
|
public bool IsBroadcasting => _config.Current.BroadcastEnabled;
|
|
private bool _syncedOnStartup = false;
|
|
private bool _waitingForTtlFetch = false;
|
|
private TimeSpan? _remainingTtl = null;
|
|
private DateTime _lastTtlCheck = DateTime.MinValue;
|
|
private DateTime _lastForcedDisableTime = DateTime.MinValue;
|
|
private static readonly TimeSpan DisableCooldown = TimeSpan.FromSeconds(5);
|
|
public TimeSpan? RemainingTtl => _remainingTtl;
|
|
public TimeSpan? RemainingCooldown
|
|
{
|
|
get
|
|
{
|
|
var elapsed = DateTime.UtcNow - _lastForcedDisableTime;
|
|
if (elapsed >= DisableCooldown) return null;
|
|
return DisableCooldown - elapsed;
|
|
}
|
|
}
|
|
public BroadcastService(ILogger<BroadcastService> logger, LightlessMediator mediator, HubFactory hubFactory, LightlessConfigService config, DalamudUtilService dalamudUtil, ApiController apiController)
|
|
{
|
|
_logger = logger;
|
|
_mediator = mediator;
|
|
_hubFactory = hubFactory;
|
|
_config = config;
|
|
_dalamudUtil = dalamudUtil;
|
|
_apiController = apiController;
|
|
}
|
|
private async Task RequireConnectionAsync(string context, Func<Task> action)
|
|
{
|
|
if (!_apiController.IsConnected)
|
|
{
|
|
_logger.LogDebug($"{context} skipped, not connected");
|
|
return;
|
|
}
|
|
await action().ConfigureAwait(false);
|
|
}
|
|
public async Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_mediator.Subscribe<EnableBroadcastMessage>(this, OnEnableBroadcast);
|
|
_mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
|
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
|
|
|
|
_apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
|
_ = CheckLightfinderSupportAsync(cancellationToken);
|
|
}
|
|
|
|
public Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
_mediator.UnsubscribeAll(this);
|
|
_apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// need to rework this, this is cooked
|
|
private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
while (!_apiController.IsConnected && !cancellationToken.IsCancellationRequested)
|
|
await Task.Delay(250, cancellationToken).ConfigureAwait(false);
|
|
|
|
if (cancellationToken.IsCancellationRequested)
|
|
return;
|
|
|
|
var hub = _hubFactory.GetOrCreate(CancellationToken.None);
|
|
var dummy = "0".PadLeft(64, '0');
|
|
|
|
await hub.InvokeAsync<BroadcastStatusInfoDto?>("IsUserBroadcasting", dummy, cancellationToken);
|
|
await hub.InvokeAsync("SetBroadcastStatus", dummy, true, null, cancellationToken);
|
|
await hub.InvokeAsync<TimeSpan?>("GetBroadcastTtl", dummy, cancellationToken);
|
|
await hub.InvokeAsync<Dictionary<string, BroadcastStatusInfoDto?>>("AreUsersBroadcasting", new[] { dummy }, cancellationToken);
|
|
|
|
IsLightFinderAvailable = true;
|
|
_logger.LogInformation("Lightfinder is available.");
|
|
}
|
|
catch (HubException ex) when (ex.Message.Contains("Method does not exist"))
|
|
{
|
|
_logger.LogWarning("Lightfinder unavailable: required method missing.");
|
|
IsLightFinderAvailable = false;
|
|
|
|
_config.Current.BroadcastEnabled = false;
|
|
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
_config.Save();
|
|
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("Lightfinder check was canceled.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Lightfinder check failed.");
|
|
IsLightFinderAvailable = false;
|
|
|
|
_config.Current.BroadcastEnabled = false;
|
|
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
_config.Save();
|
|
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
}
|
|
}
|
|
|
|
private void OnEnableBroadcast(EnableBroadcastMessage msg)
|
|
{
|
|
_ = RequireConnectionAsync(nameof(OnEnableBroadcast), async () =>
|
|
{
|
|
try
|
|
{
|
|
GroupBroadcastRequestDto? groupDto = null;
|
|
if (_config.Current.SyncshellFinderEnabled && _config.Current.SelectedFinderSyncshell != null)
|
|
{
|
|
groupDto = new GroupBroadcastRequestDto
|
|
{
|
|
HashedCID = msg.HashedCid,
|
|
GID = _config.Current.SelectedFinderSyncshell,
|
|
Enabled = msg.Enabled,
|
|
};
|
|
}
|
|
|
|
_ = _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false);
|
|
|
|
_logger.LogInformation("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid);
|
|
|
|
if (!msg.Enabled)
|
|
{
|
|
_config.Current.BroadcastEnabled = false;
|
|
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
_config.Save();
|
|
|
|
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
return;
|
|
}
|
|
|
|
_waitingForTtlFetch = true;
|
|
|
|
var ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false);
|
|
|
|
if (ttl is { } remaining && remaining > TimeSpan.Zero)
|
|
{
|
|
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
|
_config.Current.BroadcastEnabled = true;
|
|
_config.Save();
|
|
|
|
_logger.LogInformation("Fetched TTL from server: {TTL}", remaining);
|
|
_mediator.Publish(new BroadcastStatusChangedMessage(true, remaining));
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling.");
|
|
_config.Current.BroadcastEnabled = false;
|
|
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
_config.Save();
|
|
|
|
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
}
|
|
|
|
_waitingForTtlFetch = false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to toggle broadcast for {Cid}", msg.HashedCid);
|
|
_waitingForTtlFetch = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
|
{
|
|
_config.Current.BroadcastEnabled = msg.Enabled;
|
|
_config.Save();
|
|
}
|
|
|
|
public async Task<bool> CheckIfBroadcastingAsync(string targetCid)
|
|
{
|
|
bool result = false;
|
|
await RequireConnectionAsync(nameof(CheckIfBroadcastingAsync), async () =>
|
|
{
|
|
try
|
|
{
|
|
_logger.LogInformation("[BroadcastCheck] Checking CID: {cid}", targetCid);
|
|
|
|
var info = await _apiController.IsUserBroadcasting(targetCid).ConfigureAwait(false);
|
|
result = info?.TTL > TimeSpan.Zero;
|
|
|
|
|
|
_logger.LogInformation("[BroadcastCheck] Result for {cid}: {result} (TTL: {ttl}, GID: {gid})", targetCid, result, info?.TTL, info?.GID);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to check broadcast status for {cid}", targetCid);
|
|
}
|
|
}).ConfigureAwait(false);
|
|
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<TimeSpan?> GetBroadcastTtlAsync(string cid)
|
|
{
|
|
TimeSpan? ttl = null;
|
|
await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => {
|
|
try
|
|
{
|
|
ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid);
|
|
}
|
|
}).ConfigureAwait(false);
|
|
return ttl;
|
|
}
|
|
|
|
public async Task<Dictionary<string, BroadcastStatusInfoDto?>> AreUsersBroadcastingAsync(List<string> hashedCids)
|
|
{
|
|
Dictionary<string, BroadcastStatusInfoDto?> result = new();
|
|
|
|
await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () =>
|
|
{
|
|
try
|
|
{
|
|
var batch = await _apiController.AreUsersBroadcasting(hashedCids).ConfigureAwait(false);
|
|
|
|
if (batch?.Results != null)
|
|
{
|
|
foreach (var kv in batch.Results)
|
|
result[kv.Key] = kv.Value;
|
|
}
|
|
|
|
_logger.LogInformation("Batch broadcast status check complete for {Count} CIDs", hashedCids.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to batch check broadcast status");
|
|
}
|
|
}).ConfigureAwait(false);
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
public async void ToggleBroadcast()
|
|
{
|
|
if (!IsLightFinderAvailable)
|
|
{
|
|
_logger.LogWarning("ToggleBroadcast - Lightfinder is not available.");
|
|
return;
|
|
}
|
|
|
|
await RequireConnectionAsync(nameof(ToggleBroadcast), async () =>
|
|
{
|
|
var cooldown = RemainingCooldown;
|
|
if (!_config.Current.BroadcastEnabled && cooldown is { } cd && cd > TimeSpan.Zero)
|
|
{
|
|
_logger.LogWarning("Cooldown active. Must wait {Remaining}s before re-enabling.", cd.TotalSeconds);
|
|
return;
|
|
}
|
|
|
|
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
|
|
|
try
|
|
{
|
|
var isCurrentlyBroadcasting = await CheckIfBroadcastingAsync(hashedCid).ConfigureAwait(false);
|
|
var newStatus = !isCurrentlyBroadcasting;
|
|
|
|
if (!newStatus)
|
|
{
|
|
_lastForcedDisableTime = DateTime.UtcNow;
|
|
_logger.LogInformation("Manual disable: cooldown timer started.");
|
|
}
|
|
|
|
_logger.LogInformation("Toggling broadcast. Server currently broadcasting: {ServerStatus}, setting to: {NewStatus}", isCurrentlyBroadcasting, newStatus);
|
|
|
|
_mediator.Publish(new EnableBroadcastMessage(hashedCid, newStatus));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to determine current broadcast status for toggle");
|
|
}
|
|
}).ConfigureAwait(false);
|
|
}
|
|
|
|
private async void OnTick(PriorityFrameworkUpdateMessage _)
|
|
{
|
|
if (!IsLightFinderAvailable)
|
|
return;
|
|
|
|
if (_config?.Current == null)
|
|
return;
|
|
|
|
if ((DateTime.UtcNow - _lastTtlCheck).TotalSeconds < 1)
|
|
return;
|
|
|
|
_lastTtlCheck = DateTime.UtcNow;
|
|
|
|
await RequireConnectionAsync(nameof(OnTick), async () => {
|
|
if (!_syncedOnStartup && _config.Current.BroadcastEnabled)
|
|
{
|
|
_syncedOnStartup = true;
|
|
try
|
|
{
|
|
var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
|
var ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false);
|
|
if (ttl is { }
|
|
remaining && remaining > TimeSpan.Zero)
|
|
{
|
|
_config.Current.BroadcastTtl = DateTime.UtcNow + remaining;
|
|
_config.Current.BroadcastEnabled = true;
|
|
_config.Save();
|
|
_logger.LogInformation("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state.");
|
|
_config.Current.BroadcastEnabled = false;
|
|
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
_config.Save();
|
|
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to refresh TTL in OnTick");
|
|
}
|
|
}
|
|
if (_config.Current.BroadcastEnabled)
|
|
{
|
|
if (_waitingForTtlFetch)
|
|
{
|
|
_logger.LogDebug("OnTick skipped: waiting for TTL fetch");
|
|
return;
|
|
}
|
|
var expiry = _config.Current.BroadcastTtl;
|
|
var remaining = expiry - DateTime.UtcNow;
|
|
_remainingTtl = remaining > TimeSpan.Zero ? remaining : null;
|
|
if (_remainingTtl == null)
|
|
{
|
|
_logger.LogInformation("Broadcast TTL expired. Disabling broadcast locally.");
|
|
_config.Current.BroadcastEnabled = false;
|
|
_config.Current.BroadcastTtl = DateTime.MinValue;
|
|
_config.Save();
|
|
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_remainingTtl = null;
|
|
}
|
|
}).ConfigureAwait(false);
|
|
}
|
|
} |