using System.Text.Json; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Notifications; using RadicadeServer.DB; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Messages; using RadicadeServer.RecNet.Rooms; namespace RadicadeServer.Admin; public static class RoomBanStore { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; private static readonly object FileLock = new(); private const long CoachAccountId = 1; private static string ClosedMessage(string roomName, string reason) => $"We have closed the room '{roomName}' for: {reason}. We encourage creators to review our Creator Code of Conduct to help make Radicade a fun and safe place for everyone."; private static string RestoredMessage(string roomName) => $"We have restored the room '{roomName}' after a thorough review of the previous accusations. Thank you for continuing to follow the Creator Code of Conduct, and making Radicade a better place."; public static string FilePath => Path.Join(DBConfig.PersistentLogs, "banned-rooms.json"); public static List Load() { lock (FileLock) return LoadLocked(); } public static BannedRoom? Find(long roomId) => Load().FirstOrDefault(x => x.RoomId == roomId); public static bool IsBanned(long roomId) => Find(roomId) != null; public static async Task BanAsync(RecDBContext db, RoomData room, string reason, string bannedBy) { var entry = new BannedRoom { RoomId = room.RoomId, Name = room.Name, Reason = reason, BannedBy = bannedBy, BannedAt = DateTime.UtcNow, PublishedAt = room.PublishedAt, Room = room, SubRooms = await db.SubRooms.AsNoTracking().Where(x => x.RoomId == room.RoomId).ToListAsync(), Roles = await db.RoomRoles.AsNoTracking().Where(x => x.RoomId == room.RoomId).ToListAsync(), Stats = await db.RoomStats.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == room.RoomId) }; Upsert(entry); db.Rooms.Remove(room); await db.SaveChangesAsync(); var why = string.IsNullOrWhiteSpace(reason) ? "a breach of the Creator Code of Conduct" : reason; await TellOwnerAsync(room.CreatorAccountId, ClosedMessage(room.Name, why)); return entry; } public static async Task RestoreAsync(RecDBContext db, long roomId) { var entry = Find(roomId); if (entry == null) return false; await InsertRoomWithItsIdAsync(db, entry.Room, entry.PublishedAt); Remove(roomId); await TellOwnerAsync(entry.Room.CreatorAccountId, RestoredMessage(entry.Name)); return true; } private static async Task InsertRoomWithItsIdAsync(RecDBContext db, RoomData room, DateTime publishedAt) { var entity = db.Model.FindEntityType(typeof(RoomData)) ?? throw new InvalidOperationException("RoomData is not part of the model."); var table = entity.GetTableName() ?? nameof(RoomData); var qualified = entity.GetSchema() is { Length: > 0 } schema ? $"[{schema}].[{table}]" : $"[{table}]"; var columns = new List(); var cells = new List(); var parameters = new List(); foreach (var property in entity.GetProperties()) { if (property.IsShadowProperty() || property.PropertyInfo == null) continue; var value = property.Name == nameof(RoomData.PublishedAt) ? publishedAt : property.PropertyInfo.GetValue(room); columns.Add($"[{property.GetColumnName()}]"); var sqlValue = ToSqlValue(value); if (sqlValue == null) { cells.Add("NULL"); } else { cells.Add("{" + parameters.Count + "}"); parameters.Add(sqlValue); } } var sql = $"SET IDENTITY_INSERT {qualified} ON;\n" + $"INSERT INTO {qualified} ({string.Join(", ", columns)}) VALUES ({string.Join(", ", cells)});\n" + $"SET IDENTITY_INSERT {qualified} OFF;"; await db.Database.ExecuteSqlRawAsync(sql, parameters); } private static async Task TellOwnerAsync(long ownerAccountId, string message) { if (ownerAccountId <= 0) return; try { await Notify.SendMessage(ownerAccountId, new Message { Type = MessageType.CoachMessage, Data = message, FromPlayerId = CoachAccountId }); } catch (Exception) { } } private static object? ToSqlValue(object? value) => value switch { null => null, Enum enumValue => Convert.ChangeType(enumValue, Enum.GetUnderlyingType(enumValue.GetType())), _ => value }; private static void Upsert(BannedRoom entry) { lock (FileLock) { var rooms = LoadLocked(); rooms.RemoveAll(x => x.RoomId == entry.RoomId); rooms.Add(entry); SaveLocked(rooms); } } private static void Remove(long roomId) { lock (FileLock) { var rooms = LoadLocked(); if (rooms.RemoveAll(x => x.RoomId == roomId) == 0) return; SaveLocked(rooms); } } private static List LoadLocked() { if (!File.Exists(FilePath)) return new List(); try { var json = File.ReadAllText(FilePath); var rooms = JsonSerializer.Deserialize>(json, JsonOptions) ?? new List(); foreach (var room in rooms) room.Room.PublishedAt = room.PublishedAt; return rooms; } catch (Exception) { return new List(); } } private static void SaveLocked(List rooms) { Directory.CreateDirectory(DBConfig.PersistentLogs); File.WriteAllText(FilePath, JsonSerializer.Serialize(rooms, JsonOptions)); } } public class BannedRoom { public long RoomId { get; set; } public string Name { get; set; } = ""; public string Reason { get; set; } = ""; public string BannedBy { get; set; } = ""; public DateTime BannedAt { get; set; } public DateTime PublishedAt { get; set; } public RoomData Room { get; set; } = new(); public List SubRooms { get; set; } = new(); public List Roles { get; set; } = new(); public RoomStats? Stats { get; set; } }