mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
fix: answer 2023 room and playlist mutations with the success/value/error envelope
Every rooms-client mutation (roles, invite, accessibility/publish, room
fields, sub-room ops, permissions, playlists) goes through
NLDBPDCNNCF.BDBHCEGKGDC<TResult>, the same helper and instantiation as
"clone room", which reads {success, value, error}. A bare room-details body
has no success, so the client rejected it with the message-less
"Exception of type 'CEMNLBKJABA' was thrown" (seen on co-owner invite)
although the server had applied the change.
RecNetEnvelopeAttribute wraps those handlers: 2xx becomes
{success:true, value, error:""}, refusals become 200
{success:false, value:null, error:<message>}.
This commit is contained in:
@@ -48,6 +48,7 @@ public class PlaylistsController(
|
||||
/// query or JSON body so JSON callers (admin UI) keep working.</summary>
|
||||
[HttpPost("/playlists")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> Create()
|
||||
{
|
||||
var name = await ReadFieldAsync("name", "Name");
|
||||
@@ -87,6 +88,7 @@ public class PlaylistsController(
|
||||
[HttpPost("/playlists/{playlistId:long}/name")]
|
||||
[HttpPut("/playlists/{playlistId:long}/name")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> PlaylistName(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync("name", "Name", "value", "Value");
|
||||
@@ -96,6 +98,7 @@ public class PlaylistsController(
|
||||
[HttpPost("/playlists/{playlistId:long}/description")]
|
||||
[HttpPut("/playlists/{playlistId:long}/description")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> PlaylistDescription(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync("description", "Description", "value", "Value");
|
||||
@@ -105,6 +108,7 @@ public class PlaylistsController(
|
||||
[HttpPost("/playlists/{playlistId:long}/image")]
|
||||
[HttpPut("/playlists/{playlistId:long}/image")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> PlaylistImage(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync("imageName", "ImageName", "value", "Value");
|
||||
@@ -116,6 +120,7 @@ public class PlaylistsController(
|
||||
[HttpPost("/playlists/{playlistId:long}/tags")]
|
||||
[HttpPut("/playlists/{playlistId:long}/tags")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> PlaylistTags(long playlistId)
|
||||
{
|
||||
var tags = await ReadFieldsAsync("tag", "Tag", "autoTag", "AutoTag", "tags", "Tags");
|
||||
@@ -131,6 +136,7 @@ public class PlaylistsController(
|
||||
[HttpPost("/playlists/{playlistId:long}/visibility")]
|
||||
[HttpPut("/playlists/{playlistId:long}/visibility")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> PlaylistAccessibility(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync(
|
||||
@@ -144,6 +150,7 @@ public class PlaylistsController(
|
||||
[HttpPost("/playlists/{playlistId:long}/levelvoting")]
|
||||
[HttpPut("/playlists/{playlistId:long}/levelvoting")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> PlaylistLevelVoting(long playlistId)
|
||||
{
|
||||
var value = await ReadFieldAsync("supportsLevelVoting", "SupportsLevelVoting", "value", "Value");
|
||||
@@ -156,6 +163,7 @@ public class PlaylistsController(
|
||||
[HttpPost("/playlists/{playlistId:long}/restrictions")]
|
||||
[HttpPut("/playlists/{playlistId:long}/restrictions")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> PlaylistRestrictions(long playlistId)
|
||||
{
|
||||
var juniors = await ReadFieldAsync("supportsJuniors", "SupportsJuniors");
|
||||
@@ -174,6 +182,7 @@ public class PlaylistsController(
|
||||
[HttpPost("/playlists/{playlistId:long}/warning")]
|
||||
[HttpPut("/playlists/{playlistId:long}/warning")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> PlaylistWarning(long playlistId)
|
||||
{
|
||||
var mask = await ReadFieldAsync("warningMask", "WarningMask");
|
||||
@@ -295,6 +304,7 @@ public class PlaylistsController(
|
||||
[HttpPut("/playlists/{playlistId:long}/rooms/{roomId:long}")]
|
||||
[HttpPost("/playlists/{playlistId:long}/rooms/{roomId:long}")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> AddRoom(long playlistId, long roomId)
|
||||
{
|
||||
try
|
||||
@@ -307,6 +317,7 @@ public class PlaylistsController(
|
||||
|
||||
[HttpDelete("/playlists/{playlistId:long}/rooms/{roomId:long}")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> RemoveRoom(long playlistId, long roomId)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -2912,6 +2912,7 @@ public class RoomsController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/name")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/name")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> RenameRoom(
|
||||
long roomId,
|
||||
[ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] RenameBody body)
|
||||
@@ -2967,6 +2968,7 @@ public class RoomsController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> RenameSubroom(
|
||||
long roomId,
|
||||
long subRoomId,
|
||||
@@ -3616,6 +3618,7 @@ public class RoomsController(
|
||||
[HttpPost("rooms/{roomId:long}/subrooms")]
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> AddSubRoom(
|
||||
long roomId,
|
||||
[ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] CreateSubRoomRequest req)
|
||||
@@ -3658,6 +3661,7 @@ public class RoomsController(
|
||||
[HttpDelete("rooms/{roomId:long}/subrooms/{subRoomId:long}")]
|
||||
[HttpDelete("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> DeleteSubRoom(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
@@ -3742,6 +3746,7 @@ public class RoomsController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public Task<IActionResult> SubRoomMaxPlayers(
|
||||
long roomId,
|
||||
long subRoomId,
|
||||
@@ -3759,6 +3764,7 @@ public class RoomsController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/accessibility")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/accessibility")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public Task<IActionResult> SubRoomAccessibility(
|
||||
long roomId,
|
||||
long subRoomId,
|
||||
@@ -3795,6 +3801,7 @@ public class RoomsController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> SubRoomPermissions(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
@@ -3894,6 +3901,7 @@ public class RoomsController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/publish_save")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/publish_save")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> PublishSubRoomSave(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
@@ -3974,6 +3982,7 @@ public class RoomsController(
|
||||
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/clone")]
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/clone")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> CloneSubRoom(long roomId, long subRoomId)
|
||||
{
|
||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||
@@ -4027,6 +4036,7 @@ public class RoomsController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public Task<IActionResult> ModifySubRoom(
|
||||
long roomId,
|
||||
long subRoomId,
|
||||
@@ -4066,6 +4076,7 @@ public class RoomsController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/move")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/move")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> MoveSubRoom(
|
||||
long roomId,
|
||||
long subRoomId,
|
||||
@@ -4434,6 +4445,7 @@ public class RoomsController(
|
||||
[HttpPut("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}")]
|
||||
[HttpPost("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public Task<IActionResult> GrantRole(long roomId, long playerId)
|
||||
=> ChangeRoomAccountRoleAsync(roomId, playerId, isInvite: false);
|
||||
|
||||
@@ -4456,6 +4468,7 @@ public class RoomsController(
|
||||
[HttpPut("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
||||
[HttpPost("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
||||
[Authorize]
|
||||
[RecNetEnvelope]
|
||||
public Task<IActionResult> InviteRole(long roomId, long playerId)
|
||||
=> ChangeRoomAccountRoleAsync(roomId, playerId, isInvite: true);
|
||||
|
||||
|
||||
@@ -501,6 +501,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/description")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/description")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareDescription(long roomId)
|
||||
{
|
||||
// RawValue, not the whitespace-skipping reader: clearing the
|
||||
@@ -520,6 +521,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/image")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/image")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareImage(long roomId)
|
||||
{
|
||||
var payload = await ReadPayloadAsync();
|
||||
@@ -541,6 +543,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/tags")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/tags")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareTags(long roomId)
|
||||
{
|
||||
var payload = await ReadPayloadAsync();
|
||||
@@ -567,6 +570,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/accessibility")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/accessibility")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareAccessibility(long roomId)
|
||||
{
|
||||
var value = await ReadBareIntAsync("accessibility", "Accessibility", "value", "Value");
|
||||
@@ -899,6 +903,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/cloning")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/cloning")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareCloning(long roomId)
|
||||
{
|
||||
var value = await ReadBareBoolAsync("cloningAllowed", "CloningAllowed", "value", "Value");
|
||||
@@ -918,6 +923,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/automute")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/automute")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareAutomute(long roomId)
|
||||
{
|
||||
var value = await ReadBareBoolAsync("disable", "Disable", "DisableMicAutoMute", "value", "Value");
|
||||
@@ -939,6 +945,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/restrictions")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/restrictions")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareRestrictions(long roomId)
|
||||
{
|
||||
var payload = await ReadPayloadAsync();
|
||||
@@ -965,6 +972,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/warning")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/warning")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareWarning(long roomId)
|
||||
{
|
||||
var payload = await ReadPayloadAsync();
|
||||
@@ -983,6 +991,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/creator")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/creator")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareCreator(long roomId)
|
||||
{
|
||||
var creatorId = await ReadBareLongAsync(
|
||||
@@ -1001,6 +1010,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/allow_new_users")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/allow_new_users")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareAllowNewUsers(long roomId)
|
||||
{
|
||||
var value = await ReadBareBoolAsync("allowNewUsers", "AllowNewUsers", "value", "Value");
|
||||
@@ -1015,6 +1025,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/min_level")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/min_level")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareMinLevel(long roomId)
|
||||
{
|
||||
var value = await ReadBareIntAsync("minLevel", "MinLevel", "level", "Level", "value", "Value");
|
||||
@@ -1029,6 +1040,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/max_player_calculation_mode")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/max_player_calculation_mode")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareMaxPlayerCalculationMode(long roomId)
|
||||
{
|
||||
var value = await ReadBareIntAsync(
|
||||
@@ -1054,6 +1066,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/loadscreen")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/loadscreen")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareLoadScreen(long roomId)
|
||||
{
|
||||
var payload = await ReadPayloadAsync();
|
||||
@@ -1085,6 +1098,7 @@ public class RoomsModerationController(
|
||||
[HttpDelete("rooms/{roomId:long}/loadscreen")]
|
||||
[HttpDelete("roomserver/rooms/{roomId:long}/loadscreen")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareLoadScreenRemove(long roomId)
|
||||
{
|
||||
var imageName = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value")
|
||||
@@ -1121,6 +1135,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/promo_images")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/promo_images")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BarePromoImagesAdd(long roomId)
|
||||
{
|
||||
var image = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value");
|
||||
@@ -1137,6 +1152,7 @@ public class RoomsModerationController(
|
||||
[HttpDelete("rooms/{roomId:long}/promo_images")]
|
||||
[HttpDelete("roomserver/rooms/{roomId:long}/promo_images")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BarePromoImagesRemove(long roomId)
|
||||
{
|
||||
var image = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value");
|
||||
@@ -1167,6 +1183,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/promo_external")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/promo_external")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BarePromoExternalAdd(long roomId)
|
||||
{
|
||||
var payload = await ReadPayloadAsync();
|
||||
@@ -1197,6 +1214,7 @@ public class RoomsModerationController(
|
||||
[HttpDelete("rooms/{roomId:long}/promo_external")]
|
||||
[HttpDelete("roomserver/rooms/{roomId:long}/promo_external")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BarePromoExternalRemove(long roomId)
|
||||
{
|
||||
var payload = await ReadPayloadAsync();
|
||||
@@ -1275,6 +1293,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/voice_chat_encryption")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/voice_chat_encryption")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareAck(long roomId) =>
|
||||
await ApplyAndReturn(roomId, _ => { /* value not persistable: no column */ });
|
||||
|
||||
@@ -1305,6 +1324,7 @@ public class RoomsModerationController(
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/modify")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/modify")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
[RecNetEnvelope]
|
||||
public async Task<IActionResult> BareModify(long roomId)
|
||||
{
|
||||
var payload = await ReadPayloadAsync();
|
||||
@@ -1914,7 +1934,13 @@ public class RoomsModerationController(
|
||||
/// <see cref="BareClone"/>). The slim RoomService.ToWireRoom has no
|
||||
/// SubRooms/Roles/Tags/LoadScreens/PromoImages/DataBlob, so the strict
|
||||
/// reader's dispose-walk NRE'd on the missing nested lists and every
|
||||
/// mutation reported failure even though it had persisted.</summary>
|
||||
/// mutation reported failure even though it had persisted.
|
||||
///
|
||||
/// <para>That helper is <c>NLDBPDCNNCF.BDBHCEGKGDC<TResult></c>, the
|
||||
/// <c>{success, value, error}</c> envelope reader, so the details are the
|
||||
/// envelope's <c>value</c>: every caller carries
|
||||
/// <see cref="Controllers.RecNetEnvelopeAttribute"/>. A bare details body
|
||||
/// fails with "Exception of type 'CEMNLBKJABA' was thrown."</para></summary>
|
||||
private async Task<object> BuildRoomDetailsAsync(RoomEntity room)
|
||||
{
|
||||
var scenes = await db.RoomScenes
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace DorkNet.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Wraps an action's reply in the legacy <c>{success, value, error}</c>
|
||||
/// envelope the 2023 rooms client reads for every room and playlist mutation.
|
||||
///
|
||||
/// <para><b>Client contract.</b> All of these calls go through one generic
|
||||
/// helper, <c>NLDBPDCNNCF.BDBHCEGKGDC<TResult>(string opName, HTTPMethods,
|
||||
/// string route, …)</c> (RVA 0x35E50A0, recnet-runtime-decomp/NLDBPDCNNCF.cs:2310;
|
||||
/// called as <c>Call 0x1835E50A0</c> 47 times in
|
||||
/// IsilDump/RecNet.Runtime/NLDBPDCNNCF.txt). It is the same helper, with the
|
||||
/// same <c>BDBHCEGKGDC<FGCPNAACHIK></c> instantiation token
|
||||
/// <c>[0x1884C1948]</c>, that "clone room" uses (NLDBPDCNNCF.txt:4815-4823), and
|
||||
/// clone is proven in game to need <c>{success, value, error}</c>
|
||||
/// (docs/recroom-2023-room-save.md). A bare room-details body has no
|
||||
/// <c>success</c>, so the helper rejects it with no message and the player sees
|
||||
/// <c>Exception of type 'CEMNLBKJABA' was thrown.</c> even though the server
|
||||
/// applied the change. The routes are: rooms/{0}/{accessibility,
|
||||
/// allow_new_users, automute, cloning, comments, creator, description, image,
|
||||
/// loadscreen, max_player_calculation_mode, min_level, modify, name,
|
||||
/// promo_external, promo_images, restrictions, tags, voice_chat_encryption,
|
||||
/// warning}, rooms/{0}/roles/{1}[/invite], rooms/{0}/subrooms[/{1}[/accessibility,
|
||||
/// clone, maxplayers, modify, move, name, permissions, publish_save]],
|
||||
/// playlists and playlists/{0}/{accessibility, description, image, levelvoting,
|
||||
/// name, restrictions, rooms/{1}, tags, warning}.</para>
|
||||
///
|
||||
/// <para>A 2xx object result becomes <c>{success:true, value, error:""}</c>.
|
||||
/// Anything else becomes HTTP 200 <c>{success:false, value:null, error}</c>
|
||||
/// with a message the client can show, because a non-2xx reply also surfaces
|
||||
/// only as the message-less CEMNLBKJABA.</para>
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public sealed class RecNetEnvelopeAttribute : ResultFilterAttribute
|
||||
{
|
||||
public override void OnResultExecuting(ResultExecutingContext context)
|
||||
{
|
||||
context.Result = Wrap(context.Result);
|
||||
base.OnResultExecuting(context);
|
||||
}
|
||||
|
||||
public static IActionResult Wrap(IActionResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case ObjectResult obj when obj.StatusCode is null or (>= 200 and < 300):
|
||||
return IsEnvelope(obj.Value)
|
||||
? result
|
||||
: Success(obj.Value);
|
||||
case ObjectResult obj:
|
||||
return Failure(ErrorText(obj.Value) ?? DefaultError(obj.StatusCode ?? 500));
|
||||
case ForbidResult or ChallengeResult or UnauthorizedResult:
|
||||
return Failure(DefaultError(403));
|
||||
case StatusCodeResult code when code.StatusCode is >= 200 and < 300:
|
||||
return Success(null);
|
||||
case StatusCodeResult code:
|
||||
return Failure(DefaultError(code.StatusCode));
|
||||
case EmptyResult:
|
||||
return Success(null);
|
||||
default:
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static OkObjectResult Success(object? value) =>
|
||||
new(new { success = true, value, error = string.Empty });
|
||||
|
||||
public static OkObjectResult Failure(string error) =>
|
||||
new(new { success = false, value = (object?)null, error });
|
||||
|
||||
private static string DefaultError(int status) => status switch
|
||||
{
|
||||
400 => "That request was not valid.",
|
||||
401 or 403 => "You don't have permission to do that.",
|
||||
404 => "That room doesn't exist.",
|
||||
409 => "That name is already taken.",
|
||||
_ => "Something went wrong.",
|
||||
};
|
||||
|
||||
/// <summary>An <c>error</c>/<c>Error</c> string on the failure body, if any.</summary>
|
||||
private static string? ErrorText(object? value)
|
||||
{
|
||||
if (value is null) return null;
|
||||
if (value is string s) return string.IsNullOrWhiteSpace(s) ? null : s;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(JsonSerializer.Serialize(value));
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return null;
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
if (!prop.Name.Equals("error", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
if (prop.Value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var text = prop.Value.GetString();
|
||||
return string.IsNullOrWhiteSpace(text) ? null : text;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsEnvelope(object? value)
|
||||
{
|
||||
if (value is null) return false;
|
||||
var props = value.GetType().GetProperties();
|
||||
bool Has(string name) => props.Any(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
return Has("success") && Has("value") && Has("error");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user