2.0.0 (#92)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s

2.0.0 Changes:

- Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more.
- Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name.
- Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much.
- Chat has been added to the top menu, working in Zone or in Syncshells to be used there.
- Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well.
- Moved to the internal object table to have faster load times for users; people should load in faster
- Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files
- Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore.
- Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all).
- Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list.
- Lightfinder plates have been moved away from using Nameplates, but will use an overlay.
- Main UI has been changed a bit with a gradient, and on hover will glow up now.
- Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items.
- Reworked Settings UI to look more modern.
- Performance should be better due to new systems that would dispose of the collections and better caching of items.

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Minmoose <KennethBohr@outlook.com>
Reviewed-on: #92
This commit was merged in pull request #92.
This commit is contained in:
2025-12-21 17:19:34 +00:00
parent 906f401940
commit 835a0a637d
191 changed files with 32636 additions and 8841 deletions

View File

@@ -1,21 +1,16 @@
using Dalamud.Utility;
using K4os.Compression.LZ4.Legacy;
using LightlessSync.API.Data;
using LightlessSync.API.Dto.Files;
using LightlessSync.API.Routes;
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;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using LightlessSync.LightlessConfiguration;
namespace LightlessSync.WebAPI.Files;
@@ -26,25 +21,30 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private readonly FileCompactor _fileCompactor;
private readonly FileCacheManager _fileDbManager;
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,
LightlessConfigService configService,
TextureDownscaleService textureDownscaleService, TextureMetadataHelper textureMetadataHelper) : base(logger, mediator)
{
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
_orchestrator = orchestrator;
_fileDbManager = fileCacheManager;
_fileCompactor = fileCompactor;
_pairProcessingLimiter = pairProcessingLimiter;
_configService = configService;
_textureDownscaleService = textureDownscaleService;
_textureMetadataHelper = textureMetadataHelper;
_activeDownloadStreams = new();
_lastConfigDirectDownloadsState = _configService.Current.EnableDirectDownloads;
@@ -63,6 +63,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 +84,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 +100,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,30 +277,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
int bytesRead;
try
{
var readTask = stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct).AsTask();
while (!readTask.IsCompleted)
{
var completedTask = await Task.WhenAny(readTask, Task.Delay(DownloadStallTimeout)).ConfigureAwait(false);
if (completedTask == readTask)
{
break;
}
ct.ThrowIfCancellationRequested();
var snapshot = _pairProcessingLimiter.GetSnapshot();
if (snapshot.Waiting > 0)
{
throw new TimeoutException($"No data received for {DownloadStallTimeout.TotalSeconds} seconds while downloading {requestUrl} (waiting: {snapshot.Waiting})");
}
Logger.LogTrace("Download stalled for {requestUrl} but no queued pairs, continuing to wait", requestUrl);
}
bytesRead = await readTask.ConfigureAwait(false);
bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
catch (OperationCanceledException ex)
{
Logger.LogWarning(ex, "Request got cancelled : {url}", requestUrl);
throw;
}
@@ -314,11 +300,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
Logger.LogDebug("{requestUrl} downloaded to {destination}", requestUrl, destinationFilename);
}
}
catch (TimeoutException ex)
{
Logger.LogWarning(ex, "Detected stalled download for {requestUrl}, aborting transfer", requestUrl);
throw;
}
catch (OperationCanceledException)
{
throw;
@@ -352,7 +333,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 +366,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 +395,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 +437,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 +460,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 +503,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 +566,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 +637,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 +676,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 +712,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 +724,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 +754,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 +807,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 +824,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);

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Net.Sockets;
using System.Reflection;
namespace LightlessSync.WebAPI.Files;
@@ -84,27 +85,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 +166,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;
}
}

View File

@@ -11,7 +11,6 @@ using Microsoft.Extensions.Logging;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Collections.Concurrent;
using System.Threading;
namespace LightlessSync.WebAPI.Files;
@@ -44,6 +43,7 @@ public sealed class FileUploadManager : DisposableMediatorSubscriberBase
}
public List<FileTransfer> CurrentUploads { get; } = [];
public bool IsReady => _orchestrator.IsInitialized;
public bool IsUploading
{
get

View File

@@ -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,29 @@ 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 SetChatParticipantMute(ChatParticipantMuteRequestDto request)
{
if (!IsConnected || _lightlessHub is null) return;
await _lightlessHub.InvokeAsync(nameof(SetChatParticipantMute), request).ConfigureAwait(false);
}
public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null)
{
CheckConnection();
@@ -88,6 +112,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

View File

@@ -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;
}

View File

