using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; namespace LightlessSync.Services; public sealed class PairRequestService : DisposableMediatorSubscriberBase { private readonly DalamudUtilService _dalamudUtil; private readonly PairManager _pairManager; private readonly Lazy _apiController; private readonly object _syncRoot = new(); private readonly List _requests = []; private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5); public PairRequestService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtil, PairManager pairManager, Lazy apiController) : base(logger, mediator) { _dalamudUtil = dalamudUtil; _pairManager = pairManager; _apiController = apiController; Mediator.Subscribe(this, _ => { bool removed; lock (_syncRoot) { removed = CleanupExpiredUnsafe(); } if (removed) { Mediator.Publish(new PairRequestsUpdatedMessage()); } }); } public PairRequestDisplay RegisterIncomingRequest(string hashedCid, string messageTemplate) { if (string.IsNullOrWhiteSpace(hashedCid)) { hashedCid = string.Empty; } messageTemplate ??= string.Empty; PairRequestEntry entry = new(hashedCid, messageTemplate, DateTime.UtcNow); lock (_syncRoot) { CleanupExpiredUnsafe(); var index = _requests.FindIndex(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal)); if (index >= 0) { _requests[index] = entry; } else { _requests.Add(entry); } } var display = _dalamudUtil.IsOnFrameworkThread ? ToDisplay(entry) : _dalamudUtil.RunOnFrameworkThread(() => ToDisplay(entry)).GetAwaiter().GetResult(); Mediator.Publish(new PairRequestsUpdatedMessage()); return display; } public IReadOnlyList GetActiveRequests() { List entries; lock (_syncRoot) { CleanupExpiredUnsafe(); entries = _requests .OrderByDescending(r => r.ReceivedAt) .ToList(); } return _dalamudUtil.IsOnFrameworkThread ? entries.Select(ToDisplay).ToList() : _dalamudUtil.RunOnFrameworkThread(() => entries.Select(ToDisplay).ToList()).GetAwaiter().GetResult(); } public bool RemoveRequest(string hashedCid) { bool removed; lock (_syncRoot) { removed = _requests.RemoveAll(r => string.Equals(r.HashedCid, hashedCid, StringComparison.Ordinal)) > 0; } if (removed) { Mediator.Publish(new PairRequestsUpdatedMessage()); } return removed; } public bool HasPendingRequests() { lock (_syncRoot) { CleanupExpiredUnsafe(); return _requests.Count > 0; } } private PairRequestDisplay ToDisplay(PairRequestEntry entry) { var displayName = ResolveDisplayName(entry.HashedCid); var message = FormatMessage(entry.MessageTemplate, displayName); return new PairRequestDisplay(entry.HashedCid, displayName, message, entry.ReceivedAt); } private string ResolveDisplayName(string hashedCid) { if (string.IsNullOrWhiteSpace(hashedCid)) { return string.Empty; } var (name, address) = _dalamudUtil.FindPlayerByNameHash(hashedCid); if (!string.IsNullOrWhiteSpace(name)) { var worldName = _dalamudUtil.GetWorldNameFromPlayerAddress(address); return !string.IsNullOrWhiteSpace(worldName) ? $"{name} @ {worldName}" : name; } var pair = _pairManager .GetOnlineUserPairs() .FirstOrDefault(p => string.Equals(p.Ident, hashedCid, StringComparison.Ordinal)); if (pair != null) { if (!string.IsNullOrWhiteSpace(pair.PlayerName)) { return pair.PlayerName; } if (!string.IsNullOrWhiteSpace(pair.UserData.AliasOrUID)) { return pair.UserData.AliasOrUID; } } return string.Empty; } private static string FormatMessage(string template, string displayName) { var safeName = string.IsNullOrWhiteSpace(displayName) ? "Someone" : displayName; template ??= string.Empty; const string placeholder = "{DisplayName}"; if (!string.IsNullOrEmpty(template) && template.Contains(placeholder, StringComparison.Ordinal)) { return template.Replace(placeholder, safeName, StringComparison.Ordinal); } if (!string.IsNullOrWhiteSpace(template)) { return $"{safeName}: {template}"; } return $"{safeName} sent you a pair request."; } private bool CleanupExpiredUnsafe() { if (_requests.Count == 0) { return false; } var now = DateTime.UtcNow; return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0; } public void AcceptPairRequest(string hashedCid, string displayName) { _ = Task.Run(async () => { try { await _apiController.Value.TryPairWithContentId(hashedCid).ConfigureAwait(false); RemoveRequest(hashedCid); var displayText = string.IsNullOrEmpty(displayName) ? hashedCid : displayName; Mediator.Publish(new NotificationMessage( "Pair request accepted", $"Sent a pair request back to {displayText}.", NotificationType.Info, TimeSpan.FromSeconds(3))); } catch (Exception ex) { Logger.LogError(ex, "Failed to accept pair request for {HashedCid}", hashedCid); Mediator.Publish(new NotificationMessage( "Failed to Accept Pair Request", ex.Message, NotificationType.Error, TimeSpan.FromSeconds(5))); } }); } public void DeclinePairRequest(string hashedCid) { RemoveRequest(hashedCid); Logger.LogDebug("Declined pair request from {HashedCid}", hashedCid); } private record struct PairRequestEntry(string HashedCid, string MessageTemplate, DateTime ReceivedAt); public readonly record struct PairRequestDisplay(string HashedCid, string DisplayName, string Message, DateTime ReceivedAt); }