Files
Radicade/Server/Rooms/RoomServerAPI.cs
T
2026-10-16 23:33:13 -07:00

1861 lines
62 KiB
C#

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<ActionResult<long[]>> 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<Success> 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<Success> 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<RoomPlayerData> GetRoomPlayerData()
{
return Ok(new RoomPlayerData { data = "" });
}
[HttpGet("rooms/curated_playlists")]
public async Task<IActionResult> 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<List<RoomData>?> 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<RoomData>();
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<IActionResult> 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<RoomData>();
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<IActionResult> 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<RoomData>();
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<List<RoomDetail>?> 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 <List<RoomData>?> GetBulkRooms([FromQuery] string? name, [FromQuery] List<long>? id)
{
List<RoomData> rooms = new List<RoomData>();
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<IActionResult> 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<RoomSearchResult?> 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<IActionResult> 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<RoomDetail>() });
}
IQueryable<RoomData> 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<IActionResult> GetHotRoomsPaged(string? source)
{
var (query, skip, take) = ParseEmbeddedQuery(source, 24);
query = query.Trim();
IQueryable<RoomData> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<RoomData>()
: 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<IActionResult> 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<RoomSearchResult?> 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<IActionResult> 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<RoomData>();
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<IActionResult> 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<RoomData>();
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<RoomSearchResult?> 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<RoomSearchResult?> 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<PlayerInteraction?> 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<PlayerInteraction?> 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<PlayerInteraction?> 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<PlayerInteraction?> 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<IActionResult> 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<RoomData>();
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<PlayerInteraction?> 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<List<FullRoom>?> GetBaseRooms()
{
var roomData = await db.Rooms.Where(x => x.CloningAllowed && x.Accessibility != Accessibility.Private && x.CreatorAccountId == 1).ToListAsync();
var rooms = new List<FullRoom>();
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<Success?> 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<RoomPermissionRow> DefaultPermissions()
{
static RoomPermissionRow Row(string permission, long role, bool overridden) =>
new(permission, role, overridden, 0, "True");
return new List<RoomPermissionRow>
{
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<IActionResult> 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<string, object?>
{
["Permissions"] = permissions,
["PhotonAccessToken"] = "",
["RoomInstanceId"] = presence?.roomInstance
});
}
[HttpGet("rooms/{id}")]
public async Task<ActionResult<FullRoom>> 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<ActionResult<List<RestorableSubRoom>>> 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<IActionResult> 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<IActionResult> 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<SubRoomDataSave> 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<SubRoomDataSave>());
var userId = User.GetUserId();
if (userId == null) return (Unauthorized(), new List<SubRoomDataSave>());
if (!await CanReadSavesAsync((long)userId!, room)) return (StatusCode(StatusCodes.Status403Forbidden), new List<SubRoomDataSave>());
return (null, await RoomUtils.GetSaveHistory(db, roomId, subRoomId));
}
private async Task<bool> 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<SubRoomDataSave>(), TotalResults = 0, TotalCount = 0 };
private static List<SubRoomDataSave> PageSaves(List<SubRoomDataSave> 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<Dictionary<string, string>> ReadSaveValuesAsync()
{
var values = new Dictionary<string, string>(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<string, string> 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<string, string> 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<object?> 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<IActionResult> 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<Success?> 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<Success?> 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<Success?> 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<Success> 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<Success?> 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<Success?> 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<Success?> 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<Success?> 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<Success?> 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<string>())
{
if (!t.TagAllowed())
return new Success(false, "Tag not allowed: " + t, null);
}
if (tag != null || autoTag != null) {
List<ItemTag> 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<Success?> 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<IActionResult> 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<IActionResult> 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<object>() });
}
private static List<RoomPermissionRow> ParseSubRoomPermissions(string? body)
{
var entries = new List<RoomPermissionRow>();
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<RoleType>(name, true, out var parsed))
return (long)parsed;
return null;
}
[HttpPut("rooms/{roomId}/subrooms/{subRoomId}/accessibility"), Authorize]
public async Task<Success?> 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);
}
}
}