mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
1043 lines
35 KiB
C#
1043 lines
35 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Cors;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Data;
|
|
using System.IO;
|
|
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.Accounts;
|
|
using RadicadeServer.RecNet.Auth;
|
|
using RadicadeServer.RecNet.Enums;
|
|
using RadicadeServer.RecNet.Enums.Messages;
|
|
using RadicadeServer.RecNet.Enums.Moderation;
|
|
using RadicadeServer.RecNet.Enums.Rooms;
|
|
using RadicadeServer.RecNet.Notifications;
|
|
using RadicadeServer.RecNet.Progression;
|
|
using RadicadeServer.RecNet.Responses;
|
|
using RadicadeServer.RecNet.Rooms;
|
|
using RadicadeServer.Utils;
|
|
|
|
namespace RadicadeServer.Admin
|
|
{
|
|
[Route("admin/rooms")]
|
|
[EnableCors("AdminPanel")]
|
|
public class AdminRoomAPI(RecDBContext db) : Controller
|
|
{
|
|
private Task<IActionResult?> RejectInvalidKey(string key)
|
|
{
|
|
return Task.FromResult<IActionResult?>(key.IsAdminKey() ? null : Unauthorized("Invalid secret"));
|
|
}
|
|
|
|
[HttpGet("room/{roomId}")]
|
|
public async Task<IActionResult> GetRoom([FromQuery] string key, long roomId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await RoomUtils.GetRoom(roomId);
|
|
if (room == null) return NotFound();
|
|
|
|
return Ok(room);
|
|
}
|
|
|
|
[HttpPost("create/room")]
|
|
public async Task<IActionResult> CreateRoom([FromQuery] string key, [FromQuery] long newOwner, [FromBody] JsonNode fullRoom)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var roomData = new RoomData
|
|
{
|
|
EncryptVoiceChat = fullRoom.Get("EncryptVoiceChat", false),
|
|
Accessibility = fullRoom.Get("Accessibility", Accessibility.Private),
|
|
CloningAllowed = fullRoom.Get("CloningAllowed", false),
|
|
|
|
CreatedAt = fullRoom["CreatedAt"]?.GetValue<DateTime>() ?? DateTime.UtcNow,
|
|
|
|
CreatorAccountId = newOwner,
|
|
CustomWarning = fullRoom.Get("CustomWarning", ""),
|
|
Description = fullRoom.Get("Description", ""),
|
|
|
|
DisableMicAutoMute = fullRoom.Get("DisableMicAutoMute", false),
|
|
DisableRoomComments = fullRoom.Get("DisableRoomComments", false),
|
|
|
|
ImageName = fullRoom.Get("ImageName", ""),
|
|
|
|
IsDorm = fullRoom.Get("IsDorm", false),
|
|
IsRRO = fullRoom.Get("IsRRO", false),
|
|
|
|
IsDeveloperOwned = fullRoom["IsDeveloperOwned"] != null
|
|
? fullRoom.Get("IsDeveloperOwned", false)
|
|
: await db.IsDeveloper(newOwner),
|
|
|
|
loadScreen = fullRoom["LoadScreens"]?.ToJson() ?? "[]",
|
|
|
|
MaxPlayerCalculationMode = fullRoom.Get("MaxPlayerCalculationMode", MaxPlayerMode.AllSubrooms),
|
|
MaxPlayers = fullRoom.Get("MaxPlayers", 10),
|
|
MinLevel = fullRoom.Get("MinLevel", 0),
|
|
|
|
Name = fullRoom.Get("Name", ""),
|
|
State = fullRoom.Get("State", RoomState.Active),
|
|
|
|
SupportsJuniors = fullRoom.Get("SupportsJuniors", false),
|
|
SupportsLevelVoting = fullRoom.Get("SupportsLevelVoting", false),
|
|
SupportsMobile = fullRoom.Get("SupportsMobile", false),
|
|
SupportsQuest2 = fullRoom.Get("SupportsQuest2", false),
|
|
SupportsScreens = fullRoom.Get("SupportsScreens", false),
|
|
SupportsTeleportVR = fullRoom.Get("SupportsTeleportVR", false),
|
|
SupportsVRLow = fullRoom.Get("SupportsVRLow", false),
|
|
SupportsWalkVR = fullRoom.Get("SupportsWalkVR", false),
|
|
|
|
Tags = StringTools.CreateTagString(fullRoom.Get<List<ItemTag>>("Tags", [])),
|
|
|
|
Version = 1,
|
|
WarningMask = fullRoom.Get("WarningMask", WarningMask.None)
|
|
};
|
|
|
|
await db.Rooms.AddAsync(roomData);
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
foreach (var subRoom in fullRoom["SubRooms"]?.AsArray() ?? new JsonArray())
|
|
{
|
|
if (subRoom == null) continue;
|
|
var subRoomSceneId = subRoom["UnitySceneId"]?.ToString() ?? SceneLocations.MakerRoom;
|
|
if (!SceneLocations.IsValid(subRoomSceneId))
|
|
subRoomSceneId = SceneLocations.MakerRoom;
|
|
|
|
// A new room's subroom can arrive with its Unity bundle already uploaded,
|
|
// named exactly as it was imported (see UploadRoom).
|
|
var bundleName = subRoom["UnityAssetId"]?.ToString();
|
|
if (!string.IsNullOrWhiteSpace(bundleName))
|
|
{
|
|
if (!AdminUtils.TryResolveBundleName(bundleName, out var resolvedBundle, out var bundleError))
|
|
return BadRequest(bundleError);
|
|
bundleName = resolvedBundle;
|
|
}
|
|
|
|
await db.AddAsync(new SubRoom
|
|
{
|
|
Accessibility = (Accessibility)(subRoom["Accessibility"]?.GetValue<int>() ?? 0),
|
|
CreatedAt = subRoom["CreatedAt"]?.GetValue<DateTime>() ?? DateTime.UtcNow,
|
|
SavedByAccountId = newOwner,
|
|
IsSandbox = subRoom["IsSandbox"]?.GetValue<bool>() ?? false,
|
|
MaxPlayers = subRoom["MaxPlayers"]?.GetValue<int>() ?? 0,
|
|
Name = subRoom["Name"]?.ToString() ?? "",
|
|
RoomId = roomData.RoomId,
|
|
UnitySceneId = subRoomSceneId,
|
|
DataBlob = subRoom["DataBlob"]?.ToString() ?? "",
|
|
UnityAssetId = bundleName ?? "",
|
|
});
|
|
}
|
|
|
|
await db.RoomRoles.AddAsync(new RoomRole
|
|
{
|
|
AccountId = newOwner,
|
|
InvitedRole = RoleType.None,
|
|
Role = RoleType.Creator,
|
|
RoomId = roomData.RoomId
|
|
});
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Created as {roomData.RoomId}!");
|
|
}
|
|
|
|
[HttpGet("rename/room")]
|
|
public async Task<IActionResult> RenameRoom([FromQuery] string key, [FromQuery] long roomId, [FromQuery] string newName)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
room.Name = newName;
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Changed to ^{newName}!");
|
|
}
|
|
|
|
[HttpGet("delete/room")]
|
|
public async Task<IActionResult> DeleteRoom([FromQuery] string key, [FromQuery] long roomId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
if (room.IsRRO || room.IsDorm || room.CreatorAccountId == 1) return Ok("You cannot delete RROs.");
|
|
|
|
db.Rooms.Remove(room);
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Deleted.");
|
|
}
|
|
|
|
[HttpGet("set/datablob")]
|
|
public async Task<IActionResult> SetDatablob([FromQuery] string key, [FromQuery] long roomId, [FromQuery] string subRoomName, [FromQuery] string datablobName)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId && x.Name == subRoomName);
|
|
if (subRoom == null) return NotFound("Subroom not found.");
|
|
|
|
subRoom.DataBlob = datablobName;
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Set datablob to {datablobName} for {subRoom.Name}!");
|
|
}
|
|
|
|
// The other half of importing a subroom: its .assetbundle. The file name is
|
|
// the asset id - RoomServerAPI serves /rooms/unity_assets/<name> out of the CDN
|
|
// by that name - so binding means pointing the subroom at an uploaded bundle,
|
|
// and an empty name clears it again. BindAssetBundle below is the narrower,
|
|
// Studio-side counterpart for 40-character manifest ids; this one takes the
|
|
// '<name>.assetbundle' files an import actually produces.
|
|
[HttpGet("set/bundle")]
|
|
public async Task<IActionResult> SetBundle([FromQuery] string key, [FromQuery] long roomId, [FromQuery] string subRoomName, [FromQuery] string bundleName)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId && x.Name == subRoomName);
|
|
if (subRoom == null) return NotFound("Subroom not found.");
|
|
|
|
if (string.IsNullOrWhiteSpace(bundleName))
|
|
{
|
|
subRoom.UnityAssetId = "";
|
|
await db.SaveChangesAsync();
|
|
return Ok($"Cleared the Unity bundle for {subRoom.Name}!");
|
|
}
|
|
|
|
if (!AdminUtils.TryResolveBundleName(bundleName, out var resolvedBundle, out var bundleError))
|
|
return BadRequest(bundleError);
|
|
|
|
subRoom.UnityAssetId = resolvedBundle ?? "";
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Set bundle to {subRoom.UnityAssetId} for {subRoom.Name}!");
|
|
}
|
|
|
|
[HttpPost("create/subroom")]
|
|
public async Task<IActionResult> CreateSubRoom([FromQuery] string key, [FromQuery] long roomId, [FromBody] JsonNode subRoom)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
var name = (subRoom.Get("Name", "") ?? "").Trim();
|
|
if (string.IsNullOrWhiteSpace(name)) return BadRequest("Subroom name is required.");
|
|
|
|
if (await db.SubRooms.AnyAsync(x => x.RoomId == roomId && x.Name == name))
|
|
return BadRequest($"A subroom named \"{name}\" already exists in this room.");
|
|
|
|
// The Unity bundle the subroom loads, if one was imported alongside the save.
|
|
var bundleName = subRoom.Get("UnityAssetId", "") ?? "";
|
|
if (!string.IsNullOrWhiteSpace(bundleName))
|
|
{
|
|
if (!AdminUtils.TryResolveBundleName(bundleName, out var resolvedBundle, out var bundleError))
|
|
return BadRequest(bundleError);
|
|
bundleName = resolvedBundle ?? "";
|
|
}
|
|
|
|
var mainSubRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
|
|
var maxPlayers = subRoom.Get("MaxPlayers", mainSubRoom?.MaxPlayers ?? 10);
|
|
if (maxPlayers <= 0) maxPlayers = mainSubRoom?.MaxPlayers ?? 10;
|
|
|
|
var unitySceneId = subRoom.Get("UnitySceneId", "");
|
|
if (string.IsNullOrWhiteSpace(unitySceneId))
|
|
unitySceneId = mainSubRoom?.UnitySceneId ?? "a75f7547-79eb-47c6-8986-6767abcb4f92";
|
|
|
|
var dbSubRoom = new SubRoom
|
|
{
|
|
Accessibility = (Accessibility)(subRoom["Accessibility"]?.GetValue<int>() ?? (int)Accessibility.Public),
|
|
CreatedAt = subRoom.Get("CreatedAt", DateTime.UtcNow),
|
|
SavedByAccountId = room.CreatorAccountId,
|
|
IsSandbox = subRoom.Get("IsSandbox", true),
|
|
MaxPlayers = maxPlayers,
|
|
Name = name,
|
|
RoomId = roomId,
|
|
UnitySceneId = unitySceneId,
|
|
DataBlob = subRoom.Get("DataBlob", ""),
|
|
UnityAssetId = bundleName,
|
|
};
|
|
|
|
await db.AddAsync(dbSubRoom);
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Created as {dbSubRoom.SubRoomId}!");
|
|
}
|
|
|
|
[HttpGet("rename/subroom")]
|
|
public async Task<IActionResult> RenameSubRoom([FromQuery] string key, [FromQuery] long roomId, [FromQuery] long subRoomId, [FromQuery] string newName)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId && x.RoomId == roomId);
|
|
if (subRoom == null) return NotFound("Subroom not found.");
|
|
|
|
newName = (newName ?? "").Trim();
|
|
if (string.IsNullOrWhiteSpace(newName)) return BadRequest("Subroom name is required.");
|
|
|
|
if (await db.SubRooms.AnyAsync(x => x.RoomId == roomId && x.SubRoomId != subRoomId && x.Name == newName))
|
|
return BadRequest($"A subroom named \"{newName}\" already exists in this room.");
|
|
|
|
subRoom.Name = newName;
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Renamed to {newName}!");
|
|
}
|
|
|
|
[HttpGet("delete/subroom")]
|
|
public async Task<IActionResult> DeleteSubRoom([FromQuery] string key, [FromQuery] long roomId, [FromQuery] long subRoomId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId && x.RoomId == roomId);
|
|
if (subRoom == null) return NotFound("Subroom not found.");
|
|
|
|
|
|
var subroomCount = await db.SubRooms.CountAsync(x => x.RoomId == roomId);
|
|
if (subroomCount <= 1) return BadRequest("A room must keep at least one subroom.");
|
|
|
|
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();
|
|
|
|
return Ok($"Deleted {subRoom.Name}!");
|
|
}
|
|
|
|
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/assetbundle")]
|
|
public async Task<IActionResult> BindAssetBundle([FromQuery] string key, long roomId, long subRoomId,
|
|
[FromBody] string assetId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
if (!ModelState.IsValid || !RadicadeServer.API.Studio.StudioUnityAssetRegistry.IsValidAssetId(assetId))
|
|
return BadRequest("Expected a 40-character Unity asset ID.");
|
|
|
|
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
|
if (subRoom == null) return NotFound("Subroom not found in this room.");
|
|
|
|
|
|
|
|
if (!System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, assetId + ".assetbundle")))
|
|
return BadRequest("Upload the bundle as <assetId>.assetbundle before linking it.");
|
|
if (System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, assetId)))
|
|
return BadRequest("This ID belongs to a Studio manifest; choose an ID for the archived bundle.");
|
|
|
|
var previousAssetId = subRoom.UnityAssetId;
|
|
subRoom.UnityAssetId = assetId;
|
|
await db.SaveChangesAsync();
|
|
return Ok(new { RoomId = roomId, SubRoomId = subRoomId, PreviousUnityAssetId = previousAssetId, UnityAssetId = assetId });
|
|
}
|
|
|
|
[HttpPost("upload/datablob")]
|
|
[RequestSizeLimit(256L * 1024 * 1024)]
|
|
public async Task<IActionResult> UploadDatablob([FromQuery] string key, [FromQuery] string name, [FromForm] IFormFile file)
|
|
{
|
|
|
|
if (!ModelState.IsValid) return BadRequest("Invalid upload request. Use a multipart form no larger than 256 MiB.");
|
|
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var path = Path.Join(DBConfig.CDNStorage, name);
|
|
|
|
using (var stream = new FileStream(path, FileMode.Create))
|
|
{
|
|
await file.CopyToAsync(stream);
|
|
}
|
|
|
|
return Ok($"Uploaded {name}!\nURL: {DBConfig.ServerUrl}/cdn/room/{name}");
|
|
}
|
|
|
|
[HttpPost("upload/image")]
|
|
public async Task<IActionResult> UploadImage([FromQuery] string key, [FromQuery] long roomId, [FromForm] IFormFile file)
|
|
{
|
|
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
if (file == null || file.Length == 0) return BadRequest("No file provided.");
|
|
|
|
|
|
var allowed = new[] { ".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp" };
|
|
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
|
|
if (string.IsNullOrEmpty(ext) || !allowed.Contains(ext))
|
|
return BadRequest($"Unsupported image format '{(string.IsNullOrEmpty(ext) ? "no extension" : ext)}'. Use png/jpg/jpeg/gif/webp.");
|
|
|
|
string guid = Guid.NewGuid().ToString();
|
|
var path = Path.Join(DBConfig.CDNImages, guid + ext);
|
|
|
|
using (var stream = new FileStream(path, FileMode.Create))
|
|
{
|
|
await file.CopyToAsync(stream);
|
|
}
|
|
|
|
room.ImageName = guid + ext;
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Uploaded {guid}{ext}!\nURL: {DBConfig.ServerUrl}/images/img/{guid}{ext}");
|
|
}
|
|
|
|
[HttpGet("set/image")]
|
|
public async Task<IActionResult> SetImage([FromQuery] string key, [FromQuery] long roomId, [FromQuery] string imgName)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
|
|
imgName = Path.GetFileName(imgName ?? "").Trim();
|
|
|
|
|
|
room.ImageName = imgName;
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Set to {imgName}!\nURL: {DBConfig.ServerUrl}/images/img/{imgName}");
|
|
}
|
|
|
|
[HttpGet("set/role/{roleType}")]
|
|
public async Task<IActionResult> SetRoleOfUser([FromQuery] string key, [FromQuery] long roomId, [FromQuery] long target, RoleType roleType)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == target && x.RoomId == roomId);
|
|
|
|
if (existingRole == null)
|
|
{
|
|
var newRole = new RoomRole
|
|
{
|
|
AccountId = target,
|
|
InvitedRole = RoleType.None,
|
|
Role = roleType,
|
|
RoomId = roomId
|
|
};
|
|
|
|
await db.RoomRoles.AddAsync(newRole);
|
|
}
|
|
else
|
|
{
|
|
if (existingRole.Role == roleType)
|
|
return Ok($"Player already has {Enum.GetName(roleType)}.");
|
|
|
|
existingRole.Role = roleType;
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
try
|
|
{
|
|
var updatedRoom = await RoomUtils.GetRoom(roomId);
|
|
if (updatedRoom != null)
|
|
await Notify.SendNotify(target, new Notification { Id = "RoomUpdate", Msg = updatedRoom });
|
|
}
|
|
catch (Exception) { }
|
|
|
|
return Ok($"Set to {Enum.GetName(roleType)}!");
|
|
}
|
|
|
|
[HttpGet("set/rro")]
|
|
public async Task<IActionResult> SetRRO([FromQuery] string key, [FromQuery] long roomId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
room.IsRRO = true;
|
|
var tags = StringTools.ReadTags(room.Tags);
|
|
if (tags.FirstOrDefault(x => x.Tag == "rro" && x.Type == TagType.Auto) == null)
|
|
tags.Add(new ItemTag { Type = TagType.Auto, Tag = "rro" });
|
|
room.Tags = StringTools.CreateTagString(tags);
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Set to RRO {room.Name}!");
|
|
}
|
|
|
|
[HttpGet("remove/rro")]
|
|
public async Task<IActionResult> RemoveRRO([FromQuery] string key, [FromQuery] long roomId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
room.IsRRO = false;
|
|
var tags = StringTools.ReadTags(room.Tags);
|
|
var existingRROTag = tags.FirstOrDefault(x => x.Tag == "rro" && x.Type == TagType.Auto);
|
|
if (existingRROTag != null)
|
|
tags.Remove(existingRROTag);
|
|
room.Tags = StringTools.CreateTagString(tags);
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Removed RRO from {room.Name}!");
|
|
}
|
|
|
|
[HttpGet("fulldelete/room")]
|
|
public async Task<IActionResult> FullyDeleteRoom([FromQuery] string key, [FromQuery] long roomId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
if (room.IsRRO || room.IsDorm || room.CreatorAccountId == 1) return Ok("You cannot delete RROs.");
|
|
|
|
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 k in keys)
|
|
{
|
|
db.RoomKeys.Remove(k);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok("Deleted.");
|
|
}
|
|
|
|
[HttpGet("set/owner")]
|
|
public async Task<IActionResult> SetRoomOwner([FromQuery] string key, [FromQuery] long roomId, [FromQuery] long target)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
long oldOwnerId = room.CreatorAccountId;
|
|
room.CreatorAccountId = target;
|
|
|
|
var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == target && x.RoomId == roomId);
|
|
|
|
if (existingRole == null)
|
|
{
|
|
var newRole = new RoomRole
|
|
{
|
|
AccountId = target,
|
|
InvitedRole = RoleType.None,
|
|
Role = RoleType.Creator,
|
|
RoomId = roomId
|
|
};
|
|
|
|
await db.RoomRoles.AddAsync(newRole);
|
|
}
|
|
else
|
|
{
|
|
existingRole.Role = RoleType.Creator;
|
|
}
|
|
|
|
|
|
|
|
if (oldOwnerId != target)
|
|
{
|
|
var oldOwnerRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == oldOwnerId && x.RoomId == roomId);
|
|
if (oldOwnerRole != null && oldOwnerRole.Role == RoleType.Creator)
|
|
oldOwnerRole.Role = RoleType.CoOwner;
|
|
}
|
|
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Set to owner!");
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("instances/live")]
|
|
public async Task<IActionResult> GetLiveRoomInstances([FromQuery] string key)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var instances = await db.RoomInstances.ToListAsync();
|
|
var roomIds = instances.Select(i => i.roomId).Distinct().ToList();
|
|
var rooms = roomIds.Count > 0
|
|
? await db.Rooms.Where(r => roomIds.Contains(r.RoomId)).ToDictionaryAsync(r => r.RoomId)
|
|
: new Dictionary<long, RoomData>();
|
|
|
|
|
|
var presences = await db.Presences.Where(p => p.isOnline && p.roomInstance != null).ToListAsync();
|
|
var profileIds = presences.Select(p => p.playerId).Distinct().ToList();
|
|
var profiles = profileIds.Count > 0
|
|
? await db.Profiles.Where(x => profileIds.Contains(x.accountId)).ToDictionaryAsync(x => x.accountId)
|
|
: new Dictionary<long, RadicadeServer.RecNet.Auth.Profile>();
|
|
|
|
var result = instances.Select(instance =>
|
|
{
|
|
var players = presences
|
|
.Where(p => p.roomInstance == instance.roomInstanceId)
|
|
.Select(p => new
|
|
{
|
|
player_id = p.playerId,
|
|
name = profiles.TryGetValue(p.playerId, out var profile) ? profile.displayName : $"Player {p.playerId}"
|
|
})
|
|
.OrderBy(p => p.player_id)
|
|
.ToList();
|
|
|
|
return new
|
|
{
|
|
instance_id = instance.roomInstanceId,
|
|
room_id = instance.roomId,
|
|
room_name = rooms.TryGetValue(instance.roomId, out var room) ? room.Name : "Unknown Room",
|
|
sub_room_id = instance.subRoomId,
|
|
event_id = instance.eventId,
|
|
instance_type = instance.roomInstanceType.ToString(),
|
|
location = instance.location ?? "",
|
|
room_code = instance.roomCode ?? "",
|
|
photon_room_id = instance.photonRoomId,
|
|
is_private = instance.isPrivate,
|
|
is_in_progress = instance.isInProgress,
|
|
is_full = instance.isFull,
|
|
max_capacity = instance.maxCapacity,
|
|
player_count = players.Count,
|
|
players
|
|
};
|
|
})
|
|
.OrderByDescending(i => i.player_count)
|
|
.ToList();
|
|
|
|
return Ok(new { count = result.Count, instances = result });
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("instances/{instanceId:long}")]
|
|
public async Task<IActionResult> GetLiveInstanceDetail(long instanceId, [FromQuery] string key)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var instance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId);
|
|
if (instance == null) return NotFound("Instance not found.");
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == instance.roomId);
|
|
var subrooms = await db.SubRooms.Where(s => s.RoomId == instance.roomId).ToListAsync();
|
|
|
|
var presences = await db.Presences.Where(p => p.isOnline && p.roomInstance == instanceId).ToListAsync();
|
|
var profileIds = presences.Select(p => p.playerId).Distinct().ToList();
|
|
var profiles = profileIds.Count > 0
|
|
? await db.Profiles.Where(x => profileIds.Contains(x.accountId)).ToDictionaryAsync(x => x.accountId)
|
|
: new Dictionary<long, Profile>();
|
|
var playerData = profileIds.Count > 0
|
|
? await db.Players.Where(x => profileIds.Contains(x.AccountId)).ToDictionaryAsync(x => x.AccountId)
|
|
: new Dictionary<long, PlayerData>();
|
|
var progression = profileIds.Count > 0
|
|
? await db.Progression.Where(x => profileIds.Contains(x.PlayerId)).ToDictionaryAsync(x => x.PlayerId)
|
|
: new Dictionary<long, ProgressionData>();
|
|
|
|
var playerRows = presences.Select(p =>
|
|
{
|
|
profiles.TryGetValue(p.playerId, out var profile);
|
|
playerData.TryGetValue(p.playerId, out var player);
|
|
progression.TryGetValue(p.playerId, out var progress);
|
|
return new
|
|
{
|
|
player_id = p.playerId,
|
|
name = profile?.displayName ?? $"Player {p.playerId}",
|
|
username = profile?.username ?? "",
|
|
level = progress?.Level ?? 0,
|
|
device_class = p.deviceClass.ToString(),
|
|
is_online = p.isOnline
|
|
};
|
|
}).OrderBy(p => p.player_id).ToList();
|
|
|
|
return Ok(new
|
|
{
|
|
instance = new
|
|
{
|
|
instance_id = instance.roomInstanceId,
|
|
room_id = instance.roomId,
|
|
room_name = room?.Name ?? "Unknown Room",
|
|
sub_room_id = instance.subRoomId,
|
|
instance_type = instance.roomInstanceType.ToString(),
|
|
location = instance.location ?? "",
|
|
room_code = instance.roomCode ?? "",
|
|
is_private = instance.isPrivate,
|
|
is_in_progress = instance.isInProgress,
|
|
is_full = instance.isFull,
|
|
max_capacity = instance.maxCapacity,
|
|
player_count = playerRows.Count
|
|
},
|
|
players = playerRows,
|
|
subrooms = subrooms.Select(s => new
|
|
{
|
|
sub_room_id = s.SubRoomId,
|
|
name = s.Name,
|
|
max_players = s.MaxPlayers
|
|
}).ToList()
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPost("instances/{instanceId:long}/kick")]
|
|
public async Task<IActionResult> KickFromInstance(long instanceId, [FromQuery] string key, [FromQuery] long playerId, [FromQuery] string? reason = null)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var instance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId);
|
|
if (instance == null) return NotFound("Instance not found.");
|
|
|
|
var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == playerId);
|
|
if (presence == null || presence.roomInstance != instanceId) return NotFound("Player is not in this instance.");
|
|
|
|
|
|
var dorm = await RoomUtils.GetOrCreateDorm(playerId);
|
|
RoomInstance? dormInstance = null;
|
|
if (dorm != null)
|
|
dormInstance = await MatchmakingUtils.CreateRoomInstance(dorm.RoomId, true, null, null, null);
|
|
if (dormInstance == null) return StatusCode(500, "Could not resolve a dorm instance for this player.");
|
|
|
|
presence.roomInstance = dormInstance.roomInstanceId;
|
|
presence.errorCode = JoinStatus.Success;
|
|
presence.isOnline = true;
|
|
MatchmakingUtils.MarkInRoom(playerId);
|
|
|
|
await RecomputeInstanceCapacity(instance);
|
|
await db.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
try
|
|
{
|
|
await Notify.SendNotify(playerId, new Notification
|
|
{
|
|
Id = NotificationType.ModerationKick,
|
|
Msg = PlayerUtils.BuildBlockDetails(false, ReportCategory.Misc, reason ?? "Kicked from room", null, isHostKick: true)
|
|
});
|
|
}
|
|
catch (Exception) { }
|
|
|
|
try
|
|
{
|
|
await Notify.SendNotify(playerId, new Notification
|
|
{
|
|
Id = "PresenceUpdate",
|
|
Msg = await Presence.CreateAsync(db, presence, dormInstance)
|
|
});
|
|
}
|
|
catch (Exception) { }
|
|
|
|
var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId);
|
|
return Ok(new
|
|
{
|
|
player_id = playerId,
|
|
name = profile?.displayName ?? $"Player {playerId}",
|
|
kicked = true,
|
|
instance_id = instanceId
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPost("instances/{instanceId:long}/move")]
|
|
public async Task<IActionResult> MovePlayerToSubRoom(long instanceId, [FromQuery] string key, [FromQuery] long playerId, [FromQuery] long toSubRoomId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var instance = await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == instanceId);
|
|
if (instance == null) return NotFound("Instance not found.");
|
|
|
|
var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == playerId);
|
|
if (presence == null || presence.roomInstance != instanceId) return NotFound("Player is not in this instance.");
|
|
|
|
|
|
|
|
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == instance.roomId && x.SubRoomId == toSubRoomId);
|
|
if (subRoom == null) return BadRequest("Target subroom does not belong to this room.");
|
|
if (instance.subRoomId == toSubRoomId)
|
|
return Ok(new { player_id = playerId, already_in_subroom = true, instance_id = instanceId });
|
|
|
|
|
|
|
|
var targetInstance = await db.RoomInstances
|
|
.FirstOrDefaultAsync(x => x.roomId == instance.roomId && x.subRoomId == toSubRoomId && !x.isFull);
|
|
bool created = false;
|
|
if (targetInstance == null)
|
|
{
|
|
targetInstance = await MatchmakingUtils.CreateRoomInstance(instance.roomId, instance.isPrivate, toSubRoomId, instance.clubId, instance.eventId);
|
|
if (targetInstance == null) return StatusCode(500, "Could not create the target subroom instance.");
|
|
|
|
|
|
db.RoomInstances.Attach(targetInstance);
|
|
created = true;
|
|
}
|
|
|
|
presence.roomInstance = targetInstance.roomInstanceId;
|
|
presence.errorCode = JoinStatus.Success;
|
|
presence.isOnline = true;
|
|
MatchmakingUtils.MarkInRoom(playerId);
|
|
|
|
await RecomputeInstanceCapacity(instance);
|
|
await RecomputeInstanceCapacity(targetInstance);
|
|
await db.SaveChangesAsync();
|
|
|
|
try
|
|
{
|
|
await Notify.SendNotify(playerId, new Notification
|
|
{
|
|
Id = "PresenceUpdate",
|
|
Msg = await Presence.CreateAsync(db, presence, targetInstance)
|
|
});
|
|
}
|
|
catch (Exception) { }
|
|
|
|
var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId);
|
|
return Ok(new
|
|
{
|
|
player_id = playerId,
|
|
name = profile?.displayName ?? $"Player {playerId}",
|
|
instance_id = targetInstance.roomInstanceId,
|
|
sub_room_id = toSubRoomId,
|
|
created
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RecomputeInstanceCapacity(RoomInstance instance)
|
|
{
|
|
var count = await MatchmakingUtils.GetPlayerCountInInstance(instance.roomInstanceId);
|
|
instance.isFull = count >= instance.maxCapacity;
|
|
}
|
|
|
|
[HttpGet("roles")]
|
|
public async Task<IActionResult> GetRoomRoles([FromQuery] string key, [FromQuery] long roomId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
var roles = await db.RoomRoles.Where(x => x.RoomId == roomId).ToListAsync();
|
|
var accountIds = roles.Select(r => r.AccountId).Distinct().ToList();
|
|
var profiles = accountIds.Count > 0
|
|
? await db.Profiles.Where(x => accountIds.Contains(x.accountId)).ToDictionaryAsync(x => x.accountId)
|
|
: new Dictionary<long, RadicadeServer.RecNet.Auth.Profile>();
|
|
|
|
string? NameOf(long id) => profiles.TryGetValue(id, out var p)
|
|
? $"{p.displayName} (@{p.username})"
|
|
: id.ToString();
|
|
|
|
var result = roles.Select(r => new
|
|
{
|
|
account_id = r.AccountId,
|
|
name = NameOf(r.AccountId),
|
|
role = r.Role.ToString(),
|
|
role_value = (int)r.Role,
|
|
invited_role = r.InvitedRole.ToString(),
|
|
is_creator = r.AccountId == room.CreatorAccountId || r.Role == RoleType.Creator
|
|
}).ToList();
|
|
|
|
|
|
if (!result.Any(x => x.account_id == room.CreatorAccountId))
|
|
{
|
|
result.Insert(0, new
|
|
{
|
|
account_id = room.CreatorAccountId,
|
|
name = NameOf(room.CreatorAccountId),
|
|
role = "Creator",
|
|
role_value = (int)RoleType.Creator,
|
|
invited_role = "None",
|
|
is_creator = true
|
|
});
|
|
}
|
|
|
|
return Ok(new { room_id = roomId, creator_account_id = room.CreatorAccountId, roles = result });
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("room/{roomId}/bans")]
|
|
public async Task<IActionResult> GetRoomBans([FromQuery] string key, long roomId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
|
|
var bans = await db.RoomBans
|
|
.Where(x => x.roomId == roomId && (!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow))
|
|
.OrderByDescending(x => x.bannedAt)
|
|
.ToListAsync();
|
|
|
|
var ids = bans.Select(b => b.bannedPlayerId).Concat(bans.Select(b => b.actorId)).Distinct().ToList();
|
|
var profiles = ids.Count > 0
|
|
? await db.Profiles.AsNoTracking().Where(x => ids.Contains(x.accountId)).ToDictionaryAsync(x => x.accountId)
|
|
: new Dictionary<long, Profile>();
|
|
|
|
string? NameOf(long id) => profiles.TryGetValue(id, out var p) ? p.displayName : null;
|
|
|
|
var result = bans.Select(b => new
|
|
{
|
|
ban_id = b.BanId,
|
|
room_id = b.roomId,
|
|
player_id = b.bannedPlayerId,
|
|
player_name = NameOf(b.bannedPlayerId),
|
|
banned_by_id = b.actorId,
|
|
banned_by_name = string.IsNullOrWhiteSpace(b.bannedByName) ? (NameOf(b.actorId) ?? $"Player {b.actorId}") : b.bannedByName,
|
|
reason = b.reason,
|
|
banned_at = b.bannedAt,
|
|
expires_at = b.expiresAt
|
|
}).ToList();
|
|
|
|
return Ok(new { room_id = roomId, room_name = room.Name, count = result.Count, bans = result });
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPost("room/{roomId}/bans")]
|
|
public async Task<IActionResult> BanPlayerFromRoom([FromQuery] string key, long roomId, [FromBody] JsonNode body)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
long playerId = body["player_id"]?.GetValue<long>() ?? 0;
|
|
if (playerId <= 0) return BadRequest("player_id is required.");
|
|
|
|
var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == playerId);
|
|
if (profile == null) return NotFound("Player does not exist.");
|
|
|
|
var existing = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == roomId && x.bannedPlayerId == playerId &&
|
|
(!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow));
|
|
if (existing != null) return Ok($"Player is already banned from this room.");
|
|
|
|
var reason = body["reason"]?.GetValue<string>()?.Trim() ?? "";
|
|
var ban = new RoomBan
|
|
{
|
|
roomId = roomId,
|
|
actorId = 1,
|
|
bannedPlayerId = playerId,
|
|
bannedByName = "Admin",
|
|
reason = reason,
|
|
bannedAt = DateTime.UtcNow,
|
|
expiresAt = null
|
|
};
|
|
|
|
await db.RoomBans.AddAsync(ban);
|
|
await db.SaveChangesAsync();
|
|
|
|
|
|
await MatchmakingUtils.KickPlayerFromRoom(playerId, roomId,
|
|
string.IsNullOrWhiteSpace(reason) ? "You were banned from this room." : $"You were banned from this room: {reason}");
|
|
|
|
return Ok(new
|
|
{
|
|
ban_id = ban.BanId,
|
|
room_id = roomId,
|
|
player_id = playerId,
|
|
player_name = profile.displayName,
|
|
reason = reason,
|
|
banned_at = ban.bannedAt
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpDelete("room/{roomId}/bans/{playerId}")]
|
|
public async Task<IActionResult> UnbanPlayerFromRoom([FromQuery] string key, long roomId, long playerId)
|
|
{
|
|
var rejection = await RejectInvalidKey(key);
|
|
if (rejection != null) return rejection;
|
|
|
|
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
|
if (room == null) return NotFound("Room does not exist.");
|
|
|
|
var ban = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == roomId && x.bannedPlayerId == playerId);
|
|
if (ban == null) return NotFound("Player is not banned from this room.");
|
|
|
|
db.RoomBans.Remove(ban);
|
|
await db.SaveChangesAsync();
|
|
|
|
return Ok($"Ban lifted for player {playerId}.");
|
|
}
|
|
}
|
|
}
|