mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 15:38:45 -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:
@@ -88,7 +88,11 @@ public sealed class RoomPublish2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
using var modRooms = Client("rooms", moderator);
|
using var modRooms = Client("rooms", moderator);
|
||||||
using (var denied = await modRooms.PutAsync($"/roomserver/rooms/{cloneId}/accessibility",
|
using (var denied = await modRooms.PutAsync($"/roomserver/rooms/{cloneId}/accessibility",
|
||||||
new FormUrlEncodedContent(new Dictionary<string, string> { ["accessibility"] = "0" })))
|
new FormUrlEncodedContent(new Dictionary<string, string> { ["accessibility"] = "0" })))
|
||||||
Assert.False(denied.IsSuccessStatusCode);
|
{
|
||||||
|
// Refusals are 200 {success:false, error} so the client can show them.
|
||||||
|
using var doc = JsonDocument.Parse(await denied.Content.ReadAsStringAsync());
|
||||||
|
Assert.False(doc.RootElement.GetProperty("success").GetBoolean());
|
||||||
|
}
|
||||||
using var coRooms = Client("rooms", coOwner);
|
using var coRooms = Client("rooms", coOwner);
|
||||||
var unpublished = await PutAccessibilityAsync(coRooms, cloneId, "0");
|
var unpublished = await PutAccessibilityAsync(coRooms, cloneId, "0");
|
||||||
Assert.Equal(0, unpublished.GetProperty("Accessibility").GetInt32());
|
Assert.Equal(0, unpublished.GetProperty("Accessibility").GetInt32());
|
||||||
@@ -100,7 +104,11 @@ public sealed class RoomPublish2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
new FormUrlEncodedContent(new Dictionary<string, string> { ["accessibility"] = value }));
|
new FormUrlEncodedContent(new Dictionary<string, string> { ["accessibility"] = value }));
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
Assert.True(response.IsSuccessStatusCode, $"{(int)response.StatusCode}: {body}");
|
Assert.True(response.IsSuccessStatusCode, $"{(int)response.StatusCode}: {body}");
|
||||||
return JsonDocument.Parse(body).RootElement.Clone();
|
// "change room accessibility" goes through the {success, value, error}
|
||||||
|
// helper (NLDBPDCNNCF.BDBHCEGKGDC<FGCPNAACHIK>), same as clone.
|
||||||
|
using var doc = JsonDocument.Parse(body);
|
||||||
|
Assert.True(doc.RootElement.GetProperty("success").GetBoolean(), body);
|
||||||
|
return doc.RootElement.GetProperty("value").Clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<JsonElement> GetJsonAsync(HttpClient client, string path)
|
private static async Task<JsonElement> GetJsonAsync(HttpClient client, string path)
|
||||||
|
|||||||
@@ -241,14 +241,14 @@ public sealed class RoomRoles2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
|
|
||||||
// With the invite gone, the same accept is refused.
|
// With the invite gone, the same accept is refused.
|
||||||
using var again = await PutFormRawAsync(inviteeRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}", ("role", "30"));
|
using var again = await PutFormRawAsync(inviteeRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}", ("role", "30"));
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, again.StatusCode);
|
await AssertRefusedAsync(again);
|
||||||
|
|
||||||
// A stranger can neither promote themselves nor invite anyone.
|
// A stranger can neither promote themselves nor invite anyone.
|
||||||
using var strangerRooms = Client("rooms", stranger);
|
using var strangerRooms = Client("rooms", stranger);
|
||||||
using var self = await PutFormRawAsync(strangerRooms, $"/roomserver/rooms/{roomId}/roles/{stranger.PlayerId}", ("role", "20"));
|
using var self = await PutFormRawAsync(strangerRooms, $"/roomserver/rooms/{roomId}/roles/{stranger.PlayerId}", ("role", "20"));
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, self.StatusCode);
|
await AssertRefusedAsync(self);
|
||||||
using var other = await PutFormRawAsync(strangerRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}/invite", ("role", "30"));
|
using var other = await PutFormRawAsync(strangerRooms, $"/roomserver/rooms/{roomId}/roles/{invitee.PlayerId}/invite", ("role", "30"));
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, other.StatusCode);
|
await AssertRefusedAsync(other);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -262,7 +262,7 @@ public sealed class RoomRoles2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
Assert.Equal(20, RoleOf(granted, target.PlayerId).GetProperty("Role").GetInt32());
|
Assert.Equal(20, RoleOf(granted, target.PlayerId).GetProperty("Role").GetInt32());
|
||||||
|
|
||||||
using var inviteCo = await PutFormRawAsync(coRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}/invite", ("role", "30"));
|
using var inviteCo = await PutFormRawAsync(coRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}/invite", ("role", "30"));
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, inviteCo.StatusCode);
|
await AssertRefusedAsync(inviteCo);
|
||||||
|
|
||||||
// The creator can directly add and remove a co-owner.
|
// The creator can directly add and remove a co-owner.
|
||||||
using var ownerRooms = Client("rooms", owner);
|
using var ownerRooms = Client("rooms", owner);
|
||||||
@@ -271,7 +271,7 @@ public sealed class RoomRoles2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
|
|
||||||
// …and now the co-owner can't touch their peer.
|
// …and now the co-owner can't touch their peer.
|
||||||
using var demote = await PutFormRawAsync(coRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}", ("role", "0"));
|
using var demote = await PutFormRawAsync(coRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}", ("role", "0"));
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, demote.StatusCode);
|
await AssertRefusedAsync(demote);
|
||||||
|
|
||||||
var removed = await PutFormAsync(ownerRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}", ("role", "0"));
|
var removed = await PutFormAsync(ownerRooms, $"/roomserver/rooms/{roomId}/roles/{target.PlayerId}", ("role", "0"));
|
||||||
Assert.DoesNotContain(removed.GetProperty("Roles").EnumerateArray(),
|
Assert.DoesNotContain(removed.GetProperty("Roles").EnumerateArray(),
|
||||||
@@ -297,8 +297,9 @@ public sealed class RoomRoles2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
{
|
{
|
||||||
var text = await put.Content.ReadAsStringAsync();
|
var text = await put.Content.ReadAsStringAsync();
|
||||||
Assert.True(put.IsSuccessStatusCode, $"{(int)put.StatusCode}: {text}");
|
Assert.True(put.IsSuccessStatusCode, $"{(int)put.StatusCode}: {text}");
|
||||||
using var json = JsonDocument.Parse(text);
|
// {success, value: full room details, error}
|
||||||
Assert.Equal(roomId, json.RootElement.GetProperty("RoomId").GetInt64()); // full room details
|
var value = Envelope(text);
|
||||||
|
Assert.Equal(roomId, value.GetProperty("RoomId").GetInt64());
|
||||||
}
|
}
|
||||||
|
|
||||||
// The owner enters the room, then the matrix comes back with the token.
|
// The owner enters the room, then the matrix comes back with the token.
|
||||||
@@ -318,10 +319,10 @@ public sealed class RoomRoles2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
using var coRooms = Client("rooms", coOwner);
|
using var coRooms = Client("rooms", coOwner);
|
||||||
using (var denied = await PutJsonRawAsync(coRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions",
|
using (var denied = await PutJsonRawAsync(coRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions",
|
||||||
"""[{"Permission":"13","Role":30,"Override":true,"Type":0,"Value":"true"}]"""))
|
"""[{"Permission":"13","Role":30,"Override":true,"Type":0,"Value":"true"}]"""))
|
||||||
Assert.Equal(HttpStatusCode.Forbidden, denied.StatusCode);
|
await AssertRefusedAsync(denied);
|
||||||
using (var allowed = await PutJsonRawAsync(coRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions",
|
using (var allowed = await PutJsonRawAsync(coRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions",
|
||||||
"""[{"Permission":"13","Role":20,"Override":true,"Type":0,"Value":"true"}]"""))
|
"""[{"Permission":"13","Role":20,"Override":true,"Type":0,"Value":"true"}]"""))
|
||||||
Assert.True(allowed.IsSuccessStatusCode, await allowed.Content.ReadAsStringAsync());
|
Envelope(await allowed.Content.ReadAsStringAsync());
|
||||||
|
|
||||||
await using var scope = _factory.Services.CreateAsyncScope();
|
await using var scope = _factory.Services.CreateAsyncScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
|
||||||
@@ -374,7 +375,7 @@ public sealed class RoomRoles2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
Assert.True(join.IsSuccessStatusCode, await join.Content.ReadAsStringAsync());
|
Assert.True(join.IsSuccessStatusCode, await join.Content.ReadAsStringAsync());
|
||||||
using (var put = await PutJsonRawAsync(ownerRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions",
|
using (var put = await PutJsonRawAsync(ownerRooms, $"/roomserver/rooms/{roomId}/subrooms/1/permissions",
|
||||||
"""[{"Permission":"21","Role":20,"Override":true,"Type":0,"Value":"true"}]"""))
|
"""[{"Permission":"21","Role":20,"Override":true,"Type":0,"Value":"true"}]"""))
|
||||||
Assert.True(put.IsSuccessStatusCode, await put.Content.ReadAsStringAsync());
|
Envelope(await put.Content.ReadAsStringAsync());
|
||||||
|
|
||||||
var token = Assert.Single(hub.To(CapturingHubContext.PlayerGroup(owner.PlayerId), "PhotonAccessToken"));
|
var token = Assert.Single(hub.To(CapturingHubContext.PlayerGroup(owner.PlayerId), "PhotonAccessToken"));
|
||||||
Assert.True(token.Msg.GetProperty("RoomInstanceId").GetInt64() > 0);
|
Assert.True(token.Msg.GetProperty("RoomInstanceId").GetInt64() > 0);
|
||||||
@@ -382,6 +383,54 @@ public sealed class RoomRoles2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
c => c.GetProperty("Role").GetInt32() == 20 && c.GetProperty("Permission").GetString() == "21");
|
c => c.GetProperty("Role").GetInt32() == 20 && c.GetProperty("Permission").GetString() == "21");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Envelope_filter_wraps_success_and_turns_refusals_into_messages()
|
||||||
|
{
|
||||||
|
static JsonElement Body(Microsoft.AspNetCore.Mvc.IActionResult r) =>
|
||||||
|
JsonDocument.Parse(JsonSerializer.Serialize(((Microsoft.AspNetCore.Mvc.ObjectResult)r).Value)).RootElement.Clone();
|
||||||
|
|
||||||
|
var ok = Body(Controllers.RecNetEnvelopeAttribute.Wrap(new Microsoft.AspNetCore.Mvc.OkObjectResult(new { RoomId = 5 })));
|
||||||
|
Assert.True(ok.GetProperty("success").GetBoolean());
|
||||||
|
Assert.Equal(5, ok.GetProperty("value").GetProperty("RoomId").GetInt32());
|
||||||
|
Assert.Equal("", ok.GetProperty("error").GetString());
|
||||||
|
|
||||||
|
var forbid = Body(Controllers.RecNetEnvelopeAttribute.Wrap(new Microsoft.AspNetCore.Mvc.ForbidResult()));
|
||||||
|
Assert.False(forbid.GetProperty("success").GetBoolean());
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(forbid.GetProperty("error").GetString()));
|
||||||
|
|
||||||
|
var bad = Body(Controllers.RecNetEnvelopeAttribute.Wrap(
|
||||||
|
new Microsoft.AspNetCore.Mvc.BadRequestObjectResult(new { error = "cannot_change_creator_role" })));
|
||||||
|
Assert.Equal("cannot_change_creator_role", bad.GetProperty("error").GetString());
|
||||||
|
|
||||||
|
// An envelope the handler built itself is left alone.
|
||||||
|
var already = new Microsoft.AspNetCore.Mvc.OkObjectResult(new { success = false, value = (object?)null, error = "x" });
|
||||||
|
Assert.Same(already, Controllers.RecNetEnvelopeAttribute.Wrap(already));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Room_mutations_answer_the_success_value_error_envelope_on_bare_and_roomserver_paths()
|
||||||
|
{
|
||||||
|
var (owner, invitee, _) = await PlayersAsync();
|
||||||
|
var roomId = await SeedRoomAsync(owner.PlayerId);
|
||||||
|
using var rooms = Client("rooms", owner);
|
||||||
|
|
||||||
|
// The exact request the client sent in production (bare path, form role=30).
|
||||||
|
var invited = await PutFormAsync(rooms, $"/rooms/{roomId}/roles/{invitee.PlayerId}/invite", ("role", "30"));
|
||||||
|
Assert.Equal(30, RoleOf(invited, invitee.PlayerId).GetProperty("InvitedRole").GetInt32());
|
||||||
|
|
||||||
|
foreach (var (path, key, value) in new[]
|
||||||
|
{
|
||||||
|
($"/rooms/{roomId}/description", "description", "hello"),
|
||||||
|
($"/roomserver/rooms/{roomId}/accessibility", "accessibility", "0"),
|
||||||
|
($"/roomserver/rooms/{roomId}/subrooms/1/name", "name", "Lobby"),
|
||||||
|
($"/roomserver/rooms/{roomId}/subrooms/1/maxplayers", "maxPlayers", "10"),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var details = await PutFormAsync(rooms, path, (key, value));
|
||||||
|
Assert.Equal(roomId, details.GetProperty("RoomId").GetInt64());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── helpers ─────────────────────────────────────────────────────────
|
// ── helpers ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private async Task<(GameClientSession A, GameClientSession B, GameClientSession C)> PlayersAsync()
|
private async Task<(GameClientSession A, GameClientSession B, GameClientSession C)> PlayersAsync()
|
||||||
@@ -426,7 +475,32 @@ public sealed class RoomRoles2023Tests : IClassFixture<DorkNetServerFactory>
|
|||||||
using var response = await PutFormRawAsync(client, path, fields);
|
using var response = await PutFormRawAsync(client, path, fields);
|
||||||
var body = await response.Content.ReadAsStringAsync();
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
Assert.True(response.IsSuccessStatusCode, $"PUT {path} -> {(int)response.StatusCode}: {body}");
|
Assert.True(response.IsSuccessStatusCode, $"PUT {path} -> {(int)response.StatusCode}: {body}");
|
||||||
return JsonDocument.Parse(body).RootElement.Clone();
|
return Envelope(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Every room mutation answers the {success, value, error}
|
||||||
|
/// envelope (NLDBPDCNNCF.BDBHCEGKGDC<FGCPNAACHIK>): assert success and
|
||||||
|
/// return the room details in <c>value</c>.</summary>
|
||||||
|
private static JsonElement Envelope(string body)
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(body);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
Assert.True(root.GetProperty("success").GetBoolean(), body);
|
||||||
|
Assert.Equal(string.Empty, root.GetProperty("error").GetString());
|
||||||
|
return root.GetProperty("value").Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A refusal is HTTP 200 with success=false and a message: the
|
||||||
|
/// client shows the message, while a 403/404 surfaces only as the
|
||||||
|
/// message-less "Exception of type 'CEMNLBKJABA' was thrown".</summary>
|
||||||
|
private static async Task AssertRefusedAsync(HttpResponseMessage response)
|
||||||
|
{
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||||
|
using var doc = JsonDocument.Parse(body);
|
||||||
|
Assert.False(doc.RootElement.GetProperty("success").GetBoolean(), body);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(doc.RootElement.GetProperty("error").GetString()), body);
|
||||||
|
Assert.Equal(JsonValueKind.Null, doc.RootElement.GetProperty("value").ValueKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Task<HttpResponseMessage> PutJsonRawAsync(HttpClient client, string path, string json)
|
private static Task<HttpResponseMessage> PutJsonRawAsync(HttpClient client, string path, string json)
|
||||||
|
|||||||
@@ -73,9 +73,12 @@ public sealed class SubRoomMaxPlayersTests : IClassFixture<DorkNetServerFactory>
|
|||||||
Assert.True(slider.IsSuccessStatusCode,
|
Assert.True(slider.IsSuccessStatusCode,
|
||||||
$"owner PUT maxplayers -> {(int)slider.StatusCode}: {sliderBody}");
|
$"owner PUT maxplayers -> {(int)slider.StatusCode}: {sliderBody}");
|
||||||
|
|
||||||
// The client deserialises the reply as FGCPNAACHIK — the room-details
|
// The client reads the reply through the {success, value, error}
|
||||||
// shape — so the slider's own response has to carry the new value.
|
// helper with value = FGCPNAACHIK, so the slider's own response has to
|
||||||
var echoed = JsonDocument.Parse(sliderBody).RootElement
|
// carry the new value inside the envelope.
|
||||||
|
var envelope = JsonDocument.Parse(sliderBody).RootElement;
|
||||||
|
Assert.True(envelope.GetProperty("success").GetBoolean(), sliderBody);
|
||||||
|
var echoed = envelope.GetProperty("value")
|
||||||
.GetProperty("SubRooms").EnumerateArray()
|
.GetProperty("SubRooms").EnumerateArray()
|
||||||
.First(e => e.GetProperty("SubRoomId").GetInt64() == 2);
|
.First(e => e.GetProperty("SubRoomId").GetInt64() == 2);
|
||||||
Assert.Equal(24, echoed.GetProperty("MaxPlayers").GetInt32());
|
Assert.Equal(24, echoed.GetProperty("MaxPlayers").GetInt32());
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public class PlaylistsController(
|
|||||||
/// query or JSON body so JSON callers (admin UI) keep working.</summary>
|
/// query or JSON body so JSON callers (admin UI) keep working.</summary>
|
||||||
[HttpPost("/playlists")]
|
[HttpPost("/playlists")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> Create()
|
public async Task<IActionResult> Create()
|
||||||
{
|
{
|
||||||
var name = await ReadFieldAsync("name", "Name");
|
var name = await ReadFieldAsync("name", "Name");
|
||||||
@@ -87,6 +88,7 @@ public class PlaylistsController(
|
|||||||
[HttpPost("/playlists/{playlistId:long}/name")]
|
[HttpPost("/playlists/{playlistId:long}/name")]
|
||||||
[HttpPut("/playlists/{playlistId:long}/name")]
|
[HttpPut("/playlists/{playlistId:long}/name")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> PlaylistName(long playlistId)
|
public async Task<IActionResult> PlaylistName(long playlistId)
|
||||||
{
|
{
|
||||||
var value = await ReadFieldAsync("name", "Name", "value", "Value");
|
var value = await ReadFieldAsync("name", "Name", "value", "Value");
|
||||||
@@ -96,6 +98,7 @@ public class PlaylistsController(
|
|||||||
[HttpPost("/playlists/{playlistId:long}/description")]
|
[HttpPost("/playlists/{playlistId:long}/description")]
|
||||||
[HttpPut("/playlists/{playlistId:long}/description")]
|
[HttpPut("/playlists/{playlistId:long}/description")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> PlaylistDescription(long playlistId)
|
public async Task<IActionResult> PlaylistDescription(long playlistId)
|
||||||
{
|
{
|
||||||
var value = await ReadFieldAsync("description", "Description", "value", "Value");
|
var value = await ReadFieldAsync("description", "Description", "value", "Value");
|
||||||
@@ -105,6 +108,7 @@ public class PlaylistsController(
|
|||||||
[HttpPost("/playlists/{playlistId:long}/image")]
|
[HttpPost("/playlists/{playlistId:long}/image")]
|
||||||
[HttpPut("/playlists/{playlistId:long}/image")]
|
[HttpPut("/playlists/{playlistId:long}/image")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> PlaylistImage(long playlistId)
|
public async Task<IActionResult> PlaylistImage(long playlistId)
|
||||||
{
|
{
|
||||||
var value = await ReadFieldAsync("imageName", "ImageName", "value", "Value");
|
var value = await ReadFieldAsync("imageName", "ImageName", "value", "Value");
|
||||||
@@ -116,6 +120,7 @@ public class PlaylistsController(
|
|||||||
[HttpPost("/playlists/{playlistId:long}/tags")]
|
[HttpPost("/playlists/{playlistId:long}/tags")]
|
||||||
[HttpPut("/playlists/{playlistId:long}/tags")]
|
[HttpPut("/playlists/{playlistId:long}/tags")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> PlaylistTags(long playlistId)
|
public async Task<IActionResult> PlaylistTags(long playlistId)
|
||||||
{
|
{
|
||||||
var tags = await ReadFieldsAsync("tag", "Tag", "autoTag", "AutoTag", "tags", "Tags");
|
var tags = await ReadFieldsAsync("tag", "Tag", "autoTag", "AutoTag", "tags", "Tags");
|
||||||
@@ -131,6 +136,7 @@ public class PlaylistsController(
|
|||||||
[HttpPost("/playlists/{playlistId:long}/visibility")]
|
[HttpPost("/playlists/{playlistId:long}/visibility")]
|
||||||
[HttpPut("/playlists/{playlistId:long}/visibility")]
|
[HttpPut("/playlists/{playlistId:long}/visibility")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> PlaylistAccessibility(long playlistId)
|
public async Task<IActionResult> PlaylistAccessibility(long playlistId)
|
||||||
{
|
{
|
||||||
var value = await ReadFieldAsync(
|
var value = await ReadFieldAsync(
|
||||||
@@ -144,6 +150,7 @@ public class PlaylistsController(
|
|||||||
[HttpPost("/playlists/{playlistId:long}/levelvoting")]
|
[HttpPost("/playlists/{playlistId:long}/levelvoting")]
|
||||||
[HttpPut("/playlists/{playlistId:long}/levelvoting")]
|
[HttpPut("/playlists/{playlistId:long}/levelvoting")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> PlaylistLevelVoting(long playlistId)
|
public async Task<IActionResult> PlaylistLevelVoting(long playlistId)
|
||||||
{
|
{
|
||||||
var value = await ReadFieldAsync("supportsLevelVoting", "SupportsLevelVoting", "value", "Value");
|
var value = await ReadFieldAsync("supportsLevelVoting", "SupportsLevelVoting", "value", "Value");
|
||||||
@@ -156,6 +163,7 @@ public class PlaylistsController(
|
|||||||
[HttpPost("/playlists/{playlistId:long}/restrictions")]
|
[HttpPost("/playlists/{playlistId:long}/restrictions")]
|
||||||
[HttpPut("/playlists/{playlistId:long}/restrictions")]
|
[HttpPut("/playlists/{playlistId:long}/restrictions")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> PlaylistRestrictions(long playlistId)
|
public async Task<IActionResult> PlaylistRestrictions(long playlistId)
|
||||||
{
|
{
|
||||||
var juniors = await ReadFieldAsync("supportsJuniors", "SupportsJuniors");
|
var juniors = await ReadFieldAsync("supportsJuniors", "SupportsJuniors");
|
||||||
@@ -174,6 +182,7 @@ public class PlaylistsController(
|
|||||||
[HttpPost("/playlists/{playlistId:long}/warning")]
|
[HttpPost("/playlists/{playlistId:long}/warning")]
|
||||||
[HttpPut("/playlists/{playlistId:long}/warning")]
|
[HttpPut("/playlists/{playlistId:long}/warning")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> PlaylistWarning(long playlistId)
|
public async Task<IActionResult> PlaylistWarning(long playlistId)
|
||||||
{
|
{
|
||||||
var mask = await ReadFieldAsync("warningMask", "WarningMask");
|
var mask = await ReadFieldAsync("warningMask", "WarningMask");
|
||||||
@@ -295,6 +304,7 @@ public class PlaylistsController(
|
|||||||
[HttpPut("/playlists/{playlistId:long}/rooms/{roomId:long}")]
|
[HttpPut("/playlists/{playlistId:long}/rooms/{roomId:long}")]
|
||||||
[HttpPost("/playlists/{playlistId:long}/rooms/{roomId:long}")]
|
[HttpPost("/playlists/{playlistId:long}/rooms/{roomId:long}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> AddRoom(long playlistId, long roomId)
|
public async Task<IActionResult> AddRoom(long playlistId, long roomId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -307,6 +317,7 @@ public class PlaylistsController(
|
|||||||
|
|
||||||
[HttpDelete("/playlists/{playlistId:long}/rooms/{roomId:long}")]
|
[HttpDelete("/playlists/{playlistId:long}/rooms/{roomId:long}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> RemoveRoom(long playlistId, long roomId)
|
public async Task<IActionResult> RemoveRoom(long playlistId, long roomId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -2912,6 +2912,7 @@ public class RoomsController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/name")]
|
[HttpPost("roomserver/rooms/{roomId:long}/name")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/name")]
|
[HttpPut("roomserver/rooms/{roomId:long}/name")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> RenameRoom(
|
public async Task<IActionResult> RenameRoom(
|
||||||
long roomId,
|
long roomId,
|
||||||
[ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] RenameBody body)
|
[ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] RenameBody body)
|
||||||
@@ -2967,6 +2968,7 @@ public class RoomsController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
|
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
|
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/name")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> RenameSubroom(
|
public async Task<IActionResult> RenameSubroom(
|
||||||
long roomId,
|
long roomId,
|
||||||
long subRoomId,
|
long subRoomId,
|
||||||
@@ -3616,6 +3618,7 @@ public class RoomsController(
|
|||||||
[HttpPost("rooms/{roomId:long}/subrooms")]
|
[HttpPost("rooms/{roomId:long}/subrooms")]
|
||||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms")]
|
[HttpPost("roomserver/rooms/{roomId:long}/subrooms")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> AddSubRoom(
|
public async Task<IActionResult> AddSubRoom(
|
||||||
long roomId,
|
long roomId,
|
||||||
[ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] CreateSubRoomRequest req)
|
[ModelBinder(typeof(DorkNet.Server.Binding.FormOrJsonModelBinder))] CreateSubRoomRequest req)
|
||||||
@@ -3658,6 +3661,7 @@ public class RoomsController(
|
|||||||
[HttpDelete("rooms/{roomId:long}/subrooms/{subRoomId:long}")]
|
[HttpDelete("rooms/{roomId:long}/subrooms/{subRoomId:long}")]
|
||||||
[HttpDelete("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}")]
|
[HttpDelete("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> DeleteSubRoom(long roomId, long subRoomId)
|
public async Task<IActionResult> DeleteSubRoom(long roomId, long subRoomId)
|
||||||
{
|
{
|
||||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||||
@@ -3742,6 +3746,7 @@ public class RoomsController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
|
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
|
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public Task<IActionResult> SubRoomMaxPlayers(
|
public Task<IActionResult> SubRoomMaxPlayers(
|
||||||
long roomId,
|
long roomId,
|
||||||
long subRoomId,
|
long subRoomId,
|
||||||
@@ -3759,6 +3764,7 @@ public class RoomsController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/accessibility")]
|
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/accessibility")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/accessibility")]
|
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/accessibility")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public Task<IActionResult> SubRoomAccessibility(
|
public Task<IActionResult> SubRoomAccessibility(
|
||||||
long roomId,
|
long roomId,
|
||||||
long subRoomId,
|
long subRoomId,
|
||||||
@@ -3795,6 +3801,7 @@ public class RoomsController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> SubRoomPermissions(long roomId, long subRoomId)
|
public async Task<IActionResult> SubRoomPermissions(long roomId, long subRoomId)
|
||||||
{
|
{
|
||||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||||
@@ -3894,6 +3901,7 @@ public class RoomsController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/publish_save")]
|
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/publish_save")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/publish_save")]
|
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/publish_save")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> PublishSubRoomSave(long roomId, long subRoomId)
|
public async Task<IActionResult> PublishSubRoomSave(long roomId, long subRoomId)
|
||||||
{
|
{
|
||||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||||
@@ -3974,6 +3982,7 @@ public class RoomsController(
|
|||||||
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/clone")]
|
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/clone")]
|
||||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/clone")]
|
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/clone")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> CloneSubRoom(long roomId, long subRoomId)
|
public async Task<IActionResult> CloneSubRoom(long roomId, long subRoomId)
|
||||||
{
|
{
|
||||||
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
subRoomId = RoomSceneIds.ToOrderIndex(subRoomId); // wire id → OrderIndex
|
||||||
@@ -4027,6 +4036,7 @@ public class RoomsController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
|
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
|
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/modify")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public Task<IActionResult> ModifySubRoom(
|
public Task<IActionResult> ModifySubRoom(
|
||||||
long roomId,
|
long roomId,
|
||||||
long subRoomId,
|
long subRoomId,
|
||||||
@@ -4066,6 +4076,7 @@ public class RoomsController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/move")]
|
[HttpPost("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/move")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/move")]
|
[HttpPut("roomserver/rooms/{roomId:long}/subrooms/{subRoomId:long}/move")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> MoveSubRoom(
|
public async Task<IActionResult> MoveSubRoom(
|
||||||
long roomId,
|
long roomId,
|
||||||
long subRoomId,
|
long subRoomId,
|
||||||
@@ -4434,6 +4445,7 @@ public class RoomsController(
|
|||||||
[HttpPut("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}")]
|
[HttpPut("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}")]
|
||||||
[HttpPost("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}")]
|
[HttpPost("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public Task<IActionResult> GrantRole(long roomId, long playerId)
|
public Task<IActionResult> GrantRole(long roomId, long playerId)
|
||||||
=> ChangeRoomAccountRoleAsync(roomId, playerId, isInvite: false);
|
=> ChangeRoomAccountRoleAsync(roomId, playerId, isInvite: false);
|
||||||
|
|
||||||
@@ -4456,6 +4468,7 @@ public class RoomsController(
|
|||||||
[HttpPut("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
[HttpPut("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
||||||
[HttpPost("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
[HttpPost("/api/rooms/v2/rooms/{roomId:long}/roles/{playerId:long}/invite")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
|
[RecNetEnvelope]
|
||||||
public Task<IActionResult> InviteRole(long roomId, long playerId)
|
public Task<IActionResult> InviteRole(long roomId, long playerId)
|
||||||
=> ChangeRoomAccountRoleAsync(roomId, playerId, isInvite: true);
|
=> ChangeRoomAccountRoleAsync(roomId, playerId, isInvite: true);
|
||||||
|
|
||||||
|
|||||||
@@ -501,6 +501,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/description")]
|
[HttpPost("roomserver/rooms/{roomId:long}/description")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/description")]
|
[HttpPut("roomserver/rooms/{roomId:long}/description")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareDescription(long roomId)
|
public async Task<IActionResult> BareDescription(long roomId)
|
||||||
{
|
{
|
||||||
// RawValue, not the whitespace-skipping reader: clearing the
|
// RawValue, not the whitespace-skipping reader: clearing the
|
||||||
@@ -520,6 +521,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/image")]
|
[HttpPost("roomserver/rooms/{roomId:long}/image")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/image")]
|
[HttpPut("roomserver/rooms/{roomId:long}/image")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareImage(long roomId)
|
public async Task<IActionResult> BareImage(long roomId)
|
||||||
{
|
{
|
||||||
var payload = await ReadPayloadAsync();
|
var payload = await ReadPayloadAsync();
|
||||||
@@ -541,6 +543,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/tags")]
|
[HttpPost("roomserver/rooms/{roomId:long}/tags")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/tags")]
|
[HttpPut("roomserver/rooms/{roomId:long}/tags")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareTags(long roomId)
|
public async Task<IActionResult> BareTags(long roomId)
|
||||||
{
|
{
|
||||||
var payload = await ReadPayloadAsync();
|
var payload = await ReadPayloadAsync();
|
||||||
@@ -567,6 +570,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/accessibility")]
|
[HttpPost("roomserver/rooms/{roomId:long}/accessibility")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/accessibility")]
|
[HttpPut("roomserver/rooms/{roomId:long}/accessibility")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareAccessibility(long roomId)
|
public async Task<IActionResult> BareAccessibility(long roomId)
|
||||||
{
|
{
|
||||||
var value = await ReadBareIntAsync("accessibility", "Accessibility", "value", "Value");
|
var value = await ReadBareIntAsync("accessibility", "Accessibility", "value", "Value");
|
||||||
@@ -899,6 +903,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/cloning")]
|
[HttpPost("roomserver/rooms/{roomId:long}/cloning")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/cloning")]
|
[HttpPut("roomserver/rooms/{roomId:long}/cloning")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareCloning(long roomId)
|
public async Task<IActionResult> BareCloning(long roomId)
|
||||||
{
|
{
|
||||||
var value = await ReadBareBoolAsync("cloningAllowed", "CloningAllowed", "value", "Value");
|
var value = await ReadBareBoolAsync("cloningAllowed", "CloningAllowed", "value", "Value");
|
||||||
@@ -918,6 +923,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/automute")]
|
[HttpPost("roomserver/rooms/{roomId:long}/automute")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/automute")]
|
[HttpPut("roomserver/rooms/{roomId:long}/automute")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareAutomute(long roomId)
|
public async Task<IActionResult> BareAutomute(long roomId)
|
||||||
{
|
{
|
||||||
var value = await ReadBareBoolAsync("disable", "Disable", "DisableMicAutoMute", "value", "Value");
|
var value = await ReadBareBoolAsync("disable", "Disable", "DisableMicAutoMute", "value", "Value");
|
||||||
@@ -939,6 +945,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/restrictions")]
|
[HttpPost("roomserver/rooms/{roomId:long}/restrictions")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/restrictions")]
|
[HttpPut("roomserver/rooms/{roomId:long}/restrictions")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareRestrictions(long roomId)
|
public async Task<IActionResult> BareRestrictions(long roomId)
|
||||||
{
|
{
|
||||||
var payload = await ReadPayloadAsync();
|
var payload = await ReadPayloadAsync();
|
||||||
@@ -965,6 +972,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/warning")]
|
[HttpPost("roomserver/rooms/{roomId:long}/warning")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/warning")]
|
[HttpPut("roomserver/rooms/{roomId:long}/warning")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareWarning(long roomId)
|
public async Task<IActionResult> BareWarning(long roomId)
|
||||||
{
|
{
|
||||||
var payload = await ReadPayloadAsync();
|
var payload = await ReadPayloadAsync();
|
||||||
@@ -983,6 +991,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/creator")]
|
[HttpPost("roomserver/rooms/{roomId:long}/creator")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/creator")]
|
[HttpPut("roomserver/rooms/{roomId:long}/creator")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareCreator(long roomId)
|
public async Task<IActionResult> BareCreator(long roomId)
|
||||||
{
|
{
|
||||||
var creatorId = await ReadBareLongAsync(
|
var creatorId = await ReadBareLongAsync(
|
||||||
@@ -1001,6 +1010,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/allow_new_users")]
|
[HttpPost("roomserver/rooms/{roomId:long}/allow_new_users")]
|
||||||
[HttpPut("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")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareAllowNewUsers(long roomId)
|
public async Task<IActionResult> BareAllowNewUsers(long roomId)
|
||||||
{
|
{
|
||||||
var value = await ReadBareBoolAsync("allowNewUsers", "AllowNewUsers", "value", "Value");
|
var value = await ReadBareBoolAsync("allowNewUsers", "AllowNewUsers", "value", "Value");
|
||||||
@@ -1015,6 +1025,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/min_level")]
|
[HttpPost("roomserver/rooms/{roomId:long}/min_level")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/min_level")]
|
[HttpPut("roomserver/rooms/{roomId:long}/min_level")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareMinLevel(long roomId)
|
public async Task<IActionResult> BareMinLevel(long roomId)
|
||||||
{
|
{
|
||||||
var value = await ReadBareIntAsync("minLevel", "MinLevel", "level", "Level", "value", "Value");
|
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")]
|
[HttpPost("roomserver/rooms/{roomId:long}/max_player_calculation_mode")]
|
||||||
[HttpPut("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")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareMaxPlayerCalculationMode(long roomId)
|
public async Task<IActionResult> BareMaxPlayerCalculationMode(long roomId)
|
||||||
{
|
{
|
||||||
var value = await ReadBareIntAsync(
|
var value = await ReadBareIntAsync(
|
||||||
@@ -1054,6 +1066,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/loadscreen")]
|
[HttpPost("roomserver/rooms/{roomId:long}/loadscreen")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/loadscreen")]
|
[HttpPut("roomserver/rooms/{roomId:long}/loadscreen")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareLoadScreen(long roomId)
|
public async Task<IActionResult> BareLoadScreen(long roomId)
|
||||||
{
|
{
|
||||||
var payload = await ReadPayloadAsync();
|
var payload = await ReadPayloadAsync();
|
||||||
@@ -1085,6 +1098,7 @@ public class RoomsModerationController(
|
|||||||
[HttpDelete("rooms/{roomId:long}/loadscreen")]
|
[HttpDelete("rooms/{roomId:long}/loadscreen")]
|
||||||
[HttpDelete("roomserver/rooms/{roomId:long}/loadscreen")]
|
[HttpDelete("roomserver/rooms/{roomId:long}/loadscreen")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareLoadScreenRemove(long roomId)
|
public async Task<IActionResult> BareLoadScreenRemove(long roomId)
|
||||||
{
|
{
|
||||||
var imageName = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value")
|
var imageName = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value")
|
||||||
@@ -1121,6 +1135,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/promo_images")]
|
[HttpPost("roomserver/rooms/{roomId:long}/promo_images")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/promo_images")]
|
[HttpPut("roomserver/rooms/{roomId:long}/promo_images")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BarePromoImagesAdd(long roomId)
|
public async Task<IActionResult> BarePromoImagesAdd(long roomId)
|
||||||
{
|
{
|
||||||
var image = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value");
|
var image = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value");
|
||||||
@@ -1137,6 +1152,7 @@ public class RoomsModerationController(
|
|||||||
[HttpDelete("rooms/{roomId:long}/promo_images")]
|
[HttpDelete("rooms/{roomId:long}/promo_images")]
|
||||||
[HttpDelete("roomserver/rooms/{roomId:long}/promo_images")]
|
[HttpDelete("roomserver/rooms/{roomId:long}/promo_images")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BarePromoImagesRemove(long roomId)
|
public async Task<IActionResult> BarePromoImagesRemove(long roomId)
|
||||||
{
|
{
|
||||||
var image = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value");
|
var image = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value");
|
||||||
@@ -1167,6 +1183,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/promo_external")]
|
[HttpPost("roomserver/rooms/{roomId:long}/promo_external")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/promo_external")]
|
[HttpPut("roomserver/rooms/{roomId:long}/promo_external")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BarePromoExternalAdd(long roomId)
|
public async Task<IActionResult> BarePromoExternalAdd(long roomId)
|
||||||
{
|
{
|
||||||
var payload = await ReadPayloadAsync();
|
var payload = await ReadPayloadAsync();
|
||||||
@@ -1197,6 +1214,7 @@ public class RoomsModerationController(
|
|||||||
[HttpDelete("rooms/{roomId:long}/promo_external")]
|
[HttpDelete("rooms/{roomId:long}/promo_external")]
|
||||||
[HttpDelete("roomserver/rooms/{roomId:long}/promo_external")]
|
[HttpDelete("roomserver/rooms/{roomId:long}/promo_external")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BarePromoExternalRemove(long roomId)
|
public async Task<IActionResult> BarePromoExternalRemove(long roomId)
|
||||||
{
|
{
|
||||||
var payload = await ReadPayloadAsync();
|
var payload = await ReadPayloadAsync();
|
||||||
@@ -1275,6 +1293,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/voice_chat_encryption")]
|
[HttpPost("roomserver/rooms/{roomId:long}/voice_chat_encryption")]
|
||||||
[HttpPut("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")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareAck(long roomId) =>
|
public async Task<IActionResult> BareAck(long roomId) =>
|
||||||
await ApplyAndReturn(roomId, _ => { /* value not persistable: no column */ });
|
await ApplyAndReturn(roomId, _ => { /* value not persistable: no column */ });
|
||||||
|
|
||||||
@@ -1305,6 +1324,7 @@ public class RoomsModerationController(
|
|||||||
[HttpPost("roomserver/rooms/{roomId:long}/modify")]
|
[HttpPost("roomserver/rooms/{roomId:long}/modify")]
|
||||||
[HttpPut("roomserver/rooms/{roomId:long}/modify")]
|
[HttpPut("roomserver/rooms/{roomId:long}/modify")]
|
||||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||||
|
[RecNetEnvelope]
|
||||||
public async Task<IActionResult> BareModify(long roomId)
|
public async Task<IActionResult> BareModify(long roomId)
|
||||||
{
|
{
|
||||||
var payload = await ReadPayloadAsync();
|
var payload = await ReadPayloadAsync();
|
||||||
@@ -1914,7 +1934,13 @@ public class RoomsModerationController(
|
|||||||
/// <see cref="BareClone"/>). The slim RoomService.ToWireRoom has no
|
/// <see cref="BareClone"/>). The slim RoomService.ToWireRoom has no
|
||||||
/// SubRooms/Roles/Tags/LoadScreens/PromoImages/DataBlob, so the strict
|
/// SubRooms/Roles/Tags/LoadScreens/PromoImages/DataBlob, so the strict
|
||||||
/// reader's dispose-walk NRE'd on the missing nested lists and every
|
/// 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)
|
private async Task<object> BuildRoomDetailsAsync(RoomEntity room)
|
||||||
{
|
{
|
||||||
var scenes = await db.RoomScenes
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -624,7 +624,33 @@ Diffed all 45 real HTTP routes of the 2023-03-21 client's rooms subsystem agains
|
|||||||
|
|
||||||
#### Sub-room ids, IsRRO and the instance browser
|
#### Sub-room ids, IsRRO and the instance browser
|
||||||
|
|
||||||
These three cut across the whole rooms surface.
|
These cut across the whole rooms surface.
|
||||||
|
|
||||||
|
- **Every room and playlist mutation answers `{success, value, error}`.**
|
||||||
|
They all go through one generic helper,
|
||||||
|
`NLDBPDCNNCF.BDBHCEGKGDC<TResult>(opName, verb, route, …)` (RVA 0x35E50A0,
|
||||||
|
`Call 0x1835E50A0` at 47 call sites in NLDBPDCNNCF.txt). It is the same
|
||||||
|
helper and the same `BDBHCEGKGDC<FGCPNAACHIK>` instantiation token
|
||||||
|
(`[0x1884C1948]`) that "clone room" uses (NLDBPDCNNCF.txt:4815-4823), and
|
||||||
|
clone is proven in game to need the envelope. A bare room-details body has
|
||||||
|
no `success`; the helper rejects it with no message, and the player sees
|
||||||
|
"Exception of type 'CEMNLBKJABA' was thrown." (CEMNLBKJABA is the RecNet
|
||||||
|
request exception, base BCPHBJHOIEM : Exception, built with a null
|
||||||
|
message) although the server applied the change. This is what broke the
|
||||||
|
co-owner invite in game after the role fixes. Routes: 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} and /invite, rooms/{0}/subrooms (POST),
|
||||||
|
rooms/{0}/subrooms/{1} (DELETE) and its accessibility, clone, maxplayers,
|
||||||
|
modify, move, name, permissions, publish_save, plus playlists (POST) and
|
||||||
|
playlists/{0}/{accessibility, description, image, levelvoting, name,
|
||||||
|
restrictions, rooms/{1}, tags, warning}. `value` is the full FGCPNAACHIK
|
||||||
|
(playlist DTO for playlists). Refusals are HTTP 200 with `success:false`
|
||||||
|
and a message in `error`, since a 403/404 also shows only the message-less
|
||||||
|
exception. Server: `Controllers/RecNetEnvelopeAttribute.cs` on each handler.
|
||||||
|
Wherever a row below says "FGCPNAACHIK full room details" for one of these
|
||||||
|
routes, read it as the envelope's `value`.
|
||||||
|
|
||||||
- **Sub-room ids are OrderIndex + 1 on the wire.** The client refuses to load
|
- **Sub-room ids are OrderIndex + 1 on the wire.** The client refuses to load
|
||||||
a sub-room whose id is not positive: `SubroomModel.DOKCPLJOKIE`
|
a sub-room whose id is not positive: `SubroomModel.DOKCPLJOKIE`
|
||||||
|
|||||||
Reference in New Issue
Block a user