Files
LightlessClient/LightlessSync/Services/PairRequestService.cs
2025-11-25 07:14:59 +09:00

298 lines
9.0 KiB
C#

using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Services;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class PairRequestService : DisposableMediatorSubscriberBase
{
private readonly DalamudUtilService _dalamudUtil;
private readonly PairUiService _pairUiService;
private readonly Lazy<WebAPI.ApiController> _apiController;
private readonly Lock _syncRoot = new();
private readonly List<PairRequestEntry> _requests = [];
private readonly Dictionary<string, string> _displayNameCache = new(StringComparer.Ordinal);
private static readonly TimeSpan _expiration = TimeSpan.FromMinutes(5);
public PairRequestService(
ILogger<PairRequestService> logger,
LightlessMediator mediator,
DalamudUtilService dalamudUtil,
PairUiService pairUiService,
Lazy<WebAPI.ApiController> apiController)
: base(logger, mediator)
{
_dalamudUtil = dalamudUtil;
_pairUiService = pairUiService;
_apiController = apiController;
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(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<PairRequestDisplay> GetActiveRequests()
{
List<PairRequestEntry> 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)
{
_displayNameCache.Remove(hashedCid);
}
}
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;
}
if (TryGetCachedDisplayName(hashedCid, out var cached))
{
return cached;
}
var resolved = ResolveDisplayNameInternal(hashedCid);
if (!string.IsNullOrWhiteSpace(resolved))
{
CacheDisplayName(hashedCid, resolved);
return resolved;
}
return string.Empty;
}
private string ResolveDisplayNameInternal(string hashedCid)
{
var (name, address) = _dalamudUtil.FindPlayerByNameHash(hashedCid);
if (!string.IsNullOrWhiteSpace(name))
{
var worldName = _dalamudUtil.GetWorldNameFromPlayerAddress(address);
return !string.IsNullOrWhiteSpace(worldName)
? $"{name} @ {worldName}"
: name;
}
var snapshot = _pairUiService.GetSnapshot();
var pair = snapshot.PairsByUid.Values
.Where(p => !string.IsNullOrEmpty(p.GetPlayerNameHash()))
.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;
var removedAny = false;
for (var i = _requests.Count - 1; i >= 0; i--)
{
var entry = _requests[i];
if (now - entry.ReceivedAt <= _expiration)
{
continue;
}
_displayNameCache.Remove(entry.HashedCid);
_requests.RemoveAt(i);
removedAny = true;
}
return removedAny;
}
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, string displayName)
{
RemoveRequest(hashedCid);
Mediator.Publish(new NotificationMessage("Pair request declined",
"Declined " + displayName + "'s pending pair request.",
NotificationType.Info,
TimeSpan.FromSeconds(3)));
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);
private bool TryGetCachedDisplayName(string hashedCid, out string displayName)
{
lock (_syncRoot)
{
if (!string.IsNullOrWhiteSpace(hashedCid) && _displayNameCache.TryGetValue(hashedCid, out var cached))
{
displayName = cached;
return true;
}
}
displayName = string.Empty;
return false;
}
private void CacheDisplayName(string hashedCid, string displayName)
{
if (string.IsNullOrWhiteSpace(hashedCid) || string.IsNullOrWhiteSpace(displayName) || string.Equals(hashedCid, displayName, StringComparison.Ordinal))
{
return;
}
lock (_syncRoot)
{
_displayNameCache[hashedCid] = displayName;
}
}
}