mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,1042 @@
|
||||
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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user