using System.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Vanadium.Classes.DBs; using Vanadium.Classes.DBs.DBClasses; using Vanadium.Utils.NotiController; using Vanadium.Classes; using Vanadium.Services; using SixLabors.ImageSharp.PixelFormats; using System.Net.WebSockets; using System.Text.Json; using System.Text.Json.Nodes; using System.Globalization; using System.Diagnostics; using Vanadium.enums; using static Vanadium.Classes.DBs.DBClasses.RoomDBClasses; using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses; using static Vanadium.Classes.DBs.DBClasses.AnnoucementDBClasses; using System.Text.Json.Serialization; namespace Vanadium.Controllers { [ApiController] [TypeFilter(typeof(DirectorAuditFilter))] public class WebsiteController : ControllerBase { private const string DiscordClientId = "1512856861374812281"; private const string DiscordClientSecret = "9Rpo6lwGutlv-fiMnrzGMn99AIkAfeLK"; private const string DiscordRedirectUri = "https://reloxa.xyz/VanNet/Director"; private const string RequiredGuildId = "1350986037526007919"; private const string RequiredRoleId = "1517772785773051965"; // dont give urself this role or change it private static readonly TimeSpan SessionDuration = TimeSpan.FromMinutes(45); internal static readonly Dictionary DirectorSessions = new(); internal static readonly object SessionLock = new(); internal class DirectorSession { public string Token { get; set; } = ""; public DateTime ExpiresAt { get; set; } public string? DiscordUserId { get; set; } public WebSocket? Socket { get; set; } } private bool ValidateDirectorToken(out DirectorSession? session) { session = null; string? token = Request.Headers["X-Director-Token"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(token)) token = Request.Cookies["director_session"]; if (string.IsNullOrWhiteSpace(token)) return false; lock (SessionLock) { if (!DirectorSessions.TryGetValue(token, out session)) return false; if (session.ExpiresAt <= DateTime.UtcNow) { DirectorSessions.Remove(token); session = null; return false; } session.ExpiresAt = DateTime.UtcNow.Add(SessionDuration); } return true; } private bool ValidateDirectorCookie(out DirectorSession? session) { session = null; var token = Request.Cookies["director_session"]; if (string.IsNullOrWhiteSpace(token)) return false; lock (SessionLock) { if (!DirectorSessions.TryGetValue(token, out session)) return false; if (session.ExpiresAt <= DateTime.UtcNow) { DirectorSessions.Remove(token); session = null; return false; } } return true; } private string ResolveMime(string filename) => Path.GetExtension(filename).ToLowerInvariant() switch { ".css" => "text/css", ".js" => "application/javascript", ".png" => "image/png", ".jpg" or ".jpeg" => "image/jpeg", ".svg" => "image/svg+xml", ".html" => "text/html", _ => "application/octet-stream" }; [HttpGet("/p/Share")] public IActionResult ServeIndex() { string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "VanNet.html"); if (!System.IO.File.Exists(path)) return NotFound(""); return PhysicalFile(path, "text/html"); } [HttpGet("/vannet-but-secret")] public IActionResult ServeVanNet() { //string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "index.html"); //if (!System.IO.File.Exists(path)) return NotFound(""); //return PhysicalFile(path, "text/html"); return StatusCode(301, new { message = "Website has been moved to /vannet", StatusCode = 301 }); } [HttpGet("/image/{imageId}")] public IActionResult ServeImagePreviewShitty() { string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "image.html"); if (!System.IO.File.Exists(path)) return NotFound(""); return PhysicalFile(path, "text/html"); } [HttpGet("/vannet")] public IActionResult ServeVanNetForReal() { string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "New folder", "index.html");//Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "index.html"); // delete when you add recnet i think its funny if (!System.IO.File.Exists(path)) return NotFound("something went wrong"); Response.Headers.CacheControl = "no-cache"; return PhysicalFile(path, "text/html"); } [HttpGet("/vannet/asset/{filename}")] public IActionResult ServeVanNetAsset(string filename) { string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "asset", filename); if (!System.IO.File.Exists(path)) return NotFound(""); Response.Headers.CacheControl = "no-cache"; return PhysicalFile(path, ResolveMime(filename)); } [HttpGet("/vannet/Director")] public IActionResult ServeAuthorize() { string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "Director", "Authorize.html"); if (!System.IO.File.Exists(path)) return NotFound(""); return PhysicalFile(path, "text/html"); } [HttpGet("/vannet/Director/Panel")] [HttpGet("/vannet/Director/AdminController")] public IActionResult ServeDirectorPanel() { if (!ValidateDirectorCookie(out _)) return Redirect("/vannet/Director"); string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "Director", "Director.html"); if (!System.IO.File.Exists(path)) return NotFound(""); Response.Headers.CacheControl = "no-cache"; return PhysicalFile(path, "text/html"); } [HttpGet("/vannet/Director/asset/{filename}")] public IActionResult ServeDirectorAsset(string filename) { if (!ValidateDirectorCookie(out _)) return NotFound(""); string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "Director", filename); if (!System.IO.File.Exists(path)) return NotFound(""); Response.Headers.CacheControl = "no-cache"; return PhysicalFile(path, ResolveMime(filename)); } [HttpGet("/RecNet/Frontend/{filename}")] public IActionResult ServeFrontendAsset(string filename) { string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", filename); if (!System.IO.File.Exists(path)) return NotFound(""); return PhysicalFile(path, ResolveMime(filename)); } [HttpGet("/RecNet/Frontend/Director/{filename}")] public IActionResult ServeDirectorAssetLegacy(string filename) { if (!ValidateDirectorCookie(out _)) return NotFound(""); string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "Director", filename); if (!System.IO.File.Exists(path)) return NotFound(""); Response.Headers.CacheControl = "no-cache"; return PhysicalFile(path, ResolveMime(filename)); } [HttpPost("/vannet/api/director/discord-callback")] public async Task DirectorDiscordCallback([FromBody] DirectorDiscordCallbackRequest body) { if (string.IsNullOrWhiteSpace(body?.Code)) return BadRequest(new { success = false, error = "Code required." }); using var http = new HttpClient(); var tokenResp = await http.PostAsync("https://discord.com/api/oauth2/token", new FormUrlEncodedContent(new Dictionary { ["client_id"] = DiscordClientId, ["client_secret"] = DiscordClientSecret, ["grant_type"] = "authorization_code", ["code"] = body.Code, ["redirect_uri"] = DiscordRedirectUri })); if (!tokenResp.IsSuccessStatusCode) return Ok(new { success = false, error = "The code failed to exchange" }); JsonElement tokenData; try { tokenData = JsonSerializer.Deserialize(await tokenResp.Content.ReadAsStringAsync()); } catch { return Ok(new { success = false, error = "Wrong discord response" }); } if (!tokenData.TryGetProperty("access_token", out var atEl)) return Ok(new { success = false, error = "No access token from discord" }); string accessToken = atEl.GetString() ?? ""; http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); var memberResp = await http.GetAsync($"https://discord.com/api/users/@me/guilds/{RequiredGuildId}/member"); if (!memberResp.IsSuccessStatusCode) return Ok(new { success = false, error = "You arent even in the discord" }); JsonElement memberData; try { memberData = JsonSerializer.Deserialize(await memberResp.Content.ReadAsStringAsync()); } catch { return Ok(new { success = false, error = "Something parsed wrong and your member data wasnt given correctly" }); } if (!memberData.TryGetProperty("roles", out var rolesEl) || !rolesEl.EnumerateArray().Any(r => r.GetString() == RequiredRoleId)) return Ok(new { success = false, error = "You arent a dev therefore you cant access the panel" }); string? discordUserId = null; if (memberData.TryGetProperty("user", out var userEl) && userEl.TryGetProperty("id", out var idEl)) discordUserId = idEl.GetString(); string sessionToken = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); var expiry = DateTime.UtcNow.Add(SessionDuration); lock (SessionLock) { DirectorSessions[sessionToken] = new DirectorSession { Token = sessionToken, ExpiresAt = expiry, DiscordUserId = discordUserId }; } Response.Cookies.Append("director_session", sessionToken, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict, Expires = DateTimeOffset.UtcNow.Add(SessionDuration) }); return Ok(new { success = true, token = sessionToken }); } [HttpGet("/vannet/ws/director-session")] public async Task DirectorSessionWebSocket() { if (!HttpContext.WebSockets.IsWebSocketRequest) { HttpContext.Response.StatusCode = 400; return; } string token = Request.Query["token"].ToString(); if (string.IsNullOrWhiteSpace(token)) { HttpContext.Response.StatusCode = 401; return; } lock (SessionLock) { if (!DirectorSessions.TryGetValue(token, out var chk) || chk.ExpiresAt <= DateTime.UtcNow) { HttpContext.Response.StatusCode = 401; return; } } var ws = await HttpContext.WebSockets.AcceptWebSocketAsync(); lock (SessionLock) { if (DirectorSessions.TryGetValue(token, out var s)) s.Socket = ws; } using var cts = new CancellationTokenSource(); var recvBuffer = new byte[1024]; var monitor = Task.Run(async () => { while (true) { try { await Task.Delay(5000, cts.Token); } catch (TaskCanceledException) { return; } if (ws.State != WebSocketState.Open) return; bool expired; lock (SessionLock) { expired = !DirectorSessions.TryGetValue(token, out var s) || s.ExpiresAt <= DateTime.UtcNow; } if (!expired) continue; try { byte[] payload = JsonSerializer.SerializeToUtf8Bytes(new { type = "session_expired", message = "Your session has expired. Please re-authorize." }); await ws.SendAsync(new ArraySegment(payload), WebSocketMessageType.Text, true, CancellationToken.None); await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Session expired", CancellationToken.None); } catch { } return; } }); try { while (ws.State == WebSocketState.Open) { var result = await ws.ReceiveAsync(new ArraySegment(recvBuffer), CancellationToken.None); if (result.MessageType == WebSocketMessageType.Close) break; } } catch { } cts.Cancel(); try { await monitor; } catch { } } [HttpGet("/vannet/api/rooms/hot")] public IActionResult GetHotRooms([FromQuery] int skip = 0, [FromQuery] int take = 20) { var allRooms = RoomDB.Rooms.FindAll() .Where(r => r.Accessibility == RoomAccessibility.Public && r.State == RoomState.Active && !r.ExcludeFromLists) .OrderByDescending(r => r.Stats.VisitCount + r.Stats.CheerCount * 2 + r.Stats.FavoriteCount * 3) .Skip(skip) .Take(take) .Select(r => new { roomId = r.RoomId, name = r.Name, description = r.Description, imageName = r.ImageName, creatorAccountId = r.CreatorAccountId, stats = r.Stats, maxPlayers = r.MaxPlayers, ageRating = r.AgeRating, createdAt = r.CreatedAt, tags = r.Tags, supportsMobile = r.SupportsMobile, supportsQuest2 = r.SupportsQuest2, supportsWalkVR = r.SupportsWalkVR }) .ToList(); return Ok(new { Results = allRooms, TotalResults = allRooms.Count }); } /*[HttpPost("api/rooms/import-zip")] public async Task ImportRoomFromZip([FromForm] IFormFile file, [FromForm] DateTime? cutoffDate, [FromForm] DateTime? targetDate) { if (file == null || file.Length == 0) return BadRequest(new { error = "No file provided." }); string ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (ext != ".zip" && ext != ".rar") return BadRequest(new { error = "Only .zip and .rar files are accepted." }); var effectiveCutoff = cutoffDate?.ToUniversalTime() ?? new DateTime(2023, 04, 16, 23, 59, 59, DateTimeKind.Utc); var effectiveTarget = targetDate?.ToUniversalTime() ?? new DateTime(2023, 04, 15, 0, 0, 0, DateTimeKind.Utc); string tempExtractPath = Path.Combine(Path.GetTempPath(), $"room_import_{Guid.NewGuid()}"); Directory.CreateDirectory(tempExtractPath); try { string tempFilePath = Path.Combine(tempExtractPath, file.FileName); using (var fs = new FileStream(tempFilePath, FileMode.Create)) await file.CopyToAsync(fs); string extractPath = Path.Combine(tempExtractPath, "extracted"); Directory.CreateDirectory(extractPath); if (ext == ".zip") { System.IO.Compression.ZipFile.ExtractToDirectory(tempFilePath, extractPath); } else { using var archive = SharpCompress.Archives.ArchiveFactory.Open(tempFilePath); foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) entry.WriteToDirectory(extractPath, new SharpCompress.Common.ExtractionOptions { ExtractFullPath = true, Overwrite = true }); } var topLevelDirs = Directory.GetDirectories(extractPath); string roomPath = topLevelDirs.Length == 1 ? topLevelDirs[0] : extractPath; string roomName = Path.GetFileName(roomPath); string roomJsonPath = Path.Combine(roomPath, $"{roomName}.json"); RoomImportRoomData? roomData = null; if (System.IO.File.Exists(roomJsonPath)) { try { var json = await System.IO.File.ReadAllTextAsync(roomJsonPath); roomData = System.Text.Json.JsonSerializer.Deserialize(json, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } catch (Exception ex) { return StatusCode(500, new { error = $"Failed to parse room JSON: {ex.Message}" }); } } string cdnPath = Path.Combine(Environment.CurrentDirectory, "Data", "cdn", "room"); string imageServerPath = Path.Combine(Environment.CurrentDirectory, "Data", "Images"); Directory.CreateDirectory(cdnPath); long newRoomId = RoomDB.CloneRoom(24, roomName, 1) ?? throw new Exception("Failed to clone MakerRoom ID 24"); var newRoom = RoomDB.GetRoom(newRoomId); if (newRoom == null) return StatusCode(500, new { error = "Cloned room not found after creation." }); newRoom.Name = roomName; newRoom.DisplayName = roomName; newRoom.Description = roomData?.Description ?? ""; newRoom.MaxPlayers = roomData?.MaxPlayers > 0 ? roomData.MaxPlayers : 8; newRoom.MaxPlayerCalculationMode = 0; newRoom.CloningAllowed = roomData?.CloningAllowed ?? false; newRoom.PersistenceVersion = 0; newRoom.UgcSubVersion = 0; newRoom.UgcVersion = 0; newRoom.MinUgcSubVersion = 0; newRoom.IsDeveloperOwned = roomData?.IsDeveloperOwned ?? false; newRoom.IsRRO = roomData?.IsRRO ?? false; newRoom.IsRecRoomApproved = roomData?.IsRecRoomApproved ?? false; newRoom.SupportsScreens = roomData?.SupportsScreens ?? true; newRoom.SupportsWalkVR = roomData?.SupportsWalkVR ?? true; newRoom.SupportsTeleportVR = roomData?.SupportsTeleportVR ?? true; newRoom.SupportsMobile = roomData?.SupportsMobile ?? true; newRoom.SupportsJuniors = roomData?.SupportsJuniors ?? true; newRoom.AgeRating = roomData?.AgeRating ?? 0; newRoom.CreatedAt = roomData?.CreatedAt != default ? roomData.CreatedAt : DateTime.UtcNow; newRoom.PublishedAt = roomData?.PublishedAt != default ? roomData.PublishedAt : DateTime.UtcNow; newRoom.Accessibility = RoomDBClasses.RoomAccessibility.Private; newRoom.State = RoomDBClasses.RoomState.Active; newRoom.CreatorAccountId = 167; newRoom.PromoImages = new List(); newRoom.LoadScreens = new List(); newRoom.WarningMask = roomData?.WarningMask ?? default; newRoom.CustomWarning = roomData?.CustomWarning; newRoom.DisableMicAutoMute = roomData?.DisableMicAutoMute ?? false; newRoom.DisableRoomComments = roomData?.DisableRoomComments ?? false; newRoom.EncryptVoiceChat = roomData?.EncryptVoiceChat ?? false; newRoom.ToxmodEnabled = roomData?.ToxmodEnabled ?? true; newRoom.SupportsVRLow = roomData?.SupportsVRLow ?? true; newRoom.SupportsQuest2 = roomData?.SupportsQuest2 ?? true; newRoom.MinLevel = roomData?.MinLevel ?? 0; newRoom.ExcludeFromLists = roomData?.ExcludeFromLists ?? false; newRoom.ExcludeFromSearch = roomData?.ExcludeFromSearch ?? false; newRoom.Stats = new RoomDBClasses.Stats { CheerCount = 0, FavoriteCount = 0, VisitorCount = 0, VisitCount = 0 }; newRoom.LoadScreens = roomData?.LoadScreens? .Select(ls => new RoomDBClasses.LoadScreens { ImageName = ls.ImageName ?? "", Title = ls.Title ?? "", Subtitle = ls.Subtitle ?? "" }) .ToList() ?? new List(); newRoom.Tags = roomData?.Tags? .Select(t => new RoomDBClasses.Tags { Tag = t.Tag?.ToLowerInvariant() ?? "", Type = RoomDBClasses.TagType.General }) .Where(t => !string.IsNullOrWhiteSpace(t.Tag)) .ToList() ?? new List(); string localImageName = "DefaultRoomImage.png"; var roomImagesDir = Path.Combine(roomPath, "RoomImages", "RoomImage"); if (Directory.Exists(roomImagesDir)) { var localImage = Directory.GetFiles(roomImagesDir).FirstOrDefault(); if (localImage != null) { string ext2 = Path.GetExtension(localImage); string safeImageName = $"{Guid.NewGuid()}{ext2}"; string imageDest = Path.Combine(imageServerPath, safeImageName); System.IO.File.Copy(localImage, imageDest); localImageName = safeImageName; } } catch { } } newRoom.ImageName = localImageName; newRoom.SubRooms = new List(); RoomDB.Rooms.Update(newRoom); RoomDB.SubRoomSaves.DeleteMany(s => s.RoomId == newRoomId); int subRoomsImported = 0; var importLog = new List(); foreach (var subRoomDir in Directory.GetDirectories(roomPath)) { string subRoomFolderName = Path.GetFileName(subRoomDir); if (subRoomFolderName.StartsWith(".")) continue; string subJsonPath = Path.Combine(subRoomDir, $"{subRoomFolderName}.json"); RoomImportSavesResponse? subSavesData = null; if (System.IO.File.Exists(subJsonPath)) { try { var subJson = await System.IO.File.ReadAllTextAsync(subJsonPath); subSavesData = System.Text.Json.JsonSerializer.Deserialize(subJson, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } catch { } } string? selectedBlob = null; DateTime selectedDate = DateTime.UtcNow; RoomImportSaveResult? bestSaveMeta = null; var directRoomFiles = Directory.GetFiles(subRoomDir, "*.room", SearchOption.TopDirectoryOnly); if (directRoomFiles.Length > 0) { string sourceRoomFile = directRoomFiles[0]; selectedBlob = Path.GetFileName(sourceRoomFile); string destRoomFile = Path.Combine(cdnPath, selectedBlob); if (!System.IO.File.Exists(destRoomFile)) System.IO.File.Copy(sourceRoomFile, destRoomFile); selectedDate = DateTime.UtcNow; bestSaveMeta = subSavesData?.Results? .Where(x => x.CreatedAt <= effectiveCutoff) .OrderBy(x => Math.Abs((x.CreatedAt - effectiveTarget).TotalSeconds)) .FirstOrDefault(); } else { var dateFolders = Directory.GetDirectories(subRoomDir) .Select(d => { string folderName = Path.GetFileName(d); if (DateTime.TryParseExact(folderName, "yyyy-MM-dd_HH-mm-ss", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal, out DateTime parsedDate)) return new { Path = d, Date = parsedDate, Valid = true }; return new { Path = d, Date = DateTime.MinValue, Valid = false }; }) .Where(x => x.Valid && x.Date <= effectiveCutoff) .OrderBy(x => Math.Abs((x.Date - effectiveTarget).TotalSeconds)) .ToList(); if (dateFolders.Count == 0) { importLog.Add(new { subRoom = subRoomFolderName, status = "skipped", reason = "no valid saves at or before cutoff date" }); continue; } var best = dateFolders.First(); var roomFiles = Directory.GetFiles(best.Path, "*.room"); if (roomFiles.Length == 0) { importLog.Add(new { subRoom = subRoomFolderName, status = "skipped", reason = "no .room file in best folder" }); continue; } string sourceRoomFile = roomFiles[0]; selectedBlob = Path.GetFileName(sourceRoomFile); string destRoomFile = Path.Combine(cdnPath, selectedBlob); if (!System.IO.File.Exists(destRoomFile)) System.IO.File.Copy(sourceRoomFile, destRoomFile); selectedDate = best.Date; bestSaveMeta = subSavesData?.Results? .Where(s => s.CreatedAt <= effectiveCutoff) .OrderBy(s => Math.Abs((s.CreatedAt - effectiveTarget).TotalSeconds)) .FirstOrDefault(); } if (string.IsNullOrWhiteSpace(selectedBlob)) continue; long newSubRoomId = RoomDB.GetNextSubRoomId(); var newSub = new RoomDBClasses.SubRooms { SubRoomId = newSubRoomId, RoomId = newRoomId, Name = subRoomFolderName, Accessibility = RoomDBClasses.RoomAccessibility.Private, UnitySceneId = "a75f7547-79eb-47c6-8986-6767abcb4f92", MaxPlayers = 8, IsSandbox = false, DataBlob = selectedBlob, SavedByAccountId = 167, CreatorAccountId = 167, ShouldAutoStageSaves = false, StagedSubRoomDataSaveId = null, LastModeratedSaveModerationState = 0 }; newRoom.SubRooms.Add(newSub); RoomDB.Rooms.Update(newRoom); var newSave = new RoomDBClasses.currentSave { SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(), RoomId = newRoomId, SubRoomId = newSubRoomId, DataBlob = selectedBlob, DataBlobHash = bestSaveMeta?.DataBlobHash ?? "", Description = $"Imported via zip. Room Name: \"{roomName}\"", SavedByAccountId = 167, SavedOnPlatform = bestSaveMeta?.SavedOnPlatform ?? 0, SavedOnDeviceClass = bestSaveMeta?.SavedOnDeviceClass ?? 1, PersistenceVersion = 0, UgcSubVersion = 0, OMVersion = 0, Tags = new List(), ModerationState = 0, CreatedAt = selectedDate }; RoomDB.SubRoomSaves.Insert(newSave); subRoomsImported++; importLog.Add(new { subRoom = newSub.Name, subRoomId = newSubRoomId, status = "imported", blob = selectedBlob, saveDate = selectedDate }); } if (subRoomsImported == 0) { RoomDB.DeleteRoom(newRoomId); return BadRequest(new { error = "No valid subrooms found at or before cutoff date. Room was not created." }); } var createdRoom = RoomDB.GetRoom(newRoomId); var allConnectedPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allConnectedPlayers) await NotiController.SendRoomUpdate(pid, createdRoom); return Ok(new { success = true, message = $"Room '{newRoom.Name}' imported successfully.", roomId = newRoomId, subRoomsImported, log = importLog, room = createdRoom }); } finally { try { Directory.Delete(tempExtractPath, true); } catch { } } }*/ /*[HttpGet("/vannet/api/players/search")] public IActionResult SearchPlayers([FromQuery] string? q, [FromQuery] int skip = 0, [FromQuery] int take = 20) { IEnumerable query = PlayerDB.Players.FindAll(); if (!string.IsNullOrWhiteSpace(q)) { string lower = q.ToLowerInvariant(); query = query.Where(p => (p.Player?.Username != null && p.Player.Username.ToLower().Contains(lower)) || (p.Player?.DisplayName != null && p.Player.DisplayName.ToLower().Contains(lower))); } var results = query .OrderByDescending(p => p.Player?.CreatedAt ?? DateTime.MinValue) .Skip(skip) .Take(take) .Select(p => new { accountId = p.PlayerId, username = p.Player?.Username, displayName = p.Player?.DisplayName, profileImage = p.Player?.ProfileImage, level = p.Player?.Level ?? 1, createdAt = p.Player?.CreatedAt, bio = p.Player?.Bio }) .ToList(); return Ok(new { Results = results, TotalResults = results.Count }); }*/ [HttpGet("/vannet/api/players/{playerId}")] public IActionResult GetPlayerProfile(long playerId) { var player = PlayerDB.Players.FindById(playerId); if (player == null || player.Player == null) return NotFound(""); var rooms = RoomDB.Rooms.FindAll() .Where(r => r.CreatorAccountId == playerId && r.Accessibility == RoomAccessibility.Public) .OrderByDescending(r => r.Stats.VisitCount) .Take(6) .Select(r => new { roomId = r.RoomId, name = r.Name, imageName = r.ImageName, stats = r.Stats }) .ToList(); return Ok(new { accountId = player.PlayerId, username = player.Player.Username, displayName = player.Player.DisplayName, profileImage = player.Player.ProfileImage, level = player.Player.Level, createdAt = player.Player.CreatedAt, bio = player.Player.Bio, rooms }); } [HttpGet("/vannet/api/rooms/{roomId:long}")] public IActionResult GetRoomDetail(long roomId) { var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(""); var creator = PlayerDB.Players.FindById(room.CreatorAccountId); return Ok(new { roomId = room.RoomId, name = room.Name, description = room.Description, imageName = room.ImageName, creatorAccountId = room.CreatorAccountId, creatorUsername = creator?.Player?.Username, stats = room.Stats, maxPlayers = room.MaxPlayers, ageRating = room.AgeRating, createdAt = room.CreatedAt, tags = room.Tags, supportsMobile = room.SupportsMobile, supportsQuest2 = room.SupportsQuest2, supportsWalkVR = room.SupportsWalkVR, supportsTeleportVR = room.SupportsTeleportVR, supportsScreens = room.SupportsScreens, accessibility = room.Accessibility, loadScreens = room.LoadScreens, promoImages = room.PromoImages }); } [HttpGet("/vannet/api/director/players/search")] public async Task DirectorSearchPlayers([FromQuery] string? q, [FromQuery] int skip = 0, [FromQuery] int take = 50) { if (!ValidateDirectorToken(out _)) return NotFound(""); IEnumerable query = PlayerDB.Players.FindAll(); if (!string.IsNullOrWhiteSpace(q)) { string lower = q.ToLowerInvariant(); bool isId = long.TryParse(q, out long searchId); query = query.Where(p => (isId && p.PlayerId == searchId) || (p.Player?.Username != null && p.Player.Username.ToLower().Contains(lower)) || (p.Player?.DisplayName != null && p.Player.DisplayName.ToLower().Contains(lower))); } var results = query .OrderByDescending(p => p.Player?.CreatedAt ?? DateTime.MinValue) .Skip(skip) .Take(take) .Select(p => BuildPlayerSummary(p)) .ToList(); return Ok(new { Results = results, TotalResults = results.Count }); } [HttpGet("/vannet/api/director/players/list-developers")] public IActionResult DirectorListDevelopers() { if (!ValidateDirectorToken(out _)) return NotFound(""); var results = PlayerDB.Players.FindAll() .Where(p => p.PlayerRoles != null && p.PlayerRoles.Contains(PlayerRoles.Developer)) .OrderByDescending(p => p.Player?.CreatedAt ?? DateTime.MinValue) .Select(p => BuildPlayerSummary(p)) .ToList(); return Ok(new { Results = results, TotalResults = results.Count }); } [HttpGet("/vannet/api/director/players/list-moderators")] public IActionResult DirectorListModerators() { if (!ValidateDirectorToken(out _)) return NotFound(""); var results = PlayerDB.Players.FindAll() .Where(p => p.PlayerRoles != null && p.PlayerRoles.Contains(PlayerRoles.Moderator)) .OrderByDescending(p => p.Player?.CreatedAt ?? DateTime.MinValue) .Select(p => BuildPlayerSummary(p)) .ToList(); return Ok(new { Results = results, TotalResults = results.Count }); } [HttpGet("/vannet/api/director/players/{playerId:long}")] public async Task DirectorGetPlayer(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); await ToxModService.EnsureCurrent(player); return Ok(BuildPlayerFull(player)); } [HttpGet("/vannet/api/director/players/{playerId:long}/rooms")] public IActionResult DirectorGetPlayerRooms(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var rooms = RoomDB.Rooms.FindAll() .Where(r => r.CreatorAccountId == playerId) .OrderByDescending(r => r.Stats.VisitCount) .Take(50) .Select(r => new { roomId = r.RoomId, name = r.Name, imageName = r.ImageName, stats = r.Stats, accessibility = r.Accessibility, state = r.State }) .ToList(); return Ok(new { rooms }); } [HttpGet("/vannet/api/director/players/{playerId:long}/heartbeat")] public IActionResult DirectorGetHeartbeat(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); return Ok(player.Player?.PlayerExtra?.Heartbeat); } [HttpPost("/vannet/api/director/players/{playerId:long}/rename")] public async Task DirectorRenamePlayer(long playerId, [FromBody] DirectorRenameRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(body?.Username)) return BadRequest(new { error = "Username required." }); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); string old = player.Player.Username; player.Player.Username = body.Username; player.Player.DisplayName = body.Username; PlayerDB.Players.Update(player); var updated = PlayerDB.GetAccountMe(playerId); await NotiController.SendAccountUpdate(playerId, updated); await NotificationsController.RefreshAccount(playerId); return Ok(new { success = true, oldName = old, newName = body.Username }); } [HttpPost("/vannet/api/director/players/{playerId:long}/clear-name")] public async Task DirectorClearName(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); string? old = player.Player.Username; string newName = NameGen.GetRandomName(); player.Player.Username = newName; player.Player.DisplayName = newName; PlayerDB.Players.Update(player); await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId)); await NotificationsController.RefreshAccount(playerId); return Ok(new { success = true, oldName = old, newName }); } [HttpPost("/vannet/api/director/players/{playerId:long}/name-lock")] public async Task DirectorNameLock(long playerId, [FromBody] DirectorNameLockRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); bool locked = body?.Locked ?? true; player.Player.NameLocked = locked; PlayerDB.Players.Update(player); return Ok(new { success = true, playerId, nameLocked = locked }); } [HttpPost("/vannet/api/director/players/create")] public IActionResult DirectorCreatePlayer([FromBody] DirectorCreatePlayerRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (body == null || body.PlayerId <= 0) return BadRequest(new { error = "A positive PlayerId is required." }); string? username = string.IsNullOrWhiteSpace(body.Username) ? null : body.Username.Trim(); var player = PlayerDB.CreateAccountWithId( body.PlayerId, username, Platforms.Standalone, $"director-created-{Guid.NewGuid()}", body.IsJunior, out string? error); if (player == null) return BadRequest(new { error = error ?? "Failed to create account." }); return Ok(new { success = true, playerId = player.PlayerId, username = player.Player?.Username }); } [HttpPost("/vannet/api/director/announcement")] public async Task DirectorSetAnnouncement() { if (!ValidateDirectorToken(out _)) return NotFound(""); DirectorAnnouncementRequest body; try { var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() }, PropertyNameCaseInsensitive = true }; body = await JsonSerializer.DeserializeAsync(Request.Body, options); } catch (JsonException ex) { return BadRequest(new { error = "Invalid request body.", detail = ex.Message }); } if (body?.Clear == true) { await AnnoucementDB.DeleteAllAnnouncements(sendWs: true); return Ok(new { success = true, cleared = true }); } if (string.IsNullOrWhiteSpace(body?.Title) && string.IsNullOrWhiteSpace(body?.Body)) return BadRequest(new { error = "Title or body required." }); var announcement = new AnnoucementDBClasses.DirectorAnnouncementRequest { AnnouncementType = body.AnnouncementType, Body = body.Body ?? "", ImageName = body.ImageName ?? "", LinkName = body.LinkName ?? "", LinkType = body.LinkType, LinkUri = body.LinkUri ?? "", Title = body.Title ?? "" }; bool created = await AnnoucementDB.CreateAnnouncement(announcement, sendWs: true); if (!created) return BadRequest(new { error = "Failed to create announcement." }); return Ok(new { success = true }); } [HttpGet("/vannet/api/director/announcements")] public IActionResult DirectorGetAnnouncements() { if (!ValidateDirectorToken(out _)) return NotFound(""); return Ok(AnnoucementDB.GetAllAnnouncements()); } [HttpPost("/vannet/api/director/announcement/delete/{announcementId:long}")] public async Task DirectorDeleteAnnouncement(long announcementId) { if (!ValidateDirectorToken(out _)) return NotFound(""); bool deleted = AnnoucementDB.DeleteAnnouncement(announcementId); if (!deleted) return NotFound(new { error = "Announcement not found." }); await NotificationsController.SendAnnoucementDelete(announcementId); return Ok(new { success = true, announcementId }); } [HttpGet("/vannet/api/director/community-banner")] public IActionResult DirectorGetCommunityBanner() { if (!ValidateDirectorToken(out _)) return NotFound(""); string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "communityboard.json"); if (!System.IO.File.Exists(path)) return Ok(new { message = "", moreInfoUrl = "" }); var ann = System.Text.Json.Nodes.JsonNode.Parse(System.IO.File.ReadAllText(path))?["CurrentAnnouncement"]; return Ok(new { message = ann?["Message"]?.GetValue() ?? "", moreInfoUrl = ann?["MoreInfoUrl"]?.GetValue() ?? "" }); } [HttpPost("/vannet/api/director/community-banner")] public async Task DirectorSetCommunityBanner([FromBody] DirectorCommunityBannerRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "communityboard.json"); if (!System.IO.File.Exists(path)) return NotFound(new { error = "communityboard.json not found." }); if (System.Text.Json.Nodes.JsonNode.Parse(System.IO.File.ReadAllText(path)) is not System.Text.Json.Nodes.JsonObject root) return StatusCode(500, new { error = "Malformed communityboard.json." }); root["CurrentAnnouncement"] = new System.Text.Json.Nodes.JsonObject { ["Message"] = body?.Message ?? "", ["MoreInfoUrl"] = body?.MoreInfoUrl ?? "" }; string json = root.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); System.IO.File.WriteAllText(path, json); var cbPayload = new { Id = "CommunityBoardUpdate", Msg = System.Text.Json.JsonSerializer.Deserialize(json) }; await NotificationsController.SendToAll(System.Text.Json.JsonSerializer.Serialize(cbPayload)); return Ok(new { success = true }); } [HttpGet("/vannet/api/director/git/diff")] public async Task DirectorGitDiff() { if (!ValidateDirectorToken(out _)) return NotFound(""); const string repoPath = "/home/container/VanaNet"; if (!Directory.Exists(repoPath)) return NotFound(new { error = "Repo path not found.", path = repoPath }); try { async Task<(int ExitCode, string StdOut, string StdErr)> RunGit(string args) { var psi = new ProcessStartInfo { FileName = "git", Arguments = args, WorkingDirectory = repoPath, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using var process = Process.Start(psi); if (process == null) throw new InvalidOperationException("Failed to start git process."); string stdOut = await process.StandardOutput.ReadToEndAsync(); string stdErr = await process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync(); return (process.ExitCode, stdOut, stdErr); } var repoTop = await RunGit("rev-parse --show-toplevel"); var branch = await RunGit("rev-parse --abbrev-ref HEAD"); var status = await RunGit("status --porcelain"); var diff = await RunGit("diff HEAD"); var outBuilder = new StringBuilder(); if (!string.IsNullOrWhiteSpace(diff.StdOut)) { outBuilder.Append(diff.StdOut.TrimEnd()); } else { outBuilder.AppendLine("No diff output from 'git diff HEAD'."); outBuilder.AppendLine("This usually means there are no unstaged tracked file changes in that repo."); } outBuilder.AppendLine(); outBuilder.AppendLine("[debug]"); outBuilder.AppendLine($"configuredRepoPath: {repoPath}"); outBuilder.AppendLine($"gitTopLevel: {(string.IsNullOrWhiteSpace(repoTop.StdOut) ? "(none)" : repoTop.StdOut.Trim())}"); outBuilder.AppendLine($"branch: {(string.IsNullOrWhiteSpace(branch.StdOut) ? "(unknown)" : branch.StdOut.Trim())}"); if (string.IsNullOrWhiteSpace(status.StdOut)) outBuilder.AppendLine("statusPorcelain: (clean)"); else outBuilder.AppendLine($"statusPorcelain:\n{status.StdOut.TrimEnd()}"); if (!string.IsNullOrWhiteSpace(diff.StdErr)) outBuilder.AppendLine($"diffStderr:\n{diff.StdErr.TrimEnd()}"); if (repoTop.ExitCode != 0 && !string.IsNullOrWhiteSpace(repoTop.StdErr)) outBuilder.AppendLine($"revParseTopLevelStderr:\n{repoTop.StdErr.TrimEnd()}"); if (branch.ExitCode != 0 && !string.IsNullOrWhiteSpace(branch.StdErr)) outBuilder.AppendLine($"revParseBranchStderr:\n{branch.StdErr.TrimEnd()}"); if (status.ExitCode != 0 && !string.IsNullOrWhiteSpace(status.StdErr)) outBuilder.AppendLine($"statusStderr:\n{status.StdErr.TrimEnd()}"); bool allSucceeded = repoTop.ExitCode == 0 && branch.ExitCode == 0 && status.ExitCode == 0 && diff.ExitCode == 0; return Ok(new { success = allSucceeded, exitCode = diff.ExitCode, output = outBuilder.ToString() }); } catch (Exception ex) { return StatusCode(500, new { error = "Failed to run git diff.", detail = ex.Message }); } } [HttpGet("/vannet/api/director/git/status")] public async Task DirectorGitStatus() { if (!ValidateDirectorToken(out _)) return NotFound(""); const string repoPath = "/home/container/VanaNet"; if (!Directory.Exists(repoPath)) return NotFound(new { error = "Repo path not found.", path = repoPath }); var psi = new ProcessStartInfo { FileName = "git", Arguments = "log --branches --not --remotes -p @{u}..", WorkingDirectory = repoPath, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; try { using var process = Process.Start(psi); if (process == null) return StatusCode(500, new { error = "Failed to start git process." }); string stdout = await process.StandardOutput.ReadToEndAsync(); string stderr = await process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync(); string output = string.IsNullOrWhiteSpace(stderr) ? stdout : string.IsNullOrWhiteSpace(stdout) ? stderr : stdout + "\n\n[stderr]\n" + stderr; if (string.IsNullOrWhiteSpace(output)) output = "No status output."; return Ok(new { success = process.ExitCode == 0, exitCode = process.ExitCode, output = output }); } catch (Exception ex) { return StatusCode(500, new { error = "Failed to run git status.", detail = ex.Message }); } } [HttpGet("/vannet/api/director/git/push")] public async Task DirectorGitPush() { if (!ValidateDirectorToken(out _)) return NotFound(""); const string repoPath = "/home/container/VanaNet"; if (!Directory.Exists(repoPath)) return NotFound(new { error = "Repo path not found.", path = repoPath }); var psi = new ProcessStartInfo { FileName = "bash", Arguments = $"-c \"cd {repoPath} && git add -A && git commit -m 'Director Commit' && git push https://ada:Avoacado1!@ada.drewcotech.com/vanadium/vanadium HEAD:master\"", WorkingDirectory = repoPath, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; try { using var process = Process.Start(psi); if (process == null) return StatusCode(500, new { error = "Failed to start git process." }); string stdout = await process.StandardOutput.ReadToEndAsync(); string stderr = await process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync(); string output = string.IsNullOrWhiteSpace(stderr) ? stdout : string.IsNullOrWhiteSpace(stdout) ? stderr : stdout + "\n\n[stderr]\n" + stderr; if (string.IsNullOrWhiteSpace(output)) output = "No push output."; return Ok(new { success = process.ExitCode == 0, exitCode = process.ExitCode, output = output }); } catch (Exception ex) { return StatusCode(500, new { error = "Failed to run git push.", detail = ex.Message }); } } private static string RRPlusPath => Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "RRPlus.txt"); public static HashSet ReadRRPlusIds() { var ids = new HashSet(); if (!System.IO.File.Exists(RRPlusPath)) return ids; foreach (var line in System.IO.File.ReadAllLines(RRPlusPath)) if (long.TryParse(line.Trim(), out var id)) ids.Add(id); return ids; } [HttpPost("/vannet/api/director/players/{playerId:long}/rrplus")] public async Task DirectorSetRRPlus(long playerId, [FromBody] DirectorRRPlusRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); bool enabled = body?.Enabled ?? true; var ids = ReadRRPlusIds(); if (enabled) ids.Add(playerId); else ids.Remove(playerId); Directory.CreateDirectory(Path.GetDirectoryName(RRPlusPath)!); System.IO.File.WriteAllLines(RRPlusPath, ids.Select(i => i.ToString())); await NotificationsController.RefreshAccount(playerId); return Ok(new { success = true, playerId, recRoomPlus = enabled }); } [HttpPost("/vannet/api/director/players/{playerId:long}/set-bio")] public async Task DirectorSetBio(long playerId, [FromBody] DirectorBioRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); player.Player.Bio = body?.Bio ?? ""; PlayerDB.Players.Update(player); var updated = PlayerDB.GetAccountMe(playerId); await NotiController.SendAccountUpdate(playerId, updated); await NotificationsController.RefreshAccount(playerId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/set-password")] public async Task DirectorSetPassword(long playerId, [FromBody] DirectorPasswordRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); PlayerDB.PasswordManager.ChangePassword(player.PlayerId, body?.Password ?? ""); var updated = PlayerDB.GetAccountMe(playerId); await NotiController.SendAccountUpdate(playerId, updated); await NotificationsController.RefreshAccount(playerId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/upload-pfp")] public async Task DirectorUploadPlayerPfp(long playerId, [FromForm] IFormFile file) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); if (file == null || file.Length == 0) return BadRequest(new { error = "No file provided." }); string ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (ext != ".jpg" && ext != ".jpeg" && ext != ".png") return BadRequest(new { error = "Only JPG and PNG files are allowed." }); string fileName = $"pfp_{playerId}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}"; string savePath = Path.Combine(Environment.CurrentDirectory, "Data", "Images", fileName); Directory.CreateDirectory(Path.GetDirectoryName(savePath)!); using (var stream = new FileStream(savePath, FileMode.Create)) await file.CopyToAsync(stream); var db = ImageMetadataDB.GetDb(); var imageCol = db.GetCollection("images"); imageCol.Insert(new ImageMetadataDB.FullSavedImage { PlayerId = (ulong)playerId, ImageName = fileName }); player.Player.ProfileImage = fileName; PlayerDB.Players.Update(player); var updated = PlayerDB.GetAccountMe(playerId); await NotificationsController.RefreshAccount(playerId); return Ok(new { success = true, imageName = fileName }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/upload-image")] public async Task DirectorUploadRoomImage(long roomId, [FromForm] IFormFile file) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); if (file == null || file.Length == 0) return BadRequest(new { error = "No file provided." }); string ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (ext != ".jpg" && ext != ".jpeg" && ext != ".png") return BadRequest(new { error = "Only JPG and PNG files are allowed." }); string fileName = $"room_{roomId}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}"; string savePath = Path.Combine(Environment.CurrentDirectory, "Data", "Images", fileName); Directory.CreateDirectory(Path.GetDirectoryName(savePath)!); using (var stream = new FileStream(savePath, FileMode.Create)) await file.CopyToAsync(stream); room.ImageName = fileName; RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true, imageName = fileName }); } [HttpPost("/vannet/api/director/players/shove-all")] public async Task DirectorShoveAll([FromBody] DirectorShoveAllRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.GetRoom(body.RoomId); if (room == null) return NotFound(new { error = "Room not found." }); var onlinePlayers = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true) .ToList(); int count = 0; foreach (var fp in onlinePlayers) { var heartbeat = Sessions.CreateRoom(fp.PlayerId, body.RoomId, body.SubRoomId, joinMode: 0, additionalPlayersAutoFollow: false); if (!string.IsNullOrWhiteSpace(body.InstanceId) && heartbeat.roomInstance != null) heartbeat.roomInstance.photonRoomId = body.InstanceId; fp.Player.PlayerExtra ??= new PlayerExtra(); fp.Player.PlayerExtra.Heartbeat = heartbeat; PlayerDB.Players.Update(fp); await NotiController.SendPresenceUpdate(fp.PlayerId, heartbeat); count++; } return Ok(new { success = true, playersShoved = count }); } [HttpPost("/vannet/api/director/players/{playerId:long}/set-pfp")] public async Task DirectorSetPfp(long playerId, [FromBody] DirectorSetPfpRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(body?.ImageName)) return BadRequest(new { error = "ImageName required." }); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); player.Player.ProfileImage = body.ImageName; PlayerDB.Players.Update(player); var updated = PlayerDB.GetAccountMe(playerId); await NotificationsController.RefreshAccount(playerId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/reset-pfp")] public async Task DirectorResetPfp(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); player.Player.ProfileImage = "DefaultPFP.png"; PlayerDB.Players.Update(player); var updated = PlayerDB.GetAccountMe(playerId); await NotificationsController.RefreshAccount(playerId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/set-level")] public async Task DirectorSetLevel(long playerId, [FromBody] DirectorSetLevelRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); int oldLevel = player.Player.Level; int oldXP = player.Player.XP; int newXP = body.Xp.HasValue ? body.Xp.Value : PlayerDB.GetXPForLevel(body.Level); player.Player.Level = body.Level; player.Player.XP = newXP; PlayerDB.Players.Update(player); var json = JsonSerializer.Serialize(new { Id = NotificationsController.EventTypes.PlayerProgressionLevelUpdate, Msg = new { PlayerId = playerId, Level = body.Level, XP = newXP } }); await NotificationsController.SendToAll(json); return Ok(new { success = true, oldLevel, newLevel = body.Level, oldXP, newXP }); } [HttpPost("/vannet/api/director/players/{playerId:long}/set-junior")] // removed message because it unessecary lowkey public async Task DirectorSetJunior(long playerId, [FromBody] DirectorSetJuniorRequest body) { if (!ValidateDirectorToken(out var session)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); player.Player ??= new Player(); player.Player.PlayerExtra ??= new PlayerExtra(); bool wasJunior = player.Player.IsJunior == true; bool isJunior = body?.IsJunior ?? false; player.Player.IsJunior = isJunior; string? message = null; if (isJunior) { string? reason = string.IsNullOrWhiteSpace(body?.Reason) ? null : body.Reason.Trim(); player.Player.PlayerExtra.JuniorStatus = new JuniorStatusData { Reason = reason, SetBy = session?.DiscordUserId, SetAt = DateTime.UtcNow }; if (!wasJunior) { string newName = NameGen.GetRandomName(); player.Player.Username = newName; player.Player.DisplayName = newName; player.Player.Bio = ""; } message = reason != null ? $"Your communication privileges have been restricted: {reason}. Contact a moderator to appeal." : "Your communication privileges have been restricted by a moderator. Contact a moderator to appeal."; } else { player.Player.PlayerExtra.JuniorStatus = null; if (wasJunior) message = "Your communication privileges have been restored."; } PlayerDB.Players.Update(player); await NotificationsController.RefreshAccount(playerId); if (message != null) { // await SendCoachPopup(playerId, message); await NotificationsController.CreateAndSendWSMessageRecieved( playerId, 1, MessageType.TextMessage, message); } return Ok(new { success = true, playerId, isJunior }); } [HttpPost("/vannet/api/director/players/{playerId:long}/coach-dm")] public async Task DirectorCoachDm(long playerId, [FromBody] DirectorCoachDmRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(body?.Message)) return BadRequest(new { error = "Message required." }); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); bool sent = await NotificationsController.CreateAndSendWSMessageRecieved( playerId, 1, MessageType.TextMessage, body.Message); return Ok(new { success = sent, playerId }); } [HttpGet("/vannet/api/director/players/{playerId:long}/toxmod")] public async Task DirectorGetToxModStatus(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); await ToxModService.EnsureCurrent(player); return Ok(ToxModService.BuildStatus(player)); } [HttpPost("/vannet/api/director/players/{playerId:long}/toxmod/ban")] public async Task DirectorToxModBan(long playerId, [FromBody] DirectorToxModBanRequest body) { if (!ValidateDirectorToken(out var session)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); await ToxModService.IssueVoiceBan(player, body?.Category, body?.Note, body?.DurationSeconds, session?.DiscordUserId); if (body?.Loud == true) { string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject( Vanadium.Classes.WebSocket.WebsocketEvents.CreateWSEventString("ModerationQuitGame", new { })); await NotificationsController.SendToPlayer(playerId, wsEvent); } return Ok(new { success = true, status = ToxModService.BuildStatus(player) }); } [HttpPost("/vannet/api/director/players/{playerId:long}/toxmod/unban")] public async Task DirectorToxModUnban(long playerId, [FromBody] DirectorToxModUnbanRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); await ToxModService.LiftVoiceBan(player, body?.ResetStrikes ?? false); return Ok(new { success = true, status = ToxModService.BuildStatus(player) }); } [HttpPost("/vannet/api/director/players/{playerId:long}/set-platform-id")] public IActionResult DirectorSetPlatformId(long playerId, [FromBody] DirectorSetPlatformIdRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (!ulong.TryParse(body?.PlatformId, out ulong newPid)) return BadRequest(new { error = "Invalid platform ID." }); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); if (player.PlatformIds == null || player.PlatformIds.Count == 0) return BadRequest(new { error = "No platform IDs on this player." }); string old = player.PlatformIds[0].PlatformId; player.PlatformIds[0].PlatformId = body.PlatformId; PlayerDB.Players.Update(player); return Ok(new { success = true, oldPlatformId = old, newPlatformId = body.PlatformId }); } [HttpPost("/vannet/api/director/players/{playerId:long}/heartbeat-clear")] public async Task DirectorHeartbeatClear(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); player.Player.PlayerExtra ??= new PlayerExtra(); player.Player.PlayerExtra.Heartbeat = new Heartbeat { playerId = playerId, isOnline = false, roomInstance = null, errorCode = 0 }; PlayerDB.Players.Update(player); await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/shove")] public async Task DirectorShove(long playerId, [FromBody] DirectorShoveRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(new { error = "Player not found." }); var room = RoomDB.GetRoom(body.RoomId); if (room == null) return NotFound(new { error = "Room not found." }); var heartbeat = Sessions.CreateRoom(playerId, body.RoomId, body.SubRoomId, joinMode: 0, additionalPlayersAutoFollow: false); if (!string.IsNullOrWhiteSpace(body.InstanceId) && heartbeat.roomInstance != null) heartbeat.roomInstance.photonRoomId = body.InstanceId; player.Player.PlayerExtra ??= new PlayerExtra(); player.Player.PlayerExtra.Heartbeat = heartbeat; PlayerDB.Players.Update(player); await NotiController.SendPresenceUpdate(playerId, heartbeat); return Ok(new { success = true, roomId = body.RoomId }); } [HttpPost("/vannet/api/director/players/{playerId:long}/force-bring")] public async Task DirectorForceBring(long playerId, [FromBody] DirectorForceBringRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(body?.Username)) return BadRequest(new { error = "Input field for username" }); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); var target = PlayerDB.Players.FindAll() .FirstOrDefault(p => string.Equals(p.Player?.Username, body.Username, StringComparison.OrdinalIgnoreCase)); if (target == null) return NotFound(new { error = "No user found with this username!" }); var targetHb = target.Player?.PlayerExtra?.Heartbeat; if (targetHb?.roomInstance == null) return BadRequest(new { error = "This user isnt online" }); var heartbeat = Sessions.CreateRoom(playerId, targetHb.roomInstance.roomId, targetHb.roomInstance.subRoomId, joinMode: 0, additionalPlayersAutoFollow: false); if (heartbeat.roomInstance != null) { heartbeat.roomInstance.photonRoomId = targetHb.roomInstance.photonRoomId; heartbeat.roomInstance.photonRegion = targetHb.roomInstance.photonRegion; heartbeat.roomInstance.roomInstanceId = targetHb.roomInstance.roomInstanceId; } player.Player.PlayerExtra ??= new PlayerExtra(); player.Player.PlayerExtra.Heartbeat = heartbeat; PlayerDB.Players.Update(player); await NotificationsController.RefreshHeartbeat(playerId, heartbeat); return Ok(new { success = true, roomId = targetHb.roomInstance.roomId }); } [HttpPost("/vannet/api/director/players/{playerId:long}/transfer-rooms")] public async Task DirectorTransferRooms(long playerId, [FromBody] DirectorTransferRoomsRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var oldPlayer = PlayerDB.Players.FindById(playerId); var newPlayer = PlayerDB.Players.FindById(body.NewPlayerId); if (oldPlayer == null) return NotFound(new { error = "Source player not found." }); if (newPlayer == null) return NotFound(new { error = "Target player not found." }); var rooms = RoomDB.Rooms.Find(r => r.CreatorAccountId == playerId).ToList(); foreach (var room in rooms) { room.CreatorAccountId = body.NewPlayerId; room.Roles ??= new List(); room.Roles.RemoveAll(r => r.AccountId == playerId && r.Role == Role.Creator); room.Roles.RemoveAll(r => r.AccountId == body.NewPlayerId && r.Role == Role.Creator); room.Roles.Add(new Roles { AccountId = body.NewPlayerId, Role = Role.Creator, InvitedRole = Role.Creator, LastChangedByAccountId = body.NewPlayerId }); if (body.KeepCoOwner) { room.Roles.RemoveAll(r => r.AccountId == playerId); room.Roles.Add(new Roles { AccountId = playerId, Role = Role.CoOwner, InvitedRole = Role.CoOwner, LastChangedByAccountId = body.NewPlayerId }); } if (room.SubRooms != null) foreach (var sub in room.SubRooms) { sub.CreatorAccountId = body.NewPlayerId; sub.SavedByAccountId = body.NewPlayerId; } RoomDB.Rooms.Update(room); } var onlinePlayers = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true) .Select(p => p.PlayerId) .ToList(); foreach (var pid in onlinePlayers) foreach (var r in rooms) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(r.RoomId)); return Ok(new { success = true, roomsTransferred = rooms.Count }); } [HttpPost("/vannet/api/director/players/{playerId:long}/repair")] public async Task DirectorRepair(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); var now = DateTime.UtcNow; if (player.Player.CreatedAt == default || player.Player.CreatedAt.Year == 1) player.Player.CreatedAt = now; if (player.Player.LastLoginAt == default || player.Player.LastLoginAt.Year == 1) player.Player.LastLoginAt = now; player.Player.IsJunior ??= false; if (string.IsNullOrWhiteSpace(player.Player.ProfileImage)) player.Player.ProfileImage = "DefaultPFP.png"; player.Player.PlayerExtra ??= new PlayerExtra(); player.Player.PlayerExtra.Avatar ??= new Avatar(); player.Player.PlayerExtra.Influencer ??= new Influencer(); player.Player.PlayerExtra.Heartbeat ??= new Heartbeat(); player.Player.PlayerExtra.Heartbeat.isOnline = false; player.Player.PlayerExtra.Heartbeat.roomInstance = null; player.Player.PlayerExtra.Heartbeat.errorCode = 0; if (player.Player.PlayerExtra.DormRoomId == 0) { bool dormExists = RoomDB.DoesPlayerDormExist(player.PlayerId); if (!dormExists) { long newDormId = RoomDB.CloneDormRoom(player.PlayerId); player.Player.PlayerExtra.DormRoomId = newDormId; } else { var dorm = RoomDB.GetPlayerDormRoom(player.PlayerId); if (dorm != null) player.Player.PlayerExtra.DormRoomId = dorm.RoomId; } } PlayerDB.Players.Update(player); var repaired = PlayerDB.GetAccountMe(playerId); await NotificationsController.RefreshAccount(playerId); await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat); return Ok(new { success = true, dormRoomId = player.Player.PlayerExtra.DormRoomId }); } [HttpPost("/vannet/api/director/players/{playerId:long}/delete-all-images")] public IActionResult DirectorDeleteAllImages(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); var db = ImageMetadataDB.GetDb(); var imageCol = db.GetCollection("images"); var images = imageCol.Query().Where(x => x.PlayerId == (ulong)playerId).ToList(); int deletedFiles = 0; foreach (var image in images) { try { string path = Path.Combine("Data", "Images", image.ImageName); if (System.IO.File.Exists(path)) { System.IO.File.Delete(path); deletedFiles++; } } catch { } } ImageMetadataDB.DeleteImagesFromPlayerId((ulong)playerId); return Ok(new { success = true, deletedRecords = images.Count, deletedFiles }); } [HttpPost("/vannet/api/director/players/{playerId:long}/delete-all-rooms")] public IActionResult DirectorDeleteAllRooms(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); var rooms = RoomDB.Rooms.Find(r => r.CreatorAccountId == playerId).ToList(); int deletedSubroomSaves = 0; int deletedRooms = 0; foreach (var room in rooms) { try { RoomDB.SubRoomSaves.DeleteMany(s => s.RoomId == room.RoomId); deletedSubroomSaves++; if (RoomDB.Rooms.Delete(room.RoomId)) { deletedRooms++; } } catch { } } return Ok(new { success = true, deletedRecords = rooms.Count, deletedFiles = deletedRooms, deletedSaves = deletedSubroomSaves }); } [HttpDelete("/vannet/api/director/players/{playerId:long}/delete")] public IActionResult DirectorDeletePlayer(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); PlayerDB.Players.Delete(playerId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/roles")] public async Task DirectorPlayerRoles(long playerId, [FromBody] DirectorRoleRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(body?.Role) || string.IsNullOrWhiteSpace(body.Action)) return BadRequest(new { error = "Role and action required." }); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); if (!Enum.TryParse(body.Role, out var role)) return BadRequest(new { error = "Invalid role." }); player.PlayerRoles ??= new List(); if (body.Action == "add" && !player.PlayerRoles.Contains(role)) player.PlayerRoles.Add(role); else if (body.Action == "remove") player.PlayerRoles.Remove(role); PlayerDB.Players.Update(player); var updated = PlayerDB.GetAccountMe(playerId); await NotiController.SendAccountUpdate(playerId, updated); return Ok(new { success = true, roles = player.PlayerRoles.Select(r => r.ToString()).ToList() }); } [HttpPost("/vannet/api/director/players/{playerId:long}/influencer")] public IActionResult DirectorPlayerInfluencer(long playerId, [FromBody] DirectorInfluencerRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); bool isAdd = body?.Action == "add"; player.Player.PlayerExtra ??= new PlayerExtra(); player.Player.PlayerExtra.Influencer ??= new Influencer(); player.Player.PlayerExtra.Influencer.IsInfluencer = isAdd; PlayerDB.Players.Update(player); return Ok(new { success = true, isInfluencer = isAdd }); } [HttpPost("/vannet/api/director/players/{playerId:long}/ban")] public async Task DirectorBanPlayer(long playerId, [FromBody] DirectorBanRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (playerId == 1305) return Ok(new { success = true }); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); var mbd = new ModerationBlockDetails { IsBan = true, ReportCategory = ReportCategory.Moderator, Duration = body?.Duration ?? 2147483647, Message = body?.Reason ?? "Banned by admin", ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; player.Player.PlayerExtra ??= new PlayerExtra(); player.Player.PlayerExtra.ModerationBlockDetails = mbd; player.Player.PlayerExtra.Heartbeat = new Heartbeat { playerId = playerId, isOnline = false, roomInstance = null, errorCode = 0 }; PlayerDB.Players.Update(player); await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat); await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId)); await NotificationsController.SendBanned(playerId, mbd); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/unban")] public async Task DirectorUnbanPlayer(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (playerId == 1305) return Ok(new { success = true }); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); player.Player.PlayerExtra ??= new PlayerExtra(); player.Player.PlayerExtra.ModerationBlockDetails = new ModerationBlockDetails { IsBan = false }; var mbd = new ModerationBlockDetails { IsBan = false, ReportCategory = ReportCategory.Moderator, Duration = 0, Message = "Unbanned", ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; PlayerDB.Players.Update(player); await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId)); await NotificationsController.SendBanned(playerId, mbd); return Ok(new { success = true }); } /// /// Machines matching a hwid or an account, with everyone linked to them. /// /// This is the screen a moderator is meant to read before banning anything. A machine /// id is shared by everyone who has played on that computer, so the linked account /// list is not a detail - it is the decision. Eleven accounts behind one hwid is a /// different call from one, and the panel puts that in front of them first. /// /// [HttpGet("/vannet/api/director/anticheat/machines")] public IActionResult DirectorMachineLookup([FromQuery] string? hwid, [FromQuery] long? playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var records = new List(); if (!string.IsNullOrWhiteSpace(hwid)) { var single = HwidDB.GetRecord(hwid.Trim()); if (single != null) records.Add(single); } else if (playerId != null) { records.AddRange(HwidDB.RecordsForPlayer(playerId.Value)); } else { return BadRequest(new { error = "Provide either hwid or playerId." }); } return Ok(records.Select(ProjectMachine)); } /// Every machine under an active hardware ban. The review queue. [HttpGet("/vannet/api/director/anticheat/machines/banned")] public IActionResult DirectorBannedMachines() { if (!ValidateDirectorToken(out _)) return NotFound(""); return Ok(HwidDB.AllBanned().Select(ProjectMachine)); } /// /// Bans a machine, or every machine an account has been seen on, and the accounts /// linked to them. The Discord identity behind the Director session is recorded as /// the issuer: a hardware ban with no name attached is one nobody can review later. /// [HttpPost("/vannet/api/director/anticheat/machines/ban")] public async Task DirectorBanMachine([FromBody] DirectorHwidBanRequest body) { if (!ValidateDirectorToken(out var session)) return NotFound(""); if (body == null || string.IsNullOrWhiteSpace(body.Reason)) return BadRequest(new { error = "A reason is required." }); if (body.DurationDays is <= 0) return BadRequest(new { error = "DurationDays must be positive, or blank for permanent." }); DateTime? expiresAt = body.DurationDays == null ? null : DateTime.UtcNow.AddDays(body.DurationDays.Value); string issuedBy = $"director:{session?.DiscordUserId ?? "unknown"}"; List hwids; List accounts; if (!string.IsNullOrWhiteSpace(body.Hwid)) { var banned = HwidDB.BanHwid(body.Hwid.Trim(), body.Reason, issuedBy, expiresAt); if (banned == null) return NotFound(new { error = "That machine has never checked in." }); hwids = new List { body.Hwid.Trim() }; accounts = banned; } else if (body.PlayerId != null) { (hwids, accounts) = HwidDB.BanMachinesOfPlayer(body.PlayerId.Value, body.Reason, issuedBy, expiresAt); if (hwids.Count == 0) return NotFound(new { error = "No machines on record for that player." }); } else { return BadRequest(new { error = "Provide either Hwid or PlayerId." }); } // HwidDB applies the account bans; pushing them out is the Director job, so a // player caught by a machine ban drops now rather than at their next login. foreach (var id in accounts) await PushBanToClient(id); return Ok(new { success = true, hwids, bannedAccounts = accounts, expiresAt }); } /// /// Lifts a hardware ban. The accounts banned alongside it stay banned on purpose: /// some were banned for their own behaviour before the machine ever came up, and /// sweeping those back in would undo moderation nobody asked to undo. The panel says /// so, and those accounts are unbanned individually from the Players screen. /// [HttpPost("/vannet/api/director/anticheat/machines/unban")] public IActionResult DirectorUnbanMachine([FromBody] DirectorHwidUnbanRequest body) { if (!ValidateDirectorToken(out var session)) return NotFound(""); if (string.IsNullOrWhiteSpace(body?.Hwid)) return BadRequest(new { error = "Hwid is required." }); string liftedBy = $"director:{session?.DiscordUserId ?? "unknown"}"; if (!HwidDB.UnbanHwid(body.Hwid.Trim(), liftedBy)) return NotFound(new { error = "No active hardware ban on that machine." }); return Ok(new { success = true }); } /// /// Drops an already-banned account offline and tells its client why. Mirrors what /// DirectorBanPlayer does after writing the ban, so a machine ban and a manual ban /// land identically from the client side. /// private async Task PushBanToClient(long playerId) { var player = PlayerDB.Players.FindById(playerId); var mbd = player?.Player?.PlayerExtra?.ModerationBlockDetails; if (player == null || mbd == null) return; player.Player.PlayerExtra.Heartbeat = new Heartbeat { playerId = playerId, isOnline = false, roomInstance = null, errorCode = 0 }; PlayerDB.Players.Update(player); await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat); await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId)); await NotificationsController.SendBanned(playerId, mbd); } /// /// Shapes one machine for the panel. Resolves linked accounts to names and ban state /// here rather than making the browser fan out one request per player. /// private static object ProjectMachine(HwidDBClasses.HwidRecord record) { var now = DateTime.UtcNow; return new { hwid = record.Hwid, firstSeen = record.FirstSeen, lastSeen = record.LastSeen, banned = record.BanIsActive(now), banReason = record.BanReason, bannedBy = record.BannedBy, bannedAt = record.BannedAt, banExpiresAt = record.BanExpiresAt, players = (record.PlayerIds ?? new List()).Select(id => { var p = PlayerDB.Players.FindById(id); return new { playerId = id, username = p?.Player?.Username, profileImage = p?.Player?.ProfileImage, banned = p?.Player?.PlayerExtra?.ModerationBlockDetails?.IsBan == true }; }).ToList() }; } [HttpPost("/vannet/api/director/players/close-all")] // does not apply to devs public async Task DirectorCrashAll() { if (!ValidateDirectorToken(out _)) return NotFound(""); var onlinePlayers = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true && (p.PlayerRoles == null || !p.PlayerRoles.Contains(PlayerRoles.Developer))) .ToList(); int count = 0; foreach (var fp in onlinePlayers) { string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject( Vanadium.Classes.WebSocket.WebsocketEvents.CreateWSEventString("ModerationQuitGame", new { })); await NotificationsController.SendToPlayer(fp.PlayerId, wsEvent); count++; } return Ok(new { success = true, playersCrashed = count }); } [HttpPost("/vannet/api/director/players/{playerId:long}/close-game")] public async Task DirectorCloseGame(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject( Vanadium.Classes.WebSocket.WebsocketEvents.CreateWSEventString("ModerationQuitGame", new { }));// damn, this from the ancient days from sscs await NotificationsController.SendToPlayer(playerId, wsEvent); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/logout")] public async Task DirectorLogoutPlayer(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject( Vanadium.Classes.WebSocket.WebsocketEvents.CreateLogoutResponse()); await NotificationsController.SendToPlayer(playerId, wsEvent); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/send-ws-hex")] public async Task DirectorSendPlayerWsHex(long playerId, [FromBody] DirectorSendWsHexRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); var hex = body?.Hex ?? string.Empty; hex = new string(hex.Where(c => !char.IsWhiteSpace(c)).ToArray()); hex = hex.Replace("0x", "", StringComparison.OrdinalIgnoreCase); if (string.IsNullOrEmpty(hex)) return BadRequest(new { error = "Hex payload is required." }); if (hex.Length % 2 != 0) return BadRequest(new { error = "Hex payload must have an even number of characters." }); if (hex.Any(c => !Uri.IsHexDigit(c))) return BadRequest(new { error = "Hex payload contains invalid characters." }); byte[] payload; try { payload = Convert.FromHexString(hex); } catch { return BadRequest(new { error = "Hex payload could not be parsed." }); } if (body?.Exact == true) { await NotificationsController.SendRawToPlayer(playerId, payload); } else { await NotificationsController.SendToPlayer(playerId, Encoding.UTF8.GetString(payload)); } return Ok(new { success = true, bytes = payload.Length, exact = body?.Exact == true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/send-ws-json")] public async Task DirectorSendPlayerWsJson(long playerId, [FromBody] DirectorSendWsJsonRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); var json = body?.Json ?? string.Empty; if (string.IsNullOrWhiteSpace(json)) return BadRequest(new { error = "JSON payload is required." }); try { using var _ = JsonDocument.Parse(json); } catch { return BadRequest(new { error = "Payload is not valid JSON." }); } await NotificationsController.SendToPlayer(playerId, json); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/send-coach-message")] public async Task DirectorPlayerCoachMessage(long playerId, [FromBody] DirectorPlayerCoachMessageRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); await SendCoachPopup(playerId, body?.Message ?? ""); return Ok(new { success = true }); } internal static async Task SendCoachPopup(long playerId, string message) { var json = Newtonsoft.Json.JsonConvert.SerializeObject(new { Id = "2", Msg = new { FromPlayerId = 1, Id = new Random().Next(1, 0x7ffffff), SentTime = DateTime.UtcNow, Type = 100, Data = message, RoomId = 1 } }); await NotificationsController.SendToPlayer(playerId, json); } [HttpPost("/vannet/api/director/players/{playerId:long}/set-tokens")] public async Task DirectorSetTokens(long playerId, [FromBody] DirectorSetTokensRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); player.Player.PlayerExtra ??= new PlayerExtra(); player.Player.PlayerExtra.Currencies ??= new List(); var balanceType = (BalanceType)(body?.BalanceType ?? 0); var existing = player.Player.PlayerExtra.Currencies .FirstOrDefault(c => c.CurrencyType == CurrencyType.RecCenterTokens && c.BalanceType == balanceType); int old = 0; if (existing != null) { old = existing.Balance; existing.Balance = body.Amount; } else { player.Player.PlayerExtra.Currencies.Add(new PlayerCurrency { CurrencyType = CurrencyType.RecCenterTokens, BalanceType = balanceType, Balance = body.Amount }); } PlayerDB.Players.Update(player); await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId)); return Ok(new { success = true, oldBalance = old, newBalance = body.Amount }); } [HttpPost("/vannet/api/director/players/{playerId:long}/give-skin")] public IActionResult DirectorGiveSkin(long playerId, [FromBody] DirectorGiveSkinRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player == null) return NotFound(""); string equipPath = Path.Join(Program.dataDir, "APIS", "Items", "Equipment.json"); if (!System.IO.File.Exists(equipPath)) return NotFound(new { error = "Equipment.json not found." }); var allItems = System.Text.Json.JsonSerializer.Deserialize>( System.IO.File.ReadAllText(equipPath), new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); var target = allItems?.FirstOrDefault(i => string.Equals(i.FriendlyName, body?.Skin, StringComparison.OrdinalIgnoreCase)); if (target == null) return NotFound(new { error = "Skin not found." }); player.Player.PlayerExtra ??= new PlayerExtra(); if (player != null && player.Player != null && player.Player.PlayerExtra != null && player.Player.PlayerExtra.OwnedEquipment == null) { player.Player.PlayerExtra.OwnedEquipment = new List(); } bool owns = player.Player.PlayerExtra.OwnedEquipment .Any(e => string.Equals(e.ModificationGuid, target.ModificationGuid, StringComparison.OrdinalIgnoreCase)); if (body.Action == "add") { if (owns) return Conflict(new { error = "Player already owns this skin." }); player.Player.PlayerExtra.OwnedEquipment.Add(new OwnedEquipmentItem { PrefabName = target.PrefabName, ModificationGuid = target.ModificationGuid, FriendlyName = target.FriendlyName, PlatformMask = target.PlatformMask, Tooltip = target.Tooltip, Rarity = target.Rarity, Favorited = false }); } else { if (!owns) return NotFound(new { error = "Player does not own this skin." }); player.Player.PlayerExtra.OwnedEquipment.RemoveAll(e => string.Equals(e.ModificationGuid, target.ModificationGuid, StringComparison.OrdinalIgnoreCase)); } PlayerDB.Players.Update(player); return Ok(new { success = true }); } [HttpGet("/vannet/api/director/skins")] public IActionResult DirectorGetSkins() { if (!ValidateDirectorToken(out _)) return NotFound(""); string path = Path.Join(Program.dataDir, "APIS", "Items", "Equipment.json"); if (!System.IO.File.Exists(path)) return Ok(new List()); var items = System.Text.Json.JsonSerializer.Deserialize>( System.IO.File.ReadAllText(path), new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return Ok(items ?? new List()); } [HttpGet("/vannet/api/director/rooms/hot")] public IActionResult DirectorHotRooms([FromQuery] int skip = 0, [FromQuery] int take = 50) { if (!ValidateDirectorToken(out _)) return NotFound(""); var rooms = RoomDB.Rooms.FindAll() .Where(r => !r.IsDorm) .OrderByDescending(r => r.Stats.CheerCount) .Skip(skip) .Take(take) .Select(r => BuildRoomSummary(r)) .ToList(); return Ok(new { Results = rooms, TotalResults = rooms.Count }); } [HttpGet("/vannet/api/director/rooms/search")] public IActionResult DirectorSearchRooms([FromQuery] string? q, [FromQuery] int skip = 0, [FromQuery] int take = 50) { if (!ValidateDirectorToken(out _)) return NotFound(""); IEnumerable query = RoomDB.Rooms.FindAll().Where(r => !r.IsDorm); if (!string.IsNullOrWhiteSpace(q)) { string lower = q.ToLowerInvariant(); bool isId = long.TryParse(q, out long searchId); query = query.Where(r => (isId && r.RoomId == searchId) || (r.Name != null && r.Name.ToLower().Contains(lower)) || (r.Name != null && r.Name.ToLower().Contains(lower))); } var results = query .OrderByDescending(r => r.Stats.VisitCount) .Skip(skip) .Take(take) .Select(r => BuildRoomSummary(r)) .ToList(); return Ok(new { Results = results, TotalResults = results.Count }); } [HttpGet("/vannet/api/director/rooms/{roomId:long}")] public IActionResult DirectorGetRoom(long roomId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(""); var creator = PlayerDB.Players.FindById(room.CreatorAccountId); return Ok(new { roomId = room.RoomId, name = room.Name, description = room.Description, imageName = room.ImageName, creatorAccountId = room.CreatorAccountId, creatorUsername = creator?.Player?.Username, stats = room.Stats, maxPlayers = room.MaxPlayers, ageRating = room.AgeRating, createdAt = room.CreatedAt, publishedAt = room.PublishedAt, tags = room.Tags, subRooms = room.SubRooms, roles = room.Roles, supportsMobile = room.SupportsMobile, supportsQuest2 = room.SupportsQuest2, supportsWalkVR = room.SupportsWalkVR, supportsTeleportVR = room.SupportsTeleportVR, supportsScreens = room.SupportsScreens, supportsJuniors = room.SupportsJuniors, accessibility = room.Accessibility, state = room.State, cloningAllowed = room.CloningAllowed, isRRO = room.IsRRO, isDorm = room.IsDorm, loadScreens = room.LoadScreens, promoImages = room.PromoImages, warningMask = room.WarningMask }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-name")] public async Task DirectorSetRoomName(long roomId, [FromBody] DirectorRoomNameRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(body?.Name)) return BadRequest(new { error = "Name required." }); var room = RoomDB.SetRoomName(roomId, body.Name); if (room == null) return NotFound(""); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-description")] public async Task DirectorSetRoomDescription(long roomId, [FromBody] DirectorRoomDescRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.SetRoomDescription(roomId, body?.Description ?? ""); if (room == null) return NotFound(""); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-image")] public async Task DirectorSetRoomImage(long roomId, [FromBody] DirectorRoomImageRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(body?.ImageName)) return BadRequest(new { error = "ImageName required." }); var room = RoomDB.SetRoomImageName(roomId, body.ImageName); if (room == null) return NotFound(""); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/reset-image")] public async Task DirectorResetRoomImage(long roomId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); room.ImageName = "DefaultRoomImage.jpg"; RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-accessibility")] public async Task DirectorSetRoomAccessibility(long roomId, [FromBody] DirectorRoomAccessibilityRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.SetRoomAccessibility(roomId, (RoomAccessibility)body.Accessibility); if (room == null) return NotFound(""); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-cloning")] public async Task DirectorSetRoomCloning(long roomId, [FromBody] DirectorRoomCloningRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.SetRoomCloning(roomId, body.Allowed); if (room == null) return NotFound(""); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-restrictions")] public async Task DirectorSetRoomRestrictions(long roomId, [FromBody] DirectorRoomRestrictionsRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); room.SupportsScreens = body.SupportsScreens; room.SupportsWalkVR = body.SupportsWalkVR; room.SupportsTeleportVR = body.SupportsTeleportVR; room.SupportsJuniors = body.SupportsJuniors; room.SupportsMobile = body.SupportsMobile; RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-age-rating")] public async Task DirectorSetRoomAgeRating(long roomId, [FromBody] DirectorRoomAgeRatingRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); room.AgeRating = body.AgeRating; RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-max-players")] public async Task DirectorSetRoomMaxPlayers(long roomId, [FromBody] DirectorRoomMaxPlayersRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (body.MaxPlayers < 1) return BadRequest(new { error = "MaxPlayers must be at least 1." }); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); room.MaxPlayers = body.MaxPlayers; RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/change-owner")] public async Task DirectorChangeRoomOwner(long roomId, [FromBody] DirectorChangeOwnerRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); var newOwner = PlayerDB.Players.FindById(body.NewOwnerId); if (room == null) return NotFound(new { error = "Room not found." }); if (newOwner == null) return NotFound(new { error = "New owner not found." }); room.CreatorAccountId = body.NewOwnerId; room.Roles ??= new List(); room.Roles.RemoveAll(r => r.Role == Role.Creator); room.Roles.Add(new Roles { AccountId = body.NewOwnerId, Role = Role.Creator, InvitedRole = Role.Creator, LastChangedByAccountId = body.NewOwnerId }); if (room.SubRooms != null) foreach (var sub in room.SubRooms) { sub.CreatorAccountId = body.NewOwnerId; sub.SavedByAccountId = body.NewOwnerId; } RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-role")] public async Task DirectorSetRoomRole(long roomId, [FromBody] DirectorSetRoomRoleRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(""); room.Roles ??= new List(); room.Roles.RemoveAll(r => r.AccountId == body.PlayerId); var newRole = (Role)body.Role; if (newRole != Role.None) room.Roles.Add(new Roles { AccountId = body.PlayerId, Role = newRole, InvitedRole = Role.None, LastChangedByAccountId = 2 }); RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); if (newRole == Role.CoOwner || newRole == Role.TemporaryCoOwner) { var coachJson = Newtonsoft.Json.JsonConvert.SerializeObject(new { Id = "2", Msg = new { FromPlayerId = body.PlayerId, Id = Random.Shared.Next(1, 0x7ffffff), SentTime = DateTime.UtcNow, Type = Vanadium.Classes.DBs.DBClasses.PlayerDBClasses.MessageType.CoachMessage, Data = $"You were given Co-Owner inside ^{room.Name}", RoomId = roomId, PlayerEventId = (long?)null } }); await NotificationsController.SendToPlayer(body.PlayerId, coachJson); } return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/toggle-beta")] public async Task DirectorToggleBeta(long roomId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); room.Tags ??= new List(); var betaTag = room.Tags.FirstOrDefault(t => t.Tag.Equals("beta", StringComparison.OrdinalIgnoreCase) && (int)t.Type == 1); bool added; if (betaTag != null) { room.Tags.Remove(betaTag); added = false; } else { room.Tags.Add(new Tags { Tag = "beta", Type = (TagType)1 }); added = true; } RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true, betaEnabled = added }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/tags")] public async Task DirectorRoomTags(long roomId, [FromBody] DirectorRoomTagsRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); room.Tags ??= new List(); foreach (var tag in body.Tags ?? new List()) { string t = tag.Trim().ToLowerInvariant(); if (string.IsNullOrWhiteSpace(t)) continue; if (body.Action == "add") { if (!room.Tags.Any(x => x.Tag.Equals(t, StringComparison.OrdinalIgnoreCase))) room.Tags.Add(new Tags { Tag = t, Type = (TagType)(body.Type ?? 0) }); } else { room.Tags.RemoveAll(x => x.Tag.Equals(t, StringComparison.OrdinalIgnoreCase)); } } RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true, tags = room.Tags.Select(t => new { tag = t.Tag, type = (int)t.Type }).ToList() }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/change-datablob")] public async Task DirectorChangeDatablob(long roomId, [FromBody] DirectorChangeDatablobRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(new { error = "Room not found." }); var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == body.SubRoomId); if (subRoom == null) return NotFound(new { error = "SubRoom not found." }); string datablob = Path.GetFileName(body.Datablob); string cdnFolder = Path.Combine(Environment.CurrentDirectory, "Data", "cdn"); //Directory.CreateDirectory(cdnFolder); string localPath = Path.Combine(cdnFolder, datablob); string downloadStatus = "Already cached"; if (!System.IO.File.Exists(localPath)) { try { using var http = new HttpClient(); http.DefaultRequestHeaders.Add("User-Agent", "BestHTTP"); http.DefaultRequestHeaders.Add("Accept", "*/*"); http.DefaultRequestHeaders.Add("Referer", "https://rec.net/"); var response = await http.GetAsync($"https://cdn.rec.net/room/{datablob}"); if (response.IsSuccessStatusCode) { await System.IO.File.WriteAllBytesAsync(localPath, await response.Content.ReadAsByteArrayAsync()); downloadStatus = "Downloaded from RecNet CDN"; } else downloadStatus = $"Failed HTTP {(int)response.StatusCode}"; } catch (Exception ex) { downloadStatus = "Download error: " + ex.Message; } } var newSave = new currentSave { SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(), RoomId = roomId, SubRoomId = body.SubRoomId, DataBlob = datablob, Description = $"Imported via Director Panel", SavedByAccountId = 1, SavedOnPlatform = 0, SavedOnDeviceClass = 1, CreatedAt = DateTime.UtcNow }; RoomDB.SubRoomSaves.Insert(newSave); subRoom.DataBlob = datablob; subRoom.CurrentSave = newSave; subRoom.Accessibility = RoomAccessibility.Public; RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true, saveId = newSave.SubRoomDataSaveId, downloadStatus }); } [HttpGet("/vannet/api/director/rooms/{roomId:long}/instances")] public IActionResult DirectorGetRoomInstances(long roomId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(""); var live = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true && p.Player.PlayerExtra.Heartbeat.roomInstance != null && p.Player.PlayerExtra.Heartbeat.roomInstance.roomId == roomId) .Select(p => p.Player!.PlayerExtra!.Heartbeat!.roomInstance!) .ToList(); var subRooms = (room.SubRooms ?? new List()).Select(sub => { var mine = live.Where(ri => ri.subRoomId == sub.SubRoomId).ToList(); return new { subRoomId = sub.SubRoomId, publicInstances = mine.Where(ri => !ri.isPrivate).Select(ri => ri.roomInstanceId).Distinct().Count(), publicPlayers = mine.Count(ri => !ri.isPrivate), privateInstances = mine.Where(ri => ri.isPrivate).Select(ri => ri.roomInstanceId).Distinct().Count(), privatePlayers = mine.Count(ri => ri.isPrivate) }; }).ToList(); return Ok(new { roomId, subRooms }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/refresh-instances")] public async Task DirectorRefreshInstances(long roomId, [FromBody] DirectorRefreshInstancesRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(new { error = "Room not found." }); var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == body.SubRoomId); if (subRoom == null) return NotFound(new { error = "SubRoom not found." }); var (instancesRefreshed, playersMoved) = await Sessions.RefreshPublicInstances(roomId, body.SubRoomId); return Ok(new { success = true, instancesRefreshed, playersMoved }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-instances-privacy")] public async Task DirectorSetInstancesPrivacy(long roomId, [FromBody] DirectorSetInstancesPrivacyRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(new { error = "Room not found." }); var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == body.SubRoomId); if (subRoom == null) return NotFound(new { error = "SubRoom not found." }); var instancesChanged = await Sessions.SetInstancesPrivacy(roomId, body.SubRoomId, body.IsPrivate); return Ok(new { success = true, instancesChanged, isPrivate = body.IsPrivate }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/upload-datablob")] public async Task DirectorUploadDatablob(long roomId, [FromForm] long subRoomId, [FromForm] IFormFile file) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(new { error = "Room not found." }); var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId); if (subRoom == null) return NotFound(new { error = "SubRoom not found." }); if (file == null || file.Length == 0) return BadRequest(new { error = "No file provided." }); string fileName = Path.GetFileName(file.FileName); string cdnFolder = Path.Combine(Environment.CurrentDirectory, "Data", "cdn", "room"); Directory.CreateDirectory(cdnFolder); string savePath = Path.Combine(cdnFolder, fileName); using (var stream = new FileStream(savePath, FileMode.Create)) await file.CopyToAsync(stream); var newSave = new currentSave { SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(), RoomId = roomId, SubRoomId = subRoomId, DataBlob = fileName, Description = "Imported by Coach!", SavedByAccountId = 1, SavedOnPlatform = 0, SavedOnDeviceClass = 1, CreatedAt = DateTime.UtcNow }; RoomDB.SubRoomSaves.Insert(newSave); subRoom.DataBlob = fileName; subRoom.CurrentSave = newSave; subRoom.Accessibility = RoomAccessibility.Public; RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true, saveId = newSave.SubRoomDataSaveId, fileName }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-room-type")] public async Task DirectorSetRoomType(long roomId, [FromBody] DirectorRoomCloningRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); if (body.Allowed) room.UgcVersion = 2; else room.UgcVersion = 1; RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/{roomId:long}/set-modern-persistence")] public async Task DirectorSetModernPersistence(long roomId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); room.PersistenceVersion = 203; room.UgcSubVersion = 367; room.MinUgcSubVersion = 367; for (int i = 0; i < room.SubRooms.Count; i++) { var subRoom = room.SubRooms[i].CurrentSave; if (subRoom == null) continue; subRoom.PersistenceVersion = 203; subRoom.OMVersion = 164; subRoom.UgcSubVersion = 367; } RoomDB.Rooms.Update(room); await NotifyRoomUpdate(roomId); return Ok(new { success = true }); } [HttpDelete("/vannet/api/director/rooms/{roomId:long}/delete")] public async Task DirectorDeleteRoom(long roomId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var room = RoomDB.Rooms.FindById(roomId); if (room == null) return NotFound(""); RoomDB.DeleteRoom(roomId); var onlinePlayers = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true) .Select(p => p.PlayerId) .ToList(); foreach (var pid in onlinePlayers) await NotiController.SendRoomUpdate(pid, new { roomId, deleted = true }); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/rooms/import-epicquest")] // experimental 2 public async Task DirectorImportRecconnedRoom([FromQuery] string? name) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(name)) return BadRequest(new { error = "The name is required" }); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("User-Agent", "BestHTTP"); HttpResponseMessage meowResp; try { meowResp = await http.GetAsync($"https://recconned.gabethefirst.com/roomserver/rooms?name={Uri.EscapeDataString(name)}"); } catch (Exception ex) { return StatusCode(500, new { error = $"error at recconned.gabethefirst.com: {ex.Message}" }); } if (!meowResp.IsSuccessStatusCode) return StatusCode((int)meowResp.StatusCode, new { error = $"recconned.gabethefirst.com returned {(int)meowResp.StatusCode}" }); JsonElement meow; try { meow = JsonSerializer.Deserialize(await meowResp.Content.ReadAsStringAsync()); } catch (Exception ex) { return StatusCode(500, new { error = $"error parsing epicquest response {ex.Message}" }); } string meowName = meow.TryGetProperty("Name", out var nProp) ? nProp.GetString() ?? name : name; string resolvedName = meowName; if (RoomDB.GetRoomByName(resolvedName) != null) { const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; var rand = new Random(); char[] seg = new char[4]; string RandomSeg() { for (int i = 0; i < 4; i++) seg[i] = chars[rand.Next(chars.Length)]; return new string(seg); } resolvedName = $"{RandomSeg()}-{RandomSeg()}-{RandomSeg()}-{RandomSeg()}"; } string imageServerPath = Path.Combine(Environment.CurrentDirectory, "Data", "Images"); string cdnRoomPath = Path.Combine(Environment.CurrentDirectory, "Data", "cdn", "room"); Directory.CreateDirectory(imageServerPath); Directory.CreateDirectory(cdnRoomPath); string localImageName = "DefaultRoomImage.jpg"; if (meow.TryGetProperty("ImageName", out var imgProp) && !string.IsNullOrWhiteSpace(imgProp.GetString())) { string remoteImageName = imgProp.GetString()!; string localImageDest = Path.Combine(imageServerPath, remoteImageName); if (!System.IO.File.Exists(localImageDest)) { try { using var imgClient = new HttpClient(); imgClient.DefaultRequestHeaders.Add("User-Agent", "BestHTTP"); var imgResp = await imgClient.GetAsync($"https://eq-img.gabethefirst.com/{remoteImageName}"); if (imgResp.IsSuccessStatusCode) await System.IO.File.WriteAllBytesAsync(localImageDest, await imgResp.Content.ReadAsByteArrayAsync()); } catch { } } if (System.IO.File.Exists(localImageDest)) localImageName = remoteImageName; } WarningMaskType warningMask = default; if (meow.TryGetProperty("WarningMask", out var wmProp)) { try { warningMask = (WarningMaskType)wmProp.GetInt32(); } catch { } } var newRoom = new Room { Name = resolvedName, Description = (meow.TryGetProperty("Description", out var descProp) ? descProp.GetString() ?? "" : "") + " (Imported from EpicQuest)", ImageName = localImageName, CreatorAccountId = 2, Accessibility = RoomAccessibility.Public, CloningAllowed = true, State = RoomState.Active, IsDorm = false, IsRRO = false, IsDeveloperOwned = meow.TryGetProperty("IsDeveloperOwned", out var idoProp) && idoProp.GetBoolean(), ToxmodEnabled = false, AutoLocalizeRoom = meow.TryGetProperty("AutoLocalizeRoom", out var alProp) && alProp.GetBoolean(), LoadScreenLocked = meow.TryGetProperty("LoadScreenLocked", out var lslProp) && lslProp.GetBoolean(), MaxPlayerCalculationMode = meow.TryGetProperty("MaxPlayerCalculationMode", out var mpcmProp) ? mpcmProp.GetInt32() : 1, MaxPlayers = meow.TryGetProperty("MaxPlayers", out var mpProp) ? mpProp.GetInt32() : 20, MinLevel = meow.TryGetProperty("MinLevel", out var mlProp) ? mlProp.GetInt32() : 0, PersistenceVersion = meow.TryGetProperty("PersistenceVersion", out var pvProp) ? pvProp.GetInt32() : 2, UgcVersion = 1, DisableMicAutoMute = meow.TryGetProperty("DisableMicAutoMute", out var dmamProp) && dmamProp.GetBoolean(), DisableRoomComments = meow.TryGetProperty("DisableRoomComments", out var drcProp) && drcProp.GetBoolean(), EncryptVoiceChat = meow.TryGetProperty("EncryptVoiceChat", out var evcProp) && evcProp.GetBoolean(), SupportsJuniors = meow.TryGetProperty("SupportsJuniors", out var sjProp) && sjProp.GetBoolean(), SupportsLevelVoting = meow.TryGetProperty("SupportsLevelVoting", out var slvProp) && slvProp.GetBoolean(), SupportsMobile = meow.TryGetProperty("SupportsMobile", out var smProp) && smProp.GetBoolean(), SupportsQuest2 = meow.TryGetProperty("SupportsQuest2", out var sq2Prop) && sq2Prop.GetBoolean(), SupportsScreens = meow.TryGetProperty("SupportsScreens", out var ssProp) && ssProp.GetBoolean(), SupportsTeleportVR = meow.TryGetProperty("SupportsTeleportVR", out var stvrProp) && stvrProp.GetBoolean(), SupportsVRLow = meow.TryGetProperty("SupportsVRLow", out var svrlProp) && svrlProp.GetBoolean(), SupportsWalkVR = meow.TryGetProperty("SupportsWalkVR", out var swvrProp) && swvrProp.GetBoolean(), WarningMask = warningMask, CustomWarning = meow.TryGetProperty("CustomWarning", out var cwProp) ? cwProp.GetString() : null, CreatedAt = DateTime.UtcNow, PublishedAt = DateTime.UtcNow, Stats = new Stats { CheerCount = 0, FavoriteCount = 0, VisitorCount = 0, VisitCount = 0 }, Roles = new List { new Roles { AccountId = 2, Role = Role.Creator, InvitedRole = Role.None } }, Tags = new List(), PromoImages = new List(), PromoExternalContent = new List(), LoadScreens = new List(), SubRooms = new List() }; if (meow.TryGetProperty("Tags", out var tagsProp) && tagsProp.ValueKind == JsonValueKind.Array) { foreach (var t in tagsProp.EnumerateArray()) { int tagType = 0; if (t.TryGetProperty("Type", out var ttProp)) tagType = ttProp.GetInt32(); if (tagType != 0) continue; string? tagVal = t.TryGetProperty("Tag", out var tvProp) ? tvProp.GetString() : null; if (!string.IsNullOrWhiteSpace(tagVal)) newRoom.Tags.Add(new Tags { Tag = tagVal.ToLowerInvariant(), Type = TagType.General }); } } newRoom.Tags.Add(new Tags { Tag = "imported", Type = (TagType)2 }); long newRoomId = RoomDB.AddRoomSync(newRoom, shouldAssignNewIds: true); var insertedRoom = RoomDB.GetRoom(newRoomId); if (insertedRoom == null) return StatusCode(500, new { error = "made room but couldnt get data" }); var importLog = new List(); if (meow.TryGetProperty("SubRooms", out var subRoomsProp) && subRoomsProp.ValueKind == JsonValueKind.Array) { foreach (var sr in subRoomsProp.EnumerateArray()) { string? srDataBlob = sr.TryGetProperty("DataBlob", out var srDbProp) ? srDbProp.GetString() : null; string? srName = sr.TryGetProperty("Name", out var srNameProp) ? srNameProp.GetString() : null; int srAccessibility = sr.TryGetProperty("Accessibility", out var srAccProp) ? srAccProp.GetInt32() : 0; bool srIsSandbox = sr.TryGetProperty("IsSandbox", out var srIsSbProp) && srIsSbProp.GetBoolean(); int srMaxPlayers = sr.TryGetProperty("MaxPlayers", out var srMpProp) ? srMpProp.GetInt32() : 20; string srUnitySceneId = sr.TryGetProperty("UnitySceneId", out var srUsProp) ? srUsProp.GetString() ?? "a75f7547-79eb-47c6-8986-6767abcb4f92" : "a75f7547-79eb-47c6-8986-6767abcb4f92"; string? localDataBlob = null; if (!string.IsNullOrWhiteSpace(srDataBlob)) { string blobFileName = srDataBlob; string blobDest = Path.Combine(cdnRoomPath, blobFileName); if (!System.IO.File.Exists(blobDest)) { try { using var blobClient = new HttpClient(); blobClient.DefaultRequestHeaders.Add("User-Agent", "BestHTTP"); var blobResp = await blobClient.GetAsync($"https://eq-cdn.gabethefirst.com/room/{blobFileName}"); if (blobResp.IsSuccessStatusCode) { await System.IO.File.WriteAllBytesAsync(blobDest, await blobResp.Content.ReadAsByteArrayAsync()); localDataBlob = blobFileName; } else { importLog.Add(new { subRoom = srName, status = "blob_download_failed", code = (int)blobResp.StatusCode }); } } catch (Exception ex) { importLog.Add(new { subRoom = srName, status = "blob_download_error", error = ex.Message }); } } else { localDataBlob = blobFileName; } } long newSubRoomId = RoomDB.GetNextSubRoomId(); var newSub = new SubRooms { SubRoomId = newSubRoomId, RoomId = newRoomId, Name = srName ?? "SubRoom", Accessibility = (RoomAccessibility)srAccessibility, IsSandbox = srIsSandbox, MaxPlayers = srMaxPlayers, UnitySceneId = srUnitySceneId, DataBlob = localDataBlob, SavedByAccountId = 1806, CreatorAccountId = 1806, ShouldAutoStageSaves = false, LastModeratedSaveModerationState = 0 }; currentSave? newSave = null; if (localDataBlob != null) { newSave = new currentSave { SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(), RoomId = newRoomId, SubRoomId = newSubRoomId, DataBlob = localDataBlob, Description = $"Imported from EpicQuest Recconned", SavedByAccountId = 1806, SavedOnPlatform = 0, SavedOnDeviceClass = 1, PersistenceVersion = 0, UgcSubVersion = 0, OMVersion = 0, Tags = new List(), ModerationState = 0, CreatedAt = DateTime.UtcNow }; RoomDB.SubRoomSaves.Insert(newSave); newSub.CurrentSave = newSave; } insertedRoom.SubRooms.Add(newSub); importLog.Add(new { subRoom = srName, subRoomId = newSubRoomId, status = "imported", blob = localDataBlob }); } RoomDB.Rooms.Update(insertedRoom); } var onlinePlayers = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true) .Select(p => p.PlayerId) .ToList(); foreach (var pid in onlinePlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(newRoomId)); return Ok(new { success = true, roomId = newRoomId, name = resolvedName, subRoomsImported = importLog.Count, log = importLog }); } [HttpPost("/vannet/api/director/rooms/import-meownet")] // experimental public async Task DirectorImportMeowiiRoom([FromQuery] string? name) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(name)) return BadRequest(new { error = "The name is required" }); using var http = new HttpClient(); http.DefaultRequestHeaders.Add("User-Agent", "BestHTTP"); http.DefaultRequestHeaders.Add("key", "c0f5eff84baf6fc808f98a8a9813748053deb3b0ce1978d08ab275e54e08dcda"); HttpResponseMessage meowResp; try { meowResp = await http.GetAsync($"https://live.meowii.app/rooms?name={Uri.EscapeDataString(name)}"); } catch (Exception ex) { return StatusCode(500, new { error = $"error at meowii.app: {ex.Message}" }); } if (!meowResp.IsSuccessStatusCode) return StatusCode((int)meowResp.StatusCode, new { error = $"meowii.app returned {(int)meowResp.StatusCode}" }); JsonElement meow; try { meow = JsonSerializer.Deserialize(await meowResp.Content.ReadAsStringAsync()); } catch (Exception ex) { return StatusCode(500, new { error = $"error parsing meowii response {ex.Message}" }); } string meowName = meow.TryGetProperty("Name", out var nProp) ? nProp.GetString() ?? name : name; string resolvedName = meowName; if (RoomDB.GetRoomByName(resolvedName) != null) { const string chars = "abcdefghijklmnopqrstuvwxyz0123456789"; var rand = new Random(); char[] seg = new char[4]; string RandomSeg() { for (int i = 0; i < 4; i++) seg[i] = chars[rand.Next(chars.Length)]; return new string(seg); } resolvedName = $"{RandomSeg()}-{RandomSeg()}-{RandomSeg()}-{RandomSeg()}"; } string imageServerPath = Path.Combine(Environment.CurrentDirectory, "Data", "Images"); string cdnRoomPath = Path.Combine(Environment.CurrentDirectory, "Data", "cdn", "room"); Directory.CreateDirectory(imageServerPath); Directory.CreateDirectory(cdnRoomPath); string localImageName = "DefaultRoomImage.jpg"; if (meow.TryGetProperty("ImageName", out var imgProp) && !string.IsNullOrWhiteSpace(imgProp.GetString())) { string remoteImageName = imgProp.GetString()!; string localImageDest = Path.Combine(imageServerPath, remoteImageName); if (!System.IO.File.Exists(localImageDest)) { try { using var imgClient = new HttpClient(); imgClient.DefaultRequestHeaders.Add("User-Agent", "BestHTTP"); imgClient.DefaultRequestHeaders.Add("key", "c0f5eff84baf6fc808f98a8a9813748053deb3b0ce1978d08ab275e54e08dcda"); var imgResp = await imgClient.GetAsync($"https://cdn.cookedasset.com/{remoteImageName}"); if (imgResp.IsSuccessStatusCode) await System.IO.File.WriteAllBytesAsync(localImageDest, await imgResp.Content.ReadAsByteArrayAsync()); } catch { } } if (System.IO.File.Exists(localImageDest)) localImageName = remoteImageName; } WarningMaskType warningMask = default; if (meow.TryGetProperty("WarningMask", out var wmProp)) { try { warningMask = (WarningMaskType)wmProp.GetInt32(); } catch { } } var newRoom = new Room { Name = resolvedName, Description = (meow.TryGetProperty("Description", out var descProp) ? descProp.GetString() ?? "" : "") + " (Imported from MeowNet)", ImageName = localImageName, CreatorAccountId = 1806, Accessibility = RoomAccessibility.Public, CloningAllowed = true, State = RoomState.Active, IsDorm = false, IsRRO = false, IsDeveloperOwned = meow.TryGetProperty("IsDeveloperOwned", out var idoProp) && idoProp.GetBoolean(), ToxmodEnabled = false, AutoLocalizeRoom = meow.TryGetProperty("AutoLocalizeRoom", out var alProp) && alProp.GetBoolean(), LoadScreenLocked = meow.TryGetProperty("LoadScreenLocked", out var lslProp) && lslProp.GetBoolean(), MaxPlayerCalculationMode = meow.TryGetProperty("MaxPlayerCalculationMode", out var mpcmProp) ? mpcmProp.GetInt32() : 1, MaxPlayers = meow.TryGetProperty("MaxPlayers", out var mpProp) ? mpProp.GetInt32() : 20, MinLevel = meow.TryGetProperty("MinLevel", out var mlProp) ? mlProp.GetInt32() : 0, PersistenceVersion = meow.TryGetProperty("PersistenceVersion", out var pvProp) ? pvProp.GetInt32() : 2, UgcVersion = meow.TryGetProperty("UgcVersion", out var uvProp) ? uvProp.GetInt32() : 12, DisableMicAutoMute = meow.TryGetProperty("DisableMicAutoMute", out var dmamProp) && dmamProp.GetBoolean(), DisableRoomComments = meow.TryGetProperty("DisableRoomComments", out var drcProp) && drcProp.GetBoolean(), EncryptVoiceChat = meow.TryGetProperty("EncryptVoiceChat", out var evcProp) && evcProp.GetBoolean(), SupportsJuniors = meow.TryGetProperty("SupportsJuniors", out var sjProp) && sjProp.GetBoolean(), SupportsLevelVoting = meow.TryGetProperty("SupportsLevelVoting", out var slvProp) && slvProp.GetBoolean(), SupportsMobile = meow.TryGetProperty("SupportsMobile", out var smProp) && smProp.GetBoolean(), SupportsQuest2 = meow.TryGetProperty("SupportsQuest2", out var sq2Prop) && sq2Prop.GetBoolean(), SupportsScreens = meow.TryGetProperty("SupportsScreens", out var ssProp) && ssProp.GetBoolean(), SupportsTeleportVR = meow.TryGetProperty("SupportsTeleportVR", out var stvrProp) && stvrProp.GetBoolean(), SupportsVRLow = meow.TryGetProperty("SupportsVRLow", out var svrlProp) && svrlProp.GetBoolean(), SupportsWalkVR = meow.TryGetProperty("SupportsWalkVR", out var swvrProp) && swvrProp.GetBoolean(), WarningMask = warningMask, CustomWarning = meow.TryGetProperty("CustomWarning", out var cwProp) ? cwProp.GetString() : null, CreatedAt = DateTime.UtcNow, PublishedAt = DateTime.UtcNow, Stats = new Stats { CheerCount = 0, FavoriteCount = 0, VisitorCount = 0, VisitCount = 0 }, Roles = new List { new Roles { AccountId = 1806, Role = Role.Creator, InvitedRole = Role.None } }, Tags = new List(), PromoImages = new List(), PromoExternalContent = new List(), LoadScreens = new List(), SubRooms = new List() }; if (meow.TryGetProperty("Tags", out var tagsProp) && tagsProp.ValueKind == JsonValueKind.Array) { foreach (var t in tagsProp.EnumerateArray()) { int tagType = 0; if (t.TryGetProperty("Type", out var ttProp)) tagType = ttProp.GetInt32(); if (tagType != 0) continue; string? tagVal = t.TryGetProperty("Tag", out var tvProp) ? tvProp.GetString() : null; if (!string.IsNullOrWhiteSpace(tagVal)) newRoom.Tags.Add(new Tags { Tag = tagVal.ToLowerInvariant(), Type = TagType.General }); } } newRoom.Tags.Add(new Tags { Tag = "imported", Type = (TagType)2 }); long newRoomId = RoomDB.AddRoomSync(newRoom, shouldAssignNewIds: true); var insertedRoom = RoomDB.GetRoom(newRoomId); if (insertedRoom == null) return StatusCode(500, new { error = "made room but couldnt get data" }); var importLog = new List(); if (meow.TryGetProperty("SubRooms", out var subRoomsProp) && subRoomsProp.ValueKind == JsonValueKind.Array) { foreach (var sr in subRoomsProp.EnumerateArray()) { string? srDataBlob = sr.TryGetProperty("DataBlob", out var srDbProp) ? srDbProp.GetString() : null; string? srName = sr.TryGetProperty("Name", out var srNameProp) ? srNameProp.GetString() : null; int srAccessibility = sr.TryGetProperty("Accessibility", out var srAccProp) ? srAccProp.GetInt32() : 0; bool srIsSandbox = sr.TryGetProperty("IsSandbox", out var srIsSbProp) && srIsSbProp.GetBoolean(); int srMaxPlayers = sr.TryGetProperty("MaxPlayers", out var srMpProp) ? srMpProp.GetInt32() : 20; string srUnitySceneId = sr.TryGetProperty("UnitySceneId", out var srUsProp) ? srUsProp.GetString() ?? "a75f7547-79eb-47c6-8986-6767abcb4f92" : "a75f7547-79eb-47c6-8986-6767abcb4f92"; string? localDataBlob = null; if (!string.IsNullOrWhiteSpace(srDataBlob)) { string blobFileName = srDataBlob; string blobDest = Path.Combine(cdnRoomPath, blobFileName); if (!System.IO.File.Exists(blobDest)) { try { using var blobClient = new HttpClient(); blobClient.DefaultRequestHeaders.Add("User-Agent", "BestHTTP"); blobClient.DefaultRequestHeaders.Add("key", "c0f5eff84baf6fc808f98a8a9813748053deb3b0ce1978d08ab275e54e08dcda"); var blobResp = await blobClient.GetAsync($"https://cdn.cookedasset.com/room/{blobFileName}"); if (blobResp.IsSuccessStatusCode) { await System.IO.File.WriteAllBytesAsync(blobDest, await blobResp.Content.ReadAsByteArrayAsync()); localDataBlob = blobFileName; } else { importLog.Add(new { subRoom = srName, status = "blob_download_failed", code = (int)blobResp.StatusCode }); } } catch (Exception ex) { importLog.Add(new { subRoom = srName, status = "blob_download_error", error = ex.Message }); } } else { localDataBlob = blobFileName; } } long newSubRoomId = RoomDB.GetNextSubRoomId(); var newSub = new SubRooms { SubRoomId = newSubRoomId, RoomId = newRoomId, Name = srName ?? "SubRoom", Accessibility = (RoomAccessibility)srAccessibility, IsSandbox = srIsSandbox, MaxPlayers = srMaxPlayers, UnitySceneId = srUnitySceneId, DataBlob = localDataBlob, SavedByAccountId = 1806, CreatorAccountId = 1806, ShouldAutoStageSaves = false, LastModeratedSaveModerationState = 0 }; currentSave? newSave = null; if (localDataBlob != null) { newSave = new currentSave { SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(), RoomId = newRoomId, SubRoomId = newSubRoomId, DataBlob = localDataBlob, Description = $"Imported from meowii.app", SavedByAccountId = 1806, SavedOnPlatform = 0, SavedOnDeviceClass = 1, PersistenceVersion = 0, UgcSubVersion = 0, OMVersion = 0, Tags = new List(), ModerationState = 0, CreatedAt = DateTime.UtcNow }; RoomDB.SubRoomSaves.Insert(newSave); newSub.CurrentSave = newSave; } insertedRoom.SubRooms.Add(newSub); importLog.Add(new { subRoom = srName, subRoomId = newSubRoomId, status = "imported", blob = localDataBlob }); } RoomDB.Rooms.Update(insertedRoom); } var onlinePlayers = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true) .Select(p => p.PlayerId) .ToList(); foreach (var pid in onlinePlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(newRoomId)); return Ok(new { success = true, roomId = newRoomId, name = resolvedName, subRoomsImported = importLog.Count, log = importLog }); } [HttpPost("/vannet/api/director/players/gift-all-tokens")] public async Task DirectorGiftAllTokens([FromBody] DirectorGiftAllTokensRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (body == null || body.Amount <= 0) return BadRequest(new { error = "Amount must be anything above 0." }); var allPlayers = PlayerDB.Players.FindAll().ToList(); int count = 0; foreach (var fp in allPlayers) { var gift = GiftsDB.CreateGift( toPlayerId: fp.PlayerId, fromPlayerId: 1, currency: body.Amount, currencyType: 2, balanceType: -2, giftContext: GiftType.GameRewards_Tokens, giftRarity: 50, message: string.IsNullOrWhiteSpace(body.Message) ? "A gift for you <3" : body.Message, platform: -1, platformsToSpawnOn: -1 ); await NotiController.SendEvent(fp.PlayerId, "31", GiftsDB.MapToDTO(gift)); count++; } return Ok(new { success = true, playersGifted = count }); } [HttpGet("/vannet/api/director/images/player/{playerId}")] public IActionResult DirectorGetPlayerImages(long playerId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var db = ImageMetadataDB.GetDb(); var col = db.GetCollection("images"); var images = col.Query().Where(x => x.PlayerId == (ulong)playerId).ToList() .Select(x => new { savedImageId = x.Id, playerId = x.PlayerId, imageName = x.ImageName, accessibility = (int)x.Accessibility, accessibilityLocked = x.AccessibilityLocked, cheerCount = x.CheerCount, commentCount = x.CommentCount, createdAt = x.CreatedAt, description = x.Description, type = (int)x.Type, devLocked = x.DevLocked }).ToList(); return Ok(images); } [HttpGet("/vannet/api/director/images/global")] public IActionResult DirectorGetGlobalImages() { if (!ValidateDirectorToken(out _)) return NotFound(""); var images = ImageMetadataDB.GetGlobalImages(); return Ok(images); } [HttpGet("/vannet/api/director/images/{imageId:int}")] public IActionResult DirectorGetImageById(int imageId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var db = ImageMetadataDB.GetDb(); var col = db.GetCollection("images"); var img = col.FindById(imageId); if (img == null) return NotFound(new { error = "Image not found." }); return Ok(img); } [HttpPost("/vannet/api/director/images/{imageId:int}/set-accessibility")] public IActionResult DirectorSetImageAccessibility(int imageId, [FromBody] DirectorImageAccessibilityRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var db = ImageMetadataDB.GetDb(); var col = db.GetCollection("images"); var img = col.FindById(imageId); if (img == null) return NotFound(new { error = "Image not found." }); img.Accessibility = (ImageMetadataDB.ImageAccessibility)body.Accessibility; col.Update(img); return Ok(new { success = true, accessibility = img.Accessibility }); } [HttpPost("/vannet/api/director/images/{imageId:int}/set-accessibility-lock")] public IActionResult DirectorSetImageAccessibilityLock(int imageId, [FromBody] DirectorImageLockRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var db = ImageMetadataDB.GetDb(); var col = db.GetCollection("images"); var img = col.FindById(imageId); if (img == null) return NotFound(new { error = "Image not found." }); img.AccessibilityLocked = body.Locked; col.Update(img); return Ok(new { success = true, accessibilityLocked = img.AccessibilityLocked }); } [HttpPost("/vannet/api/director/images/{imageId:int}/set-description")] public IActionResult DirectorSetImageDescription(int imageId, [FromBody] DirectorImageDescriptionRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var db = ImageMetadataDB.GetDb(); var col = db.GetCollection("images"); var img = col.FindById(imageId); if (img == null) return NotFound(new { error = "Image not found." }); img.Description = string.IsNullOrWhiteSpace(body.Description) ? null : body.Description.Trim(); col.Update(img); return Ok(new { success = true, description = img.Description }); } [HttpPost("/vannet/api/director/images/{imageId:int}/reset-cheers")] public IActionResult DirectorResetImageCheers(int imageId) { if (!ValidateDirectorToken(out _)) return NotFound(""); /*var db = ImageMetadataDB.GetDb(); var imgCol = db.GetCollection("images"); var cheerCol = db.GetCollection("cheers"); var img = imgCol.FindById(imageId); if (img == null) return NotFound(new { error = "Image not found." }); cheerCol.DeleteMany(c => c.SavedImageId == imageId); img.CheerCount = 0; imgCol.Update(img);*/ return NotFound(new { error = "This has been disabled" }); } [HttpPost("/vannet/api/director/images/{imageId:int}/delete")] public IActionResult DirectorDeleteImage(int imageId) { if (!ValidateDirectorToken(out _)) return NotFound(""); var db = ImageMetadataDB.GetDb(); var imgCol = db.GetCollection("images"); var cheerCol = db.GetCollection("cheers"); var img = imgCol.FindById(imageId); if (img == null) return NotFound(new { error = "Image not found." }); var filePath = Path.Combine("Data", "Images", img.PlayerId.ToString(), img.ImageName); bool fileDeleted = false; if (System.IO.File.Exists(filePath)) { System.IO.File.Delete(filePath); fileDeleted = true; } cheerCol.DeleteMany(c => c.SavedImageId == imageId); imgCol.Delete(imageId); return Ok(new { success = true, fileDeleted }); } [HttpPost("/vannet/api/director/images/{imageId:int}/set-dev-lock")] public IActionResult DirectorSetImageDevLock(int imageId, [FromBody] DirectorImageDevLockRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var db = ImageMetadataDB.GetDb(); var col = db.GetCollection("images"); var img = col.FindById(imageId); if (img == null) return NotFound(new { error = "Image not found." }); img.DevLocked = body.DevLocked; col.Update(img); return Ok(new { success = true, devLocked = img.DevLocked }); } [HttpPost("/vannet/api/director/images/{imageId:int}/transfer")] public IActionResult DirectorTransferImage(int imageId, [FromBody] DirectorImageTransferRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var db = ImageMetadataDB.GetDb(); var col = db.GetCollection("images"); var img = col.FindById(imageId); if (img == null) return NotFound(new { error = "Image not found." }); var newOwner = PlayerDB.Players.FindById(body.NewPlayerId); if (newOwner == null) return NotFound(new { error = "Target player not found." }); img.PlayerId = (ulong)body.NewPlayerId; col.Update(img); return Ok(new { success = true, imageId, newPlayerId = body.NewPlayerId }); } public class DirectorImageAccessibilityRequest { public int Accessibility { get; set; } } public class DirectorImageLockRequest { public bool Locked { get; set; } } public class DirectorImageDescriptionRequest { public string? Description { get; set; } } public class DirectorImageDevLockRequest { public bool DevLocked { get; set; } } public class DirectorImageTransferRequest { public long NewPlayerId { get; set; } } [HttpGet("/vannet/apis/api/images/v3/feed/global")] public IActionResult GetGlobalImagesPublic([FromQuery] int skip = 0, [FromQuery] int take = 10, [FromQuery] string? since = null) { DateTime? parsedSince = null; if (!string.IsNullOrWhiteSpace(since)) { if (!DateTimeOffset.TryParse(since, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var sinceDto)) return BadRequest(); parsedSince = sinceDto.UtcDateTime.AddDays(-1); } return Ok(ImageMetadataDB.GetGlobalImagesPublic(skip, take, parsedSince)); } [HttpGet("/vannet/api/director/db/info")] public IActionResult DirectorDbInfo() { if (!ValidateDirectorToken(out _)) return NotFound(""); return Ok(new { players = PlayerDB.Players.Count(), rooms = RoomDB.Rooms.Count(), saves = RoomDB.SubRoomSaves.Count(), inventions = InventionDB.Inventions.Count() }); } [HttpPost("/vannet/api/director/db/execute")] public async Task DirectorDbExecute([FromBody] DirectorDbExecRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); if (string.IsNullOrWhiteSpace(body?.Expression)) return BadRequest(new { error = "Expression required." }); string expression = body.Expression.Trim(); object? result = null; string? error = null; try { var setRoomMatch = System.Text.RegularExpressions.Regex.Match(expression, @"^RoomDB\.Rooms\.Set\((\d+),\s*(\w+),\s*(.*)\)$", System.Text.RegularExpressions.RegexOptions.IgnoreCase); var setPlayerMatch = System.Text.RegularExpressions.Regex.Match(expression, @"^PlayerDB\.Players\.Set\((\d+),\s*(\w+),\s*(.*)\)$", System.Text.RegularExpressions.RegexOptions.IgnoreCase); if (setRoomMatch.Success) { long rid = long.Parse(setRoomMatch.Groups[1].Value); string field = setRoomMatch.Groups[2].Value; string rawValue = setRoomMatch.Groups[3].Value.Trim().Trim('"'); var room = RoomDB.Rooms.FindOne(r => r.RoomId == rid); if (room == null) { error = $"Room {rid} not found."; } else { var prop = typeof(Room).GetProperty(field, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase); if (prop == null) { error = $"Room has no property '{field}'."; } else { object? converted = prop.PropertyType.IsEnum ? Enum.Parse(prop.PropertyType, rawValue, true) : Convert.ChangeType(rawValue, prop.PropertyType); string oldVal = prop.GetValue(room)?.ToString() ?? "null"; prop.SetValue(room, converted); RoomDB.Rooms.Update(room); await NotifyRoomUpdate(rid); result = new { updated = true, roomId = rid, field, oldValue = oldVal, newValue = rawValue }; } } } else if (setPlayerMatch.Success) { long pid = long.Parse(setPlayerMatch.Groups[1].Value); string field = setPlayerMatch.Groups[2].Value; string rawValue = setPlayerMatch.Groups[3].Value.Trim().Trim('"'); var player = PlayerDB.Players.FindById(pid); if (player == null) { error = $"Player {pid} not found."; } else { var prop = typeof(Player).GetProperty(field, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase); if (prop == null) { error = $"Player has no property '{field}'."; } else { object? converted = prop.PropertyType.IsEnum ? Enum.Parse(prop.PropertyType, rawValue, true) : Convert.ChangeType(rawValue, prop.PropertyType); string oldVal = prop.GetValue(player.Player)?.ToString() ?? "null"; prop.SetValue(player.Player, converted); PlayerDB.Players.Update(player); await NotiController.SendAccountUpdate(pid, PlayerDB.GetAccountMe(pid)); result = new { updated = true, playerId = pid, field, oldValue = oldVal, newValue = rawValue }; } } } else if (expression.StartsWith("RoomDB.Rooms.Count()", StringComparison.OrdinalIgnoreCase)) result = RoomDB.Rooms.Count(); else if (expression.StartsWith("RoomDB.Rooms.FindAll()", StringComparison.OrdinalIgnoreCase)) result = RoomDB.Rooms.FindAll().Select(r => new { roomId = r.RoomId, name = r.Name }).ToList(); else if (expression.StartsWith("RoomDB.SubRoomSaves.Count()", StringComparison.OrdinalIgnoreCase)) result = RoomDB.SubRoomSaves.Count(); else if (expression.StartsWith("PlayerDB.Players.Count()", StringComparison.OrdinalIgnoreCase)) result = PlayerDB.Players.Count(); else if (expression.StartsWith("PlayerDB.Players.FindAll()", StringComparison.OrdinalIgnoreCase)) result = PlayerDB.Players.FindAll().Select(p => new { playerId = p.PlayerId, username = p.Player?.Username, displayName = p.Player?.DisplayName }).ToList(); else if (expression.StartsWith("reset artixsu dorm room", StringComparison.OrdinalIgnoreCase)) { var player = PlayerDB.Players.FindById(577); if (player?.Player?.PlayerExtra != null) { player.Player.PlayerExtra.DormRoomId = 4298; PlayerDB.Players.Update(player); result = true; } else { result = false; } } else if (expression.StartsWith("GetLogins With Platform ID: ", StringComparison.OrdinalIgnoreCase)) { var platformIdStr = expression.Substring("GetLogins With Platform ID: ".Length); if (long.TryParse(platformIdStr, out long platformId)) { result = PlayerDB.Players.FindAll() .Where(p => p.PlatformIds != null && p.PlatformIds.Any(pid => pid.Platform == 0 && pid.PlatformId == platformIdStr)) .OrderByDescending(x => x.Player.LastLoginAt) .ToList(); } else { error = "Invalid platform ID."; } } else if (expression.StartsWith("PlayerDB.ResetAllHeartbeats()", StringComparison.OrdinalIgnoreCase)) { PlayerDB.ResetAllHeartbeats(); result = true; } else if (expression.StartsWith("LeaderboardDB.ClearRoomLeaderboard(4974)", StringComparison.OrdinalIgnoreCase)) result = LeaderboardDB.ClearRoomLeaderboard(4974); else if (expression.StartsWith("Add everyone to group chat 61", StringComparison.OrdinalIgnoreCase)) { var chat = ChatDB.GetThread(61); foreach (var player in PlayerDB.Players.FindAll()) { if (player.Player != null) { Console.WriteLine($"[Director] Adding player {player.PlayerId} ({player.Player.Username}) to chat thread 61."); if (!chat.PlayerIds.Any(m => m == player.PlayerId)) { ChatDB.AddMemberToThread(61, 1, player.PlayerId); Console.Write("Done"); } } } result = true; } else if (expression.StartsWith("Clear group chat 61", StringComparison.OrdinalIgnoreCase)) { var thread = ChatDB.GetThread(61); thread.Messages.Clear(); ChatDB.ChatDBFile.GetCollection("threads").Update(thread); result = true; } else if (expression.StartsWith("Erase all player ids from group chat 61", StringComparison.OrdinalIgnoreCase)) { var thread = ChatDB.GetThread(61); thread.PlayerIds.Clear(); ChatDB.ChatDBFile.GetCollection("threads").Update(thread); result = true; } else if (expression.StartsWith("Delete 49 data", StringComparison.OrdinalIgnoreCase)) { PlayerDB.SetPlayerData(49, 181, ""); PlayerDB.SetPlayerData(49, 4974, ""); result = true; } else if (expression.StartsWith("Delete 786 data", StringComparison.OrdinalIgnoreCase)) { PlayerDB.SetPlayerData(786, 181, ""); PlayerDB.SetPlayerData(786, 4974, ""); result = true; } else if (expression.StartsWith("publish my invention", StringComparison.OrdinalIgnoreCase)) { var invention = InventionDB.Inventions.FindOne(i => i.InventionId == 5012352472715840770); invention.Accessibility = RoomAccessibility.Public; invention.GeneralPermission = 20; invention.IsAGInvention = true; invention.IsCertifiedInvention = true; invention.IsPublished = true; invention.IsFeatured = true; invention.IsRecRoomApproved = true; InventionDB.Inventions.Update(invention); result = true; } else if (expression.StartsWith("GetInventionBlobNameByInventionName:", StringComparison.OrdinalIgnoreCase)) { expression = expression.Substring("GetInventionBlobNameByInventionName:".Length).Trim(); var invention = InventionDB.Inventions.FindOne(i => i.Name.Equals(expression, StringComparison.OrdinalIgnoreCase)); if (invention != null) result = invention.CurrentVersion.BlobName; else result = false; } else if (expression.StartsWith("Get 2340 avatar", StringComparison.OrdinalIgnoreCase)) { var player = PlayerDB.Players.FindById(2672); player.Player.PlayerExtra.Avatar = PlayerDB.Players.FindById(2340)?.Player.PlayerExtra.Avatar; PlayerDB.Players.Update(player); result = player.Player.PlayerExtra?.Avatar; } else if (expression.StartsWith("playersettings 2300", StringComparison.OrdinalIgnoreCase)) result = PlayerDB.Players.FindById(1904800648)?.Player?.PlayerExtra?.Settings; else if (expression.StartsWith("Backfill HAS_COMPLETED_ORIENTATION for unfinished accounts", StringComparison.OrdinalIgnoreCase)) { var updatedPlayerIds = new List(); foreach (var player in PlayerDB.Players.FindAll().ToList()) { var settings = PlayerDB.SettingsPlayers.FindById(player.PlayerId)?.Player?.PlayerExtra?.Settings ?? player.Player?.PlayerExtra?.Settings; if (settings == null) continue; var hasFinished = settings.FirstOrDefault(s => s.Key == "Recroom.AccountCreation.HasFinished"); Console.WriteLine($"[Director] Player {player.PlayerId} ({player.Player?.Username}) has HAS_COMPLETED_ORIENTATION: {hasFinished?.Value ?? "null"}"); if (player.PlayerId > 1186) { Console.Write($"[Director] Backfilling HAS_COMPLETED_ORIENTATION for player {player.PlayerId} ({player.Player?.Username})"); PlayerDB.SetPlayerSetting("HAS_COMPLETED_ORIENTATION", "True", player.PlayerId); PlayerDB.SetPlayerSetting("OrientationCompletionTime", "2025-07-09T21%3A52%3A18.8129095Z", player.PlayerId); updatedPlayerIds.Add(player.PlayerId); Console.WriteLine(" - Done"); } } result = new { updatedCount = updatedPlayerIds.Count, playerIds = updatedPlayerIds }; } else if (expression.StartsWith("backfill new", StringComparison.OrdinalIgnoreCase)) { foreach (var player in PlayerDB.Players.FindAll().ToList()) { if (player.Player != null) { var id = player.PlayerId; Console.WriteLine($"[Director] Setting player {id} ({player.Player.Username}) setting."); PlayerDB.SetPlayerSetting("Recroom.AccountCreation.HasFinished", "False", id); PlayerDB.SetPlayerSetting("Recroom.AccountCreation.HasCreatedPassword", "False", id); PlayerDB.SetPlayerSetting("HAS_COMPLETED_ORIENTATION", "True", id); PlayerDB.SetPlayerSetting("OrientationCompletionTime", "2025-07-09T21%3A52%3A18.8129095Z", id); if (player.Player.Username.Length >= 4 && char.IsDigit(player.Player.Username[player.Player.Username.Length - 4]) && char.IsDigit(player.Player.Username[player.Player.Username.Length - 3]) && char.IsDigit(player.Player.Username[player.Player.Username.Length - 2]) && char.IsDigit(player.Player.Username[player.Player.Username.Length - 1])) PlayerDB.SetPlayerSetting("Recroom.AccountCreation.HasChosenUsername", "False", id); else PlayerDB.SetPlayerSetting("Recroom.AccountCreation.HasChosenUsername", "True", id); Console.WriteLine($"[Director] Player {id} ({player.Player.Username}) settings updated."); } } result = true; } else if (expression.StartsWith("list players 100000+ tokens", StringComparison.OrdinalIgnoreCase)) { var richPlayers = new List(); foreach (var player in PlayerDB.Players.FindAll().ToList()) { var tokenBalance = player.Player?.PlayerExtra?.Currencies? .FirstOrDefault(c => c.CurrencyType == CurrencyType.RecCenterTokens && c.BalanceType == BalanceType.NonPurchasedDefault)?.Balance ?? 0; if (tokenBalance >= 100000) { richPlayers.Add(new { PlayerId = player.PlayerId, Username = player.Player?.Username, Tokens = tokenBalance }); } } result = richPlayers; } else { var ridMatch = System.Text.RegularExpressions.Regex.Match(expression, @"RoomDB\.Rooms\.FindOne.*?(\d+)"); var pidMatch = System.Text.RegularExpressions.Regex.Match(expression, @"PlayerDB\.Players\.FindOne.*?(\d+)"); if (ridMatch.Success && long.TryParse(ridMatch.Groups[1].Value, out long rid)) result = RoomDB.GetRoom(rid); else if (pidMatch.Success && long.TryParse(pidMatch.Groups[1].Value, out long pid)) result = PlayerDB.Players.FindById(pid); else error = "Unrecognised expression pattern."; } } catch (Exception ex) { error = ex.Message; } if (error != null) return BadRequest(new { error }); return Ok(new { result }); } [HttpPost("/vannet/api/director/rooms/clear")] public async Task DirectorClearRooms() { if (!ValidateDirectorToken(out _)) return NotFound(""); //await RoomDB.ClearRooms(); //await RoomDB.ImportRooms(Path.Join(Environment.CurrentDirectory, "Data", "Imports", "ImportRooms.json")); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/db/reset")] public async Task DirectorDbReset() { if (!ValidateDirectorToken(out _)) return NotFound(""); //await RoomDB.ClearRooms(); //PlayerDB.Players.DeleteAll(); //InventionDB.Inventions.DeleteAll(); //FriendsDB.FriendsDBFile.GetCollection("Friends").DeleteAll(); //EventDB.Events.DeleteAll(); //await RoomDB.ImportRooms(Path.Join(Environment.CurrentDirectory, "Data", "Imports", "ImportRooms.json")); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/coach/send-msg")] public async Task SendCoachMessage([FromBody] DirectorCoachMessageRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); await NotiController.SendCoahMessage(body?.Message ?? ""); return Ok(new { success = true }); } private static readonly object MaintenanceConfigLock = new(); // i might not have a brain gentlemen but i have an IDEA [HttpPost("/vannet/api/director/maintenance/schedule")] public async Task DirectorScheduleMaintenance([FromBody] DirectorMaintenanceRequest body) { if (!ValidateDirectorToken(out var session)) return NotFound(""); var minutes = Math.Clamp(body?.Minutes ?? 10, 0, 1440); var path = Path.Join(Program.dataDir, "APIS", "ConfigV2.json"); try { lock (MaintenanceConfigLock) { if (!System.IO.File.Exists(path)) return NotFound(new { error = "ConfigV2.json not found." }); var root = JsonNode.Parse(System.IO.File.ReadAllText(path))?.AsObject(); if (root == null) return StatusCode(500, new { error = "ConfigV2.json is malformed." }); root.Remove("ServerMaintainence"); root["ServerMaintenance"] = new JsonObject { ["StartsInMinutes"] = minutes }; System.IO.File.WriteAllText(path, root.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); } } catch (Exception ex) { Console.WriteLine($"[Director] Failed to write maintenance config: {ex.Message}"); return StatusCode(500, new { error = "Failed to update config." }); } var push = JsonSerializer.Serialize(new { Id = ((int)Vanadium.Classes.WebSocket.WebsocketEvents.ResponseResults.ServerMaintenance).ToString(), Msg = new { StartsInMinutes = minutes } }); await NotificationsController.SendToAll(push); Console.WriteLine($"[Director] Maintenance set to {minutes}m by {session?.DiscordUserId ?? "unknown"}"); return Ok(new { success = true, minutes }); } [HttpGet("/vannet/api/director/system-prompt")] public IActionResult DirectorGetSystemPrompt() { if (!ValidateDirectorToken(out _)) return NotFound(""); string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "systemprompt.txt"); if (!System.IO.File.Exists(path)) return Ok(new { message = "" }); return Ok(new { message = System.IO.File.ReadAllText(path) }); } [HttpPost("/vannet/api/director/system-prompt")] public async Task DirectorSetSystemPrompt([FromBody] string body) { if (!ValidateDirectorToken(out _)) return NotFound(""); string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "systemprompt.txt"); if (!System.IO.File.Exists(path)) return NotFound(new { error = "systemprompt.txt not found." }); System.IO.File.WriteAllText(path, body ?? ""); return Ok(new { success = true }); } [HttpPost("/vannet/api/director/players/{playerId:long}/raw-mod-details")] public async Task DirectorSetRawModDetails(long playerId, [FromBody] DirectorRawModDetailsRequest body) { if (!ValidateDirectorToken(out _)) return NotFound(""); var player = PlayerDB.Players.FindById(playerId); if (player?.Player == null) return NotFound(new { error = "Player not found." }); var mbd = new ModerationBlockDetails { ReportCategory = body.ReportCategory, Duration = body.Duration, GameSessionId = body.GameSessionId, IsHostKick = body.IsHostKick, Message = body.Message, PlayerIdReporter = body.PlayerIdReporter, IsBan = body.IsBan, IsVoiceModAutoban = body.IsVoiceModAutoban, IsDeviceBan = body.IsDeviceBan, IsWarning = body.IsWarning, VoteKickReason = body.VoteKickReason, TimeoutStartedAt = body.TimeoutStartedAt, AssociatedAccountUsername = body.AssociatedAccountUsername }; player.Player.PlayerExtra ??= new PlayerExtra(); player.Player.PlayerExtra.ModerationBlockDetails = mbd; if (body.BootToDorm) { player.Player.PlayerExtra.Heartbeat = new Heartbeat { playerId = playerId, isOnline = false, roomInstance = null, errorCode = 0 }; PlayerDB.Players.Update(player); await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat); await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId)); await NotificationsController.SendBanned(playerId, mbd); } else { PlayerDB.Players.Update(player); } return Ok(new { success = true }); } [HttpGet("/vannet/api/players/v1/online")] public async Task PlayersOnline() { var onlinePlayers = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true) .Select(p => new { accountId = p.PlayerId, username = p.Player?.Username, displayName = p.Player?.DisplayName, roomName = p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.Name }) .ToList(); return Ok(new { count = onlinePlayers.Count, players = onlinePlayers }); } private async Task NotifyRoomUpdate(long roomId) { var updated = RoomDB.GetRoom(roomId); var onlinePlayers = PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true) .Select(p => p.PlayerId) .ToList(); foreach (var pid in onlinePlayers) await NotiController.SendRoomUpdate(pid, updated); } private static object BuildPlayerSummary(FullPlayer p) => new { accountId = p.PlayerId, username = p.Player?.Username, displayName = p.Player?.DisplayName, profileImage = p.Player?.ProfileImage, level = p.Player?.Level ?? 1, createdAt = p.Player?.CreatedAt, isOnline = p.Player?.PlayerExtra?.Heartbeat?.isOnline ?? false }; private static object BuildPlayerFull(FullPlayer p) => new { accountId = p.PlayerId, username = p.Player?.Username, displayName = p.Player?.DisplayName, profileImage = p.Player?.ProfileImage, level = p.Player?.Level ?? 1, xp = p.Player?.XP ?? 0, bio = p.Player?.Bio, createdAt = p.Player?.CreatedAt, lastLoginAt = p.Player?.LastLoginAt, isJunior = p.Player?.IsJunior, roles = p.PlayerRoles?.Select(r => r.ToString()).ToList() ?? new List(), heartbeat = p.Player?.PlayerExtra?.Heartbeat, dormRoomId = p.Player?.PlayerExtra?.DormRoomId, moderationBlockDetails = p.Player?.PlayerExtra?.ModerationBlockDetails, toxMod = p.Player?.PlayerExtra?.ToxMod, juniorStatus = p.Player?.PlayerExtra?.JuniorStatus, currencies = p.Player?.PlayerExtra?.Currencies, ownedEquipment = p.Player?.PlayerExtra?.OwnedEquipment, influencer = p.Player?.PlayerExtra?.Influencer, platformIds = p.PlatformIds, nameLocked = p.Player?.NameLocked ?? false, recRoomPlus = ReadRRPlusIds().Contains(p.PlayerId), discordUserId = p.Player?.DiscordUserId, discordUsername = p.Player?.DiscordUsername, discordLinked = p.Player?.DiscordLinked ?? false, discordLinkedAt = p.Player?.DiscordLinkedAt }; private static object BuildRoomSummary(Room r) => new { roomId = r.RoomId, name = r.Name, imageName = r.ImageName, creatorAccountId = r.CreatorAccountId, stats = r.Stats, accessibility = r.Accessibility, state = r.State, tags = r.Tags, maxPlayers = r.MaxPlayers, ageRating = r.AgeRating, createdAt = r.CreatedAt }; public class DirectorCoachMessageRequest { public string? Message { get; set; } } public class DirectorPlayerCoachMessageRequest { public string? Message { get; set; } } public class DirectorSendWsHexRequest { public string? Hex { get; set; } public bool Exact { get; set; } } public class DirectorSendWsJsonRequest { public string? Json { get; set; } } public class DirectorShoveAllRequest { public long RoomId { get; set; } public long SubRoomId { get; set; } public string? InstanceId { get; set; } } public class DirectorDiscordCallbackRequest { public string? Code { get; set; } } public class DirectorRenameRequest { public string? Username { get; set; } } public class DirectorCreatePlayerRequest { public long PlayerId { get; set; } public string? Username { get; set; } public bool IsJunior { get; set; } = false; } public class DirectorNameLockRequest { public bool Locked { get; set; } = true; } public class DirectorAnnouncementRequest { public string? Title { get; set; } public string? Body { get; set; } public string? ImageName { get; set; } public string? LinkName { get; set; } public string? LinkUri { get; set; } public AnnoucementDBClasses.LinkType LinkType { get; set; } public AnnoucementDBClasses.AnnouncementType AnnouncementType { get; set; } public bool Clear { get; set; } = false; } public class DirectorCommunityBannerRequest { public string? Message { get; set; } public string? MoreInfoUrl { get; set; } } public class DirectorRRPlusRequest { public bool Enabled { get; set; } = true; } public class DirectorBioRequest { public string? Bio { get; set; } } public class DirectorPasswordRequest { public string? Password { get; set; } } public class DirectorSetPfpRequest { public string? ImageName { get; set; } } public class DirectorSetLevelRequest { public int Level { get; set; } public int? Xp { get; set; } } public class DirectorSetJuniorRequest { public bool IsJunior { get; set; } = false; public string? Reason { get; set; } } public class DirectorCoachDmRequest { public string? Message { get; set; } } public class DirectorSetPlatformIdRequest { public string? PlatformId { get; set; } } public class DirectorShoveRequest { public long RoomId { get; set; } public long? SubRoomId { get; set; } public string? InstanceId { get; set; } } public class DirectorTransferRoomsRequest { public long NewPlayerId { get; set; } public bool KeepCoOwner { get; set; } = false; } public class DirectorRoleRequest { public string? Role { get; set; } public string? Action { get; set; } } public class DirectorInfluencerRequest { public string? Action { get; set; } } public class DirectorBanRequest { public string? Reason { get; set; } public int Duration { get; set; } = 99999; } public class DirectorHwidBanRequest { public string? Hwid { get; set; } public long? PlayerId { get; set; } public string? Reason { get; set; } public int? DurationDays { get; set; } } public class DirectorHwidUnbanRequest { public string? Hwid { get; set; } } public class DirectorSetTokensRequest { public int Amount { get; set; } public int BalanceType { get; set; } = 0; } public class DirectorGiveSkinRequest { public string? Skin { get; set; } public string? Action { get; set; } } public class DirectorRoomNameRequest { public string? Name { get; set; } } public class DirectorRoomDescRequest { public string? Description { get; set; } } public class DirectorRoomImageRequest { public string? ImageName { get; set; } } public class DirectorRoomAccessibilityRequest { public int Accessibility { get; set; } } public class DirectorRoomCloningRequest { public bool Allowed { get; set; } } public class DirectorRoomRestrictionsRequest { public bool SupportsScreens { get; set; } public bool SupportsWalkVR { get; set; } public bool SupportsTeleportVR { get; set; } public bool SupportsJuniors { get; set; } public bool SupportsMobile { get; set; } } public class DirectorRoomAgeRatingRequest { public int AgeRating { get; set; } } public class DirectorRoomMaxPlayersRequest { public int MaxPlayers { get; set; } } public class DirectorChangeOwnerRequest { public long NewOwnerId { get; set; } } public class DirectorSetRoomRoleRequest { public long PlayerId { get; set; } public int Role { get; set; } } public class DirectorRoomTagsRequest { public List? Tags { get; set; } public string? Action { get; set; } public int? Type { get; set; } } public class DirectorChangeDatablobRequest { public long SubRoomId { get; set; } public string? Datablob { get; set; } } public class DirectorRefreshInstancesRequest { // the panel can hand us the id as a string when safeParse quotes large ids [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public long SubRoomId { get; set; } } public class DirectorSetInstancesPrivacyRequest { // the panel can hand us the id as a string when safeParse quotes large ids [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public long SubRoomId { get; set; } public bool IsPrivate { get; set; } } public class DirectorDbExecRequest { public string? Expression { get; set; } } public class DirectorMaintenanceRequest { public int Minutes { get; set; } = 10; } public class DirectorToxModBanRequest { public string? Category { get; set; } public string? Note { get; set; } public long? DurationSeconds { get; set; } public bool Loud { get; set; } = false; } public class DirectorToxModUnbanRequest { public bool ResetStrikes { get; set; } = false; } public class DirectorGiftAllTokensRequest { public int Amount { get; set; } public string? Message { get; set; } } public class DirectorForceBringRequest { public string? Username { get; set; } } public class DirectorRawModDetailsRequest { public ReportCategory ReportCategory { get; set; } = ReportCategory.Moderator; public int Duration { get; set; } = 0; public long GameSessionId { get; set; } = 0; public bool? IsHostKick { get; set; } = false; public string? Message { get; set; } public ulong? PlayerIdReporter { get; set; } public bool? IsBan { get; set; } = false; public bool? IsVoiceModAutoban { get; set; } = false; public bool? IsDeviceBan { get; set; } = false; public bool? IsWarning { get; set; } = false; public string? VoteKickReason { get; set; } public DateTime? TimeoutStartedAt { get; set; } public string? AssociatedAccountUsername { get; set; } public bool BootToDorm { get; set; } = false; } public class DirectorAuditFilter : IAsyncActionFilter { private const string WebhookUrl = "https://discord.com/api/webhooks/1545617304208023625/rRR-9CQjtLZtWpnOoHtCrIKfelX1EPn8AI0I4OopG6e-TN0MBl8e8QWmrWpiXDqOK--O"; public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var request = context.HttpContext.Request; if (!request.Path.Value?.StartsWith("/vannet/api/director/") == true || request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) || request.Path.Value!.Equals("/vannet/api/director/discord-callback", StringComparison.OrdinalIgnoreCase)) { await next(); return; } string? discordUserId = null; string? token = request.Headers["X-Director-Token"].FirstOrDefault() ?? request.Cookies["director_session"]; if (!string.IsNullOrWhiteSpace(token)) { lock (WebsiteController.SessionLock) { if (WebsiteController.DirectorSessions.TryGetValue(token, out var s) && s.ExpiresAt > DateTime.UtcNow) discordUserId = s.DiscordUserId; } } string requestData = ReadRequestDataAsync(context); var executed = await next(); string responseData = ReadResponseData(executed.Result); string action = request.Path.Value ?? "unknown"; if (request.QueryString.HasValue) action += request.QueryString.Value; _ = SendEmbedAsync(discordUserId, action, requestData, responseData); } private static string ReadRequestDataAsync(ActionExecutingContext context) { try { var request = context.HttpContext.Request; var parts = new List(); if (request.Query.Count > 0) parts.AddRange(request.Query.Keys.Select(k => $"{k}: {request.Query[k]}")); foreach (var arg in context.ActionArguments.Values) { if (arg == null) continue; try { var json = JsonSerializer.Serialize(arg, new JsonSerializerOptions { WriteIndented = false }); var doc = JsonDocument.Parse(json); foreach (var prop in doc.RootElement.EnumerateObject()) parts.Add($"{prop.Name}: {prop.Value}"); } catch { parts.Add(arg.ToString() ?? ""); } } return parts.Count > 0 ? string.Join("\n", parts) : "*(no body)*"; } catch { } return "*(no body)*"; } private static string ReadResponseData(IActionResult? result) { try { if (result is ObjectResult obj && obj.Value != null) { string json = JsonSerializer.Serialize(obj.Value, new JsonSerializerOptions { WriteIndented = false }); if (json.Length > 800) json = json[..800] + "…"; try { var doc = JsonDocument.Parse(json); var sb = new StringBuilder(); foreach (var prop in doc.RootElement.EnumerateObject()) sb.Append($"{prop.Name}: {prop.Value} "); return sb.ToString().TrimEnd(); } catch { return json; } } if (result is StatusCodeResult sc) return $"status: {sc.StatusCode}"; } catch { } return "*(no response)*"; } private static async Task SendEmbedAsync(string? discordUserId, string action, string requestData, string responseData) { try { using var http = new HttpClient(); var embed = new { color = 0x1a1a1a, description = $"<@{discordUserId ?? "unknown"}> executed action: **{action}**", fields = new[] { new { name = "Request", value = string.IsNullOrWhiteSpace(requestData) ? "*(no body)*" : $"```{requestData}```", inline = false }, new { name = "Response", value = string.IsNullOrWhiteSpace(responseData) ? "*(no response)*" : $"```{responseData}```", inline = false } } }; var payload = JsonSerializer.Serialize(new { embeds = new[] { embed } }); await http.PostAsync(WebhookUrl, new StringContent(payload, Encoding.UTF8, "application/json")); } catch { } } } } }