Update UI & Syncshell Public & MCDF Share
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.Notifications;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -14,17 +15,19 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
||||
private readonly MareMediator _mediator;
|
||||
private readonly ApiController _api;
|
||||
private readonly AutoDetectRequestService _requestService;
|
||||
private readonly NotificationTracker _notificationTracker;
|
||||
private readonly ConcurrentDictionary<string, string> _pending = new(StringComparer.Ordinal);
|
||||
private static readonly TimeSpan RegexTimeout = TimeSpan.FromSeconds(1);
|
||||
private static readonly Regex ReqRegex = new(@"^Nearby Request: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
|
||||
private static readonly Regex AcceptRegex = new(@"^Nearby Accept: .+ \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, RegexTimeout);
|
||||
|
||||
public NearbyPendingService(ILogger<NearbyPendingService> logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService)
|
||||
public NearbyPendingService(ILogger<NearbyPendingService> logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService, NotificationTracker notificationTracker)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_api = api;
|
||||
_requestService = requestService;
|
||||
_notificationTracker = notificationTracker;
|
||||
_mediator.Subscribe<NotificationMessage>(this, OnNotification);
|
||||
_mediator.Subscribe<ManualPairInviteMessage>(this, OnManualPairInvite);
|
||||
}
|
||||
@@ -46,6 +49,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
||||
_ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
|
||||
_pending.TryRemove(uidA, out _);
|
||||
_requestService.RemovePendingRequestByUid(uidA);
|
||||
_notificationTracker.Remove(NotificationCategory.AutoDetect, uidA);
|
||||
_logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
|
||||
}
|
||||
return;
|
||||
@@ -67,6 +71,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
||||
catch { name = uid; }
|
||||
_pending[uid] = name;
|
||||
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name);
|
||||
_notificationTracker.Upsert(NotificationEntry.AutoDetect(uid, name));
|
||||
}
|
||||
|
||||
private void OnManualPairInvite(ManualPairInviteMessage msg)
|
||||
@@ -81,12 +86,14 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
||||
_pending[msg.SourceUid] = display;
|
||||
_logger.LogInformation("NearbyPending: received manual invite from {uid} ({name})", msg.SourceUid, display);
|
||||
_mediator.Publish(new NotificationMessage("Nearby request", $"{display} vous a envoyé une invitation de pair.", NotificationType.Info, TimeSpan.FromSeconds(5)));
|
||||
_notificationTracker.Upsert(NotificationEntry.AutoDetect(msg.SourceUid, display));
|
||||
}
|
||||
|
||||
public void Remove(string uid)
|
||||
{
|
||||
_pending.TryRemove(uid, out _);
|
||||
_requestService.RemovePendingRequestByUid(uid);
|
||||
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
|
||||
}
|
||||
|
||||
public async Task<bool> AcceptAsync(string uid)
|
||||
@@ -97,6 +104,7 @@ public sealed class NearbyPendingService : IMediatorSubscriber
|
||||
_pending.TryRemove(uid, out _);
|
||||
_requestService.RemovePendingRequestByUid(uid);
|
||||
_ = _requestService.SendAcceptNotifyAsync(uid);
|
||||
_notificationTracker.Remove(NotificationCategory.AutoDetect, uid);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
146
MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs
Normal file
146
MareSynchronos/Services/AutoDetect/SyncshellDiscoveryService.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.API.Data;
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Services.AutoDetect;
|
||||
|
||||
public sealed class SyncshellDiscoveryService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
private readonly ILogger<SyncshellDiscoveryService> _logger;
|
||||
private readonly MareMediator _mediator;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly SemaphoreSlim _refreshSemaphore = new(1, 1);
|
||||
private readonly object _entriesLock = new();
|
||||
private List<SyncshellDiscoveryEntryDto> _entries = [];
|
||||
private string? _lastError;
|
||||
private bool _isRefreshing;
|
||||
|
||||
public SyncshellDiscoveryService(ILogger<SyncshellDiscoveryService> logger, MareMediator mediator, ApiController apiController)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
public MareMediator Mediator => _mediator;
|
||||
|
||||
public IReadOnlyList<SyncshellDiscoveryEntryDto> Entries
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_entriesLock)
|
||||
{
|
||||
return _entries.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRefreshing => _isRefreshing;
|
||||
public string? LastError => _lastError;
|
||||
|
||||
public async Task<bool> JoinAsync(string gid, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _apiController.SyncshellDiscoveryJoin(new GroupDto(new GroupData(gid))).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Join syncshell discovery failed for {gid}", gid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SyncshellDiscoveryStateDto?> GetStateAsync(string gid, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _apiController.SyncshellDiscoveryGetState(new GroupDto(new GroupData(gid))).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch syncshell discovery state for {gid}", gid);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SetVisibilityAsync(string gid, bool visible, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = new SyncshellDiscoveryVisibilityRequestDto
|
||||
{
|
||||
GID = gid,
|
||||
AutoDetectVisible = visible,
|
||||
};
|
||||
var success = await _apiController.SyncshellDiscoverySetVisibility(request).ConfigureAwait(false);
|
||||
if (!success) return false;
|
||||
|
||||
var state = await GetStateAsync(gid, ct).ConfigureAwait(false);
|
||||
if (state != null)
|
||||
{
|
||||
_mediator.Publish(new SyncshellAutoDetectStateChanged(state.GID, state.AutoDetectVisible, state.PasswordTemporarilyDisabled));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to set syncshell visibility for {gid}", gid);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshAsync(CancellationToken ct)
|
||||
{
|
||||
if (!await _refreshSemaphore.WaitAsync(0, ct).ConfigureAwait(false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_isRefreshing = true;
|
||||
var discovered = await _apiController.SyncshellDiscoveryList().ConfigureAwait(false);
|
||||
lock (_entriesLock)
|
||||
{
|
||||
_entries = discovered ?? [];
|
||||
}
|
||||
_lastError = null;
|
||||
_mediator.Publish(new SyncshellDiscoveryUpdated(Entries.ToList()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to refresh syncshell discovery list");
|
||||
_lastError = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isRefreshing = false;
|
||||
_refreshSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.Subscribe<ConnectedMessage>(this, msg =>
|
||||
{
|
||||
_ = RefreshAsync(CancellationToken.None);
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_mediator.UnsubscribeAll(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using MareSynchronos.Services.CharaData.Models;
|
||||
using MareSynchronos.Utils;
|
||||
using MareSynchronos.WebAPI.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Threading;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
@@ -295,6 +296,32 @@ public sealed class CharaDataFileHandler : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<byte[]?> CreateCharaFileBytesAsync(string description, CancellationToken token = default)
|
||||
{
|
||||
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
|
||||
try
|
||||
{
|
||||
await SaveCharaFileAsync(description, tempFilePath).ConfigureAwait(false);
|
||||
if (!File.Exists(tempFilePath)) return null;
|
||||
token.ThrowIfCancellationRequested();
|
||||
return await File.ReadAllBytesAsync(tempFilePath, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(tempFilePath))
|
||||
{
|
||||
File.Delete(tempFilePath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
||||
{
|
||||
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
||||
|
||||
@@ -13,7 +13,9 @@ using MareSynchronos.Utils;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
@@ -457,6 +459,14 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
||||
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(filePath);
|
||||
}
|
||||
|
||||
public async Task<string> LoadMcdfFromBytes(byte[] data, CancellationToken token = default)
|
||||
{
|
||||
var tempFilePath = Path.Combine(Path.GetTempPath(), "umbra_mcdfshare_" + Guid.NewGuid().ToString("N") + ".mcdf");
|
||||
await File.WriteAllBytesAsync(tempFilePath, data, token).ConfigureAwait(false);
|
||||
LoadedMcdfHeader = _fileHandler.LoadCharaFileHeader(tempFilePath);
|
||||
return tempFilePath;
|
||||
}
|
||||
|
||||
public void McdfApplyToTarget(string charaName)
|
||||
{
|
||||
if (LoadedMcdfHeader == null || !LoadedMcdfHeader.IsCompletedSuccessfully) return;
|
||||
|
||||
309
MareSynchronos/Services/CharaData/McdfShareManager.cs
Normal file
309
MareSynchronos/Services/CharaData/McdfShareManager.cs
Normal file
@@ -0,0 +1,309 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MareSynchronos.API.Dto.McdfShare;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Services.CharaData;
|
||||
|
||||
public sealed class McdfShareManager
|
||||
{
|
||||
private readonly ILogger<McdfShareManager> _logger;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly CharaDataFileHandler _fileHandler;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly SemaphoreSlim _operationSemaphore = new(1, 1);
|
||||
private readonly List<McdfShareEntryDto> _ownShares = new();
|
||||
private readonly List<McdfShareEntryDto> _sharedWithMe = new();
|
||||
private Task? _currentTask;
|
||||
|
||||
public McdfShareManager(ILogger<McdfShareManager> logger, ApiController apiController,
|
||||
CharaDataFileHandler fileHandler, CharaDataManager charaDataManager,
|
||||
ServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_apiController = apiController;
|
||||
_fileHandler = fileHandler;
|
||||
_charaDataManager = charaDataManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
public IReadOnlyList<McdfShareEntryDto> OwnShares => _ownShares;
|
||||
public IReadOnlyList<McdfShareEntryDto> SharedShares => _sharedWithMe;
|
||||
public bool IsBusy => _currentTask is { IsCompleted: false };
|
||||
public string? LastError { get; private set; }
|
||||
public string? LastSuccess { get; private set; }
|
||||
|
||||
public Task RefreshAsync(CancellationToken token)
|
||||
{
|
||||
return RunOperation(() => InternalRefreshAsync(token));
|
||||
}
|
||||
|
||||
public Task CreateShareAsync(string description, IReadOnlyList<string> allowedIndividuals, IReadOnlyList<string> allowedSyncshells, DateTime? expiresAtUtc, CancellationToken token)
|
||||
{
|
||||
return RunOperation(async () =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var mcdfBytes = await _fileHandler.CreateCharaFileBytesAsync(description, token).ConfigureAwait(false);
|
||||
if (mcdfBytes == null || mcdfBytes.Length == 0)
|
||||
{
|
||||
LastError = "Impossible de préparer les données MCDF.";
|
||||
return;
|
||||
}
|
||||
|
||||
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
|
||||
if (hasMultiple)
|
||||
{
|
||||
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage. Corrigez cela dans les paramètres.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(secretKey))
|
||||
{
|
||||
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
|
||||
return;
|
||||
}
|
||||
|
||||
var shareId = Guid.NewGuid();
|
||||
byte[] salt = RandomNumberGenerator.GetBytes(16);
|
||||
byte[] nonce = RandomNumberGenerator.GetBytes(12);
|
||||
byte[] key = DeriveKey(secretKey, shareId, salt);
|
||||
|
||||
byte[] cipher = new byte[mcdfBytes.Length];
|
||||
byte[] tag = new byte[16];
|
||||
|
||||
using (var aes = new AesGcm(key, 16))
|
||||
{
|
||||
aes.Encrypt(nonce, mcdfBytes, cipher, tag);
|
||||
}
|
||||
|
||||
var uploadDto = new McdfShareUploadRequestDto
|
||||
{
|
||||
ShareId = shareId,
|
||||
Description = description,
|
||||
CipherData = cipher,
|
||||
Nonce = nonce,
|
||||
Salt = salt,
|
||||
Tag = tag,
|
||||
ExpiresAtUtc = expiresAtUtc,
|
||||
AllowedIndividuals = allowedIndividuals.ToList(),
|
||||
AllowedSyncshells = allowedSyncshells.ToList()
|
||||
};
|
||||
|
||||
await _apiController.McdfShareUpload(uploadDto).ConfigureAwait(false);
|
||||
await InternalRefreshAsync(token).ConfigureAwait(false);
|
||||
LastSuccess = "Partage MCDF créé.";
|
||||
});
|
||||
}
|
||||
|
||||
public Task DeleteShareAsync(Guid shareId)
|
||||
{
|
||||
return RunOperation(async () =>
|
||||
{
|
||||
var result = await _apiController.McdfShareDelete(shareId).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
LastError = "Le serveur a refusé de supprimer le partage MCDF.";
|
||||
return;
|
||||
}
|
||||
|
||||
_ownShares.RemoveAll(s => s.Id == shareId);
|
||||
_sharedWithMe.RemoveAll(s => s.Id == shareId);
|
||||
await InternalRefreshAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
LastSuccess = "Partage MCDF supprimé.";
|
||||
});
|
||||
}
|
||||
|
||||
public Task UpdateShareAsync(McdfShareUpdateRequestDto updateRequest)
|
||||
{
|
||||
return RunOperation(async () =>
|
||||
{
|
||||
var updated = await _apiController.McdfShareUpdate(updateRequest).ConfigureAwait(false);
|
||||
if (updated == null)
|
||||
{
|
||||
LastError = "Le serveur a refusé de mettre à jour le partage MCDF.";
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _ownShares.FindIndex(s => s.Id == updated.Id);
|
||||
if (idx >= 0)
|
||||
{
|
||||
_ownShares[idx] = updated;
|
||||
}
|
||||
LastSuccess = "Partage MCDF mis à jour.";
|
||||
});
|
||||
}
|
||||
|
||||
public Task ApplyShareAsync(Guid shareId, CancellationToken token)
|
||||
{
|
||||
return RunOperation(async () =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
|
||||
if (plainBytes == null)
|
||||
{
|
||||
LastError ??= "Échec du téléchargement du partage MCDF.";
|
||||
return;
|
||||
}
|
||||
|
||||
var tempPath = await _charaDataManager.LoadMcdfFromBytes(plainBytes, token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _charaDataManager.McdfApplyToGposeTarget().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(tempPath))
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
LastSuccess = "Partage MCDF appliqué sur la cible GPose.";
|
||||
});
|
||||
}
|
||||
|
||||
public Task ExportShareAsync(Guid shareId, string filePath, CancellationToken token)
|
||||
{
|
||||
return RunOperation(async () =>
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var plainBytes = await DownloadAndDecryptShareAsync(shareId, token).ConfigureAwait(false);
|
||||
if (plainBytes == null)
|
||||
{
|
||||
LastError ??= "Échec du téléchargement du partage MCDF.";
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
await File.WriteAllBytesAsync(filePath, plainBytes, token).ConfigureAwait(false);
|
||||
LastSuccess = "Partage MCDF exporté.";
|
||||
});
|
||||
}
|
||||
|
||||
public Task DownloadShareToFileAsync(McdfShareEntryDto entry, string filePath, CancellationToken token)
|
||||
{
|
||||
return ExportShareAsync(entry.Id, filePath, token);
|
||||
}
|
||||
|
||||
private async Task<byte[]?> DownloadAndDecryptShareAsync(Guid shareId, CancellationToken token)
|
||||
{
|
||||
var payload = await _apiController.McdfShareDownload(shareId).ConfigureAwait(false);
|
||||
if (payload == null)
|
||||
{
|
||||
LastError = "Partage indisponible.";
|
||||
return null;
|
||||
}
|
||||
|
||||
var secretKey = _serverConfigurationManager.GetSecretKey(out bool hasMultiple);
|
||||
if (hasMultiple)
|
||||
{
|
||||
LastError = "Plusieurs clés secrètes sont configurées pour ce personnage.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(secretKey))
|
||||
{
|
||||
LastError = "Aucune clé secrète n'est configurée pour ce personnage.";
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] key = DeriveKey(secretKey, payload.ShareId, payload.Salt);
|
||||
byte[] plaintext = new byte[payload.CipherData.Length];
|
||||
try
|
||||
{
|
||||
using var aes = new AesGcm(key, 16);
|
||||
aes.Decrypt(payload.Nonce, payload.CipherData, payload.Tag, plaintext);
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to decrypt MCDF share {ShareId}", shareId);
|
||||
LastError = "Impossible de déchiffrer le partage MCDF.";
|
||||
return null;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
private async Task InternalRefreshAsync(CancellationToken token)
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
var own = await _apiController.McdfShareGetOwn().ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
var shared = await _apiController.McdfShareGetShared().ConfigureAwait(false);
|
||||
_ownShares.Clear();
|
||||
_ownShares.AddRange(own);
|
||||
_sharedWithMe.Clear();
|
||||
_sharedWithMe.AddRange(shared);
|
||||
LastSuccess = "Partages MCDF actualisés.";
|
||||
}
|
||||
|
||||
private Task RunOperation(Func<Task> operation)
|
||||
{
|
||||
async Task Wrapper()
|
||||
{
|
||||
await _operationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
LastError = null;
|
||||
LastSuccess = null;
|
||||
await operation().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during MCDF share operation");
|
||||
LastError = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_operationSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
var task = Wrapper();
|
||||
_currentTask = task;
|
||||
return task;
|
||||
}
|
||||
|
||||
private static byte[] DeriveKey(string secretKey, Guid shareId, byte[] salt)
|
||||
{
|
||||
byte[] secretBytes;
|
||||
try
|
||||
{
|
||||
secretBytes = Convert.FromHexString(secretKey);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// fallback to UTF8 if not hex
|
||||
secretBytes = System.Text.Encoding.UTF8.GetBytes(secretKey);
|
||||
}
|
||||
|
||||
byte[] shareBytes = shareId.ToByteArray();
|
||||
byte[] material = new byte[secretBytes.Length + shareBytes.Length + salt.Length];
|
||||
Buffer.BlockCopy(secretBytes, 0, material, 0, secretBytes.Length);
|
||||
Buffer.BlockCopy(shareBytes, 0, material, secretBytes.Length, shareBytes.Length);
|
||||
Buffer.BlockCopy(salt, 0, material, secretBytes.Length + shareBytes.Length, salt.Length);
|
||||
return SHA256.HashData(material);
|
||||
}
|
||||
}
|
||||
@@ -109,9 +109,23 @@ public sealed class MareMediator : IHostedService, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_loopCts.Cancel();
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
if (!_loopCts.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_loopCts.Cancel();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// already disposed, swallow
|
||||
}
|
||||
}
|
||||
_loopCts.Dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -115,12 +115,15 @@ public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMa
|
||||
public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
|
||||
public record NearbyDetectionToggled(bool Enabled) : MessageBase;
|
||||
public record AllowPairRequestsToggled(bool Enabled) : MessageBase;
|
||||
public record SyncshellDiscoveryUpdated(List<SyncshellDiscoveryEntryDto> Entries) : MessageBase;
|
||||
public record SyncshellAutoDetectStateChanged(string Gid, bool Visible, bool PasswordTemporarilyDisabled) : MessageBase;
|
||||
public record ManualPairInviteMessage(string SourceUid, string SourceAlias, string TargetUid, string? DisplayName, string InviteId) : MessageBase;
|
||||
public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
|
||||
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase;
|
||||
public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : MessageBase;
|
||||
public record PairSyncOverrideChanged(string Uid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
||||
public record GroupSyncOverrideChanged(string Gid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
|
||||
public record NotificationStateChanged(int TotalCount) : MessageBase;
|
||||
|
||||
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
|
||||
#pragma warning restore S2094
|
||||
|
||||
73
MareSynchronos/Services/Notification/NotificationTracker.cs
Normal file
73
MareSynchronos/Services/Notification/NotificationTracker.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
|
||||
namespace MareSynchronos.Services.Notifications;
|
||||
|
||||
public enum NotificationCategory
|
||||
{
|
||||
AutoDetect,
|
||||
}
|
||||
|
||||
public sealed record NotificationEntry(NotificationCategory Category, string Id, string Title, string? Description, DateTime CreatedAt)
|
||||
{
|
||||
public static NotificationEntry AutoDetect(string uid, string displayName)
|
||||
=> new(NotificationCategory.AutoDetect, uid, displayName, "Nouvelle demande d'appairage via AutoDetect.", DateTime.UtcNow);
|
||||
}
|
||||
|
||||
public sealed class NotificationTracker
|
||||
{
|
||||
private readonly MareMediator _mediator;
|
||||
private readonly Dictionary<(NotificationCategory Category, string Id), NotificationEntry> _entries = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
public NotificationTracker(MareMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public void Upsert(NotificationEntry entry)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_entries[(entry.Category, entry.Id)] = entry;
|
||||
}
|
||||
PublishState();
|
||||
}
|
||||
|
||||
public void Remove(NotificationCategory category, string id)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_entries.Remove((category, id));
|
||||
}
|
||||
PublishState();
|
||||
}
|
||||
|
||||
public IReadOnlyList<NotificationEntry> GetEntries()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _entries.Values
|
||||
.OrderBy(e => e.CreatedAt)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _entries.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishState()
|
||||
{
|
||||
_mediator.Publish(new NotificationStateChanged(Count));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using MareSynchronos.API.Dto.Group;
|
||||
using MareSynchronos.PlayerData.Pairs;
|
||||
using MareSynchronos.Services.AutoDetect;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Services.ServerConfiguration;
|
||||
using MareSynchronos.UI;
|
||||
@@ -19,9 +20,10 @@ public class UiFactory
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly MareProfileManager _mareProfileManager;
|
||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||
private readonly SyncshellDiscoveryService _syncshellDiscoveryService;
|
||||
|
||||
public UiFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, ApiController apiController,
|
||||
UiSharedService uiSharedService, PairManager pairManager, ServerConfigurationManager serverConfigManager,
|
||||
UiSharedService uiSharedService, PairManager pairManager, SyncshellDiscoveryService syncshellDiscoveryService, ServerConfigurationManager serverConfigManager,
|
||||
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
@@ -29,6 +31,7 @@ public class UiFactory
|
||||
_apiController = apiController;
|
||||
_uiSharedService = uiSharedService;
|
||||
_pairManager = pairManager;
|
||||
_syncshellDiscoveryService = syncshellDiscoveryService;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_mareProfileManager = mareProfileManager;
|
||||
_performanceCollectorService = performanceCollectorService;
|
||||
@@ -37,7 +40,7 @@ public class UiFactory
|
||||
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
||||
{
|
||||
return new SyncshellAdminUI(_loggerFactory.CreateLogger<SyncshellAdminUI>(), _mareMediator,
|
||||
_apiController, _uiSharedService, _pairManager, dto, _performanceCollectorService);
|
||||
_apiController, _uiSharedService, _pairManager, _syncshellDiscoveryService, dto, _performanceCollectorService);
|
||||
}
|
||||
|
||||
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
||||
|
||||
Reference in New Issue
Block a user