using System.Text.RegularExpressions; using LiteDB; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Configuration.UserSecrets; using Newtonsoft.Json; using Vanadium.Auth; using Vanadium.Classes; using Vanadium.Classes.DBs; using Vanadium.Classes.DBs.DBClasses; using Vanadium.Classes.WebSocket; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Text.Json; using System.Text; using Vanadium.Utils; using static Vanadium.Classes.DBs.DBClasses.RoomDBClasses; using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses; using Vanadium.Utils.NotiController; namespace Vanadium.Controllers { [ApiController] [Route("/roomserver")] public partial class RoomController : ControllerBase { private static readonly System.Collections.Concurrent.ConcurrentDictionary> _cloneRateLimits = new(); private IActionResult RoomError(string errorId, string message, int status = 200) => StatusCode(status, new { success = false, error_id = errorId, error = message, value = (object?)null }); private bool CanEdit(Room room, long playerId) => room.CreatorAccountId == playerId || (room.Roles?.Any(r => r.AccountId == playerId && (r.Role == Role.CoOwner || r.Role == Role.Creator || r.Role == Role.TemporaryCoOwner)) ?? false); private static bool IsBaseRoom(Room r) => r.Tags != null && r.Tags.Any(t => t.Tag != null && t.Tag.Equals("base", StringComparison.OrdinalIgnoreCase)); private bool CanViewSaves(Room room, long playerId) => room.CreatorAccountId == playerId || (room.Roles?.Any(r => r.AccountId == playerId && (r.Role == Role.CoOwner || r.Role == Role.Creator || r.Role == Role.TemporaryCoOwner)) ?? false) || (PlayerDB.Players.FindById(playerId)?.PlayerRoles?.Contains(PlayerDBClasses.PlayerRoles.Developer) ?? false); private bool RequestHasGameClientRole() { var auth = Request.Headers["Authorization"].FirstOrDefault(); if (string.IsNullOrEmpty(auth) || !auth.StartsWith("Bearer ")) return false; var tokenStr = auth.Substring("Bearer ".Length).Trim(); try { var handler = new JwtSecurityTokenHandler(); var jwt = handler.ReadJwtToken(tokenStr); var roleClaim = jwt.Claims.FirstOrDefault(c => c.Type == "role")?.Value; if (string.IsNullOrEmpty(roleClaim)) return false; var roles = System.Text.Json.JsonSerializer.Deserialize>(roleClaim); return roles?.Contains("gameClient") ?? false; } catch { return false; } } [HttpGet("rooms")] public async Task GetRoomBy([FromQuery] string? name) { if (!string.IsNullOrEmpty(name)) { var room = RoomDB.GetRoomByName(name); return room != null ? Ok(room) : NotFound(); } return NotFound(""); } /*[HttpGet("rooms/{roomId}/subrooms/{subroomId}/saves/{subRoomDataSaveId}")] public async Task GetRoomSave(ulong roomId, ulong subroomId, ulong subRoomDataSaveId, [FromQuery] int unityAssetTarget, [FromQuery] int unityAssetVersion) { var playerId = AuthStuff.GetPlayerId(Request); if (playerId == null) return Unauthorized(""); var room = RoomDB.GetRoom((long)roomId); if (room == null) return new ContentResult() { Content = "", ContentType = "application/json", StatusCode = 404 }; if (!CanViewSaves(room, (long)playerId)) return StatusCode(403); var save = RoomDB.GetRoomSaveById(roomId, subroomId, subRoomDataSaveId, unityAssetTarget, unityAssetVersion); if (save == null) { return new ContentResult() { Content = "", ContentType = "application/json", StatusCode = 404 }; } return Ok(save); }*/ [HttpGet("rooms/{roomid}")] public IActionResult GetRoomById(long roomid) { var room = RoomDB.GetRoom(roomid); if (room == null) { return Ok(ServerConfig.Bracket); } var roomResponse = new { room.RoomId, room.IsDorm, room.MaxPlayerCalculationMode, room.MaxPlayers, room.CloningAllowed, room.DisableMicAutoMute, room.DisableRoomComments, room.EncryptVoiceChat, room.ToxmodEnabled, room.LoadScreenLocked, room.PersistenceVersion, room.UgcSubVersion, room.UgcVersion, room.AutoLocalizeRoom, room.IsDeveloperOwned, room.IsRRO, room.IsRecRoomApproved, room.IsJuniorCreated, room.IsPlacePlay, room.ExcludeFromLists, room.ExcludeFromSearch, room.Name, room.Description, ImageName = room.ImageName == "DormRoom.jpg" ? null : room.ImageName, room.WarningMask, room.CustomWarning, room.CreatorAccountId, room.State, room.Accessibility, room.SupportsLevelVoting, room.SupportsScreens, room.SupportsWalkVR, room.SupportsTeleportVR, room.SupportsVRLow, room.SupportsQuest2, room.SupportsMobile, room.SupportsJuniors, room.MinLevel, room.MinUgcSubVersion, room.AgeRating, room.CreatedAt, room.PublishedAt, room.BecameRRStudioRoomAt, room.Stats, room.RankedEntityId, room.RankingContext, DataBlob = room.DataBlob, room.CurrentSnapshotId, room.NeedsSnapshotId, room.ShouldAutoStageSaves, room.Roles, room.Tags, room.PromoImages, room.PromoExternalContent, room.LoadScreens, room.RestrictedCircuitsAllowListNames, room.LocalizationContext, room.RoomBanDetails, SubRooms = room.SubRooms?.Select(sub => new { CurrentSave = (sub.CurrentSave != null ? new { sub.CurrentSave.SubRoomDataSaveId, sub.CurrentSave.SubRoomId, sub.CurrentSave.RoomId, DataBlob = sub.CurrentSave.DataBlob, sub.CurrentSave.SavedByAccountId, sub.CurrentSave.SavedOnPlatform, sub.CurrentSave.SavedOnDeviceClass, sub.CurrentSave.Description, sub.CurrentSave.CreatedAt, sub.CurrentSave.UnityAssetId } : RoomDB.SubRoomSaves .Find(x => x.RoomId == room.RoomId && x.SubRoomId == sub.SubRoomId) .OrderByDescending(x => x.CreatedAt) .Select(save => new { save.SubRoomDataSaveId, save.SubRoomId, save.RoomId, DataBlob = save.DataBlob, save.SavedByAccountId, save.SavedOnPlatform, save.SavedOnDeviceClass, save.Description, save.CreatedAt, save.UnityAssetId }) .FirstOrDefault()), sub.Accessibility, sub.IsSandbox, sub.MaxPlayers, sub.Name, sub.RoomId, sub.SubRoomId, sub.UnitySceneId, DataBlob = sub.DataBlob, sub.SavedByAccountId, sub.ShouldAutoStageSaves, sub.StagedSubRoomDataSaveId, sub.LastModeratedSaveModerationState, sub.CreatorAccountId }).ToList() }; return new ContentResult { Content = JsonConvert.SerializeObject(roomResponse), ContentType = "application/json", StatusCode = 200 }; } [HttpGet("unity_assets/{guid}/{target}/{version}")] public IActionResult GetUnityAssets(string guid, int target, int version) { var account = AuthStuff.GetPlayerId(Request); if (account == null) return StatusCode(403); return Ok(new { UnityAssetId = guid, Target = target, Version = version, Filename = $"{guid}_{target}_{version}.assetbundle", // hacky?>?? it a stub?? but ok and it will hold up for now Hash = (string?)null }); // ts will fail when we get rr studio or 2025 support (2025 uses more stupid shit) // someone test vana on 2025 tho lowkey || as of 8/25/2026, 2025 work so good soonnnnioonnn } [HttpPut("rooms/{roomId}/roles/{playerId}/invite")] public async Task SetRoomRole(long roomId, long playerId, [FromForm] int role) { var account = AuthStuff.GetPlayerId(Request); if (account == null) return StatusCode(403); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(new { success = false, error = "Room not found" }); if (room.IsDorm) return RoomError("Rooms.UserCannotBeInvitedInsideDorm", "You do not have permission to change roles inside of your Dorm Room."); room.Roles ??= new List(); var newRole = (RoomDBClasses.Role)role; var callerEntry = room.Roles.FirstOrDefault(r => r.AccountId == account.Value); bool isOwner = room.CreatorAccountId == account.Value; bool isCoOwner = callerEntry != null && (callerEntry.Role == Role.CoOwner || callerEntry.Role == Role.TemporaryCoOwner); if (!isOwner && !isCoOwner) return StatusCode(403); var targetEntry = room.Roles.FirstOrDefault(r => r.AccountId == playerId); bool targetIsOwner = room.CreatorAccountId == playerId; if (targetIsOwner) return StatusCode(403); var targetCurrentRole = targetEntry?.Role ?? Role.None; var targetCurrentInvited = targetEntry?.InvitedRole ?? Role.None; bool targetIsCoOwnerTier = targetCurrentRole == Role.CoOwner || targetCurrentRole == Role.TemporaryCoOwner || targetCurrentInvited == Role.CoOwner || targetCurrentInvited == Role.TemporaryCoOwner; if (targetIsCoOwnerTier && !isOwner) return StatusCode(403); if (newRole == Role.None || role == 0) { if (targetEntry != null) { bool wasCoOwnerTier = targetEntry.Role == Role.CoOwner || targetEntry.Role == Role.TemporaryCoOwner || targetEntry.InvitedRole == Role.CoOwner || targetEntry.InvitedRole == Role.TemporaryCoOwner; room.Roles.Remove(targetEntry); RoomDB.Rooms.Update(room); if (wasCoOwnerTier) { var removePayload = JsonConvert.SerializeObject(new { Id = "2", Msg = new { FromPlayerId = account, Id = Random.Shared.Next(1, 0x7ffffff), SentTime = DateTime.UtcNow, Type = WebsocketEvents.WebsocketMessageType.RoomCoOwnerRemoved, Data = (string?)null, RoomId = roomId, PlayerEventId = (long?)null } }); await NotificationsController.SendToPlayer(playerId, removePayload); } } var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); var updatedRoom = RoomDB.GetRoom(roomId); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, updatedRoom); return Ok(new { success = true }); } bool isCoOwnerInvite = newRole == Role.CoOwner || newRole == Role.TemporaryCoOwner; if (isCoOwnerInvite) { if (!isOwner) return StatusCode(403); if (targetEntry != null) { targetEntry.Role = Role.None; targetEntry.InvitedRole = newRole; targetEntry.LastChangedByAccountId = account; } else { room.Roles.Add(new RoomDBClasses.Roles { AccountId = playerId, Role = Role.None, InvitedRole = newRole, LastChangedByAccountId = account }); } RoomDB.Rooms.Update(room); var json = JsonConvert.SerializeObject(new { Id = "2", Msg = new { FromPlayerId = account, Id = Random.Shared.Next(1, 0x7ffffff), SentTime = DateTime.UtcNow, Type = MessageType.RoomCoOwnerInvited, Data = (string?)null, RoomId = roomId, PlayerEventId = (long?)null } }); await NotificationsController.SendToPlayer(playerId, json); } else { if (targetEntry != null) { targetEntry.Role = newRole; targetEntry.InvitedRole = Role.None; targetEntry.LastChangedByAccountId = account; } else { room.Roles.Add(new RoomDBClasses.Roles { AccountId = playerId, Role = newRole, InvitedRole = Role.None, LastChangedByAccountId = account }); } RoomDB.Rooms.Update(room); } await NotiController.SendRoomUpdate(playerId, RoomDB.GetRoom(roomId)); await NotiController.SendRoomUpdate((long)account, RoomDB.GetRoom(roomId)); return Ok(new { success = true }); } [HttpGet("rooms/{roomId}/roles/{playerId}")] // made a temp workaround i think the response is correct? public IActionResult GetRoomRole(long roomId, long playerId) { var account = AuthStuff.GetPlayerId(Request); if (account == null) return StatusCode(403); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(new { success = false, error = "Room not found" }); var entry = room.Roles?.FirstOrDefault(r => r.AccountId == playerId); if (entry == null) return Ok(new { AccountId = playerId, Role = Role.None, InvitedRole = Role.None }); return Ok(new { entry.AccountId, entry.Role, entry.InvitedRole, entry.LastChangedByAccountId }); } [HttpGet("rooms/{roomId}/roles")] public IActionResult GetRoomRoles(long roomId) { var account = AuthStuff.GetPlayerId(Request); if (account == null) return StatusCode(403); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(new { success = false, error = "Room not found" }); var roles = (room.Roles ?? new List()) .Select(r => new { r.AccountId, r.Role, r.InvitedRole, r.LastChangedByAccountId }) .ToList(); return Ok(roles); } [HttpPut("rooms/{roomId}/roles/{playerId}")] public async Task RemoveRoomRole(long roomId, long playerId, [FromForm] int role) { var account = AuthStuff.GetPlayerId(Request); if (account == null) return StatusCode(403); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(new { success = false, error = "Room not found" }); room.Roles ??= new List(); var existing = room.Roles.FirstOrDefault(r => r.AccountId == playerId); var existingTwo = room.Roles.FirstOrDefault(r => r.AccountId == account.Value); if (account == playerId) { if (existing == null) return Ok(new { success = true }); var incomingRole = (RoomDBClasses.Role)role; if (incomingRole == Role.None) { bool wasCoOwnerSelf = existing.Role == Role.CoOwner || existing.Role == Role.TemporaryCoOwner; room.Roles.Remove(existing); RoomDB.Rooms.Update(room); if (wasCoOwnerSelf) { var wsPayload = JsonConvert.SerializeObject(new { Id = "2", Msg = new { FromPlayerId = account, Id = Random.Shared.Next(1, 0x7ffffff), SentTime = DateTime.UtcNow, Type = WebsocketEvents.WebsocketMessageType.RoomCoOwnerRemoved, Data = (string?)null, RoomId = roomId, PlayerEventId = (long?)null } }); await NotificationsController.SendToPlayer(account.Value, wsPayload); } var allPids = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); var updatedRoomSelf = RoomDB.GetRoom(roomId); foreach (var pid in allPids) await NotiController.SendRoomUpdate(pid, updatedRoomSelf); return Ok(new { success = true }); } if (existing.InvitedRole == Role.CoOwner) { if (incomingRole == Role.CoOwner) { existing.Role = Role.CoOwner; existing.InvitedRole = Role.None; existing.LastChangedByAccountId = account; } else { existing.InvitedRole = Role.None; existing.LastChangedByAccountId = account; if (existing.Role == Role.None) room.Roles.Remove(existing); } } } else { var newRole = (RoomDBClasses.Role)role; bool wasCoOwner = existing != null && (existing.Role == Role.CoOwner || existing.Role == Role.TemporaryCoOwner); bool beingDemoted = (int)newRole < (int)Role.CoOwner; if (newRole == Role.None) { if (existing != null) room.Roles.Remove(existing); } else { if (existingTwo != null && existingTwo.Role < newRole) { APIController.SendNonHileWebhook(playerId, "not a hile, but someone tried to claim " + newRole + " role in room " + roomId); return StatusCode(403); } if (existing != null) { existing.Role = newRole; existing.InvitedRole = Role.None; existing.LastChangedByAccountId = account; } else { room.Roles.Add(new RoomDBClasses.Roles { AccountId = playerId, Role = newRole, InvitedRole = Role.None, LastChangedByAccountId = account }); } } if (wasCoOwner && beingDemoted) { var wsPayload = JsonConvert.SerializeObject(new { Id = "2", Msg = new { FromPlayerId = account, Id = Random.Shared.Next(1, 0x7ffffff), SentTime = DateTime.UtcNow, Type = WebsocketEvents.WebsocketMessageType.RoomCoOwnerRemoved, Data = (string?)null, RoomId = roomId, PlayerEventId = (long?)null } }); await NotificationsController.SendToPlayer(playerId, wsPayload); } } RoomDB.Rooms.Update(room); var allPlayerIds = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); var updatedRoom = RoomDB.GetRoom(roomId); foreach (var pid in allPlayerIds) await NotiController.SendRoomUpdate(pid, updatedRoom); return Ok(new { success = true }); } [HttpGet("rooms/bulk")] public async Task GetRoomsBulk([FromQuery] List name, [FromQuery] List id) { var userAgent = Request.Headers.UserAgent.ToString(); if (!userAgent.Contains("BestHTTP", StringComparison.OrdinalIgnoreCase) && !Request.Headers.Cookie.ToString().Contains("cf_clearance")) return StatusCode(404); var roomsByName = RoomDB.GetRoomsByNames(name); var roomsById = RoomDB.GetRoomsByIds(id); var combinedRooms = roomsByName.Concat(roomsById).DistinctBy(r => r.RoomId).ToList(); return Ok(combinedRooms); } [HttpGet("rooms/hot")] public async Task HotRooms([FromQuery] string tag = "", [FromQuery] int skip = 0, [FromQuery] int take = 30) { string? passingTag = string.IsNullOrWhiteSpace(tag) ? null : tag; var (results, total) = RoomDB.GetHotRooms(passingTag, skip, take); return Ok(new { Results = results ?? new List(), TotalResults = total }); } [HttpGet("rooms/search")] public async Task SearchRooms([FromQuery] string query = "", [FromQuery] int skip = 0, [FromQuery] int take = 30) { if (string.IsNullOrWhiteSpace(query)) { var (hotResults, hotTotal) = RoomDB.GetHotRooms(null, skip, take); return Ok(new { Results = hotResults ?? new List(), TotalResults = hotTotal }); } var (searchList, searchTotal) = RoomDB.Search(query, skip: skip, take: take); return Ok(new { Results = searchList ?? new List(), TotalResults = searchTotal }); } [HttpGet("rooms/base")] public async Task GetBaseRooms() { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var rooms = RoomDB.GetBaseRooms(); foreach(var room in rooms) { room.Accessibility = RoomAccessibility.Public; } return Ok(rooms); } [HttpGet("rooms/contributedby/me")] [HttpGet("rooms/createdby/me")] [HttpGet("rooms/ownedby/me")] public async Task GetCreatedByMe( [FromQuery] int skip = 0, [FromQuery] int take = 9999) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var results = RoomDB.Rooms.FindAll() .Where(r => !IsBaseRoom(r) && ( r.CreatorAccountId == (long)id || (r.Roles?.Any(x => x.AccountId == (long)id && (x.Role == Role.CoOwner || x.Role == Role.Creator || x.Role == Role.TemporaryCoOwner )) ?? false) )) .OrderByDescending(r => r.CreatedAt) .Skip(skip) .Take(take) .ToList(); return Ok(results); } [HttpGet("rooms/ownedby/{accountId}")] public async Task GetRoomsOwnedBy(long accountId) { return Ok(RoomDB.GetRoomsByCreator(accountId).Where(r => !IsBaseRoom(r)).ToList()); } [HttpGet("rooms/visitedby/me")] public async Task GetVisitedByMe([FromQuery] int skip = 0, [FromQuery] int take = 9) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var visits = PlayerDB.GetPlayerRoomVisits((long)id); var results = visits .OrderByDescending(rv => rv.LastVisitedAt) .Skip(skip).Take(take) .Select(rv => RoomDB.GetRoom(rv.RoomId)) .Where(r => r != null && !IsBaseRoom(r)) .ToList(); return Ok(results); } [HttpGet("rooms/visitedby/{playerId}")] public async Task GetVisitedByOther(long playerId, [FromQuery] int skip = 0, [FromQuery] int take = 10) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var visits = PlayerDB.GetPlayerRoomVisits(playerId); var results = visits .OrderByDescending(rv => rv.LastVisitedAt) .Skip(skip).Take(take) .Select(rv => RoomDB.GetRoom(rv.RoomId)) .Where(r => r != null && !IsBaseRoom(r)) .ToList(); return Ok(results); } [HttpGet("rooms/favoritedby/me")] public async Task GetFavoritedByMe([FromQuery] int skip = 0, [FromQuery] int take = 100) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var col = RoomDB.RoomDBFile.GetCollection("Rooms"); var rooms = new List(); foreach (var doc in col.FindAll()) { try { var room = BsonMapper.Global.Deserialize(doc); if (room == null) continue; if (IsBaseRoom(room)) continue; if (!PlayerDB.HasFavoritedRoom((long)id, room.RoomId)) continue; rooms.Add(room); } catch (Exception ex) { Console.WriteLine($"[GetFavoritedByMe] Skipped corrupt room {doc["_id"]}: {ex.Message}"); } } var results = rooms .OrderByDescending(r => r.Stats?.FavoriteCount ?? 0) .Skip(skip) .Take(take) .ToList(); return Ok(results); } [HttpGet("/roomserver/dormroom/me")] public async Task GetMyDormRoom() { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var player = PlayerDB.GetCurrentPlayer((long)id); if (player == null) return NotFound(""); return Ok(player.Player?.PlayerExtra?.DormRoomId ?? 0); } [HttpGet("rooms/{roomId}/interactionby/me")] public async Task GetInteractionByMe(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); return Ok(new { Cheered = PlayerDB.HasCheeredRoom((long)id, roomId), Favorited = PlayerDB.HasFavoritedRoom((long)id, roomId), LastVisitedAt = PlayerDB.GetRoomLastVisitedAt((long)id, roomId) ?? DateTime.UtcNow }); } [HttpPut("rooms/{roomId}/interactionby/me/favorite")] public async Task FavoriteRoom(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); if (!PlayerDB.HasFavoritedRoom((long)id, roomId)) { PlayerDB.AddFavoriteRoom((long)id, roomId); RoomDB.IncrementRoomFavoriteCount(roomId); } await NotiController.SendRoomUpdate((long)id, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = new { Favorited = true } }); } [HttpDelete("rooms/{roomId}/interactionby/me/favorite")] public async Task UnfavoriteRoom(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); if (PlayerDB.HasFavoritedRoom((long)id, roomId)) { PlayerDB.RemoveFavoriteRoom((long)id, roomId); RoomDB.DecrementRoomFavoriteCount(roomId); } await NotiController.SendRoomUpdate((long)id, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = new { Favorited = false } }); } [HttpPut("rooms/{roomId}/interactionby/me/cheer")] public async Task CheerRoom(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); if (!PlayerDB.HasCheeredRoom((long)id, roomId)) { PlayerDB.AddCheerRoom((long)id, roomId); RoomDB.IncrementRoomCheerCount(roomId); } await NotiController.SendRoomUpdate((long)id, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = new { Cheered = true } }); } [HttpDelete("rooms/{roomId}/interactionby/me/cheer")] public async Task UncheerRoom(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); if (PlayerDB.HasCheeredRoom((long)id, roomId)) { PlayerDB.RemoveCheerRoom((long)id, roomId); RoomDB.DecrementRoomCheerCount(roomId); } await NotiController.SendRoomUpdate((long)id, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = new { Cheered = false } }); } [HttpPut("rooms/{roomId}/subrooms/{subRoomId}/permissions")] public async Task SetSubroomPermissions(long roomId, long subRoomId, [FromBody] List permissions) { var playerId = AuthStuff.GetPlayerId(Request); if (playerId == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(new { success = false, error = "Subroom was not found", error_id = "Rooms.Subroom.NotFound" }); if (!RoomDB.UserCanEditRoom(roomId, (long)playerId)) return Forbid(); var subroom = room.SubRooms?.FirstOrDefault(x => x.SubRoomId == subRoomId); if (subroom == null) return NotFound(new { success = false, error = "Subroom was not found", error_id = "Rooms.Subroom.NotFound" }); var storedEntries = permissions.Select(p => new PhotonAccessTokenDBClasses.StoredPermissionEntry { Permission = p.Permission, Role = (int)p.Role, Type = p.Type, Override = p.Override, Value = p.Value }).ToList(); var updatedSet = PhotonAccessTokenDB.SetPermissions(roomId, subRoomId, storedEntries); var fullPermissions = updatedSet.Permissions.Select(p => new { Override = p.Override, Permission = p.Permission, Role = p.Role, Type = p.Type, Value = p.Value }).ToList(); var callerHeartbeat = PlayerDB.GetPlayerHeartbeat((long)playerId); var instanceId = callerHeartbeat?.roomInstance?.roomInstanceId; var playersInInstance = instanceId != null ? PlayerDB.Players.FindAll() .Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == instanceId) .Select(p => p.PlayerId) .ToList() : new List { (long)playerId }; foreach (var pid in playersInInstance) { PlayerDB.SetPlayerPermissions(pid, permissions); await NotificationsController.SendEvent(pid, "PhotonAccessToken", new { RoomId = roomId, SubRoomId = subRoomId, Permissions = fullPermissions }); } var updatedRoom = RoomDB.GetRoom(roomId); return Ok(new { value = updatedRoom, success = true, error_id = (string?)null, error = (string?)null }); } [HttpPost("rooms/{roomId}/clone")] public async Task CloneRoom(long roomId, [FromForm] CloneRoomRequest body) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); Console.WriteLine($"clone {id}"); var query = RoomDB.Rooms.FindAll() .Where(r => !IsBaseRoom(r) && ( r.CreatorAccountId == (long)id || (r.Roles?.Any(x => x.AccountId == (long)id && (x.Role == Role.CoOwner || x.Role == Role.Creator)) ?? false) )); var roomCount = query.Count(); if (roomCount > 50) return RoomError("Rooms.AccountOwnsTooManyRooms", "Account cannot create or co-own any more rooms"); //if () // return RoomError("Rooms.AccountIsNotAboveAge", "Junior accounts are prohibited from creating rooms."); if (body == null || string.IsNullOrWhiteSpace(body.Name)) return RoomError("Rooms.MissingName", "You must enter a name for your room."); if (body.Name.Length < 3 || body.Name.Length > 128) return RoomError("Rooms.TooShortOrLong", "Room Name must be within 3 - 128 Characters."); if (await ContainsProhibitedText(body.Name)) return RoomError("Rooms.ContainsProhibitedText", "Name contains prohibited text!"); long playerId = (long)id; var now = DateTime.UtcNow; var twoMinutesAgo = now.AddMinutes(-2); var cloneHistory = _cloneRateLimits.GetOrAdd(playerId, _ => new List()); lock (cloneHistory) { cloneHistory.RemoveAll(timestamp => timestamp < twoMinutesAgo); if (cloneHistory.Count() >= 10) { return RoomError("Rooms.TooManyClonesRateLimited", "An error occured. Wait a few minutes and try again."); } cloneHistory.Add(now); } var originalRoom = RoomDB.GetRoom(roomId); if (originalRoom == null) return RoomError("Rooms.CannotClone", "The room you are trying to clone does not exist."); bool isBaseRoom = originalRoom.Tags != null && originalRoom.Tags.Any(t => string.Equals(t.Tag, "base", StringComparison.OrdinalIgnoreCase)); if (RoomDB.GetRoomByName(body.Name) != null) return RoomError("Rooms.DuplicateName", "A room with that name already exists!"); if (!isBaseRoom) { var playerRoomCount = RoomDB.GetRoomsByCreator(playerId).Count; if (playerRoomCount >= 101) return RoomError("Rooms.AccountOwnsTooManyRooms", "Account cannot create or co-own any more rooms"); } var newId = RoomDB.CloneRoom(roomId, body.Name, playerId); if (newId == null) return RoomError("Rooms.CannotClone", "You can't clone this room!"); var newRoom = RoomDB.GetRoom(newId.Value); var playerRecord = PlayerDB.Players.FindById(playerId); bool isDeveloper = playerRecord?.PlayerRoles?.Contains(PlayerDBClasses.PlayerRoles.Developer) ?? false; if (isDeveloper && newRoom != null) { newRoom.IsDeveloperOwned = true; RoomDB.Rooms.Update(newRoom); } if (AuthStuff.GetAndEnsureAppVersion(Request) == "20250724" && newRoom != null) { if (!newRoom.Tags.Any(x => x.Tag.Equals("2025", StringComparison.OrdinalIgnoreCase))) newRoom.Tags.Add(new Tags { Tag = "2025", Type = TagType.AGOnly }); RoomDB.Rooms.Update(newRoom); } var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = newRoom }); } [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/clone")] public async Task CloneSubroom(long roomId, long subRoomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); if (RoomDB.GetPlayerDormRoom((long)id)?.RoomId == roomId) return RoomError("Rooms.Subroom.CloneDormRoom", "You cannot clone subrooms from your dorm room!"); var cloned = RoomDB.CloneSubroom(roomId, subRoomId, (long)id); if (cloned == null) return RoomError("Rooms.SubRoom.DoesntExist", "This subroom does not exist!"); var updatedRoom = RoomDB.GetRoom(roomId); var allPlayers = PlayerDB.Players .FindAll() .Select(p => p.PlayerId) .ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, updatedRoom); return Ok(new { value = updatedRoom, success = true, error_id = (string?)null, error = (string?)null }); } [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/move")] public IActionResult MoveSubRoom( long roomId, long subRoomId, [FromForm] long newRoomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); if (RoomDB.GetPlayerDormRoom((long)id)?.RoomId == roomId) return RoomError("Rooms.Subroom.MoveDormRoom", "You cannot move your dorm room!"); var movedRoom = RoomDB.MoveSubRoomToRoom( roomId, subRoomId, newRoomId, (long)id); if (movedRoom == null) return RoomError("Subroom.MoveFailed", "Failed to move subroom."); return Ok(new { success = true, error = "", value = movedRoom }); } [HttpDelete("rooms/{roomId}")] public async Task DeleteRoom(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); RoomDB.DeleteRoom(roomId); return Ok(new { success = true, error = "", value = (object?)null }); } [HttpPut("rooms/{roomId}/subrooms/{subRoomId}/accessibility")] public async Task SetSubRoomAccessibility(long roomId, long subRoomId, [FromForm] string accessibility) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); var sub = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId); if (sub == null) return RoomError("Rooms.SubRoom.DoesntExist", "This subroom does not exist!"); sub.Accessibility = accessibility?.ToLower() switch { "public" => RoomAccessibility.Public, "private" => RoomAccessibility.Private, "unlisted" => RoomAccessibility.Unlisted, _ => RoomAccessibility.Private }; RoomDB.Rooms.Update(room); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true }); } [HttpPut("rooms/{roomId}/name")] public async Task SetRoomName(long roomId, [FromForm] string name) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); if (room.IsDorm) return RoomError("Rooms.Dorm", "You cannot edit your dorm room's name!"); if (string.IsNullOrWhiteSpace(name)) return RoomError("Rooms.InvalidName", "Name cannot be empty."); if (name.Length < 3 || name.Length > 128) return RoomError("Rooms.TooShortOrLong", "Room Name must be within 3 - 128 Characters."); if (name.Contains(" ")) return RoomError("Rooms.NoSpaces", "Your room name must not contain spaces!"); if (await ContainsProhibitedText(name)) return RoomError("Rooms.ContainsProhibitedText", "Name contains prohibited text!"); var updated = RoomDB.SetRoomName(roomId, name); if (updated == null) return RoomError("Rooms.DuplicateName", "A room with the same name already exists!"); var updatedRoom = RoomDB.GetRoom(roomId); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, updatedRoom); return Ok(new { success = true, error = "", value = updatedRoom }); } [HttpPut("rooms/{roomId}/description")] public async Task SetRoomDescription(long roomId, [FromForm] string description) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); if (!string.IsNullOrWhiteSpace(description) && await ContainsProhibitedText(description)) return RoomError("Rooms.ContainsProhibitedText", "Description must not contain prohibited text."); RoomDB.SetRoomDescription(roomId, description ?? ""); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/image")] public async Task SetRoomImage(long roomId, [FromForm] string imageName) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); RoomDB.SetRoomImageName(roomId, imageName); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/accessibility")] public async Task SetRoomAccessibility(long roomId, [FromForm] int accessibility) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); if (!Enum.IsDefined(typeof(RoomAccessibility), accessibility)) return BadRequest(); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); if ((RoomAccessibility)accessibility == RoomAccessibility.Public && room.ImageName == "DefaultRoomImage.png") return RoomError("Rooms.MissingRoomThumbnail", "Cannot publish room: Missing room photo"); // wowe just like ingame RoomDB.SetRoomAccessibility(roomId, (RoomAccessibility)accessibility); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/cloning")] public async Task SetRoomCloning(long roomId, [FromForm] bool cloningAllowed) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (room.IsDorm) return RoomError("Rooms.DormNotAllowed", "You cannot edit that in this dorm."); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); RoomDB.SetRoomCloning(roomId, cloningAllowed); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/restrictions")] public async Task SetRoomRestrictions( long roomId, [FromForm] bool supportsScreens, [FromForm] bool supportsWalkVR, [FromForm] bool supportsTeleportVR, [FromForm] bool supportsJuniors) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (room.IsDorm) return RoomError("Rooms.DormNotAllowed", "You cannot edit that in dorms."); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); RoomDB.SetRoomRestrictions(roomId, supportsScreens, supportsWalkVR, supportsTeleportVR, supportsJuniors); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/loadscreen")] public async Task SetRoomLoadscreen( long roomId, [FromForm] string? imageName, [FromForm] string? title, [FromForm] string? subtitle) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); if (!string.IsNullOrEmpty(title) && title.Length > 24) return RoomError("Room.LoadScreen.Title", "Title cannot exceed 24 characters!"); if (!string.IsNullOrEmpty(subtitle) && subtitle.Length > 150) return RoomError("Room.LoadScreen.Subtitle", "Subtitle cannot exceed 150 characters!"); RoomDB.SetRoomLoadscreen(roomId, imageName, title, subtitle); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/warning")] public async Task SetRoomWarning(long roomId, [FromForm] WarningMaskType warningMask, [FromForm] string? customWarning = null) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); RoomDB.SetRoomWarningMask(roomId, warningMask); if (warningMask == (WarningMaskType)29) RoomDB.SetRoomCustomWarning(roomId, null); else if (customWarning != null) RoomDB.SetRoomCustomWarning(roomId, customWarning); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/tags")] public async Task SetRoomTags(long roomId) { var account = AuthStuff.GetPlayerId(Request); if (account == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return Vanadium.Utils.Utils.ErrorRoom("Rooms.DoesntExist", "This room does not exist!"); if (!RoomDB.UserCanEditRoom(roomId, account.Value)) return Vanadium.Utils.Utils.ErrorRoom("Rooms.PermissionDenied", "You are not the owner of this room!"); var restrictedTags = new HashSet(StringComparer.OrdinalIgnoreCase) { "rro", "recroomoriginal", "featured", "janrrmonthly", "febrrmonthly", "marchrrmonthly", "aprilrrmonthly", "mayrrmonthly", "junerrmonthly", "julyrrmonthly", "augrrmonthly", "seprrmonthly", "octrrmonthly", "novrrmonthly", "decrrmonthly" }; var newTags = new List(); var ct = Request.ContentType ?? ""; bool hasForm = ct.Contains("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) || ct.Contains("multipart/form-data", StringComparison.OrdinalIgnoreCase); if (hasForm) { var autoTagKeys = Request.Form.Keys .Where(k => k.Equals("autotag", StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var key in autoTagKeys) { foreach (var val in Request.Form[key]) { var t = (val ?? "").Trim().ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(t)) { if (restrictedTags.Contains(t)) return Vanadium.Utils.Utils.ErrorRoom("Rooms.InvalidTags", "You cannot use this room tag."); newTags.Add(new RoomDBClasses.Tags { Tag = t, Type = RoomDBClasses.TagType.Auto }); } } } var tagKeys = Request.Form.Keys .Where(k => k.Equals("tag", StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var key in tagKeys) { foreach (var val in Request.Form[key]) { var t = (val ?? "").Trim().ToLowerInvariant(); if (!string.IsNullOrWhiteSpace(t)) { if (restrictedTags.Contains(t)) return Vanadium.Utils.Utils.ErrorRoom("Rooms.InvalidTags", "Permission denied."); newTags.Add(new RoomDBClasses.Tags { Tag = t, Type = RoomDBClasses.TagType.General }); } } } } room.Tags = newTags; RoomDB.Rooms.Update(room); var updatedRoom = RoomDB.GetRoom(roomId); await NotiController.SendToAll(Newtonsoft.Json.JsonConvert.SerializeObject(new WebsocketEvents.Response { Id = NotiController.EventTypes.RoomUpdate, Msg = updatedRoom })); return Ok(new { success = true, error = "", value = updatedRoom }); } [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/data")] public async Task PostSubRoomData(long roomId, long subRoomId, [FromBody] RoomDataRequest request) { try { var playerId = AuthStuff.GetPlayerId(Request); if (playerId == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(new { success = false, error_id = "Rooms.DoesntExist", error = "Room not found", value = (object?)null }); if (!CanEdit(room, (long)playerId)) return Ok(new { success = false, error_id = "Rooms.PermissionDenied", error = "You are not the owner of this room!", value = (object?)null }); if (AuthStuff.GetAndEnsureAppVersion(Request) == "20250724" && !room.Tags.Any(x => x.Tag.Equals("2025", StringComparison.OrdinalIgnoreCase))) { 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 = "You can't save 2023 rooms on 2025. Clone the room or go back to 2023.", RoomId = 1 } }); await NotificationsController.SendToPlayer((long)playerId, json); return Ok(new { success = false, error_id = "Rooms.UpdateRequired", error = "You can't save 2023 rooms on a 2025 version!", value = (object?)null }); } var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId); if (subRoom == null) return Ok(new { success = false, error_id = "Rooms.SubRoom.DoesntExist", error = "Subroom not found", value = (object?)null }); string dataBlob = request.SubRoomData?.Filename ?? ""; string dataBlobHash = request.SubRoomData?.Hash ?? ""; string description = request.Description ?? ""; int platform = 0; int deviceClass = 5; var authHeader = Request.Headers["Authorization"].FirstOrDefault(); if (!string.IsNullOrEmpty(authHeader)) { try { var tokenStr = authHeader .Replace("Bearer ", "", StringComparison.OrdinalIgnoreCase) .Trim(); var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler(); var jwt = handler.ReadJwtToken(tokenStr); var platformClaim = jwt.Claims.FirstOrDefault(c => c.Type == "platform")?.Value; if (platformClaim != null) platform = int.Parse(platformClaim); } catch { } } if (!string.IsNullOrEmpty(request.RoomData?.Filename)) room.DataBlob = request.RoomData.Filename; room.PersistenceVersion = request.PersistenceVersion; var save = new currentSave { SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(), RoomId = roomId, SubRoomId = subRoomId, DataBlob = dataBlob, DataBlobHash = dataBlobHash, PersistenceVersion = request.PersistenceVersion, UgcSubVersion = request.PersistenceVersion, OMVersion = 0, SavedByAccountId = (long)playerId, SavedOnPlatform = platform, SavedOnDeviceClass = deviceClass, Description = description, Tags = new List(), ModerationState = 0, CreatedAt = DateTime.UtcNow }; RoomDB.SubRoomSaves.Insert(save); //subRoom.DataBlob = dataBlob; subRoom.SavedByAccountId = (long)playerId; if (request.AutoPublish && !string.IsNullOrEmpty(dataBlob)) { subRoom.DataBlob = dataBlob; subRoom.CurrentSave = save; } RoomDB.Rooms.Update(room); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); var updatedRoom = RoomDB.GetRoom(roomId); var subRoomsProjected = updatedRoom.SubRooms?.Select(sub => new { sub.SubRoomId, sub.RoomId, CreatorAccountId = sub.CreatorAccountId, sub.UnitySceneId, sub.Name, CurrentSave = (sub.CurrentSave != null ? new { sub.CurrentSave.SubRoomDataSaveId, sub.CurrentSave.SubRoomId, sub.CurrentSave.RoomId, DataBlob = sub.CurrentSave.DataBlob, sub.CurrentSave.SavedByAccountId, sub.CurrentSave.SavedOnPlatform, sub.CurrentSave.SavedOnDeviceClass, sub.CurrentSave.Description, sub.CurrentSave.CreatedAt, sub.CurrentSave.UnityAssetId } : RoomDB.SubRoomSaves .Find(x => x.RoomId == room.RoomId && x.SubRoomId == sub.SubRoomId) .OrderByDescending(x => x.CreatedAt) .Select(save => new { save.SubRoomDataSaveId, save.SubRoomId, save.RoomId, DataBlob = save.DataBlob, save.SavedByAccountId, save.SavedOnPlatform, save.SavedOnDeviceClass, save.Description, save.CreatedAt, save.UnityAssetId }) .FirstOrDefault()), sub.LastModeratedSaveModerationState, sub.IsSandbox, sub.MaxPlayers, sub.Accessibility, sub.ShouldAutoStageSaves, sub.StagedSubRoomDataSaveId }).ToList(); return Ok(new { value = new { Room = new { updatedRoom.RoomId, updatedRoom.Name, updatedRoom.Description, updatedRoom.ImageName, updatedRoom.WarningMask, updatedRoom.CustomWarning, updatedRoom.CreatorAccountId, updatedRoom.State, updatedRoom.Accessibility, PublishState = 0, updatedRoom.SupportsLevelVoting, updatedRoom.IsRRO, updatedRoom.IsRecRoomApproved, updatedRoom.ExcludeFromLists, updatedRoom.ExcludeFromSearch, updatedRoom.SupportsScreens, updatedRoom.SupportsWalkVR, updatedRoom.SupportsTeleportVR, updatedRoom.SupportsVRLow, updatedRoom.SupportsQuest2, updatedRoom.SupportsMobile, updatedRoom.SupportsJuniors, updatedRoom.MinLevel, updatedRoom.AgeRating, updatedRoom.CreatedAt, updatedRoom.PublishedAt, updatedRoom.BecameRRStudioRoomAt, updatedRoom.Stats, updatedRoom.RankingContext, updatedRoom.IsDorm, updatedRoom.IsPlacePlay, updatedRoom.MaxPlayerCalculationMode, updatedRoom.MaxPlayers, updatedRoom.CloningAllowed, updatedRoom.DisableMicAutoMute, updatedRoom.DisableRoomComments, updatedRoom.EncryptVoiceChat, updatedRoom.ToxmodEnabled, updatedRoom.LoadScreenLocked, updatedRoom.UgcVersion, updatedRoom.PersistenceVersion, updatedRoom.UgcSubVersion, updatedRoom.MinUgcSubVersion, updatedRoom.AutoLocalizeRoom, updatedRoom.LocalizationContext, updatedRoom.IsDeveloperOwned, updatedRoom.RankedEntityId, BoostCount = 0, updatedRoom.DataBlob, DataBlobHash = request.RoomData?.Hash, updatedRoom.CurrentSnapshotId, LatestSnapshotId = updatedRoom.CurrentSnapshotId, StagedSnapshotId = updatedRoom.CurrentSnapshotId, SubRooms = subRoomsProjected, updatedRoom.Roles, updatedRoom.IsJuniorCreated, updatedRoom.Tags, updatedRoom.PromoImages, updatedRoom.PromoExternalContent, updatedRoom.LoadScreens, CurrentRoomModerationState = 0, LastRoomModerationState = 0, PublishStateAvailability = new { CanSaveAsBeta = false, CanSaveAsUpdate = true, AvailableUpdateTokenCount = 3 }, updatedRoom.RestrictedCircuitsAllowListNames }, SubRoomDataSave = new { save.SubRoomDataSaveId, save.SubRoomId, save.DataBlob, save.DataBlobHash, save.ReferencedUnityAssetIds, save.PersistenceVersion, save.OMVersion, save.UgcSubVersion, save.SavedByAccountId, save.SavedOnPlatform, save.SavedOnDeviceClass, save.Description, save.Tags, save.ModerationState, save.CreatedAt } }, success = true, error_id = (string?)null, error = (string?)null }); } catch (Exception ex) { return Ok(new { success = false, error_id = "Internal.Error", error = ex.Message, value = (object?)null }); } } [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/publish_save")] public async Task RestoreSubRoomData(long roomId, long subRoomId, [FromForm] long subRoomDataSaveId) { try { var playerId = AuthStuff.GetPlayerId(Request); if (playerId == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(new { success = false, error_id = "Rooms.DoesntExist", error = "Room not found", value = (object?)null }); if (!CanEdit(room, (long)playerId)) return Ok(new { success = false, error_id = "Rooms.PermissionDenied", error = "You are not the owner of this room!", value = (object?)null }); var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId); if (subRoom == null) return Ok(new { success = false, error_id = "Rooms.SubRoom.DoesntExist", error = "Subroom not found", value = (object?)null }); var save = RoomDB.SubRoomSaves.Find(x => x.RoomId == (long)roomId && x.SubRoomId == (long)subRoomId && x.SubRoomDataSaveId == (long)subRoomDataSaveId).FirstOrDefault(); subRoom.DataBlob = save.DataBlob; subRoom.CurrentSave = save; RoomDB.Rooms.Update(room); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); var updatedRoom = RoomDB.GetRoom(roomId); var subRoomsProjected = updatedRoom.SubRooms?.Select(sub => new { sub.SubRoomId, sub.RoomId, CreatorAccountId = sub.CreatorAccountId, sub.UnitySceneId, sub.Name, CurrentSave = RoomDB.SubRoomSaves .Find(x => x.RoomId == updatedRoom.RoomId && x.SubRoomId == sub.SubRoomId) .OrderByDescending(x => x.CreatedAt) .Select(s => new { s.SubRoomDataSaveId, s.SubRoomId, s.DataBlob, s.DataBlobHash, s.ReferencedUnityAssetIds, s.PersistenceVersion, s.OMVersion, s.UgcSubVersion, s.SavedByAccountId, s.SavedOnPlatform, s.SavedOnDeviceClass, s.Description, s.Tags, s.ModerationState, s.CreatedAt }) .FirstOrDefault(), sub.LastModeratedSaveModerationState, sub.IsSandbox, sub.MaxPlayers, sub.Accessibility, sub.ShouldAutoStageSaves, sub.StagedSubRoomDataSaveId }).ToList(); return Ok(new { success = true, error_id = (string?)null, error = (string?)null }); } catch (Exception ex) { return Ok(new { success = false, error_id = "Internal.Error", error = ex.Message, value = (object?)null }); } } /*[HttpGet("rooms/{roomId}/subrooms/{subroomId}/saves")] public ContentResult GetRoomSaves(ulong roomId, ulong subroomId, int unityAssetTarget = 0, int unityAssetVersion = 1, int skip = 0, int take = 20) { var playerId = AuthStuff.GetPlayerId(Request); if (playerId == null) return Unauthorized(""); var room = RoomDB.GetRoom((long)roomId); if (room == null) return NotFound(""); if (!CanViewSaves(room, (long)playerId)) return Forbidden(""); if (PlayerDB.Players.FindById((long)playerId)?.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false) take = 999; var save = RoomDB.GetRoomSaves(roomId, subroomId, skip, take); if (save == null) return Ok(ServerConfig.Bracket); return Ok(new { Results = save.Results, TotalResults = save.TotalCount} ); }*/ [HttpGet("rooms/magic_door")] public async Task MagicDoor([FromQuery] int partySize = 1) { if (partySize <= 0) partySize = 1; var random = new Random(); var col = RoomDB.RoomDBFile.GetCollection("Rooms"); var publicRooms = new List(); foreach (var doc in col.FindAll()) { try { var room = BsonMapper.Global.Deserialize(doc); if (room == null) continue; if (room.IsDorm) continue; if (room.Accessibility != RoomAccessibility.Public) continue; if (room.SubRooms == null || room.SubRooms.Count == 0) continue; publicRooms.Add(room); } catch (Exception ex) { Console.WriteLine($"[-] Skipped corrupt room doc {doc["_id"]}: {ex.Message}"); } } if (publicRooms.Count == 0) return RoomError("Rooms.NotFound", "No"); var triedRoomIds = new HashSet(); for (int i = 0; i < 20; i++) { var remainingRooms = publicRooms .Where(r => !triedRoomIds.Contains(r.RoomId)) .ToList(); if (remainingRooms.Count == 0) break; var room = remainingRooms[random.Next(remainingRooms.Count)]; triedRoomIds.Add(room.RoomId); var publicSubrooms = room.SubRooms .Where(s => s.Accessibility == RoomAccessibility.Public) .ToList(); if (publicSubrooms.Count == 0) continue; SubRooms? targetSubroom = null; if (publicSubrooms.Count == 1) { var onlySubroom = publicSubrooms[0]; if (onlySubroom.MaxPlayers < partySize) continue; targetSubroom = onlySubroom; } else { var validSubrooms = publicSubrooms .Where(s => s.MaxPlayers >= partySize) .ToList(); if (validSubrooms.Count == 0) continue; targetSubroom = validSubrooms[random.Next(validSubrooms.Count)]; } if (targetSubroom == null) continue; return Ok(new { RefreshIntervalMinutes = 1, RefreshesAt = DateTime.UtcNow.AddMinutes(1), Room = room }); } return BadRequest(""); } [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves/no_unity_assets")] public async Task GetSubRoomSavesNoUnity( long roomId, long subRoomId, [FromQuery] int skip = 0, [FromQuery] int take = 20) { var (results, total) = RoomDB.GetSubRoomSaves(roomId, subRoomId, skip, take); return Ok(new { Results = results, TotalResults = total }); } [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves/{saveId}")] public async Task GetSubRoomSaveById(long roomId, long subRoomId, long saveId) { var save = RoomDB.GetSubRoomSaveById(roomId, subRoomId, saveId); if (save == null) return NotFound(""); return Ok(save); } [HttpPost("rooms/{roomId}/subrooms")] public async Task CreateSubroom(long roomId, [FromForm] string name) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); if (room.IsDorm) return RoomError("Rooms.PermissionDenied", "You cannot create a subroom for your dorm room!"); if (string.IsNullOrWhiteSpace(name)) return RoomError("Rooms.InvalidName", "Subroom name cannot be empty."); var updated = RoomDB.CreateSubroomForRoom(roomId, name); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = updated }); } [HttpDelete("rooms/{roomId}/subrooms/{subRoomId}")] public async Task DeleteSubroom(long roomId, long subRoomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); RoomDB.DeleteSubroom(roomId, subRoomId); var updatedRoom = RoomDB.GetRoom(roomId); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, updatedRoom); return Ok(new { success = true, error = "", value = updatedRoom }); } [HttpPut("rooms/{roomId}/subrooms/{subRoomId}/modify")] public async Task ModifySubroom( long roomId, long subRoomId, [FromForm] string name, [FromForm] RoomAccessibility accessibility, [FromForm] int maxPlayers = 32) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); RoomDB.ModifySubroom(roomId, subRoomId, name, accessibility, maxPlayers); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpGet("photon_access_token")] public async Task GetPhotonAccessToken() { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var heartbeat = PlayerDB.GetPlayerHeartbeat((long)id); var roomInstance = heartbeat?.roomInstance; List permissions = new List(); if (roomInstance != null) { var stored = PhotonAccessTokenDB.GetPermissions(roomInstance.roomId, roomInstance.subRoomId); if (stored != null) { permissions = stored.Permissions.Select(p => (object)new { Override = p.Override, Permission = p.Permission, Role = p.Role, Type = p.Type, Value = p.Value }).ToList(); } } return Ok(new { PhotonAccessToken = "", RoomInstanceId = roomInstance?.roomInstanceId, Permissions = permissions.ToArray() }); } [HttpGet("/roomserver/rooms/{elroom}/similar")] public async Task SimilarRooms(long elroom) { var playerId = AuthStuff.GetPlayerId(Request); if (playerId == null) return Unauthorized(""); var target = RoomDB.GetRoom(elroom); if (target == null) return NotFound(""); var allRooms = RoomDB.Rooms.FindAll().Where(r => !IsBaseRoom(r)).ToList(); var results = allRooms .Where(r => r.RoomId != elroom && (int)r.Accessibility != 0) .Select(r => { double score = 0; var aTags = target.Tags?.Select(t => t.Tag.ToLower()).ToHashSet() ?? new HashSet(); var bTags = r.Tags?.Select(t => t.Tag.ToLower()).ToHashSet() ?? new HashSet(); int tagMatches = aTags.Intersect(bTags).Count(); int totalTags = aTags.Union(bTags).Count(); if (totalTags > 0) score += (double)tagMatches / totalTags * 40; if (!string.IsNullOrEmpty(target.Name) && !string.IsNullOrEmpty(r.Name)) { var aWords = target.Name.ToLower().Split(' '); var bWords = r.Name.ToLower().Split(' '); int wordMatches = aWords.AsEnumerable().Intersect(bWords).Count(); int totalWords = aWords.AsEnumerable().Union(bWords).Count(); if (totalWords > 0) score += (double)wordMatches / totalWords * 20; } if (!string.IsNullOrEmpty(target.Description) && !string.IsNullOrEmpty(r.Description)) { if (r.Description.ToLower().Contains(target.Name?.ToLower() ?? string.Empty)) score += 5; } if (target.Stats != null && r.Stats != null) { if (!(target.Stats.VisitCount == 0 && r.Stats.VisitCount == 0)) { double diff = Math.Abs(target.Stats.VisitCount - r.Stats.VisitCount); double max = Math.Max(target.Stats.VisitCount, r.Stats.VisitCount); if (max > 0) score += (1 - diff / max) * 10; } if (!(target.Stats.FavoriteCount == 0 && r.Stats.FavoriteCount == 0)) { double diff = Math.Abs(target.Stats.FavoriteCount - r.Stats.FavoriteCount); double max = Math.Max(target.Stats.FavoriteCount, r.Stats.FavoriteCount); if (max > 0) score += (1 - diff / max) * 10; } if (!(target.Stats.CheerCount == 0 && r.Stats.CheerCount == 0)) { double diff = Math.Abs(target.Stats.CheerCount - r.Stats.CheerCount); double max = Math.Max(target.Stats.CheerCount, r.Stats.CheerCount); if (max > 0) score += (1 - diff / max) * 10; } } if (target.MaxPlayers == r.MaxPlayers) score += 5; double daysDiff = Math.Abs((target.CreatedAt - r.CreatedAt).TotalDays); score += Math.Max(0, 5 - daysDiff / 30); if (target.Accessibility == r.Accessibility) score += 5; return new { Room = r, Score = score }; }) .OrderByDescending(x => x.Score) .Take(50) .Select(x => x.Room) .ToList(); return Ok(new { rooms = results }); } [HttpGet("/roomserver/rooms/{roomId}/experience/player")] public async Task GetPlayerRoomExperience(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); return Ok(new { Enabled = false, Experience = (object?)null, ConcurrencyCode = (string?)null }); } [HttpGet("/roomserver/rooms/{roomId}/bans/{playerId}/isBanned")] public async Task GetIsBanned(long roomId, long playerId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); return Ok(new { success = true, error = (string?)null, error_id = (string?)null, value = false }); } [HttpGet("/roomserver/rooms/{roomId}/assets")] public async Task GetRoomAssets(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); return Ok(new { success = true, error = (string?)null, error_id = (string?)null, value = Array.Empty() }); } [HttpGet("/roomserver/rooms/{roomId}/snapshots/{snapshotId}/loadinfo")] public async Task GetSnapshotLoadInfo(long roomId, string snapshotId, [FromQuery] long subRoomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return NotFound(""); var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId); var latestSave = RoomDB.GetSubRoomSaves(roomId, subRoomId, 0, 1).Results.FirstOrDefault(); return Ok(new { success = true, error = (string?)null, error_id = (string?)null, value = new { Assets = Array.Empty(), RoomId = roomId, SnapshotId = snapshotId, SubRoom = new { SubRoomDataSaveId = latestSave?.SubRoomDataSaveId }, SubRoomId = subRoomId, UgcSubVersion = room.UgcSubVersion, UgcVersion = room.UgcVersion } }); } [HttpGet("/roomserver/publishState/configs")] public async Task GetPublishStateConfigs() { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); return Ok(new { value = new { UpdateMaxCount = 3, UpdateRollingWindowInDays = 365, UpdateExpirationInDays = 30, UpdateCooldownInDays = 45 }, success = true, error_id = (string?)null, error = (string?)null }); } /*[HttpPost("/api/rooms/v1/verifyRole")] public async Task VerifyRole([FromQuery] long roomId, [FromQuery] int role, [FromQuery] string? context) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return Ok(false); bool hasRole = room.Roles?.Any(r => r.AccountId == (long)id && (int)r.Role >= role) ?? false; return Ok(hasRole); }*/ [HttpPost("/api/rooms/v1/verifyRole")] public async Task VerifyRole([FromQuery] long roomId, [FromQuery] int role, [FromQuery] string? context) { return Ok(true); // temp } [HttpGet("/roomserver/rooms/{roomId}/playerdata/me")] public async Task GetMyPlayerData(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); if ((long)id == 55) return NotFound(""); var data = PlayerDB.GetPlayerData((long)id, roomId); if (string.IsNullOrEmpty(data)) return Ok(new { Data = "" }); // modern did this, so i am too return Ok(new { Data = data }); } [HttpPut("/roomserver/rooms/{roomId}/playerdata/me")] public async Task SetMyPlayerData(long roomId, [FromForm] string data) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); //if (AuthStuff.GetAndEnsureAppVersion(Request) != "20230406") return NoContent(); bool success = PlayerDB.SetPlayerData((long)id, roomId, data); if (!success) return BadRequest(); return NoContent(); } [HttpGet("/roomserver/rooms/{roomId}/boosts/{playerId}/count")] public async Task GetBoostCount(long roomId, long playerId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); return Ok("0"); } [HttpGet("/algorithmiclists/topengagementrooms")] public async Task AlgorithmicListTop() { var featured = RoomDB.Rooms.FindAll() .Where(r => r.Accessibility == RoomAccessibility.Public && !r.IsDorm && r.Tags != null && r.Tags.Any(t => t.Tag.Equals("featured", StringComparison.OrdinalIgnoreCase))) .Select(r => new { Id = r.RoomId.ToString(), Context = "{\"algo_name\":\"Rooms_TopEngagement\",\"algo_version\":\"1\",\"user_segment\":\"11\",\"score\":1.0,\"is_shuffled\":false,\"is_rotated\":false}" }) .ToList(); return Ok(new { Type = 1, Entities = featured }); } [HttpGet("/algorithmiclists/{thing}")] public async Task AlgorithmicList() { string path = Path.Join(Program.dataDir, "APIS", "2025thing.json"); return System.IO.File.Exists(path) ? Content(System.IO.File.ReadAllText(path), "application/json") : NotFound(); } [HttpGet("rooms/carousel/rising")] public async Task RisingRooms([FromQuery] string tag = "", [FromQuery] int skip = 0, [FromQuery] int take = 30) { string? passingTag = string.IsNullOrWhiteSpace(tag) ? null : tag; var (results, total) = RoomDB.GetHotRooms(passingTag, skip, take, true); return Ok(new { Results = results ?? new List(), TotalResults = total }); } [HttpGet("featuredrooms/current")] public async Task CurrentFeaturedRooms() { var featured = RoomDB.Rooms.FindAll() .Where(r => r.Accessibility == RoomAccessibility.Public && !r.IsDorm && r.Tags != null && r.Tags.Any(t => t.Tag.Equals("featured", StringComparison.OrdinalIgnoreCase))) .ToList(); return Ok(new { FeaturedRoomGroupId = 300, Name = "Featured Rooms", StartAt = "2024-05-06T10:01:00Z", EndAt = "2024-05-13T10:00:00Z", Rooms = featured }); } [HttpGet("rooms/curated_playlists")] public async Task RoomsCurated() { return Ok(ServerConfig.Bracket); } [HttpGet("rooms/recommendations")] public async Task RoomsRecommended() { return Ok(ServerConfig.Bracket); } [HttpGet("showcase/{accountId}")] public async Task RoomShowcase(long accountId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); return Ok(new List()); } [HttpPut("rooms/{roomId}/voice_chat_encryption")] public async Task SetVoiceChatEncryption(long roomId, [FromForm] bool encryptVoiceChat) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); room.EncryptVoiceChat = encryptVoiceChat; RoomDB.Rooms.Update(room); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/comments")] public async Task SetRoomComments(long roomId, [FromForm] bool disable) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); room.DisableRoomComments = disable; RoomDB.Rooms.Update(room); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/automute")] public async Task SetRoomAutomute(long roomId, [FromForm] bool disable) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); room.DisableMicAutoMute = disable; RoomDB.Rooms.Update(room); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpPut("rooms/{roomId}/allow_new_users")] public async Task SetAllowNewUsers(long roomId, [FromForm] bool allowNewUsers) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); room.MinLevel = allowNewUsers ? 0 : 5; RoomDB.Rooms.Update(room); var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList(); foreach (var pid in allPlayers) await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId)); return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) }); } [HttpGet("rooms/moderatedby/me")] public async Task GetRoomsModeratedByMe() { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var rooms = RoomDB.Rooms.FindAll() .Where(r => r.Roles?.Any(x => x.AccountId == (long)id && (int)x.Role >= (int)Role.Moderator) ?? false) .ToList(); return Ok(rooms); } [HttpPost("rooms/{roomId}/bans/import")] public async Task ImportRoomBans(long roomId, [FromForm] long sourceRoomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); return Ok(new { success = true, error = (string?)null, value = (object?)null }); } [HttpPost("rooms/{roomId}/bans")] public async Task BanFromRoom(long roomId, [FromForm] int banMask, [FromForm(Name = "id")] long targetId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); room.Roles ??= new List(); var existing = room.Roles.FirstOrDefault(r => r.AccountId == targetId); if (existing != null) existing.Role = Role.Banned; else room.Roles.Add(new RoomDBClasses.Roles { AccountId = targetId, Role = Role.Banned, InvitedRole = Role.None }); RoomDB.Rooms.Update(room); var targetPlayer = PlayerDB.Players.FindById(targetId); if (targetPlayer?.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomId == roomId) { var dormPlayer = PlayerDB.GetCurrentPlayer(targetId); if (dormPlayer != null) { var dormRoomId = Vanadium.Controllers.MatchController.EnsurePlayerDorm(dormPlayer); var dormHeartbeat = Vanadium.Classes.Sessions.CreateDorm(targetId, dormPlayer.Player.Username, dormRoomId); await NotificationsController.RefreshHeartbeat(targetId, dormHeartbeat); } } return Ok(new { success = true, error = (string?)null, value = (object?)null }); } [HttpGet("rooms/{roomId}/bans")] public async Task GetRoomBans(long roomId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); var banned = (room.Roles ?? new List()) .Where(r => r.Role == Role.Banned) .Select(r => new { AccountId = r.AccountId, BanStartTime = DateTime.UtcNow }) .ToList(); return Ok(banned); } [HttpDelete("rooms/{roomId}/bans/{playerId}")] public async Task UnbanFromRoom(long roomId, long playerId) { var id = AuthStuff.GetPlayerId(Request); if (id == null) return Unauthorized(""); var room = RoomDB.GetRoom(roomId); if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!"); if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!"); room.Roles?.RemoveAll(r => r.AccountId == playerId && r.Role == Role.Banned); RoomDB.Rooms.Update(room); return Ok(new { success = true, error = (string?)null, value = (object?)null }); } public class RoomDataRequest { public bool AutoPublish { get; set; } public string? Description { get; set; } public string? InventionUsage { get; set; } public int PersistenceVersion { get; set; } public RoomFileData? RoomData { get; set; } public RoomFileData? SubRoomData { get; set; } public string? UnityAssetId { get; set; } } private async Task ContainsProhibitedText(string text) { if (string.IsNullOrWhiteSpace(text)) return false; string filePath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "swears.txt"); if (!System.IO.File.Exists(filePath)) return false; var swearWords = await System.IO.File.ReadAllLinesAsync(filePath); return swearWords .Select(w => w.Trim()) .Where(w => !string.IsNullOrEmpty(w)) .Any(word => Regex.IsMatch(text, $@"(?