init 2
This commit is contained in:
@@ -7,6 +7,7 @@ using LightlessSync.FileCache;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using LightlessSync.WebAPI.Files.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
@@ -28,16 +29,23 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
private readonly FileTransferOrchestrator _orchestrator;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
||||
private static readonly TimeSpan DownloadStallTimeout = TimeSpan.FromSeconds(30);
|
||||
private volatile bool _disableDirectDownloads;
|
||||
private int _consecutiveDirectDownloadFailures;
|
||||
private bool _lastConfigDirectDownloadsState;
|
||||
|
||||
public FileDownloadManager(ILogger<FileDownloadManager> logger, LightlessMediator mediator,
|
||||
public FileDownloadManager(
|
||||
ILogger<FileDownloadManager> logger,
|
||||
LightlessMediator mediator,
|
||||
FileTransferOrchestrator orchestrator,
|
||||
FileCacheManager fileCacheManager, FileCompactor fileCompactor,
|
||||
PairProcessingLimiter pairProcessingLimiter, LightlessConfigService configService) : base(logger, mediator)
|
||||
FileCacheManager fileCacheManager,
|
||||
FileCompactor fileCompactor,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
LightlessConfigService configService,
|
||||
TextureDownscaleService textureDownscaleService, TextureMetadataHelper textureMetadataHelper) : base(logger, mediator)
|
||||
{
|
||||
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
||||
_orchestrator = orchestrator;
|
||||
@@ -45,6 +53,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
_fileCompactor = fileCompactor;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_configService = configService;
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
_textureMetadataHelper = textureMetadataHelper;
|
||||
_activeDownloadStreams = new();
|
||||
_lastConfigDirectDownloadsState = _configService.Current.EnableDirectDownloads;
|
||||
|
||||
@@ -63,6 +73,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
public List<DownloadFileTransfer> CurrentDownloads { get; private set; } = [];
|
||||
|
||||
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
|
||||
public Guid? CurrentOwnerToken { get; private set; }
|
||||
|
||||
public bool IsDownloading => CurrentDownloads.Any();
|
||||
|
||||
@@ -83,14 +94,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
CurrentDownloads.Clear();
|
||||
_downloadStatus.Clear();
|
||||
CurrentOwnerToken = null;
|
||||
}
|
||||
|
||||
public async Task DownloadFiles(GameObjectHandler gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct)
|
||||
public async Task DownloadFiles(GameObjectHandler? gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct, bool skipDownscale = false)
|
||||
{
|
||||
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
|
||||
try
|
||||
{
|
||||
await DownloadFilesInternal(gameObject, fileReplacementDto, ct).ConfigureAwait(false);
|
||||
await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -98,7 +110,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
finally
|
||||
{
|
||||
Mediator.Publish(new DownloadFinishedMessage(gameObject));
|
||||
if (gameObject is not null)
|
||||
{
|
||||
Mediator.Publish(new DownloadFinishedMessage(gameObject));
|
||||
}
|
||||
Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles)));
|
||||
}
|
||||
}
|
||||
@@ -272,7 +287,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
int bytesRead;
|
||||
try
|
||||
{
|
||||
var readTask = stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct).AsTask();
|
||||
using var readCancellation = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
var readTask = stream.ReadAsync(buffer.AsMemory(0, buffer.Length), readCancellation.Token).AsTask();
|
||||
while (!readTask.IsCompleted)
|
||||
{
|
||||
var completedTask = await Task.WhenAny(readTask, Task.Delay(DownloadStallTimeout)).ConfigureAwait(false);
|
||||
@@ -286,6 +302,20 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
var snapshot = _pairProcessingLimiter.GetSnapshot();
|
||||
if (snapshot.Waiting > 0)
|
||||
{
|
||||
readCancellation.Cancel();
|
||||
try
|
||||
{
|
||||
await readTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// expected when cancelling the read due to timeout
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Error finishing read task after stall detection for {requestUrl}", requestUrl);
|
||||
}
|
||||
|
||||
throw new TimeoutException($"No data received for {DownloadStallTimeout.TotalSeconds} seconds while downloading {requestUrl} (waiting: {snapshot.Waiting})");
|
||||
}
|
||||
|
||||
@@ -352,7 +382,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DecompressBlockFileAsync(string downloadStatusKey, string blockFilePath, List<FileReplacementData> fileReplacement, string downloadLabel)
|
||||
private async Task DecompressBlockFileAsync(string downloadStatusKey, string blockFilePath, List<FileReplacementData> fileReplacement, string downloadLabel, bool skipDownscale)
|
||||
{
|
||||
if (_downloadStatus.TryGetValue(downloadStatusKey, out var status))
|
||||
{
|
||||
@@ -385,7 +415,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
var decompressedFile = LZ4Wrapper.Unwrap(compressedFileContent);
|
||||
await _fileCompactor.WriteAllBytesAsync(filePath, decompressedFile, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
PersistFileToStorage(fileHash, filePath);
|
||||
var gamePath = fileReplacement.FirstOrDefault(f => string.Equals(f.Hash, fileHash, StringComparison.OrdinalIgnoreCase))?.GamePaths.FirstOrDefault() ?? string.Empty;
|
||||
PersistFileToStorage(fileHash, filePath, gamePath, skipDownscale);
|
||||
}
|
||||
catch (EndOfStreamException)
|
||||
{
|
||||
@@ -413,7 +444,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
private async Task PerformDirectDownloadFallbackAsync(DownloadFileTransfer directDownload, List<FileReplacementData> fileReplacement,
|
||||
IProgress<long> progress, CancellationToken token, bool slotAlreadyAcquired)
|
||||
IProgress<long> progress, CancellationToken token, bool skipDownscale, bool slotAlreadyAcquired)
|
||||
{
|
||||
if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
|
||||
{
|
||||
@@ -455,7 +486,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile);
|
||||
}
|
||||
|
||||
await DecompressBlockFileAsync(downloadKey, blockFile, fileReplacement, $"fallback-{directDownload.Hash}").ConfigureAwait(false);
|
||||
await DecompressBlockFileAsync(downloadKey, blockFile, fileReplacement, $"fallback-{directDownload.Hash}", skipDownscale).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -478,8 +509,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
||||
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(GameObjectHandler? gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct, Guid? ownerToken = null)
|
||||
{
|
||||
CurrentOwnerToken = ownerToken;
|
||||
var objectName = gameObjectHandler?.Name ?? "Unknown";
|
||||
Logger.LogDebug("Download start: {id}", objectName);
|
||||
|
||||
@@ -520,7 +552,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
return CurrentDownloads;
|
||||
}
|
||||
|
||||
private async Task DownloadFilesInternal(GameObjectHandler gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct)
|
||||
private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct, bool skipDownscale)
|
||||
{
|
||||
var objectName = gameObjectHandler?.Name ?? "Unknown";
|
||||
|
||||
@@ -583,7 +615,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
Logger.LogWarning("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length);
|
||||
}
|
||||
|
||||
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
|
||||
if (gameObjectHandler is not null)
|
||||
{
|
||||
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
|
||||
}
|
||||
|
||||
Task batchDownloadsTask = downloadBatches.Length == 0 ? Task.CompletedTask : Parallel.ForEachAsync(downloadBatches, new ParallelOptions()
|
||||
{
|
||||
@@ -651,7 +686,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
await DecompressBlockFileAsync(fileGroup.Key, blockFile, fileReplacement, fi.Name).ConfigureAwait(false);
|
||||
await DecompressBlockFileAsync(fileGroup.Key, blockFile, fileReplacement, fi.Name, skipDownscale).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -690,14 +725,13 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
if (!ShouldUseDirectDownloads())
|
||||
{
|
||||
await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, slotAlreadyAcquired: false).ConfigureAwait(false);
|
||||
await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, skipDownscale, slotAlreadyAcquired: false).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var tempFilename = _fileDbManager.GetCacheFilePath(directDownload.Hash, "bin");
|
||||
var slotAcquired = false;
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
downloadTracker.DownloadStatus = DownloadStatus.WaitingForSlot;
|
||||
@@ -727,7 +761,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename).ConfigureAwait(false);
|
||||
var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes);
|
||||
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, CancellationToken.None).ConfigureAwait(false);
|
||||
PersistFileToStorage(directDownload.Hash, finalFilename);
|
||||
PersistFileToStorage(directDownload.Hash, finalFilename, replacement.GamePaths[0], skipDownscale);
|
||||
|
||||
downloadTracker.TransferredFiles = 1;
|
||||
Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash);
|
||||
@@ -739,8 +773,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Logger.LogDebug("{hash}: Detected cancellation of direct download, discarding file.", directDownload.Hash);
|
||||
Logger.LogError(ex, "{hash}: Error during direct download.", directDownload.Hash);
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
Logger.LogDebug("{hash}: Direct download cancelled by caller, discarding file.", directDownload.Hash);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning(ex, "{hash}: Direct download cancelled unexpectedly.", directDownload.Hash);
|
||||
}
|
||||
|
||||
ClearDownload();
|
||||
return;
|
||||
}
|
||||
@@ -762,7 +803,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
try
|
||||
{
|
||||
downloadTracker.DownloadStatus = DownloadStatus.WaitingForQueue;
|
||||
await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, slotAcquired).ConfigureAwait(false);
|
||||
await PerformDirectDownloadFallbackAsync(directDownload, fileReplacement, progress, token, skipDownscale, slotAcquired).ConfigureAwait(false);
|
||||
|
||||
if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads)
|
||||
{
|
||||
@@ -815,7 +856,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? [];
|
||||
}
|
||||
|
||||
private void PersistFileToStorage(string fileHash, string filePath)
|
||||
private void PersistFileToStorage(string fileHash, string filePath, string gamePath, bool skipDownscale)
|
||||
{
|
||||
var fi = new FileInfo(filePath);
|
||||
Func<DateTime> RandomDayInThePast()
|
||||
@@ -832,6 +873,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
try
|
||||
{
|
||||
var entry = _fileDbManager.CreateCacheEntry(filePath);
|
||||
var mapKind = _textureMetadataHelper.DetermineMapKind(gamePath, filePath);
|
||||
if (!skipDownscale)
|
||||
{
|
||||
_textureDownscaleService.ScheduleDownscale(fileHash, filePath, mapKind);
|
||||
}
|
||||
if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Logger.LogError("Hash mismatch after extracting, got {hash}, expected {expectedHash}, deleting file", entry.Hash, fileHash);
|
||||
|
||||
@@ -4,8 +4,10 @@ using LightlessSync.WebAPI.Files.Models;
|
||||
using LightlessSync.WebAPI.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Net.Sockets;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LightlessSync.WebAPI.Files;
|
||||
@@ -84,27 +86,46 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
||||
bool withToken = true)
|
||||
{
|
||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||
return await SendRequestInternalAsync(requestMessage, ct, httpCompletionOption, withToken).ConfigureAwait(false);
|
||||
return await SendRequestInternalAsync(() => new HttpRequestMessage(method, uri),
|
||||
ct, httpCompletionOption, withToken, allowRetry: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct,
|
||||
bool withToken = true) where T : class
|
||||
{
|
||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||
if (content is not ByteArrayContent)
|
||||
requestMessage.Content = JsonContent.Create(content);
|
||||
else
|
||||
requestMessage.Content = content as ByteArrayContent;
|
||||
return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false);
|
||||
return await SendRequestInternalAsync(() =>
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage(method, uri);
|
||||
if (content is not ByteArrayContent byteArrayContent)
|
||||
{
|
||||
requestMessage.Content = JsonContent.Create(content);
|
||||
}
|
||||
else
|
||||
{
|
||||
var clonedContent = new ByteArrayContent(byteArrayContent.ReadAsByteArrayAsync().GetAwaiter().GetResult());
|
||||
foreach (var header in byteArrayContent.Headers)
|
||||
{
|
||||
clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
requestMessage.Content = clonedContent;
|
||||
}
|
||||
|
||||
return requestMessage;
|
||||
}, ct, HttpCompletionOption.ResponseContentRead, withToken,
|
||||
allowRetry: content is not HttpContent || content is ByteArrayContent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content,
|
||||
CancellationToken ct, bool withToken = true)
|
||||
{
|
||||
using var requestMessage = new HttpRequestMessage(method, uri);
|
||||
requestMessage.Content = content;
|
||||
return await SendRequestInternalAsync(requestMessage, ct, withToken: withToken).ConfigureAwait(false);
|
||||
return await SendRequestInternalAsync(() =>
|
||||
{
|
||||
var requestMessage = new HttpRequestMessage(method, uri)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
return requestMessage;
|
||||
}, ct, HttpCompletionOption.ResponseContentRead, withToken, allowRetry: false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task WaitForDownloadSlotAsync(CancellationToken token)
|
||||
@@ -146,39 +167,78 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
|
||||
return Math.Clamp(dividedLimit, 1, long.MaxValue);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> SendRequestInternalAsync(HttpRequestMessage requestMessage,
|
||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead, bool withToken = true)
|
||||
private async Task<HttpResponseMessage> SendRequestInternalAsync(Func<HttpRequestMessage> requestFactory,
|
||||
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
|
||||
bool withToken = true, bool allowRetry = true)
|
||||
{
|
||||
if (withToken)
|
||||
{
|
||||
var token = await _tokenProvider.GetToken().ConfigureAwait(false);
|
||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
const int maxAttempts = 2;
|
||||
var attempt = 0;
|
||||
|
||||
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
|
||||
while (true)
|
||||
{
|
||||
var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false);
|
||||
Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("Sending {method} to {uri}", requestMessage.Method, requestMessage.RequestUri);
|
||||
}
|
||||
attempt++;
|
||||
using var requestMessage = requestFactory();
|
||||
|
||||
try
|
||||
{
|
||||
if (ct != null)
|
||||
return await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false);
|
||||
return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri);
|
||||
throw;
|
||||
if (withToken)
|
||||
{
|
||||
var token = await _tokenProvider.GetToken().ConfigureAwait(false);
|
||||
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
|
||||
{
|
||||
var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false);
|
||||
Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("Sending {method} to {uri}", requestMessage.Method, requestMessage.RequestUri);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (ct != null)
|
||||
return await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false);
|
||||
return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (allowRetry && attempt < maxAttempts && IsTransientNetworkException(ex))
|
||||
{
|
||||
Logger.LogWarning(ex, "Transient error during SendRequestInternal for {uri}, retrying attempt {attempt}/{maxAttempts}",
|
||||
requestMessage.RequestUri, attempt, maxAttempts);
|
||||
if (ct.HasValue)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(200), ct.Value).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(200)).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Error during SendRequestInternal for {uri}", requestMessage.RequestUri);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsTransientNetworkException(Exception ex)
|
||||
{
|
||||
var current = ex;
|
||||
while (current != null)
|
||||
{
|
||||
if (current is SocketException socketEx)
|
||||
{
|
||||
return socketEx.SocketErrorCode is SocketError.ConnectionReset or SocketError.ConnectionAborted or SocketError.TimedOut;
|
||||
}
|
||||
|
||||
current = current.InnerException;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
public List<FileTransfer> CurrentUploads { get; } = [];
|
||||
public bool IsReady => _orchestrator.IsInitialized;
|
||||
public bool IsUploading
|
||||
{
|
||||
get
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
@@ -41,6 +42,30 @@ public partial class ApiController
|
||||
await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UpdateChatPresence(ChatPresenceUpdateDto presence)
|
||||
{
|
||||
if (!IsConnected || _lightlessHub is null) return;
|
||||
await _lightlessHub.InvokeAsync(nameof(UpdateChatPresence), presence).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SendChatMessage(ChatSendRequestDto request)
|
||||
{
|
||||
if (!IsConnected || _lightlessHub is null) return;
|
||||
await _lightlessHub.InvokeAsync(nameof(SendChatMessage), request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task ReportChatMessage(ChatReportSubmitDto request)
|
||||
{
|
||||
if (!IsConnected || _lightlessHub is null) return;
|
||||
await _lightlessHub.InvokeAsync(nameof(ReportChatMessage), request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<ChatParticipantResolveResultDto?> ResolveChatParticipant(ChatParticipantResolveRequestDto request)
|
||||
{
|
||||
if (!IsConnected || _lightlessHub is null) return null;
|
||||
return await _lightlessHub.InvokeAsync<ChatParticipantResolveResultDto?>(nameof(ResolveChatParticipant), request).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null)
|
||||
{
|
||||
CheckConnection();
|
||||
@@ -88,6 +113,12 @@ public partial class ApiController
|
||||
return await _lightlessHub!.InvokeAsync<UserProfileDto>(nameof(UserGetProfile), dto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<UserProfileDto?> UserGetLightfinderProfile(string hashedCid)
|
||||
{
|
||||
if (!IsConnected) return null;
|
||||
return await _lightlessHub!.InvokeAsync<UserProfileDto?>(nameof(UserGetLightfinderProfile), hashedCid).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UserPushData(UserCharaDataMessageDto dto)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
@@ -24,21 +26,21 @@ public partial class ApiController
|
||||
public Task Client_GroupChangePermissions(GroupPermissionDto groupPermission)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupChangePermissions: {perm}", groupPermission);
|
||||
ExecuteSafely(() => _pairManager.SetGroupPermissions(groupPermission));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleGroupChangePermissions(groupPermission));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupChangeUserPairPermissions(GroupPairUserPermissionDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_GroupChangeUserPairPermissions: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdateGroupPairPermissions(dto));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleGroupPairPermissions(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupDelete(GroupDto groupDto)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupDelete: {dto}", groupDto);
|
||||
ExecuteSafely(() => _pairManager.RemoveGroup(groupDto.Group));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleGroupRemoved(groupDto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -47,8 +49,8 @@ public partial class ApiController
|
||||
Logger.LogTrace("Client_GroupPairChangeUserInfo: {dto}", userInfo);
|
||||
ExecuteSafely(() =>
|
||||
{
|
||||
if (string.Equals(userInfo.UID, UID, StringComparison.Ordinal)) _pairManager.SetGroupStatusInfo(userInfo);
|
||||
else _pairManager.SetGroupPairStatusInfo(userInfo);
|
||||
var isSelf = string.Equals(userInfo.UID, UID, StringComparison.Ordinal);
|
||||
_pairCoordinator.HandleGroupPairStatus(userInfo, isSelf);
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -56,28 +58,28 @@ public partial class ApiController
|
||||
public Task Client_GroupPairJoined(GroupPairFullInfoDto groupPairInfoDto)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupPairJoined: {dto}", groupPairInfoDto);
|
||||
ExecuteSafely(() => _pairManager.AddGroupPair(groupPairInfoDto));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleGroupPairJoined(groupPairInfoDto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupPairLeft(GroupPairDto groupPairDto)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupPairLeft: {dto}", groupPairDto);
|
||||
ExecuteSafely(() => _pairManager.RemoveGroupPair(groupPairDto));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleGroupPairLeft(groupPairDto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupSendFullInfo(GroupFullInfoDto groupInfo)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupSendFullInfo: {dto}", groupInfo);
|
||||
ExecuteSafely(() => _pairManager.AddGroup(groupInfo));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleGroupFullInfo(groupInfo));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_GroupSendInfo(GroupInfoDto groupInfo)
|
||||
{
|
||||
Logger.LogTrace("Client_GroupSendInfo: {dto}", groupInfo);
|
||||
ExecuteSafely(() => _pairManager.SetGroupInfo(groupInfo));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleGroupInfoUpdate(groupInfo));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -129,52 +131,62 @@ public partial class ApiController
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_ChatReceive(ChatMessageDto message)
|
||||
{
|
||||
Logger.LogTrace("Client_ChatReceive: {@channel}", message.Channel);
|
||||
ExecuteSafely(() => ChatMessageReceived?.Invoke(message));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UpdateUserIndividualPairStatusDto(UserIndividualPairStatusDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UpdateUserIndividualPairStatusDto: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdateIndividualPairStatus(dto));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleUserStatus(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserAddClientPair(UserPairDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserAddClientPair: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.AddUserPair(dto, addToLastAddedUser: true));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleUserAddPair(dto, addToLastAddedUser: true));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto)
|
||||
{
|
||||
Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User);
|
||||
ExecuteSafely(() => _pairManager.ReceiveCharaData(dataDto));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleCharacterData(dataDto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserReceiveUploadStatus(UserDto dto)
|
||||
{
|
||||
Logger.LogTrace("Client_UserReceiveUploadStatus: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.ReceiveUploadStatus(dto));
|
||||
ExecuteSafely(() =>
|
||||
{
|
||||
_pairCoordinator.HandleUploadStatus(dto);
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserRemoveClientPair(UserDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserRemoveClientPair: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.RemoveUserPair(dto));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleUserRemovePair(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserSendOffline(UserDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserSendOffline: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.MarkPairOffline(dto.User));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleUserOffline(dto.User));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task Client_UserSendOnline(OnlineUserIdentDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserSendOnline: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.MarkPairOnline(dto));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleUserOnline(dto, sendNotification: true));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -188,7 +200,7 @@ public partial class ApiController
|
||||
public Task Client_UserUpdateOtherPairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserUpdateOtherPairPermissions: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdatePairPermissions(dto));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleUserPermissions(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -209,7 +221,7 @@ public partial class ApiController
|
||||
public Task Client_UserUpdateSelfPairPermissions(UserPermissionsDto dto)
|
||||
{
|
||||
Logger.LogDebug("Client_UserUpdateSelfPairPermissions: {dto}", dto);
|
||||
ExecuteSafely(() => _pairManager.UpdateSelfPairPermissions(dto));
|
||||
ExecuteSafely(() => _pairCoordinator.HandleSelfPermissions(dto));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Utility;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto;
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.API.SignalR;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
@@ -16,7 +21,6 @@ using LightlessSync.WebAPI.SignalR;
|
||||
using LightlessSync.WebAPI.SignalR.Utils;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Reflection;
|
||||
|
||||
namespace LightlessSync.WebAPI;
|
||||
|
||||
@@ -28,7 +32,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly HubFactory _hubFactory;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly PairCoordinator _pairCoordinator;
|
||||
private readonly PairRequestService _pairRequestService;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly TokenProvider _tokenProvider;
|
||||
@@ -42,14 +46,17 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
private HubConnection? _lightlessHub;
|
||||
private ServerState _serverState;
|
||||
private CensusUpdateMessage? _lastCensus;
|
||||
private IReadOnlyList<ZoneChatChannelInfoDto> _zoneChatChannels = Array.Empty<ZoneChatChannelInfoDto>();
|
||||
private IReadOnlyList<GroupChatChannelInfoDto> _groupChatChannels = Array.Empty<GroupChatChannelInfoDto>();
|
||||
private event Action<ChatMessageDto>? ChatMessageReceived;
|
||||
|
||||
public ApiController(ILogger<ApiController> logger, HubFactory hubFactory, DalamudUtilService dalamudUtil,
|
||||
PairManager pairManager, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator,
|
||||
PairCoordinator pairCoordinator, PairRequestService pairRequestService, ServerConfigurationManager serverManager, LightlessMediator mediator,
|
||||
TokenProvider tokenProvider, LightlessConfigService lightlessConfigService, NotificationService lightlessNotificationService) : base(logger, mediator)
|
||||
{
|
||||
_hubFactory = hubFactory;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairManager = pairManager;
|
||||
_pairCoordinator = pairCoordinator;
|
||||
_pairRequestService = pairRequestService;
|
||||
_serverManager = serverManager;
|
||||
_tokenProvider = tokenProvider;
|
||||
@@ -61,7 +68,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
Mediator.Subscribe<HubClosedMessage>(this, (msg) => LightlessHubOnClosed(msg.Exception));
|
||||
Mediator.Subscribe<HubReconnectedMessage>(this, (msg) => _ = LightlessHubOnReconnectedAsync());
|
||||
Mediator.Subscribe<HubReconnectingMessage>(this, (msg) => LightlessHubOnReconnecting(msg.Exception));
|
||||
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => _ = CyclePauseAsync(msg.UserData));
|
||||
Mediator.Subscribe<CyclePauseMessage>(this, (msg) => _ = CyclePauseAsync(msg.Pair));
|
||||
Mediator.Subscribe<CensusUpdateMessage>(this, (msg) => _lastCensus = msg);
|
||||
Mediator.Subscribe<PauseMessage>(this, (msg) => _ = PauseAsync(msg.UserData));
|
||||
|
||||
@@ -106,15 +113,65 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
|
||||
public SystemInfoDto SystemInfoDto { get; private set; } = new();
|
||||
|
||||
public IReadOnlyList<ZoneChatChannelInfoDto> ZoneChatChannels => _zoneChatChannels;
|
||||
public IReadOnlyList<GroupChatChannelInfoDto> GroupChatChannels => _groupChatChannels;
|
||||
public string UID => _connectionDto?.User.UID ?? string.Empty;
|
||||
|
||||
public event Action? OnConnected;
|
||||
|
||||
public async Task<bool> CheckClientHealth()
|
||||
{
|
||||
return await _lightlessHub!.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
|
||||
var hub = _lightlessHub;
|
||||
if (hub is null || !IsConnected)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await hub.InvokeAsync<bool>(nameof(CheckClientHealth)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Client health check failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshChatChannelsAsync()
|
||||
{
|
||||
if (_lightlessHub is null || !IsConnected)
|
||||
return;
|
||||
|
||||
await Task.WhenAll(GetZoneChatChannelsAsync(), GetGroupChatChannelsAsync()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ZoneChatChannelInfoDto>> GetZoneChatChannelsAsync()
|
||||
{
|
||||
if (_lightlessHub is null || !IsConnected)
|
||||
return _zoneChatChannels;
|
||||
|
||||
var channels = await _lightlessHub.InvokeAsync<IReadOnlyList<ZoneChatChannelInfoDto>>("GetZoneChatChannels").ConfigureAwait(false);
|
||||
_zoneChatChannels = channels;
|
||||
return channels;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<GroupChatChannelInfoDto>> GetGroupChatChannelsAsync()
|
||||
{
|
||||
if (_lightlessHub is null || !IsConnected)
|
||||
return _groupChatChannels;
|
||||
|
||||
var channels = await _lightlessHub.InvokeAsync<IReadOnlyList<GroupChatChannelInfoDto>>("GetGroupChatChannels").ConfigureAwait(false);
|
||||
_groupChatChannels = channels;
|
||||
return channels;
|
||||
}
|
||||
|
||||
Task<IReadOnlyList<ZoneChatChannelInfoDto>> ILightlessHub.GetZoneChatChannels()
|
||||
=> _lightlessHub!.InvokeAsync<IReadOnlyList<ZoneChatChannelInfoDto>>("GetZoneChatChannels");
|
||||
|
||||
Task<IReadOnlyList<GroupChatChannelInfoDto>> ILightlessHub.GetGroupChatChannels()
|
||||
=> _lightlessHub!.InvokeAsync<IReadOnlyList<GroupChatChannelInfoDto>>("GetGroupChatChannels");
|
||||
|
||||
public async Task CreateConnectionsAsync()
|
||||
{
|
||||
if (!_serverManager.ShownCensusPopup)
|
||||
@@ -337,35 +394,86 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
|
||||
private bool _naggedAboutLod = false;
|
||||
|
||||
public Task CyclePauseAsync(UserData userData)
|
||||
public Task CyclePauseAsync(Pair pair)
|
||||
{
|
||||
CancellationTokenSource cts = new();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
ArgumentNullException.ThrowIfNull(pair);
|
||||
return CyclePauseAsync(pair.UniqueIdent);
|
||||
}
|
||||
|
||||
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
||||
{
|
||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData);
|
||||
var perm = pair.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(paused: true);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false);
|
||||
// wait until it's changed
|
||||
while (pair.UserPair!.OwnPermissions != perm)
|
||||
var token = timeoutCts.Token;
|
||||
try
|
||||
{
|
||||
await Task.Delay(250, cts.Token).ConfigureAwait(false);
|
||||
Logger.LogTrace("Waiting for permissions change for {data}", userData);
|
||||
if (!_pairCoordinator.Ledger.TryGetEntry(ident, out var entry) || entry is null)
|
||||
{
|
||||
Logger.LogWarning("CyclePauseAsync: pair {uid} not found in ledger", ident.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
var originalPermissions = entry.SelfPermissions;
|
||||
var targetPermissions = originalPermissions;
|
||||
targetPermissions.SetPaused(!originalPermissions.IsPaused());
|
||||
|
||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
||||
|
||||
var applied = false;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
||||
{
|
||||
if (updated.SelfPermissions == targetPermissions)
|
||||
{
|
||||
applied = true;
|
||||
entry = updated;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(250, token).ConfigureAwait(false);
|
||||
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
|
||||
}
|
||||
|
||||
if (!applied)
|
||||
{
|
||||
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogDebug("CyclePauseAsync toggled paused for {uid} to {state}", ident.UserId, targetPermissions.IsPaused());
|
||||
}
|
||||
perm.SetPaused(paused: false);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false);
|
||||
}, cts.Token).ContinueWith((t) => cts.Dispose());
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogDebug("CyclePauseAsync cancelled for {uid}", ident.UserId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "CyclePauseAsync failed for {uid}", ident.UserId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
timeoutCts.Dispose();
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task PauseAsync(UserData userData)
|
||||
{
|
||||
var pair = _pairManager.GetOnlineUserPairs().Single(p => p.UserPair != null && p.UserData == userData);
|
||||
var perm = pair.UserPair!.OwnPermissions;
|
||||
perm.SetPaused(paused: true);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(userData, perm)).ConfigureAwait(false);
|
||||
var pairIdent = new PairUniqueIdentifier(userData.UID);
|
||||
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
|
||||
{
|
||||
Logger.LogWarning("PauseAsync: pair {uid} not found in ledger", userData.UID);
|
||||
return;
|
||||
}
|
||||
|
||||
var permissions = entry.SelfPermissions;
|
||||
permissions.SetPaused(paused: true);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<ConnectionDto> GetConnectionDto() => GetConnectionDtoAsync(true);
|
||||
@@ -388,8 +496,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
|
||||
private async Task ClientHealthCheckAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested && _lightlessHub != null)
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
if (_lightlessHub is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false);
|
||||
Logger.LogDebug("Checking Client Health State");
|
||||
|
||||
@@ -455,6 +568,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
|
||||
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
|
||||
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
|
||||
_lightlessHub.On(nameof(Client_ChatReceive), (Func<ChatMessageDto, Task>)Client_ChatReceive);
|
||||
|
||||
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
|
||||
OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto));
|
||||
@@ -470,18 +584,36 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
private readonly HashSet<Action<ChatMessageDto>> _chatHandlers = new();
|
||||
|
||||
public void RegisterChatMessageHandler(Action<ChatMessageDto> handler)
|
||||
{
|
||||
if (_chatHandlers.Add(handler))
|
||||
{
|
||||
ChatMessageReceived += handler;
|
||||
}
|
||||
}
|
||||
|
||||
public void UnregisterChatMessageHandler(Action<ChatMessageDto> handler)
|
||||
{
|
||||
if (_chatHandlers.Remove(handler))
|
||||
{
|
||||
ChatMessageReceived -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadIninitialPairsAsync()
|
||||
{
|
||||
foreach (var entry in await GroupsGetAll().ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Group: {entry}", entry);
|
||||
_pairManager.AddGroup(entry);
|
||||
_pairCoordinator.HandleGroupFullInfo(entry);
|
||||
}
|
||||
|
||||
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Individual Pair: {userPair}", userPair);
|
||||
_pairManager.AddUserPair(userPair);
|
||||
_pairCoordinator.HandleUserAddPair(userPair);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +630,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
foreach (var entry in await UserGetOnlinePairs(dto).ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogDebug("Pair online: {pair}", entry);
|
||||
_pairManager.MarkPairOnline(entry, sendNotif: false);
|
||||
_pairCoordinator.HandleUserOnline(entry, sendNotification: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,49 +734,12 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
Mediator.Publish(new DisconnectedMessage());
|
||||
_lightlessHub = null;
|
||||
_connectionDto = null;
|
||||
_zoneChatChannels = Array.Empty<ZoneChatChannelInfoDto>();
|
||||
_groupChatChannels = Array.Empty<GroupChatChannelInfoDto>();
|
||||
}
|
||||
|
||||
ServerState = state;
|
||||
}
|
||||
|
||||
public Task<UserProfileDto?> UserGetLightfinderProfile(string hashedCid)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task UpdateChatPresence(ChatPresenceUpdateDto presence)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task Client_ChatReceive(ChatMessageDto message)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<ZoneChatChannelInfoDto>> GetZoneChatChannels()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<GroupChatChannelInfoDto>> GetGroupChatChannels()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task SendChatMessage(ChatSendRequestDto request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task ReportChatMessage(ChatReportSubmitDto request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<ChatParticipantResolveResultDto?> ResolveChatParticipant(ChatParticipantResolveRequestDto request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
#pragma warning restore MA0040
|
||||
#pragma warning restore MA0040
|
||||
|
||||
Reference in New Issue
Block a user