using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; using MareSynchronos.WebAPI.AutoDetect; using MareSynchronos.MareConfiguration; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType; namespace MareSynchronos.Services.AutoDetect; public class AutoDetectRequestService { private readonly ILogger _logger; private readonly DiscoveryConfigProvider _configProvider; private readonly DiscoveryApiClient _client; private readonly MareConfigService _configService; private readonly DalamudUtilService _dalamud; private readonly MareMediator _mediator; private readonly object _syncRoot = new(); private readonly Dictionary _activeCooldowns = new(StringComparer.Ordinal); private readonly Dictionary _refusalTrackers = new(StringComparer.Ordinal); private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5); private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15); public AutoDetectRequestService(ILogger logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService) { _logger = logger; _configProvider = configProvider; _client = client; _configService = configService; _mediator = mediator; _dalamud = dalamudUtilService; } public async Task SendRequestAsync(string token, string? uid = null, string? targetDisplayName = null, CancellationToken ct = default) { if (!_configService.Current.AllowAutoDetectPairRequests) { _logger.LogDebug("Nearby request blocked: AllowAutoDetectPairRequests is disabled"); _mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info)); return false; } var targetKey = BuildTargetKey(uid, token, targetDisplayName); if (!string.IsNullOrEmpty(targetKey)) { var now = DateTime.UtcNow; lock (_syncRoot) { if (_refusalTrackers.TryGetValue(targetKey, out var tracker)) { if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now) { PublishLockNotification(tracker.LockUntil.Value - now); return false; } if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now) { tracker.LockUntil = null; tracker.Count = 0; if (tracker.Count == 0 && tracker.LockUntil == null) { _refusalTrackers.Remove(targetKey); } } } if (_activeCooldowns.TryGetValue(targetKey, out var lastSent)) { var elapsed = now - lastSent; if (elapsed < RequestCooldown) { PublishCooldownNotification(RequestCooldown - elapsed); return false; } if (elapsed >= RequestCooldown) { _activeCooldowns.Remove(targetKey); } } } } var endpoint = _configProvider.RequestEndpoint; if (string.IsNullOrEmpty(endpoint)) { _logger.LogDebug("No request endpoint configured"); _mediator.Publish(new NotificationMessage("Nearby request failed", "Server does not expose request endpoint.", NotificationType.Error)); return false; } string? displayName = null; try { var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false); displayName = me?.Name.TextValue; } catch { } _logger.LogInformation("Nearby: sending pair request via {endpoint}", endpoint); var ok = await _client.SendRequestAsync(endpoint!, token, displayName, ct).ConfigureAwait(false); if (ok) { if (!string.IsNullOrEmpty(targetKey)) { lock (_syncRoot) { _activeCooldowns[targetKey] = DateTime.UtcNow; if (_refusalTrackers.TryGetValue(targetKey, out var tracker)) { tracker.Count = 0; tracker.LockUntil = null; if (tracker.Count == 0 && tracker.LockUntil == null) { _refusalTrackers.Remove(targetKey); } } } } _mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info)); } else { if (!string.IsNullOrEmpty(targetKey)) { var now = DateTime.UtcNow; lock (_syncRoot) { _activeCooldowns.Remove(targetKey); if (!_refusalTrackers.TryGetValue(targetKey, out var tracker)) { tracker = new RefusalTracker(); _refusalTrackers[targetKey] = tracker; } if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now) { tracker.LockUntil = null; tracker.Count = 0; } tracker.Count++; if (tracker.Count >= 3) { tracker.LockUntil = now.Add(RefusalLockDuration); } } } _mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning)); } return ok; } public async Task SendAcceptNotifyAsync(string targetUid, CancellationToken ct = default) { var endpoint = _configProvider.AcceptEndpoint; if (string.IsNullOrEmpty(endpoint)) { _logger.LogDebug("No accept endpoint configured"); return false; } string? displayName = null; try { var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false); displayName = me?.Name.TextValue; } catch { } _logger.LogInformation("Nearby: sending accept notify via {endpoint}", endpoint); return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false); } private static string? BuildTargetKey(string? uid, string? token, string? displayName) { if (!string.IsNullOrEmpty(uid)) return "uid:" + uid; if (!string.IsNullOrEmpty(token)) return "token:" + token; if (!string.IsNullOrEmpty(displayName)) return "name:" + displayName; return null; } private void PublishCooldownNotification(TimeSpan remaining) { var durationText = FormatDuration(remaining); _mediator.Publish(new NotificationMessage("Nearby request en attente", $"Nearby request déjà envoyée. Merci d'attendre environ {durationText} avant de réessayer.", NotificationType.Info, TimeSpan.FromSeconds(5))); } private void PublishLockNotification(TimeSpan remaining) { var durationText = FormatDuration(remaining); _mediator.Publish(new NotificationMessage("Nearby request bloquée", $"Nearby request bloquée après plusieurs refus. Réessayez dans {durationText}.", NotificationType.Warning, TimeSpan.FromSeconds(5))); } private static string FormatDuration(TimeSpan remaining) { if (remaining.TotalMinutes >= 1) { var minutes = Math.Max(1, (int)Math.Ceiling(remaining.TotalMinutes)); return minutes == 1 ? "1 minute" : minutes + " minutes"; } var seconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds)); return seconds == 1 ? "1 seconde" : seconds + " secondes"; } private sealed class RefusalTracker { public int Count; public DateTime? LockUntil; } }