using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using SixLabors.ImageSharp; using System; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using RadicadeServer.API.Notifications; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet; using RadicadeServer.RecNet.Enums; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Enums.Rooms; using RadicadeServer.RecNet.Messages; using RadicadeServer.RecNet.Notifications; using RadicadeServer.RecNet.Responses; using RadicadeServer.RecNet.Rooms; using RadicadeServer.API.Studio; using RadicadeServer.Radicade.Discord; using RadicadeServer.Radicade.Discord.Templates; using RadicadeServer.Utils; namespace RadicadeServer.RoomServer { [Route("rooms/")] public class RoomServerAPI : Controller { private const int RoomSearchHardLimit = 50; RecDBContext db; public RoomServerAPI(RecDBContext context) { this.db = context; } [HttpGet("showcase/{playerId}")] public async Task> GetPlayerShowcaseRooms(long playerId) { var roomIds = await db.RoomRoles .Where(x => x.AccountId == playerId && x.Role >= RoleType.CoOwner) .Select(x => x.RoomId) .ToListAsync(); if (roomIds.Count == 0) { roomIds = await db.Rooms .Where(x => x.CreatorAccountId == playerId && x.Accessibility >= Accessibility.Public) .Select(x => x.RoomId) .ToListAsync(); } var showcaseRooms = await RoomUtils.VisibleRooms( await db.Rooms.Where(x => roomIds.Contains(x.RoomId)).ToListAsync(), User.GetUserId()); return Ok(showcaseRooms.Select(x => x.RoomId).Distinct().ToArray()); } [HttpDelete("rooms/{roomId}"), Authorize] public async Task DeleteRoom(long roomId) { var userId = User.GetUserId(); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return new Success(false, "Room does not exist.", null); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.Creator); if (!canModify) return Success.Error("Insufficient permissions"); var roles = await db.RoomRoles.Where(x => x.RoomId == roomId).ToListAsync(); foreach (var role in roles) { db.RoomRoles.Remove(role); } var interactions = await db.RoomInteractions.Where(x => x.RoomId == roomId).ToListAsync(); foreach (var i in interactions) { db.RoomInteractions.Remove(i); } var keys = await db.RoomKeys.Where(x => x.RoomId == roomId).ToListAsync(); foreach (var key in keys) { db.RoomKeys.Remove(key); } var stats = await db.RoomStats.FirstOrDefaultAsync(x => x.RoomId == roomId); if (stats != null) db.RoomStats.Remove(stats); var subrooms = await db.SubRooms.Where(x => x.RoomId == roomId).ToListAsync(); foreach (var subroom in subrooms) { db.SubRooms.Remove(subroom); } var prevSubrooms = await db.SubRoomHistory.Where(x => x.RoomId == roomId).ToListAsync(); foreach (var subroom in prevSubrooms) { db.SubRoomHistory.Remove(subroom); } db.Rooms.Remove(room); await db.SaveChangesAsync(); return new Success(true, null, null); } [HttpDelete("rooms/{roomId}/subrooms/{subroomId}"), Authorize] public async Task DeleteSubRoom(long roomId, long subroomId) { var userId = User.GetUserId(); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return new Success(false, "Room does not exist.", null); var subroom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subroomId && x.RoomId == roomId); if (subroom == null) return new Success(false, "Subroom does not exist.", null); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.Creator); if (!canModify) return Success.Error("Insufficient permissions"); var history = await db.SubRoomHistory.Where(x => x.RoomId == roomId && x.SubRoomId == subroomId).ToListAsync(); foreach (var historyItem in history) { db.SubRoomHistory.Remove(historyItem); } db.SubRooms.Remove(subroom); await db.SaveChangesAsync(); var fullRoom = await RoomUtils.GetRoom(roomId); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); return new Success(true, null, null); } [HttpGet("rooms/{roomId}/playerdata/me")] public ActionResult GetRoomPlayerData() { return Ok(new RoomPlayerData { data = "" }); } [HttpGet("rooms/curated_playlists")] public async Task GetCuratedPlaylists() { var playlistIds = await db.Playlists .OrderBy(x => x.PlaylistId) .Select(x => x.PlaylistId) .ToListAsync(); if (playlistIds.Count == 0) playlistIds.Add(24226488); return Ok(playlistIds.Distinct().ToArray()); } [HttpGet("rooms/moderatedby/me"), Authorize] public async Task?> GetRoomsModeratedByMe() { long? userId = User.GetUserId(); if (userId == null) return null; var roles = await db.RoomRoles.Where(x => x.AccountId == userId && x.Role >= RoleType.Moderator).ToListAsync(); var rooms = new List(); foreach (var role in roles) { var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == role.RoomId); if (room != null) rooms.Add(room); } return await RoomUtils.VisibleRooms(rooms, userId); } [HttpGet("rooms/visitedby/{playerId:long}"), Authorize] public async Task GetVisitedRoomsForPlayer(long playerId, [FromQuery] int skip = 0, [FromQuery] int take = 10) { var viewerId = User.GetUserId(); skip = Math.Max(skip, 0); take = Math.Clamp(take, 1, 100); var roomIds = await db.RoomInteractions .Where(x => x.PlayerId == playerId && x.Visited) .OrderByDescending(x => x.LastVisted) .Select(x => x.RoomId) .Distinct() .Skip(skip) .Take(take) .ToListAsync(); var rooms = new List(); foreach (var roomId in roomIds) { var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room != null) rooms.Add(room); } rooms = await RoomUtils.VisibleRooms(rooms, viewerId); var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); return Ok(convert); } [HttpGet("rooms/visitedby/me"), Authorize] public async Task GetVisitedRooms([FromQuery] int skip = 0, [FromQuery] int take = 100) { long? userId = User.GetUserId(); if (userId == null) return Unauthorized(); skip = Math.Max(skip, 0); take = Math.Clamp(take, 1, 100); var roomIds = await db.RoomInteractions .Where(x => x.PlayerId == userId && x.Visited) .OrderByDescending(x => x.LastVisted) .Select(x => x.RoomId) .Distinct() .Skip(skip) .Take(take) .ToListAsync(); var rooms = new List(); foreach (var roomId in roomIds) { var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room != null) rooms.Add(room); } rooms = await RoomUtils.VisibleRooms(rooms, userId); var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); return Ok(convert); } [HttpGet("rooms/topcreators")] public async Task?> GetRoomsFromTopCreators() { var topCreators = await db.RoomInteractions.Where(i => i.Favorited) .Join(db.Rooms, interaction => interaction.RoomId, room => room.RoomId, (interaction, room) => new { room.CreatorAccountId }).GroupBy(x => x.CreatorAccountId) .Select(g => new { CreatorId = g.Key, FavoriteCount = g.Count() }).OrderByDescending(x => x.FavoriteCount).Take(10).Select(x => x.CreatorId).ToListAsync(); if (!topCreators.Any()) return []; var rooms = await db.Rooms.Where(r => topCreators.Contains(r.CreatorAccountId)).OrderByDescending(r => r.RoomId).Take(10 * 3).ToListAsync(); rooms.RemoveAll(x => x.Accessibility != Accessibility.Public); return await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); } [HttpGet("rooms/bulk")] public async Task ?> GetBulkRooms([FromQuery] string? name, [FromQuery] List? id) { List rooms = new List(); if (name != null) { rooms = await db.Rooms.Where(x => x.Name.Contains(name)).ToListAsync(); } else if (id != null) { rooms.Clear(); foreach (var i in id) { var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == i); if (room != null) rooms.Add(room); } } return await RoomUtils.VisibleRooms(rooms, User.GetUserId()); } [HttpGet("featuredrooms/current")] public async Task GetCurrentFeaturedRooms() { var candidates = await db.Rooms.AsNoTracking() .Where(x => x.State == RoomState.Active && x.Tags.ToLower().Contains("featured")) .OrderBy(x => x.RoomId) .ToListAsync(); var featured = candidates .Where(room => StringTools.ReadTags(room.Tags) .Any(tag => string.Equals(tag.Tag, "featured", StringComparison.OrdinalIgnoreCase))) .ToList(); featured = await RoomUtils.VisibleRooms(featured, User.GetUserId()); if (featured.Count == 0) { return NotFound(); } return Ok(new FeaturedRoomGroup { FeaturedRoomGroupId = 1, Name = "Featured Rooms", Rooms = featured.Select(room => new FeaturedRoom { RoomId = room.RoomId, RoomName = room.Name, ImageName = string.IsNullOrWhiteSpace(room.ImageName) ? room.Name : room.ImageName }).ToList() }); } [HttpGet("roomsandplaylists/search")] public async Task GetBulkRoomsOld([FromQuery] string query) { if (query == null) return null; query = query.Replace("#", "").Trim(); var rooms = await db.Rooms.Where(x => (x.Name.ToLower().Contains(query) || x.Tags.ToLower().Contains(query)) && x.Accessibility == Accessibility.Public).ToListAsync(); var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); return new RoomSearchResult { TotalResults = rooms.Count, Results = convert }; } [HttpGet("rooms/search_rooms/{source}")] public async Task SearchRoomsBySource(string source, [FromQuery] int skip = -1, [FromQuery] int take = 20) { var (src, pSkip, pTake) = ParseEmbeddedQuery(source, 20); if (skip < 0) skip = pSkip; if (take == 20 && pTake != 20) take = pTake; skip = Math.Max(skip, 0); take = Math.Clamp(take, 1, 100); if (string.Equals(src, "MyClubhouses", StringComparison.OrdinalIgnoreCase) || string.Equals(src, "MyClubs", StringComparison.OrdinalIgnoreCase) || string.Equals(src, "Clubhouse", StringComparison.OrdinalIgnoreCase)) { return Ok(new RoomSearchResult { TotalResults = 0, Results = new List() }); } IQueryable q; if (!string.IsNullOrWhiteSpace(src) && !src.Contains('&')) { var kw = Uri.UnescapeDataString(src).Replace('+', ' ').Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(k => k.TrimStart('#')).Where(k => k.Length > 0).ToList(); q = db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active && kw.All(k => x.Name.ToLower().Contains(k) || x.Tags.ToLower().Contains(k))); } else { q = db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active); } var rooms = await q.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); return Ok(new RoomSearchResult { TotalResults = convert.Count, Results = convert }); } private static (string Source, int Skip, int Take) ParseEmbeddedQuery(string? source, int defTake) { var src = source ?? string.Empty; int skip = 0, take = defTake; int amp = src.IndexOf('&'); if (amp >= 0) { foreach (var pair in src.Substring(amp + 1).Split('&', StringSplitOptions.RemoveEmptyEntries)) { var kv = pair.Split('=', 2); if (kv.Length == 2 && int.TryParse(kv[1], out var n)) { if (kv[0] == "skip") skip = n; else if (kv[0] == "take") take = n; } } src = src.Substring(0, amp); } return (Uri.UnescapeDataString(src), Math.Max(skip, 0), Math.Clamp(take, 1, 100)); } [HttpGet("rooms/hot_rooms/{*source}")] [HttpGet("hot_rooms/{*source}")] public async Task GetHotRoomsPaged(string? source) { var (query, skip, take) = ParseEmbeddedQuery(source, 24); query = query.Trim(); IQueryable q = db.Rooms .Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active); if (!string.IsNullOrWhiteSpace(query) && query != "all") { var tags = query.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(t => t.TrimStart('#')).Where(t => t.Length > 0).ToList(); if (tags.Count > 0) q = q.Where(x => tags.All(t => x.Tags.Contains(t))); } var rooms = await q.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); if (rooms.Count == 0 && skip == 0) { rooms = await db.Rooms .Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active) .OrderBy(x => x.RoomId).Take(take).ToListAsync(); } return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms)); } [HttpGet("rooms/carousel/{*source}")] public async Task GetCarouselRooms(string? source, [FromQuery] int skip = -1, [FromQuery] int take = 20) { var (src, pSkip, pTake) = ParseEmbeddedQuery(source, 20); if (skip < 0) skip = pSkip; if (take == 20 && pTake != 20) take = pTake; skip = Math.Max(skip, 0); take = Math.Clamp(take, 1, 100); var q = db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active); var keyword = src.Trim().TrimStart('#').ToLowerInvariant(); if (keyword.Length > 0) q = q.Where(x => x.Name.ToLower().Contains(keyword) || x.Tags.ToLower().Contains(keyword)); var rooms = await q.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); if (rooms.Count == 0 && skip == 0) { rooms = await db.Rooms .Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active) .OrderBy(x => x.RoomId).Take(take).ToListAsync(); } var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); return Ok(new RoomSearchResult { Results = convert, TotalResults = convert.Count }); } [HttpGet("rooms/my_visited_rooms/{*source}"), Authorize] [HttpGet("my_visited_rooms/{*source}"), Authorize] public async Task GetMyVisitedRoomsPaged(string? source) { var userId = User.GetUserId(); if (!userId.HasValue) return Unauthorized(); var (_, skip, take) = ParseEmbeddedQuery(source, 24); var rooms = await db.Rooms .Where(x => db.RoomInteractions.Any(i => i.PlayerId == userId && i.RoomId == x.RoomId) && x.Accessibility == Accessibility.Public) .OrderByDescending(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms)); } [HttpGet("rooms/roomsbycreators/{*source}")] [HttpGet("roomsbycreators/{*source}")] public async Task GetRoomsByCreatorsPaged(string? source) { var (ids, skip, take) = ParseEmbeddedQuery(source, 24); var creatorIds = ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Select(s => long.TryParse(s, out var n) ? n : -1) .Where(n => n > 0).ToList(); var rooms = creatorIds.Count == 0 ? new List() : await db.Rooms.Where(x => creatorIds.Contains(x.CreatorAccountId) && x.Accessibility == Accessibility.Public && x.State == RoomState.Active) .OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync(); return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms)); } [HttpGet("rooms/rro_ids")] public async Task GetRroRoomIds() { var ids = await db.Rooms.Where(x => x.IsRRO && x.Accessibility == Accessibility.Public) .OrderBy(x => x.RoomId).Select(x => x.RoomId).ToListAsync(); return Ok(ids); } [HttpGet("rooms/search")] public async Task SearchRooms([FromQuery] string query) { if (query == null) return null; query = query.ToLower(); query = query.Replace("+", " ").Trim(); query = query.Replace("#", "").Trim(); var keywords = query.Split(' ', StringSplitOptions.TrimEntries); var rooms = await db.Rooms.Where(x => keywords.All(k => (x.Name.ToLower().Contains(k) || x.Tags.ToLower().Contains(k)) && x.Accessibility == Accessibility.Public) ).ToListAsync(); var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms); return new RoomSearchResult { TotalResults = rooms.Count, Results = convert }; } [HttpGet("rooms/createdby/{who}"), Authorize] public async Task GetRoomsBy(string who) { long? userId; if (who == "me") userId = User.GetUserId(); else userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId; if (userId == null) userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId; if (userId == null) return Unauthorized(); var roles = await db.RoomRoles.Where(x => x.AccountId == userId && x.Role >= RoleType.CoOwner).ToListAsync(); var rooms = new List(); foreach (var role in roles) { var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == role.RoomId); if (room != null) rooms.Add(room); } return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail( await RoomUtils.VisibleRooms(rooms, User.GetUserId()))); } [HttpGet("rooms/ownedby/{who}"), Authorize] public async Task GetRoomsByOld(string who) { long? userId; if (who == "me") userId = User.GetUserId(); else userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId; if (userId == null) userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId; if (userId == null) return Unauthorized(); var roomIds = await db.RoomRoles .Where(x => x.AccountId == userId && x.Role >= RoleType.CoOwner) .Select(x => x.RoomId) .Distinct() .ToListAsync(); var rooms = new List(); foreach (var roomId in roomIds) { var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room != null) rooms.Add(room); } return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail( await RoomUtils.VisibleRooms(rooms, User.GetUserId()))); } [HttpGet("rooms/hot")] public async Task GetHotRooms(string? tag) { if (tag == null) { var list = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.Name != "DormRoom").ToListAsync(); var bulkConvert = await RoomUtils.BulkConvertRoomDataToRoomDetail(list); return new RoomSearchResult { Results = bulkConvert, TotalResults = bulkConvert.Count }; } else { switch (tag) { default: var list1 = await db.Rooms.Where(x => x.Tags.Contains(tag) && x.Accessibility == Accessibility.Public && !x.IsDorm).ToListAsync(); if (list1.Count == 0) { list1 = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && !x.IsDorm).ToListAsync(); } var bulkConvert1 = await RoomUtils.BulkConvertRoomDataToRoomDetail(list1); return new RoomSearchResult { Results = bulkConvert1, TotalResults = bulkConvert1.Count }; case "new": var list2 = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && !x.IsDorm).OrderByDescending(x => x.PublishedAt).ToListAsync(); var bulkConvert2 = await RoomUtils.BulkConvertRoomDataToRoomDetail(list2); return new RoomSearchResult { Results = bulkConvert2, TotalResults = bulkConvert2.Count }; case "rro": var list3 = await db.Rooms.Where(x => x.IsRRO && !x.IsDorm).OrderByDescending(x => x.PublishedAt).ToListAsync(); var bulkConvert3 = await RoomUtils.BulkConvertRoomDataToRoomDetail(list3); return new RoomSearchResult { Results = bulkConvert3, TotalResults = bulkConvert3.Count }; } } } [HttpGet("roomsandplaylists/hot")] public async Task GetHotRoomsOld() { var list = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.Name != "DormRoom").ToListAsync(); var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(list); return new RoomSearchResult { Results = convert, TotalResults = convert.Count }; } [HttpPut("rooms/{id}/interactionby/me/favorite"), Authorize] public async Task FavouriteRoom(long id) { var userId = User.GetUserId(); PlayerInteractionDB? interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); if (interaction == null) { var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = false, Favorited = true }; await db.RoomInteractions.AddAsync(favoriteResult); await db.SaveChangesAsync(); interaction = favoriteResult; } else { interaction.Favorited = true; await db.SaveChangesAsync(); } return new PlayerInteraction(interaction); } [HttpDelete("rooms/{id}/interactionby/me/favorite"), Authorize] public async Task UnfavouriteRoom(long id) { var userId = User.GetUserId(); PlayerInteractionDB? interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); if (interaction == null) { var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = false, Favorited = false }; await db.RoomInteractions.AddAsync(favoriteResult); await db.SaveChangesAsync(); interaction = favoriteResult; } else { interaction.Favorited = false; await db.SaveChangesAsync(); } return new PlayerInteraction(interaction); } [HttpPut("rooms/{id}/interactionby/me/cheer"), Authorize] public async Task CheerRoom(long id) { var userId = User.GetUserId(); var interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); if (interaction == null) { var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = true, Favorited = false }; await db.RoomInteractions.AddAsync(favoriteResult); await db.SaveChangesAsync(); interaction = favoriteResult; } else { interaction.Cheered = true; await db.SaveChangesAsync(); } return new PlayerInteraction(interaction); } [HttpDelete("rooms/{id}/interactionby/me/cheer"), Authorize] public async Task UncheerRoom(long id) { var userId = User.GetUserId(); var interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); if (interaction == null) { var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = false, Favorited = false }; await db.RoomInteractions.AddAsync(favoriteResult); await db.SaveChangesAsync(); interaction = favoriteResult; } else { interaction.Cheered = false; await db.SaveChangesAsync(); } return new PlayerInteraction(interaction); } [HttpGet("rooms/favoritedby/{who}"), Authorize] public async Task GetFavouritedBy(string who) { long? userId; if (who == "me") userId = User.GetUserId(); else userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId; if (userId == null) userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId; if (userId == null) return Unauthorized(); var roomIds = await db.RoomInteractions .Where(x => x.PlayerId == userId && x.Favorited) .OrderByDescending(x => x.FavoritedAt) .Select(x => x.RoomId) .Distinct() .ToListAsync(); var rooms = new List(); foreach (var roomId in roomIds) { var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room != null) rooms.Add(room); } return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail( await RoomUtils.VisibleRooms(rooms, User.GetUserId()))); } [HttpGet("rooms/{id}/interactionby/{who}"), Authorize] public async Task GetInteractionBy(long id, string who) { long? userId; if (who == "me") userId = User.GetUserId(); else userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId; if (userId == null) userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId; if (userId == null) return null; var interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id); if (interaction == null) return new PlayerInteraction { Cheered = false, Favorited = false }; return new PlayerInteraction(interaction); } [HttpGet("rooms/base")] public async Task?> GetBaseRooms() { var roomData = await db.Rooms.Where(x => x.CloningAllowed && x.Accessibility != Accessibility.Private && x.CreatorAccountId == 1).ToListAsync(); var rooms = new List(); foreach (var room in roomData) { var fullRoom = await RoomUtils.GetRoom(room.RoomId); if (fullRoom == null) continue; rooms.Add(fullRoom); } return rooms; } [HttpPost("rooms/{id}/clone"), Authorize] public async Task CloneRoom(long id, [FromForm] string name) { var userId = User.GetUserId(); if (userId == null) return new Success(false, "You are not signed in.", null); if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null); var source = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == id); if (source == null) return new Success(false, "Unknown error", null); if (!source.CloningAllowed) { return new Success(false, "This room cannot be cloned.", null); } if (await RoomUtils.IsHiddenFrom(source, userId)) { return new Success(false, "This room cannot be cloned.", null); } var doesRoomExist = (await db.Rooms.FirstOrDefaultAsync(x => x.Name == name)) != null; if (doesRoomExist) return new Success(false, "Room already exists.", null); var roomData = await RoomUtils.CloneRoom(id, (long)userId!, name); if (roomData == null) return new Success(false, "Unknown error", null); return new Success(true, null, roomData); } private sealed record RoomPermissionRow(string Permission, long Role, bool Override, int Type, string Value); private static List DefaultPermissions() { static RoomPermissionRow Row(string permission, long role, bool overridden) => new(permission, role, overridden, 0, "True"); return new List { Row("CAN_USE_ROOM_RESET_BUTTON", 0, true), Row("CAN_USE_DELETE_ALL_BUTTON", 0, true), Row("CAN_SAVE_INVENTIONS", 0, true), Row("CAN_SPAWN_INVENTIONS", 0, true), Row("CAN_USE_PLAY_GIZMOS_TOGGLE", 0, true), Row("CAN_INVITE", 0, true), Row("CAN_USE_MAKER_PEN", 30, false), Row("CAN_USE_ROOM_RESET_BUTTON", 30, true), Row("CAN_USE_DELETE_ALL_BUTTON", 30, true), Row("CAN_SAVE_INVENTIONS", 30, true), Row("CAN_SPAWN_INVENTIONS", 30, true), Row("CAN_USE_PLAY_GIZMOS_TOGGLE", 30, true), }; } [HttpGet("photon_access_token")] public async Task GetPhotonAccessToken() { long? userId = User.GetUserId(); if (userId == null) return Unauthorized(); var presence = await db.Presences .AsNoTracking() .FirstOrDefaultAsync(x => x.playerId == userId.Value); var permissions = DefaultPermissions(); var profile = await db.Profiles .AsNoTracking() .FirstOrDefaultAsync(x => x.accountId == userId.Value); if (profile != null && (profile.HasRole("developer") || profile.HasRole("moderator"))) permissions.Insert(0, new RoomPermissionRow("CAN_USE_MAKER_PEN", 0, true, 0, "True")); long? subRoomId = null; if (presence?.roomInstance != null) { subRoomId = await db.RoomInstances .AsNoTracking() .Where(x => x.roomInstanceId == presence.roomInstance) .Select(x => (long?)x.subRoomId) .FirstOrDefaultAsync(); } if (subRoomId != null) { var overrides = await db.SubRoomPermissions .AsNoTracking() .Where(x => x.subRoomId == subRoomId) .OrderBy(x => x.Id) .ToListAsync(); foreach (var stored in overrides) { var entry = new RoomPermissionRow(stored.permission, stored.role, true, stored.type, stored.value); var index = permissions.FindIndex(x => x.Permission == entry.Permission && x.Role == entry.Role); if (index == -1) permissions.Add(entry); else permissions[index] = entry; } } return Ok(new Dictionary { ["Permissions"] = permissions, ["PhotonAccessToken"] = "", ["RoomInstanceId"] = presence?.roomInstance }); } [HttpGet("rooms/{id}")] public async Task> GetRoomById(long id) { var roomData = db.Rooms.FirstOrDefault(x => x.RoomId == id); if (roomData == null) { var userId = User.GetUserId(); if (userId != null) { var dorm = await RoomUtils.GetOrCreateDorm((long)userId!); if (dorm != null) return Ok(dorm); } return NotFound(); } if (!await RoomUtils.CanSeeRoom(roomData, User.GetUserId())) return roomData.IsDorm ? NotFound() : Ok(RoomUtils.MaskHiddenRoom(roomData)); var fullRoom = await RoomUtils.GetRoom(roomData.RoomId); if (fullRoom == null) return NotFound(); return Ok(fullRoom); } [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/datahistory"), Authorize] public async Task>> GetRoomDataHistory(long roomId, long subRoomId) { var userId = User.GetUserId(); var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (roomData == null) return NotFound(); var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == (long)subRoomId); if (subRoom == null) return NotFound(); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Forbid(); var history = await db.SubRoomHistory.Where(x => x.RoomId == roomId && x.SubRoomId == subRoomId).ToListAsync(); return Ok(history); } [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves"), Authorize] public async Task GetSubRoomSaves(long roomId, long subRoomId, int? skip = null, int? take = null) { var (refusal, saves) = await LoadSubRoomSavesAsync(roomId, subRoomId); if (refusal != null) return refusal; var page = PageSaves(saves, skip, take); return Ok(new { Results = page, TotalResults = saves.Count, TotalCount = saves.Count }); } [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves/no_unity_assets"), Authorize] public async Task GetSubRoomSavesWithoutUnityAssets(long roomId, long subRoomId, int? skip = null, int? take = null) { var (refusal, saves) = await LoadSubRoomSavesAsync(roomId, subRoomId); if (refusal != null) return refusal; var page = PageSaves(saves, skip, take) .Select(x => new { x.SubRoomDataSaveId, x.SubRoomId, x.DataBlob, x.ReferencedUnityAssetIds, x.PersistenceVersion, x.OMVersion, x.UgcSubVersion, x.SavedByAccountId, x.SavedOnPlatform, x.SavedOnDeviceClass, x.Description, x.ModerationState, x.CreatedAt, x.UnityAssetId }) .ToList(); return Ok(new { Results = page, TotalResults = saves.Count, TotalCount = saves.Count }); } private async Task<(IActionResult? Refusal, List Saves)> LoadSubRoomSavesAsync(long roomId, long subRoomId) { var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId); var subRoomExists = await db.SubRooms.AsNoTracking().AnyAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); if (room == null || !subRoomExists) return (Ok(EmptySavePage()), new List()); var userId = User.GetUserId(); if (userId == null) return (Unauthorized(), new List()); if (!await CanReadSavesAsync((long)userId!, room)) return (StatusCode(StatusCodes.Status403Forbidden), new List()); return (null, await RoomUtils.GetSaveHistory(db, roomId, subRoomId)); } private async Task CanReadSavesAsync(long userId, RoomData room) { if (room.CreatorAccountId == userId) return true; if (await MatchmakingUtils.IsAtLeastRole(userId, room.RoomId, RoleType.CoOwner)) return true; var presence = await db.Presences.AsNoTracking().FirstOrDefaultAsync(x => x.playerId == userId); if (presence?.roomInstance == null) return false; var instance = await db.RoomInstances.AsNoTracking() .FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance); return instance != null && instance.roomId == room.RoomId; } private static object EmptySavePage() => new { Results = new List(), TotalResults = 0, TotalCount = 0 }; private static List PageSaves(List saves, int? skip, int? take) { var from = skip == null || skip < 0 ? 0 : skip.Value; var remaining = saves.Skip(from); return (take == null || take < 0 ? remaining : remaining.Take(take.Value)).ToList(); } private async Task> ReadSaveValuesAsync() { var values = new Dictionary(StringComparer.OrdinalIgnoreCase); if (Request.HasFormContentType) { foreach (var (key, value) in Request.Form) values[key] = value.ToString(); return values; } Request.EnableBuffering(); string raw; using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true)) raw = (await reader.ReadToEndAsync()).Trim(); Request.Body.Position = 0; if (raw.Length == 0) return values; try { if (JsonNode.Parse(raw) is JsonObject json) FlattenSaveJson(json, string.Empty, values); } catch (JsonException) { } return values; } private static void FlattenSaveJson(JsonObject json, string prefix, Dictionary values) { foreach (var (key, node) in json) { var path = prefix.Length == 0 ? key : prefix + "." + key; if (node is JsonObject nested) FlattenSaveJson(nested, path, values); else if (node != null && node is not JsonArray) values[path] = node.ToString(); } } private static string? ReadSaveValue(Dictionary values, params string[] keys) { foreach (var key in keys) if (values.TryGetValue(key, out var raw) && !string.IsNullOrWhiteSpace(raw)) return raw; return null; } private static JsonObject BuildRoomSaveValue(FullRoom room, SubRoom subRoom, string? dataBlobHash) { var roomNode = JsonSerializer.SerializeToNode(room) as JsonObject ?? new JsonObject(); var value = new JsonObject { ["room"] = roomNode.DeepClone() }; foreach (var property in roomNode) value[property.Key] = property.Value?.DeepClone(); value["subRoomDataSave"] = JsonSerializer.SerializeToNode(RoomUtils.BuildSaveResponse(subRoom, dataBlobHash)); return value; } private sealed record StudioSaveResult(bool success, string? error, object? value); [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/data"), Authorize] public async Task UploadRoomData(long roomId, long subRoomId) { var userId = User.GetUserId(); if (userId == null) return Success.Error("Invalid auth"); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (roomData == null) return new Success(false, "Invalid room.", null); var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId); if (subRoom == null) return new Success(false, "Invalid subroom.", null); var values = await ReadSaveValuesAsync(); var blobName = ReadSaveValue(values, "subroomdata.filename", "filename", "datablob", "blob"); if (string.IsNullOrWhiteSpace(blobName)) { return new Success(false, "No room data was uploaded.", null); } var savedBy = (long)userId!; var unityAssetId = ReadSaveValue(values, "unityassetid", "subroomdata.unityassetid"); var unityAssetFilename = ReadSaveValue(values, "unityasset.filename", "unityasset"); if (!string.IsNullOrWhiteSpace(unityAssetFilename)) { if (!StudioUnityAssetRegistry.IsValidAssetId(unityAssetFilename)) return new StudioSaveResult(false, "Invalid Unity source-manifest filename.", null); if (await StudioUnityAssetRegistry.ReadAsync(unityAssetFilename, HttpContext.RequestAborted) == null) return new StudioSaveResult(false, "Unity source manifest has not been uploaded.", null); } var blobHash = ReadSaveValue(values, "subroomdata.hash", "hash", "datablobhash"); await db.SubRoomHistory.AddAsync(new RestorableSubRoom(subRoom)); subRoom.DataBlob = blobName; subRoom.SavedByAccountId = savedBy; subRoom.CreatedAt = DateTime.UtcNow; if (!string.IsNullOrWhiteSpace(unityAssetId)) subRoom.UnityAssetId = unityAssetId; if (!string.IsNullOrWhiteSpace(unityAssetFilename)) subRoom.UnityAssetId = unityAssetFilename; await db.SaveChangesAsync(); var room = await RoomUtils.GetRoom(roomId); if (room == null) return new Success(false, "Invalid room.", null); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = room }); return new StudioSaveResult(true, null, BuildRoomSaveValue(room, subRoom, blobHash)); } [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/unityasset")] public async Task GetStudioUnityAsset(long roomId, long subRoomId, [FromQuery] string? unityAssetId = null) { var subRoom = await db.SubRooms.AsNoTracking() .FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); if (subRoom == null) return NotFound(); var assetId = string.IsNullOrWhiteSpace(unityAssetId) ? subRoom.UnityAssetId : unityAssetId; if (!string.Equals(assetId, subRoom.UnityAssetId, StringComparison.Ordinal)) return NotFound(); var userId = User.GetUserId(); var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null || !await RoomUtils.CanSeeRoom(room, userId)) return NotFound(); var asset = await StudioUnityAssetRegistry.ReadAsync(assetId, HttpContext.RequestAborted); return asset == null ? NotFound() : Ok(asset); } [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/restoredata"), Authorize] public async Task RestoreRoomData(long roomId, long subRoomId) { var userId = User.GetUserId(); if (userId == null) return Success.Error("Invalid auth"); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (roomData == null) return new Success(false, "Invalid room.", null); var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId); if (subRoom == null) return new Success(false, "Invalid subroom.", null); var values = await ReadSaveValuesAsync(); var blobName = ReadSaveValue(values, "filename", "subroomdata.filename", "datablob", "blob"); if (string.IsNullOrWhiteSpace(blobName)) return new Success(false, "No room data was given.", null); if (!System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, blobName))) return new Success(false, "That save's data is no longer available.", null); if (!string.IsNullOrEmpty(subRoom.DataBlob) && subRoom.DataBlob != blobName) { await db.SubRoomHistory.AddAsync(new RestorableSubRoom(subRoom)); } subRoom.DataBlob = blobName; subRoom.SavedByAccountId = (long)userId!; subRoom.CreatedAt = DateTime.UtcNow; await db.SaveChangesAsync(); var room = await RoomUtils.GetRoom(roomId); if (room == null) return new Success(false, "Invalid room.", null); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = room }); return new Success(true, null, BuildRoomSaveValue(room, subRoom, null)); } [HttpPost("rooms/{roomId}/subrooms"), Authorize] public async Task CreateSubroom(long roomId, [FromForm] string name) { var userId = User.GetUserId(); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (roomData == null) return new Success(false, "Invalid room.", null); var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.Name == name && x.RoomId == roomId); if (subRoom != null) return new Success(false, "A subroom already exists with that name!", null); var mainSubRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (mainSubRoom == null) return new Success(false, "There is no main subroom?", null); var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId); if (presence == null) return new Success(false, "Invalid auth", null); if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null); var newSubRoom = new SubRoom { DataBlob = "", CreatedAt = DateTime.Now, Accessibility = Accessibility.Unlised, IsSandbox = true, MaxPlayers = mainSubRoom.MaxPlayers, Name = name, RoomId = roomId, SavedByAccountId = (long)userId!, UnitySceneId = mainSubRoom.UnitySceneId }; await db.SubRooms.AddAsync(newSubRoom); await db.SaveChangesAsync(); var room = await RoomUtils.GetRoom((long)roomId); if (room == null) return new Success(false, "WHAT THE FUCK???", null); return new Success(true, null, room); } [HttpPost("rooms/{roomId}/subrooms/{subRoomId}/move"), Authorize] public async Task MoveSubroom(long roomId, long subRoomId, [FromForm] long newRoomId) { var userId = User.GetUserId(); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, newRoomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); var newRoom = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == newRoomId); var subroom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId); if (room == null) return Success.Error("Room not found"); if (newRoom == null) return new Success(false, "Target room not found", null); if (subroom == null) return new Success(false, "Subroom not found", null); subroom.RoomId = newRoomId; await db.SaveChangesAsync(); var fullRoom = await RoomUtils.GetRoom(roomId); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); return new Success(true, null, null); } [HttpPut("rooms/{roomId}/subrooms/{subRoomId}/modify")] public async Task ModifySubroom(long roomId, long subRoomId, string? name, Accessibility? accessibility, int? maxPlayers) { var userId = User.GetUserId(); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); var subroom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId); if (room == null) return Success.Error("Room not found"); if (subroom == null) return new Success(false, "Subroom not found", null); if (name != null) subroom.Name = name; if (accessibility != null) subroom.Accessibility = (Accessibility)accessibility; if (maxPlayers != null) subroom.MaxPlayers = (int)maxPlayers; await db.SaveChangesAsync(); var fullRoom = await RoomUtils.GetRoom(roomId); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); return new Success(true, null, null); } [HttpPut("rooms/{roomId}/modify"), Authorize] public async Task ModifyRoom(long roomId, string name, string? description, Accessibility accessibility, bool supportsScreens, bool supportsWalkVR, bool supportsTeleportVR, bool supportsJuniors, bool cloningAllowed, bool disableMicAutoMute, bool disableRoomComments, bool encryptVoiceChat) { var userId = User.GetUserId(); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return Success.Error("Room not found"); if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null); if (!(description ?? string.Empty).IsPure()) return new Success(false, "Description contains offensive language.", null); room.Name = name; room.Description = description; room.SupportsScreens = supportsScreens; room.SupportsWalkVR = supportsWalkVR; room.SupportsTeleportVR = supportsTeleportVR; room.SupportsJuniors = supportsJuniors; room.Accessibility = accessibility; room.CloningAllowed = cloningAllowed; room.DisableMicAutoMute = disableMicAutoMute; room.DisableRoomComments = disableRoomComments; room.EncryptVoiceChat = encryptVoiceChat; await db.SaveChangesAsync(); var fullRoom = await RoomUtils.GetRoom(roomId); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); return new Success(true, null, null); } [HttpPut("rooms/{roomId}/image"), Authorize] public async Task ModifyImage(long roomId, string imageName) { var userId = User.GetUserId(); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return Success.Error("Room not found"); room.ImageName = imageName; await db.SaveChangesAsync(); var fullRoom = await RoomUtils.GetRoom(roomId); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); return new Success(true, null, null); } private static readonly RoleType[] AssignableRoles = { RoleType.None, RoleType.Host, RoleType.Moderator, RoleType.CoOwner, RoleType.TemporaryCoOwner }; private async Task<(RoleType? Role, long? ActorId)> ReadRoleRequestAsync() { var userId = User.GetUserId(); if (userId == null) return (null, null); foreach (var key in new[] { "role", "Role", "roletype", "RoleType", "roleType", "invitedrole", "InvitedRole" }) { var raw = Request.HasFormContentType && Request.Form.TryGetValue(key, out var form) ? form.ToString() : Request.Query.TryGetValue(key, out var query) ? query.ToString() : null; if (raw == null && Request.RouteValues.TryGetValue(key, out var route)) raw = route?.ToString(); if (!int.TryParse(raw, out var parsed)) continue; if (!Enum.IsDefined(typeof(RoleType), parsed)) continue; return ((RoleType)parsed, userId); } return (null, userId); } private async Task PushRoomRoleUpdate(long roomId, long actorId, long targetId, FullRoom? room) { room ??= await RoomUtils.GetRoom(roomId); if (room == null) return; foreach (var accountId in new[] { targetId, actorId }.Distinct()) await Notify.SendNotify(accountId, new Notification { Id = "RoomUpdate", Msg = room }); } [HttpPut("rooms/{roomId}/roles/{targetId}/invite"), Authorize] public async Task InvitePlayerToRole(long roomId, long targetId) { var (role, userId) = await ReadRoleRequestAsync(); if (userId == null) return Success.Error("Invalid auth"); if (targetId == userId.Value) return Success.Error("You cannot invite yourself"); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return Success.Error("Room not found"); var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == targetId); if (player == null) return Success.Error("Player not found"); var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == targetId && x.RoomId == roomId); if (role == null) return Success.Error("You must provide a valid role"); if (!AssignableRoles.Contains(role.Value)) return Success.Error($"Unknown role {role.Value}"); if (role.Value == RoleType.None) return Success.Error("You must provide a valid role"); if (existingRole?.Role == role.Value && existingRole.InvitedRole == RoleType.None) return Success.Error("Player already has this role"); if (existingRole != null && existingRole.Role != RoleType.None && existingRole.Role != RoleType.Banned) return Success.Error("Player already has a room role"); if (existingRole == null) { var newRole = new RoomRole { AccountId = targetId, InvitedRole = role.Value, Role = RoleType.None, RoomId = roomId, LastChangedByAccountId = userId.Value }; await db.RoomRoles.AddAsync(newRole); } else { existingRole.InvitedRole = role.Value; existingRole.LastChangedByAccountId = userId.Value; } await db.SaveChangesAsync(); if (role.Value == RoleType.CoOwner) { await Notify.SendMessage(targetId, new Message { FromPlayerId = userId.Value, RoomId = roomId, Type = MessageType.RoomCoOwnerInvited }); } var fullRoom = await RoomUtils.GetRoom(roomId); await PushRoomRoleUpdate(roomId, userId.Value, targetId, fullRoom); return new Success(true, null, fullRoom); } [HttpPut("rooms/{roomId}/roles/{targetId}"), Authorize] public async Task SetPlayerRole(long roomId, long targetId) { var (role, userId) = await ReadRoleRequestAsync(); if (userId == null) return Success.Error("Invalid auth"); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return Success.Error("Room not found"); if (targetId == room.CreatorAccountId) return Success.Error("The room's creator cannot be given a different role"); var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == targetId); if (player == null) return Success.Error("Player not found"); var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == targetId && x.RoomId == roomId); var wasCoOwner = existingRole?.Role == RoleType.CoOwner; var acceptingInvite = targetId == userId.Value && existingRole is { Role: RoleType.None, InvitedRole: not RoleType.None }; if (acceptingInvite && role == null) role = existingRole!.InvitedRole; if (role == null) return Success.Error("You must provide a valid role"); if (!AssignableRoles.Contains(role.Value)) return Success.Error($"Unknown role {role.Value}"); if (acceptingInvite) { if (role.Value != existingRole!.InvitedRole) return Success.Error("The requested role does not match the pending invitation"); } else { var canModify = await MatchmakingUtils.IsAtLeastRole(userId.Value, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); } if (role.Value == RoleType.None) { if (existingRole == null) return Success.Error("Player already has this role"); if (acceptingInvite) return Success.Error("The pending invitation cannot be accepted as None"); db.RoomRoles.Remove(existingRole); } else if (existingRole == null) { await db.RoomRoles.AddAsync(new RoomRole { AccountId = targetId, InvitedRole = RoleType.None, Role = role.Value, RoomId = roomId, LastChangedByAccountId = userId.Value }); } else { if (existingRole.Role == role.Value && existingRole.InvitedRole == RoleType.None) return Success.Error("Player already has this role"); existingRole.Role = role.Value; existingRole.InvitedRole = RoleType.None; existingRole.LastChangedByAccountId = (long)userId!; } await db.SaveChangesAsync(); if (wasCoOwner && role.Value < RoleType.CoOwner) { await Notify.SendMessage(targetId, new Message { FromPlayerId = userId.Value, RoomId = roomId, Type = MessageType.RoomCoOwnerRemoved }); } if (role.Value == RoleType.CoOwner && !wasCoOwner) { await Notify.SendMessage(targetId, new Message { FromPlayerId = userId.Value, RoomId = roomId, Type = MessageType.RoomCoOwnerAdded }); } var fullRoom = await RoomUtils.GetRoom(roomId); await PushRoomRoleUpdate(roomId, userId.Value, targetId, fullRoom); return new Success(true, null, fullRoom); } [HttpPut("rooms/{roomId}/tags"), Authorize] public async Task ModifyTags(long roomId, string[]? tag, string[]? autoTag) { var userId = User.GetUserId(); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return Success.Error("Room not found"); foreach (var t in tag ?? Array.Empty()) { if (!t.TagAllowed()) return new Success(false, "Tag not allowed: " + t, null); } if (tag != null || autoTag != null) { List tagsToAdd = new(); if (tag != null) foreach (var t in tag) tagsToAdd.Add(new ItemTag { Tag = t, Type = TagType.General }); if (autoTag != null) foreach (var t in autoTag) tagsToAdd.Add(new ItemTag { Tag = t, Type = TagType.Auto }); room.Tags = StringTools.CreateTagString(tagsToAdd); await db.SaveChangesAsync(); var fullRoom = await RoomUtils.GetRoom(roomId); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); return new Success(true, null, null); } else { room.Tags = ""; await db.SaveChangesAsync(); var fullRoom = await RoomUtils.GetRoom(roomId); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); return new Success(true, null, null); } } [HttpPut("rooms/{roomId}/accessibility"), Authorize] public async Task ModifyAccessibility(long roomId, Accessibility accessibility) { var userId = User.GetUserId(); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return Success.Error("Room not found"); bool wasPublished = room.WasPublished; room.Accessibility = accessibility; if (!room.WasPublished && accessibility == Accessibility.Public) { room.WasPublished = true; room.PublishedAt = DateTime.Now; } await db.SaveChangesAsync(); var fullRoom = await RoomUtils.GetRoom(roomId); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); return new Success(true, null, null); } [HttpGet("rooms/{roomId}/subrooms/{subRoomId}/permissions"), Authorize] public async Task GetSubRoomPermissions(long roomId, long subRoomId) { if (User.GetUserId() == null) return Unauthorized(); var subRoom = await db.SubRooms .AsNoTracking() .FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); if (subRoom == null) return NotFound(); var stored = await db.SubRoomPermissions .AsNoTracking() .Where(x => x.subRoomId == subRoomId) .OrderBy(x => x.Id) .ToListAsync(); return Ok(stored .Select(x => new RoomPermissionRow(x.permission, x.role, true, x.type, x.value)) .ToList()); } [HttpPut("rooms/{roomId}/subrooms/{subRoomId}/permissions"), Authorize] public async Task SetSubRoomPermissions(long roomId, long subRoomId) { long? userId = User.GetUserId(); if (userId == null) return Unauthorized(); var subRoom = await db.SubRooms .AsNoTracking() .FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); if (subRoom == null) return NotFound(); if (!await MatchmakingUtils.IsAtLeastRole(userId.Value, roomId, RoleType.CoOwner)) return Forbid(); string? body; using (var reader = new StreamReader(Request.Body, Encoding.UTF8)) body = await reader.ReadToEndAsync(); var entries = ParseSubRoomPermissions(body); foreach (var entry in entries) { var existing = await db.SubRoomPermissions.FirstOrDefaultAsync(x => x.subRoomId == subRoomId && x.permission == entry.Permission && x.role == entry.Role); if (!entry.Override) { if (existing != null) db.SubRoomPermissions.Remove(existing); continue; } if (existing == null) { await db.SubRoomPermissions.AddAsync(new SubRoomPermission { subRoomId = subRoomId, permission = entry.Permission, role = entry.Role, type = entry.Type, value = entry.Value, }); } else { existing.type = entry.Type; existing.value = entry.Value; } } await db.SaveChangesAsync(); var logged = body ?? ""; if (logged.Length > 800) logged = logged[..800] + "..."; return Ok(new { Permissions = Array.Empty() }); } private static List ParseSubRoomPermissions(string? body) { var entries = new List(); if (string.IsNullOrWhiteSpace(body)) return entries; JsonDocument document; try { document = JsonDocument.Parse(body); } catch (JsonException) { return entries; } using (document) { if (document.RootElement.ValueKind != JsonValueKind.Array) return entries; foreach (var element in document.RootElement.EnumerateArray()) { if (element.ValueKind != JsonValueKind.Object) continue; var permission = ReadPermissionName(element); if (permission.Length == 0) continue; var role = ReadPermissionRole(element); if (role == null) continue; entries.Add(new RoomPermissionRow( permission, role.Value, ReadPermissionBool(element, "Override"), (int)(ReadPermissionLong(element, "Type") ?? 0), ReadPermissionValue(element, "Value"))); } } return entries; } private static bool TryGetPermissionField(JsonElement entry, string key, out JsonElement value) { foreach (var property in entry.EnumerateObject()) { if (string.Equals(property.Name, key, StringComparison.OrdinalIgnoreCase)) { value = property.Value; return true; } } value = default; return false; } private static string? ReadPermissionString(JsonElement entry, string key) { if (!TryGetPermissionField(entry, key, out var value)) return null; return value.ValueKind == JsonValueKind.String ? value.GetString() : value.ToString(); } private static long? ReadPermissionLong(JsonElement entry, string key) { if (!TryGetPermissionField(entry, key, out var value)) return null; if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var number)) return number; if (value.ValueKind == JsonValueKind.String && long.TryParse(value.GetString(), out var parsed)) return parsed; return null; } private static bool ReadPermissionBool(JsonElement entry, string key) { if (!TryGetPermissionField(entry, key, out var value)) return false; if (value.ValueKind == JsonValueKind.True) return true; if (value.ValueKind == JsonValueKind.False) return false; return string.Equals(value.ToString(), "true", StringComparison.OrdinalIgnoreCase); } private static string ReadPermissionValue(JsonElement entry, string key) { if (!TryGetPermissionField(entry, key, out var value)) return ""; if (value.ValueKind == JsonValueKind.String) return value.GetString() ?? ""; if (value.ValueKind == JsonValueKind.True) return "True"; if (value.ValueKind == JsonValueKind.False) return "False"; return value.ToString(); } private static string ReadPermissionName(JsonElement entry) { var raw = (ReadPermissionString(entry, "Permission") ?? ReadPermissionString(entry, "PermissionType") ?? ReadPermissionString(entry, "PermissionTypeOld") ?? "").Trim(); if (raw.Length == 0) return ""; if (long.TryParse(raw, out var number) && Enum.IsDefined(typeof(RolePermissions), (int)number)) return ((RolePermissions)(int)number).ToString(); return raw; } private static long? ReadPermissionRole(JsonElement entry) { var numeric = ReadPermissionLong(entry, "Role") ?? ReadPermissionLong(entry, "RoleId"); if (numeric != null) return numeric; var name = ReadPermissionString(entry, "Role")?.Trim(); if (!string.IsNullOrEmpty(name) && Enum.TryParse(name, true, out var parsed)) return (long)parsed; return null; } [HttpPut("rooms/{roomId}/subrooms/{subRoomId}/accessibility"), Authorize] public async Task ModifySubRoomAccessibility(long roomId, long subRoomId, Accessibility accessibility) { var userId = User.GetUserId(); var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner); if (!canModify) return Success.Error("Insufficient permissions"); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return Success.Error("Room not found"); var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId); if (subRoom == null) return new Success(false, "Subroom not found", null); subRoom.Accessibility = accessibility; await db.SaveChangesAsync(); var fullRoom = await RoomUtils.GetRoom(roomId); await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom }); return new Success(true, null, null); } } }