using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Notifications; using RadicadeServer.DB; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Enums.Moderation; using RadicadeServer.RecNet.Messages; using RadicadeServer.RecNet.Moderation; using RadicadeServer.RecNet.Notifications; namespace RadicadeServer.Utils { public static class VoiceChatRestrictions { public const string NotificationId = "VoiceChatRestriction"; public const string Method = "Voice Restriction"; public const long CoachAccountId = 1; public const string CoachMessage = "You have had your voice privileges temporarily restricted due to speech which violates the Code of Conduct."; public static readonly TimeSpan DefaultDuration = TimeSpan.FromHours(24); public static readonly TimeSpan MinimumDuration = TimeSpan.FromHours(1); public static readonly TimeSpan MaximumDuration = TimeSpan.FromDays(30); public static TimeSpan ClampDuration(TimeSpan duration) { if (duration < MinimumDuration) return MinimumDuration; if (duration > MaximumDuration) return MaximumDuration; return duration; } public static Task ActiveAsync(RecDBContext db, long playerId) => db.VoiceChatRestrictions.AsNoTracking() .Where(x => x.PlayerId == playerId && x.ClearedAt == null && x.ExpiresAt > DateTime.UtcNow) .OrderByDescending(x => x.ExpiresAt) .FirstOrDefaultAsync(); public static async Task IsRestrictedAsync(RecDBContext db, long playerId) => await ActiveAsync(db, playerId) != null; public static async Task ApplyAsync( RecDBContext db, long playerId, string reason, ReportCategory category, TimeSpan duration, string issuedBy, string? issuedByUsername = null, string? moderatorNote = null) { var now = DateTime.UtcNow; var clamped = ClampDuration(duration); var restriction = await db.VoiceChatRestrictions .Where(x => x.PlayerId == playerId && x.ClearedAt == null && x.ExpiresAt > now) .OrderByDescending(x => x.ExpiresAt) .FirstOrDefaultAsync(); if (restriction == null) { restriction = new VoiceChatRestriction { PlayerId = playerId, StartedAt = now, ExpiresAt = now + clamped }; await db.VoiceChatRestrictions.AddAsync(restriction); } else { var extended = now + clamped; if (extended > restriction.ExpiresAt) restriction.ExpiresAt = extended; restriction.StartedAt = now; } restriction.Reason = reason; restriction.Category = category; restriction.IssuedBy = issuedBy; restriction.IssuedByUsername = issuedByUsername; restriction.ModeratorNote = moderatorNote; await db.SaveChangesAsync(); await TellPlayerAsync(playerId); return restriction; } private static async Task TellPlayerAsync(long playerId) { try { await Notify.SendMessage(playerId, new Message { Type = MessageType.CoachMessage, Data = CoachMessage, FromPlayerId = CoachAccountId }); } catch { } } public static async Task ClearAsync(RecDBContext db, long playerId, string? clearedBy = null) { var now = DateTime.UtcNow; var active = await db.VoiceChatRestrictions .Where(x => x.PlayerId == playerId && x.ClearedAt == null && x.ExpiresAt > now) .ToListAsync(); if (active.Count == 0) return false; foreach (var restriction in active) { restriction.ClearedAt = now; restriction.ClearedBy = clearedBy; } await db.SaveChangesAsync(); return true; } public static object Payload(VoiceChatRestriction? restriction) => new { restricted = restriction != null, playerId = restriction?.PlayerId ?? 0, reason = restriction?.Reason ?? "", category = restriction?.Category.ToString() ?? "", startedAtUtc = restriction?.StartedAt, expiresAtUtc = restriction?.ExpiresAt, secondsRemaining = restriction == null ? 0 : (int)Math.Max(0, (restriction.ExpiresAt - DateTime.UtcNow).TotalSeconds) }; public static async Task PushAsync(long playerId, VoiceChatRestriction? restriction) { try { await Notify.SendNotify(playerId, new Notification { Id = NotificationId, Msg = Payload(restriction) }); } catch { } } } }