using Microsoft.EntityFrameworkCore; using RadicadeServer.DB; namespace RadicadeServer.Utils { public static class GameBlockMonitor { private static readonly object Lock = new(); private static readonly SemaphoreSlim RefreshGate = new(1, 1); private static Dictionary Active = new(); private static DateTime LastRefresh = DateTime.MinValue; private const double RefreshIntervalSeconds = 10; public static async Task IsBlockedAsync(IDbContextFactory factory, string ipAddress) { if (string.IsNullOrWhiteSpace(ipAddress)) return false; DateTime lastRefresh; lock (Lock) lastRefresh = LastRefresh; if ((DateTime.UtcNow - lastRefresh).TotalSeconds > RefreshIntervalSeconds) await RefreshAsync(factory); lock (Lock) { if (Active.TryGetValue(NormalizeIp(ipAddress), out var expiresAt)) return !expiresAt.HasValue || expiresAt.Value > DateTime.UtcNow; return false; } } public static void Invalidate() { lock (Lock) LastRefresh = DateTime.MinValue; } public static async Task RefreshAsync(IDbContextFactory factory) { await RefreshGate.WaitAsync(); try { DateTime lastRefresh; lock (Lock) lastRefresh = LastRefresh; if ((DateTime.UtcNow - lastRefresh).TotalSeconds <= RefreshIntervalSeconds) return; try { await using var db = await factory.CreateDbContextAsync(); var now = DateTime.UtcNow; var rows = await db.GameBlocks.AsNoTracking().ToListAsync(); var expired = rows.Where(b => b.ExpiresAt.HasValue && b.ExpiresAt.Value < now).ToList(); if (expired.Count > 0) { db.GameBlocks.RemoveRange(expired); await db.SaveChangesAsync(); } var active = new Dictionary(); foreach (var row in rows.Where(b => !expired.Contains(b))) active[NormalizeIp(row.IpAddress)] = row.ExpiresAt; lock (Lock) { Active = active; LastRefresh = DateTime.UtcNow; } } catch { lock (Lock) LastRefresh = DateTime.UtcNow; } } finally { RefreshGate.Release(); } } public static string NormalizeIp(string ip) { ip = (ip ?? "").Trim(); if (ip.StartsWith("::ffff:", StringComparison.OrdinalIgnoreCase)) ip = ip.Substring("::ffff:".Length); return ip; } } }