@@ -151,6 +151,20 @@ public partial class ApiController
.ConfigureAwait(false);
}
public async Task<GroupPruneSettingsDto> GroupGetPruneSettings(GroupDto dto)
{
CheckConnection();
return await _lightlessHub!.InvokeAsync<GroupPruneSettingsDto>(nameof(GroupGetPruneSettings), dto)
.ConfigureAwait(false);
}
public async Task GroupSetPruneSettings(GroupPruneSettingsDto dto)
{
CheckConnection();
await _lightlessHub!.SendAsync(nameof(GroupSetPruneSettings), dto)
.ConfigureAwait(false);
}
private void CheckConnection()
{
if (ServerState is not (ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting)) throw new InvalidDataException("Not connected");

View File

@@ -1,9 +1,9 @@
using System.Reflection;
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.User;
using LightlessSync.API.SignalR;
using LightlessSync.LightlessConfiguration;
@@ -16,7 +16,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 +27,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 +41,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 +63,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 +108,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)
@@ -133,7 +185,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogInformation("Not recreating Connection, paused");
_connectionDto = null;
await StopConnectionAsync(ServerState.Disconnected).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
@@ -147,7 +202,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.",
NotificationType.Error));
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
@@ -156,7 +214,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogWarning("No secret key set for current character");
_connectionDto = null;
await StopConnectionAsync(ServerState.NoSecretKey).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
}
@@ -170,7 +231,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Mediator.Publish(new NotificationMessage("Multiple Identical Characters detected", "Your Service configuration has multiple characters with the same name and world set up. Delete the duplicates in the character management to be able to connect to Lightless.",
NotificationType.Error));
await StopConnectionAsync(ServerState.MultiChara).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
@@ -179,7 +243,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogWarning("No UID/OAuth set for current character");
_connectionDto = null;
await StopConnectionAsync(ServerState.OAuthMisconfigured).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
@@ -188,7 +255,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogWarning("OAuth2 login token could not be updated");
_connectionDto = null;
await StopConnectionAsync(ServerState.OAuthLoginTokenStale).ConfigureAwait(false);
_connectionCancellationTokenSource?.Cancel();
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
return;
}
}
@@ -199,7 +269,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(ApiController), Services.Events.EventSeverity.Informational,
$"Starting Connection to {_serverManager.CurrentServer.ServerName}")));
_connectionCancellationTokenSource?.Cancel();
if (_connectionCancellationTokenSource != null)
{
await _connectionCancellationTokenSource.CancelAsync().ConfigureAwait(false);
}
_connectionCancellationTokenSource?.Dispose();
_connectionCancellationTokenSource = new CancellationTokenSource();
var token = _connectionCancellationTokenSource.Token;
@@ -337,35 +410,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 +512,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 +584,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
if (!_initialized)
{
_lightlessHub.On(nameof(Client_ChatReceive), (Func<ChatMessageDto, Task>)Client_ChatReceive);
}
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto));
@@ -470,18 +603,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 +649,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);
}
}
@@ -598,53 +749,18 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
$"Stopping existing connection to {_serverManager.CurrentServer.ServerName}")));
_initialized = false;
_healthCheckTokenSource?.Cancel();
if (_healthCheckTokenSource != null)
{
await _healthCheckTokenSource.CancelAsync().ConfigureAwait(false);
}
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

View File

@@ -71,6 +71,7 @@ public class HubFactory : MediatorSubscriberBase
};
Logger.LogDebug("Building new HubConnection using transport {transport}", transportType);
var msgpackOptions = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block).WithResolver(ContractlessStandardResolver.Instance);
_instance = new HubConnectionBuilder()
.WithUrl(_serverConfigurationManager.CurrentApiUrl + ILightlessHub.Path, options =>
@@ -80,22 +81,7 @@ public class HubFactory : MediatorSubscriberBase
})
.AddMessagePackProtocol(opt =>
{
var resolver = CompositeResolver.Create(StandardResolverAllowPrivate.Instance,
BuiltinResolver.Instance,
AttributeFormatterResolver.Instance,
// replace enum resolver
DynamicEnumAsStringResolver.Instance,
DynamicGenericResolver.Instance,
DynamicUnionResolver.Instance,
DynamicObjectResolver.Instance,
PrimitiveObjectResolver.Instance,
// final fallback(last priority)
StandardResolver.Instance);
opt.SerializerOptions =
MessagePackSerializerOptions.Standard
.WithCompression(MessagePackCompression.Lz4Block)
.WithResolver(resolver);
opt.SerializerOptions = msgpackOptions;
})
.WithAutomaticReconnect(new ForeverRetryPolicy(Mediator))
.ConfigureLogging(a =>