mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
feat: Add account privacy settings endpoint
- Implemented new endpoints for retrieving account privacy settings. - Added fields for privacy preferences such as profile visibility, online status, and activity visibility. feat: Extend play menu tags functionality - Updated PlayMenuTagsRequest to include TrendingTags. - Modified SetPlayMenuTags to handle trending tags. - Enhanced logging to include trending tags. fix: Increase maximum archive size for room zip imports - Updated MaxArchiveBytes from 12GB to 25GB in RoomZipImportController. feat: Improve CDN image handling - Added local image path resolution for room images. - Implemented lazy loading of room image aliases from JSON file. feat: Enhance match player presence resolution - Updated ResolveDataBlobForPresenceAsync to consider sub-room IDs and private instance status. feat: Extend RoomEntity with new properties - Added AllowNewUsers, MinLevel, MaxPlayerCalculationMode, LoadScreensJson, PromoImagesJson, and PromoExternalContentJson fields. feat: Update database schema for new room properties - Added new columns to the Rooms table for AllowNewUsers, MinLevel, MaxPlayerCalculationMode, LoadScreensJson, PromoImagesJson, and PromoExternalContentJson. feat: Implement platform notifications controller - Created PlatformNotificationsController for managing user notification preferences. - Added endpoints for retrieving and setting notification preferences and categories.
This commit is contained in:
@@ -13,6 +13,7 @@ namespace DorkNet.Server.Controllers.API.CustomAvatarItems;
|
||||
[ApiController]
|
||||
[Route("api/customAvatarItems")]
|
||||
[Route("api/customAvatarItems/v1")]
|
||||
[Route("api/customAvatarItems/v2")]
|
||||
public class CustomAvatarItemsController(
|
||||
DorkNetDbContext db,
|
||||
LevelService level,
|
||||
@@ -110,6 +111,26 @@ public class CustomAvatarItemsController(
|
||||
return Ok(rows.Select(ToWire));
|
||||
}
|
||||
|
||||
[HttpGet("fromCreator/{creatorId:long}")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> FromCreator(
|
||||
long creatorId,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 100)
|
||||
{
|
||||
skip = Math.Max(0, skip);
|
||||
take = Math.Clamp(take, 1, 100);
|
||||
var currentPlayerId = this.CurrentPlayerId();
|
||||
var rows = await db.CustomAvatarItems
|
||||
.Where(i => i.CreatorPlayerId == creatorId
|
||||
&& (i.IsPublic || i.CreatorPlayerId == currentPlayerId))
|
||||
.OrderByDescending(i => i.UpdatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
return Ok(rows.Select(ToWire));
|
||||
}
|
||||
|
||||
[HttpGet("me")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> Me([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||
|
||||
@@ -396,19 +396,63 @@ public class ImagesController(
|
||||
[HttpGet("api/images/v3/byaccount/{accountId:long}")]
|
||||
[HttpGet("api/images/v4/byaccount/{accountId:long}")]
|
||||
[HttpGet("api/images/v5/byaccount/{accountId:long}")]
|
||||
[HttpGet("api/images/v2/player/{accountId:long}")]
|
||||
[HttpGet("api/images/v3/player/{accountId:long}")]
|
||||
[HttpGet("api/images/v4/player/{accountId:long}")]
|
||||
[HttpGet("api/images/v5/player/{accountId:long}")]
|
||||
public async Task<IActionResult> GetByAccount(long accountId,
|
||||
[FromQuery] int take = 50, [FromQuery] int skip = 0)
|
||||
[FromQuery] int take = 50, [FromQuery] int skip = 0, [FromQuery] int sort = 0)
|
||||
{
|
||||
take = Math.Clamp(take, 1, 200);
|
||||
skip = Math.Max(0, skip);
|
||||
var rows = await db.Photos
|
||||
var query = db.Photos
|
||||
.Where(p => p.UploaderPlayerId == accountId && p.IsPublic && p.DeletedAt == null)
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.Skip(skip).Take(take)
|
||||
.AsQueryable();
|
||||
query = sort switch
|
||||
{
|
||||
1 => query.OrderByDescending(p => p.CheerCount).ThenByDescending(p => p.CreatedAt),
|
||||
2 => query.OrderByDescending(p => p.ViewCount).ThenByDescending(p => p.CreatedAt),
|
||||
_ => query.OrderByDescending(p => p.CreatedAt),
|
||||
};
|
||||
|
||||
var rows = await query
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
return Ok(rows.Select(p => BuildImageInfo(p, accountId)));
|
||||
}
|
||||
|
||||
[HttpGet("api/images/v2/room/{roomId:long}")]
|
||||
[HttpGet("api/images/v3/room/{roomId:long}")]
|
||||
[HttpGet("api/images/v4/room/{roomId:long}")]
|
||||
[HttpGet("api/images/v5/room/{roomId:long}")]
|
||||
public async Task<IActionResult> GetByRoom(
|
||||
long roomId,
|
||||
[FromQuery] int take = 50,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int sort = 0,
|
||||
[FromQuery] int filter = 0)
|
||||
{
|
||||
take = Math.Clamp(take, 1, 200);
|
||||
skip = Math.Max(0, skip);
|
||||
|
||||
var query = db.Photos
|
||||
.Where(p => p.RoomId == roomId && p.IsPublic && p.DeletedAt == null)
|
||||
.AsQueryable();
|
||||
query = sort switch
|
||||
{
|
||||
1 => query.OrderByDescending(p => p.CheerCount).ThenByDescending(p => p.CreatedAt),
|
||||
2 => query.OrderByDescending(p => p.ViewCount).ThenByDescending(p => p.CreatedAt),
|
||||
_ => query.OrderByDescending(p => p.CreatedAt),
|
||||
};
|
||||
|
||||
var rows = await query
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
return Ok(rows.Select(p => BuildImageInfo(p, p.UploaderPlayerId)));
|
||||
}
|
||||
|
||||
/// <summary>POST api/images/v{2-5}/share/{id} — flip a saved photo
|
||||
/// to public so it shows up on feed.rec.net. The watch's "Share"
|
||||
/// button hits this; if the watch posts to a different URL we'll
|
||||
|
||||
@@ -56,6 +56,30 @@ public class KeepsakesController(DorkNetDbContext db, DomainConfig domain) : Con
|
||||
return Ok(rows.Select(ToWire));
|
||||
}
|
||||
|
||||
[HttpGet("events/{eventId:long}")]
|
||||
public async Task<IActionResult> EventInstances(long eventId)
|
||||
{
|
||||
var eventKey = eventId.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
var rows = await db.Keepsakes
|
||||
.Where(k => k.PlayerId == Me
|
||||
&& k.Category == "event"
|
||||
&& (k.EventKey == eventKey
|
||||
|| k.EventKey == $"event:{eventKey}"
|
||||
|| k.EventKey == $"event/{eventKey}"
|
||||
|| EF.Functions.Like(k.EventKey, $"event:{eventKey}:%")
|
||||
|| EF.Functions.Like(k.EventKey, $"event/{eventKey}/%")))
|
||||
.OrderByDescending(k => k.EarnedAt)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
KeepsakeProgressionEventId = eventId,
|
||||
Instances = rows.Select(ToWire),
|
||||
CollectionRecords = Array.Empty<object>(),
|
||||
KeepsakeProgressionEventIds = new[] { eventId },
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId:long}")]
|
||||
public async Task<IActionResult> RoomKeepsakes(long roomId)
|
||||
{
|
||||
|
||||
@@ -74,6 +74,95 @@ public class RoomsController(
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("rooms/topcreators")]
|
||||
[HttpGet("roomserver/rooms/topcreators")]
|
||||
public async Task<IActionResult> TopCreators([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||
{
|
||||
var rows = await PublicRoomQuery()
|
||||
.OrderByDescending(r => r.HotScore)
|
||||
.ThenByDescending(r => r.VisitCount)
|
||||
.Take(250)
|
||||
.ToListAsync();
|
||||
var page = rows
|
||||
.GroupBy(r => r.CreatorPlayerId)
|
||||
.Select(g => g.First())
|
||||
.Skip(Math.Max(skip, 0))
|
||||
.Take(Math.Clamp(take, 1, 100))
|
||||
.ToList();
|
||||
return Ok(await BuildRoomServerListAsync(page));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/contestwinners")]
|
||||
[HttpGet("roomserver/rooms/contestwinners")]
|
||||
public async Task<IActionResult> ContestWinners([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||
{
|
||||
var rows = await PublicRoomQuery()
|
||||
.Where(r =>
|
||||
EF.Functions.Like(r.TagsCsv, "%contest%") ||
|
||||
EF.Functions.Like(r.TagsCsv, "%winner%") ||
|
||||
EF.Functions.Like(r.TagsCsv, "%featured%"))
|
||||
.OrderByDescending(r => r.HotScore)
|
||||
.ThenBy(r => r.Name)
|
||||
.Skip(Math.Max(skip, 0))
|
||||
.Take(Math.Clamp(take, 1, 100))
|
||||
.ToListAsync();
|
||||
|
||||
if (rows.Count == 0)
|
||||
{
|
||||
rows = await PublicRoomQuery()
|
||||
.OrderByDescending(r => r.HotScore)
|
||||
.ThenBy(r => r.Name)
|
||||
.Skip(Math.Max(skip, 0))
|
||||
.Take(Math.Clamp(take, 1, 100))
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
return Ok(await BuildRoomServerListAsync(rows));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/fromcreators")]
|
||||
[HttpPost("rooms/fromcreators")]
|
||||
[HttpGet("roomserver/rooms/fromcreators")]
|
||||
[HttpPost("roomserver/rooms/fromcreators")]
|
||||
public async Task<IActionResult> FromCreators([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||
{
|
||||
var creatorIds = Request.Query["creatorId"]
|
||||
.Concat(Request.Query["creatorIds"])
|
||||
.Concat(Request.Query["accountId"])
|
||||
.Concat(Request.Query["accountIds"])
|
||||
.Concat(Request.Query["id"])
|
||||
.SelectMany(v => (v ?? string.Empty).Split(',',
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
.Select(v => long.TryParse(v, out var id) ? id : 0)
|
||||
.Where(id => id > 0)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
var query = PublicRoomQuery();
|
||||
if (creatorIds.Count > 0)
|
||||
query = query.Where(r => creatorIds.Contains(r.CreatorPlayerId));
|
||||
|
||||
var rows = await query
|
||||
.OrderByDescending(r => r.HotScore)
|
||||
.ThenByDescending(r => r.UpdatedAt)
|
||||
.Skip(Math.Max(skip, 0))
|
||||
.Take(Math.Clamp(take, 1, 100))
|
||||
.ToListAsync();
|
||||
return Ok(await BuildRoomServerListAsync(rows));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/magic_door")]
|
||||
[HttpGet("roomserver/rooms/magic_door")]
|
||||
public async Task<IActionResult> MagicDoor()
|
||||
{
|
||||
var room = await PublicRoomQuery()
|
||||
.OrderByDescending(r => r.HotScore)
|
||||
.ThenBy(r => r.Id)
|
||||
.FirstOrDefaultAsync();
|
||||
if (room is null) return NotFound();
|
||||
return Ok((await BuildRoomServerListAsync(new[] { room })).First());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET <c>/roomserver/rooms/recommendations</c> — the home-tab
|
||||
/// "Recommended" carousel. <b>Wire shape is NOT a flat
|
||||
@@ -127,6 +216,38 @@ public class RoomsController(
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId:long}/similar")]
|
||||
[HttpGet("roomserver/rooms/{roomId:long}/similar")]
|
||||
public async Task<IActionResult> SimilarRooms(long roomId, [FromQuery] int take = 12)
|
||||
{
|
||||
var seed = await rooms.GetByIdAsync(roomId);
|
||||
if (seed is null) return NotFound();
|
||||
|
||||
var tags = (seed.TagsCsv ?? string.Empty)
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToArray();
|
||||
var query = PublicRoomQuery().Where(r => r.Id != roomId);
|
||||
if (tags.Length > 0)
|
||||
{
|
||||
var primaryTag = tags[0];
|
||||
query = query.Where(r => EF.Functions.Like(r.TagsCsv, $"%{primaryTag}%"));
|
||||
}
|
||||
|
||||
var similar = await query
|
||||
.OrderByDescending(r => r.HotScore)
|
||||
.ThenBy(r => r.Name)
|
||||
.Take(Math.Clamp(take, 1, 100))
|
||||
.ToListAsync();
|
||||
|
||||
var seedWire = (await BuildRoomServerListAsync(new[] { seed })).First();
|
||||
var similarWire = await BuildRoomServerListAsync(similar);
|
||||
return Ok(new Dictionary<string, object>(StringComparer.Ordinal)
|
||||
{
|
||||
["SeedRoom"] = seedWire,
|
||||
["Rooms"] = similarWire,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// `Rooms.SearchForRooms(query)` — free-text room search.
|
||||
/// </summary>
|
||||
@@ -172,6 +293,7 @@ public class RoomsController(
|
||||
{
|
||||
PinnedFilters = tags.PinnedTags,
|
||||
PopularFilters = tags.PopularTags,
|
||||
TrendingFilters = tags.TrendingTags,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,7 +301,11 @@ public class RoomsController(
|
||||
public async Task<IActionResult> Tags()
|
||||
{
|
||||
var tags = await serverSettings.GetPlayMenuTagsAsync();
|
||||
return Ok(tags.PinnedTags.Concat(tags.PopularTags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray());
|
||||
return Ok(tags.PinnedTags
|
||||
.Concat(tags.PopularTags)
|
||||
.Concat(tags.TrendingTags)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
[HttpGet("api/rooms/v1/pinnedtags")]
|
||||
@@ -190,6 +316,10 @@ public class RoomsController(
|
||||
public async Task<IActionResult> PopularTags()
|
||||
=> Ok((await serverSettings.GetPlayMenuTagsAsync()).PopularTags);
|
||||
|
||||
[HttpGet("api/rooms/v1/trendingtags")]
|
||||
public async Task<IActionResult> TrendingTags()
|
||||
=> Ok((await serverSettings.GetPlayMenuTagsAsync()).TrendingTags);
|
||||
|
||||
// ── Single room lookups ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
@@ -1388,10 +1518,19 @@ public class RoomsController(
|
||||
SupportsWalkVR = src.SupportsWalkVR,
|
||||
SupportsTeleportVR = src.SupportsTeleportVR,
|
||||
AllowsJuniors = src.AllowsJuniors,
|
||||
AllowNewUsers = src.AllowNewUsers,
|
||||
MinLevel = src.MinLevel,
|
||||
MaxPlayerCalculationMode = src.MaxPlayerCalculationMode,
|
||||
LoadScreensJson = src.LoadScreensJson,
|
||||
PromoImagesJson = src.PromoImagesJson,
|
||||
PromoExternalContentJson = src.PromoExternalContentJson,
|
||||
RoomWarningMask = src.RoomWarningMask,
|
||||
CustomRoomWarning = src.CustomRoomWarning,
|
||||
DisableMicAutoMute = src.DisableMicAutoMute,
|
||||
LocationReplicationId = src.LocationReplicationId,
|
||||
IsStudioRoom = src.IsStudioRoom,
|
||||
IsRoomLinkedToRecRoomStudio = src.IsRoomLinkedToRecRoomStudio,
|
||||
StudioSessionId = src.StudioSessionId,
|
||||
TagsCsv = src.TagsCsv,
|
||||
CheerCount = src.CheerCount,
|
||||
FavoriteCount = src.FavoriteCount,
|
||||
@@ -1425,7 +1564,12 @@ public class RoomsController(
|
||||
? Array.Empty<object>()
|
||||
: room.TagsCsv
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(t => (object)new { Type = 0, Tag = t })
|
||||
.Select(t => (object)new
|
||||
{
|
||||
Type = RoomTagTypeForWire(t),
|
||||
Tag = t,
|
||||
IsPrimaryGenre = false,
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
// Scenes population: every room must produce a Scenes[0] entry
|
||||
@@ -1710,7 +1854,12 @@ public class RoomsController(
|
||||
? Array.Empty<object>()
|
||||
: room.TagsCsv
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(t => (object)new { Type = 0, Tag = t })
|
||||
.Select(t => (object)new
|
||||
{
|
||||
Type = RoomTagTypeForWire(t),
|
||||
Tag = t,
|
||||
IsPrimaryGenre = false,
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return new
|
||||
@@ -1733,6 +1882,9 @@ public class RoomsController(
|
||||
SupportsQuest2 = true,
|
||||
SupportsMobile = room.SupportsMobile,
|
||||
SupportsJuniors = room.AllowsJuniors,
|
||||
AllowNewUsers = room.AllowNewUsers,
|
||||
AllowsNewUsers = room.AllowNewUsers,
|
||||
MinLevel = room.MinLevel,
|
||||
CreatedAt = (room.CreatedAt == default ? DateTime.UtcNow : room.CreatedAt)
|
||||
.ToString("yyyy-MM-ddTHH:mm:ssZ"),
|
||||
Stats = new
|
||||
@@ -1743,10 +1895,14 @@ public class RoomsController(
|
||||
VisitCount = room.VisitCount,
|
||||
},
|
||||
IsDorm = room.IsDormRoom,
|
||||
IsStudioRoom = room.IsStudioRoom,
|
||||
IsRoomLinkedToRecRoomStudio = room.IsRoomLinkedToRecRoomStudio,
|
||||
StudioSessionId = room.StudioSessionId,
|
||||
CloningAllowed = room.CloningAllowed,
|
||||
DisableMicAutoMute = room.DisableMicAutoMute,
|
||||
DisableRoomComments = false,
|
||||
EncryptVoiceChat = false,
|
||||
LoadScreenLocked = false,
|
||||
// Studio/UGC version family. The 2023 client reads UgcVersion to
|
||||
// decide the asset-bundle version it requests
|
||||
// (unity_assets/{id}/{target}/{UgcVersion}) and PersistenceVersion
|
||||
@@ -1758,17 +1914,74 @@ public class RoomsController(
|
||||
UgcVersion = room.IsStudioRoom ? StudioUgcVersion : 0,
|
||||
UgcSubVersion = room.IsStudioRoom ? StudioPersistenceVersion : 0,
|
||||
MinUgcSubVersion = room.IsStudioRoom ? StudioPersistenceVersion : 0,
|
||||
IsDeveloperOwned = room.IsStudioRoom || HasRoomTag(room, "developer"),
|
||||
IsJuniorCreated = false,
|
||||
IsRecRoomApproved = false,
|
||||
ExcludeFromLists = false,
|
||||
BoostCount = 0,
|
||||
RestrictedCircuitsAllowListNames = Array.Empty<string>(),
|
||||
PublishStateAvailability = new
|
||||
{
|
||||
CanSaveAsBeta = false,
|
||||
CanSaveAsUpdate = true,
|
||||
AvailableUpdateTokenCount = 3,
|
||||
NextAvailableUpdateDateTimeUtc = (string?)null,
|
||||
},
|
||||
PublishState = 0,
|
||||
MaxPlayerCalculationMode = 0,
|
||||
MaxPlayerCalculationMode = room.MaxPlayerCalculationMode,
|
||||
SubRooms = subRooms,
|
||||
Roles = wireRoles,
|
||||
LoadScreens = Array.Empty<object>(),
|
||||
PromoImages = Array.Empty<string>(),
|
||||
PromoExternalContent = Array.Empty<object>(),
|
||||
LoadScreens = JsonArrayOrEmpty(room.LoadScreensJson),
|
||||
PromoImages = JsonStringArrayOrEmpty(room.PromoImagesJson),
|
||||
PromoExternalContent = JsonArrayOrEmpty(room.PromoExternalContentJson),
|
||||
Tags = tags,
|
||||
};
|
||||
}
|
||||
|
||||
private static int RoomTagTypeForWire(string tag)
|
||||
{
|
||||
if (string.Equals(tag, "beta", StringComparison.OrdinalIgnoreCase)) return 1;
|
||||
return tag.ToLowerInvariant() switch
|
||||
{
|
||||
"rrstudio" or "community" or "screen" or "walkvr" or "teleportvr" or "junior" or "pickup" => 2,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasRoomTag(RoomEntity room, string tag)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(room.TagsCsv) &&
|
||||
room.TagsCsv
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Any(t => string.Equals(t, tag, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static JsonElement[] JsonArrayOrEmpty(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return Array.Empty<JsonElement>();
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<JsonElement[]>(json) ?? Array.Empty<JsonElement>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Array.Empty<JsonElement>();
|
||||
}
|
||||
}
|
||||
|
||||
private static string[] JsonStringArrayOrEmpty(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return Array.Empty<string>();
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<string[]>(json) ?? Array.Empty<string>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>PersistenceVersion / UgcSubVersion stamped on a baked Studio
|
||||
/// scene's CurrentSave. The 2023 client's room-load gate treats anything
|
||||
/// below the Studio-asset-bundle era as a legacy non-baked save and skips
|
||||
@@ -1845,7 +2058,9 @@ public class RoomsController(
|
||||
[HttpGet("api/rooms/v2/createdby/{otherPlayerId:long}")]
|
||||
[HttpGet("api/rooms/v3/createdby/{otherPlayerId:long}")]
|
||||
[HttpGet("roomserver/rooms/createdby/{otherPlayerId:long}")]
|
||||
[HttpGet("roomserver/rooms/ownedby/{otherPlayerId:long}")]
|
||||
[HttpGet("rooms/createdby/{otherPlayerId:long}")]
|
||||
[HttpGet("rooms/ownedby/{otherPlayerId:long}")]
|
||||
public async Task<IActionResult> CreatedByOther(long otherPlayerId)
|
||||
=> Ok((await rooms.CreatedByAsync(otherPlayerId)).Select(RoomService.ToWireRoom).ToList());
|
||||
|
||||
@@ -1855,7 +2070,36 @@ public class RoomsController(
|
||||
[HttpGet("roomserver/rooms/visitedby/me")]
|
||||
[HttpGet("rooms/visitedby/me")]
|
||||
[Authorize]
|
||||
public IActionResult VisitedByMe() => Ok(Array.Empty<object>());
|
||||
public async Task<IActionResult> VisitedByMe()
|
||||
{
|
||||
var pid = CurrentPlayerId;
|
||||
if (pid is null) return Ok(Array.Empty<object>());
|
||||
return await VisitedBy(pid.Value);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/visitedby/{playerId:long}")]
|
||||
[HttpGet("roomserver/rooms/visitedby/{playerId:long}")]
|
||||
public async Task<IActionResult> VisitedBy(long playerId)
|
||||
{
|
||||
var ids = await db.RoomVisits.AsNoTracking()
|
||||
.Where(v => v.PlayerId == playerId)
|
||||
.OrderByDescending(v => v.LastVisitAt)
|
||||
.Select(v => v.RoomId)
|
||||
.Take(100)
|
||||
.ToListAsync();
|
||||
if (ids.Count == 0) return Ok(Array.Empty<object>());
|
||||
|
||||
var rows = await db.Rooms.AsNoTracking()
|
||||
.Where(r => ids.Contains(r.Id))
|
||||
.ToListAsync();
|
||||
var byId = rows.ToDictionary(r => r.Id);
|
||||
var ordered = ids
|
||||
.Select(id => byId.TryGetValue(id, out var room) ? room : null)
|
||||
.Where(r => r is not null)
|
||||
.Select(r => r!)
|
||||
.ToList();
|
||||
return Ok(await BuildRoomServerListAsync(ordered));
|
||||
}
|
||||
|
||||
[HttpGet("api/rooms/v1/moderatedby/me")]
|
||||
[HttpGet("api/rooms/v2/moderatedby/me")]
|
||||
@@ -1863,7 +2107,23 @@ public class RoomsController(
|
||||
[HttpGet("roomserver/rooms/moderatedby/me")]
|
||||
[HttpGet("rooms/moderatedby/me")]
|
||||
[Authorize]
|
||||
public IActionResult ModeratedByMe() => Ok(Array.Empty<object>());
|
||||
public async Task<IActionResult> ModeratedByMe()
|
||||
{
|
||||
var pid = CurrentPlayerId;
|
||||
if (pid is null) return Ok(Array.Empty<object>());
|
||||
var ids = await db.RoomRoles.AsNoTracking()
|
||||
.Where(r => r.PlayerId == pid.Value && r.Accepted)
|
||||
.OrderByDescending(r => r.GrantedAt)
|
||||
.Select(r => r.RoomId)
|
||||
.Distinct()
|
||||
.Take(100)
|
||||
.ToListAsync();
|
||||
var rows = await db.Rooms.AsNoTracking()
|
||||
.Where(r => ids.Contains(r.Id))
|
||||
.OrderByDescending(r => r.UpdatedAt)
|
||||
.ToListAsync();
|
||||
return Ok(await BuildRoomServerListAsync(rows));
|
||||
}
|
||||
|
||||
[HttpGet("api/rooms/v1/favoritedby/me")]
|
||||
[HttpGet("api/rooms/v2/favoritedby/me")]
|
||||
@@ -1871,7 +2131,13 @@ public class RoomsController(
|
||||
[HttpGet("roomserver/rooms/favoritedby/me")]
|
||||
[HttpGet("rooms/favoritedby/me")]
|
||||
[Authorize]
|
||||
public IActionResult FavoritedByMe() => Ok(Array.Empty<object>());
|
||||
public async Task<IActionResult> FavoritedByMe()
|
||||
{
|
||||
var pid = CurrentPlayerId;
|
||||
if (pid is null) return Ok(Array.Empty<object>());
|
||||
var rows = await rooms.BookmarkedByAsync(pid.Value);
|
||||
return Ok(await BuildRoomServerListAsync(rows));
|
||||
}
|
||||
|
||||
[HttpGet("api/rooms/v1/cheeredby/me")]
|
||||
[HttpGet("api/rooms/v2/cheeredby/me")]
|
||||
@@ -1879,7 +2145,23 @@ public class RoomsController(
|
||||
[HttpGet("roomserver/rooms/cheeredby/me")]
|
||||
[HttpGet("rooms/cheeredby/me")]
|
||||
[Authorize]
|
||||
public IActionResult CheeredByMe() => Ok(Array.Empty<object>());
|
||||
public async Task<IActionResult> CheeredByMe()
|
||||
{
|
||||
var pid = CurrentPlayerId;
|
||||
if (pid is null) return Ok(Array.Empty<object>());
|
||||
var ids = await db.Cheers.AsNoTracking()
|
||||
.Where(c => c.FromPlayerId == pid.Value && c.TargetRoomId != 0)
|
||||
.OrderByDescending(c => c.CheeredAt)
|
||||
.Select(c => c.TargetRoomId)
|
||||
.Distinct()
|
||||
.Take(100)
|
||||
.ToListAsync();
|
||||
var rows = await db.Rooms.AsNoTracking()
|
||||
.Where(r => ids.Contains(r.Id))
|
||||
.OrderByDescending(r => r.HotScore)
|
||||
.ToListAsync();
|
||||
return Ok(await BuildRoomServerListAsync(rows));
|
||||
}
|
||||
|
||||
[HttpGet("api/rooms/v2/mybookmarks")]
|
||||
[HttpGet("api/rooms/v2/mybookmarkedrooms")]
|
||||
@@ -2061,6 +2343,40 @@ public class RoomsController(
|
||||
return Ok(new { success = true, error = "" });
|
||||
}
|
||||
|
||||
private IQueryable<RoomEntity> PublicRoomQuery() =>
|
||||
db.Rooms.AsNoTracking().Where(r =>
|
||||
r.State == 0 &&
|
||||
r.Accessibility == 1 &&
|
||||
!r.IsDormRoom &&
|
||||
!r.HiddenFromBrowse);
|
||||
|
||||
private async Task<List<object>> BuildRoomServerListAsync(IReadOnlyCollection<RoomEntity> roomRows)
|
||||
{
|
||||
if (roomRows.Count == 0) return new List<object>();
|
||||
|
||||
var ids = roomRows.Select(r => r.Id).Distinct().ToList();
|
||||
var sceneRows = await db.RoomScenes.AsNoTracking()
|
||||
.Where(s => ids.Contains(s.RoomId))
|
||||
.OrderBy(s => s.OrderIndex)
|
||||
.ToListAsync();
|
||||
var scenesByRoom = sceneRows
|
||||
.GroupBy(s => s.RoomId)
|
||||
.ToDictionary(g => g.Key, g => (IReadOnlyList<RoomSceneEntity>)g.ToList());
|
||||
var roleRows = await db.RoomRoles.AsNoTracking()
|
||||
.Where(r => ids.Contains(r.RoomId))
|
||||
.ToListAsync();
|
||||
var rolesByRoom = roleRows
|
||||
.GroupBy(r => r.RoomId)
|
||||
.ToDictionary(g => g.Key, g => (IReadOnlyList<RoomRoleEntity>)g.ToList());
|
||||
|
||||
return roomRows
|
||||
.Select(r => BuildRoomServerDetails(
|
||||
r,
|
||||
scenesByRoom.GetValueOrDefault(r.Id),
|
||||
roles: rolesByRoom.GetValueOrDefault(r.Id)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>GET <c>api/rooms/v1/agRoomIds</c> — list of every
|
||||
@@ -2585,7 +2901,12 @@ public class RoomsController(
|
||||
? new List<object>()
|
||||
: p.TagsCsv
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(t => (object)new { Type = 0, Tag = t })
|
||||
.Select(t => (object)new
|
||||
{
|
||||
Type = RoomTagTypeForWire(t),
|
||||
Tag = t,
|
||||
IsPrimaryGenre = false,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Start from BuildPlaylistUnionEntry for base + PlaylistId, then
|
||||
@@ -2865,6 +3186,15 @@ public class RoomsController(
|
||||
public string? RoomSceneLocationId { get; set; }
|
||||
}
|
||||
public sealed class SubRoomMoveRequest { public int? NewIndex { get; set; } }
|
||||
public sealed class PublishSubRoomSaveRequest
|
||||
{
|
||||
public long? SubRoomDataSaveId { get; set; }
|
||||
public long? SaveId { get; set; }
|
||||
public long? StagedSubRoomDataSaveId { get; set; }
|
||||
public string? DataBlob { get; set; }
|
||||
public string? DataBlobName { get; set; }
|
||||
public string? Filename { get; set; }
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
|
||||
[HttpPut("rooms/{roomId:long}/subrooms/{subRoomId:long}/maxplayers")]
|
||||
@@ -2878,6 +3208,95 @@ public class RoomsController(
|
||||
public Task<IActionResult> SubRoomAccessibility(long roomId, long subRoomId, [FromBody] SubRoomBoolRequest req) =>
|
||||
MutateScene(roomId, subRoomId, s => { if (req.Value is bool v) s.CanMatchmakeInto = v; });
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/permissions")]
|
||||
[HttpPut("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")]
|
||||
[Authorize]
|
||||
public Task<IActionResult> SubRoomPermissions(long roomId, long subRoomId, [FromBody] SubRoomModifyRequest? req) =>
|
||||
MutateScene(roomId, subRoomId, s =>
|
||||
{
|
||||
if (req?.CanMatchmakeInto is bool canMatchmakeInto) s.CanMatchmakeInto = canMatchmakeInto;
|
||||
if (req?.MaxPlayers is int maxPlayers) s.MaxPlayers = Math.Max(1, maxPlayers);
|
||||
if (req?.IsSandbox is bool isSandbox) s.IsSandbox = isSandbox;
|
||||
});
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/subrooms/{subRoomId:long}/publish_save")]
|
||||
[HttpPut("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")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> PublishSubRoomSave(long roomId, long subRoomId)
|
||||
{
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (room.CreatorPlayerId != pid) return Forbid();
|
||||
|
||||
var scene = await GetOrCreateSceneForMutationAsync(room, subRoomId);
|
||||
if (scene is null) return NotFound();
|
||||
|
||||
var request = await ReadPublishSubRoomSaveRequestAsync();
|
||||
var saveId = request.SubRoomDataSaveId ?? request.SaveId ?? request.StagedSubRoomDataSaveId;
|
||||
var dataBlob = request.DataBlobName ?? request.DataBlob ?? request.Filename;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(dataBlob))
|
||||
{
|
||||
var blobExists = await db.RoomDataBlobs.AnyAsync(b => b.RoomId == roomId && b.BlobName == dataBlob);
|
||||
if (!blobExists) return NotFound(new { error = "blob_not_found", dataBlob });
|
||||
scene.DataBlobName = dataBlob;
|
||||
if (scene.OrderIndex == 0) room.CurrentDataBlobName = dataBlob;
|
||||
}
|
||||
|
||||
if (saveId is > 0)
|
||||
scene.StudioSubRoomDataSaveId = saveId.Value;
|
||||
|
||||
scene.DataModifiedAt = DateTime.UtcNow;
|
||||
room.UpdatedAt = scene.DataModifiedAt;
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(SceneWire(scene));
|
||||
}
|
||||
|
||||
private async Task<PublishSubRoomSaveRequest> ReadPublishSubRoomSaveRequestAsync()
|
||||
{
|
||||
var request = new PublishSubRoomSaveRequest();
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
if (long.TryParse(FirstFormValue(form, "subRoomDataSaveId", "SubRoomDataSaveId", "saveId", "SaveId", "stagedSubRoomDataSaveId", "StagedSubRoomDataSaveId"), out var saveId))
|
||||
request.SubRoomDataSaveId = saveId;
|
||||
request.DataBlobName = FirstFormValue(form, "dataBlobName", "DataBlobName", "dataBlob", "DataBlob", "filename", "Filename");
|
||||
return request;
|
||||
}
|
||||
|
||||
if (long.TryParse(Request.Query["subRoomDataSaveId"].FirstOrDefault()
|
||||
?? Request.Query["SubRoomDataSaveId"].FirstOrDefault()
|
||||
?? Request.Query["saveId"].FirstOrDefault()
|
||||
?? Request.Query["SaveId"].FirstOrDefault()
|
||||
?? Request.Query["stagedSubRoomDataSaveId"].FirstOrDefault()
|
||||
?? Request.Query["StagedSubRoomDataSaveId"].FirstOrDefault(), out var querySaveId))
|
||||
request.SubRoomDataSaveId = querySaveId;
|
||||
request.DataBlobName = Request.Query["dataBlobName"].FirstOrDefault()
|
||||
?? Request.Query["DataBlobName"].FirstOrDefault()
|
||||
?? Request.Query["dataBlob"].FirstOrDefault()
|
||||
?? Request.Query["DataBlob"].FirstOrDefault()
|
||||
?? Request.Query["filename"].FirstOrDefault()
|
||||
?? Request.Query["Filename"].FirstOrDefault();
|
||||
|
||||
if ((Request.ContentLength ?? 0) <= 0) return request;
|
||||
try
|
||||
{
|
||||
var body = await JsonSerializer.DeserializeAsync<PublishSubRoomSaveRequest>(
|
||||
Request.Body,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
return body ?? request;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>POST <c>rooms/{id}/subrooms/{sub}/clone</c> — duplicate
|
||||
/// a scene (DataBlobName + location pointer). New scene goes at the
|
||||
/// end with " Copy" suffix; per-room name uniqueness preserved.</summary>
|
||||
@@ -3033,8 +3452,7 @@ public class RoomsController(
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(r => r.Id == roomId);
|
||||
if (room is null) return NotFound();
|
||||
if (room.CreatorPlayerId != pid) return Forbid();
|
||||
var scene = await db.RoomScenes
|
||||
.FirstOrDefaultAsync(s => s.RoomId == roomId && s.OrderIndex == subRoomId);
|
||||
var scene = await GetOrCreateSceneForMutationAsync(room, subRoomId);
|
||||
if (scene is null) return NotFound();
|
||||
mutator(scene);
|
||||
scene.DataModifiedAt = DateTime.UtcNow;
|
||||
@@ -3042,6 +3460,28 @@ public class RoomsController(
|
||||
return Ok(SceneWire(scene));
|
||||
}
|
||||
|
||||
private async Task<RoomSceneEntity?> GetOrCreateSceneForMutationAsync(RoomEntity room, long subRoomId)
|
||||
{
|
||||
var scene = await db.RoomScenes
|
||||
.FirstOrDefaultAsync(s => s.RoomId == room.Id && s.OrderIndex == subRoomId);
|
||||
if (scene is not null || subRoomId != 0) return scene;
|
||||
|
||||
scene = new RoomSceneEntity
|
||||
{
|
||||
RoomId = room.Id,
|
||||
OrderIndex = 0,
|
||||
Name = "Home",
|
||||
RoomSceneLocationId = room.LocationReplicationId,
|
||||
DataBlobName = CurrentOrSyntheticDataBlobName(room),
|
||||
MaxPlayers = room.MaxCapacity,
|
||||
IsSandbox = false,
|
||||
CanMatchmakeInto = true,
|
||||
DataModifiedAt = DateTime.UtcNow,
|
||||
};
|
||||
db.RoomScenes.Add(scene);
|
||||
return scene;
|
||||
}
|
||||
|
||||
private static object SceneWire(RoomSceneEntity s) => new
|
||||
{
|
||||
Result = 0,
|
||||
@@ -3079,6 +3519,9 @@ public class RoomsController(
|
||||
if (room.IsDormRoom)
|
||||
return RoomService.ResolveWireRoomDataBlobName(room.Id, room.CurrentDataBlobName);
|
||||
|
||||
if (RoomService.IsBakedOriginalRoom(room) && string.IsNullOrWhiteSpace(room.CurrentDataBlobName))
|
||||
return string.Empty;
|
||||
|
||||
return !string.IsNullOrWhiteSpace(room.CurrentDataBlobName)
|
||||
? room.CurrentDataBlobName
|
||||
: RoomService.SyntheticDefaultRoomDataBlobName(room.Id);
|
||||
|
||||
@@ -527,6 +527,204 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<long?> ReadBareLongAsync(params string[] keys)
|
||||
{
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var key in keys)
|
||||
if (long.TryParse(form[key].FirstOrDefault(), out var value)) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
if (long.TryParse(Request.Query[key].FirstOrDefault(), out var value)) return value;
|
||||
|
||||
if ((Request.ContentLength ?? 0) <= 0) return null;
|
||||
try
|
||||
{
|
||||
using var doc = await JsonDocument.ParseAsync(Request.Body);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return null;
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (!doc.RootElement.TryGetProperty(key, out var prop)) continue;
|
||||
if (prop.ValueKind == JsonValueKind.Number && prop.TryGetInt64(out var n)) return n;
|
||||
if (prop.ValueKind == JsonValueKind.String && long.TryParse(prop.GetString(), out n)) return n;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<bool?> ReadBareBoolAsync(params string[] keys)
|
||||
{
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var key in keys)
|
||||
if (TryParseBool(form[key].FirstOrDefault(), out var value)) return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
if (TryParseBool(Request.Query[key].FirstOrDefault(), out var value)) return value;
|
||||
|
||||
if ((Request.ContentLength ?? 0) <= 0) return null;
|
||||
try
|
||||
{
|
||||
using var doc = await JsonDocument.ParseAsync(Request.Body);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object)
|
||||
return doc.RootElement.ValueKind == JsonValueKind.True
|
||||
? true
|
||||
: doc.RootElement.ValueKind == JsonValueKind.False
|
||||
? false
|
||||
: null;
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (!doc.RootElement.TryGetProperty(key, out var prop)) continue;
|
||||
if (prop.ValueKind == JsonValueKind.True) return true;
|
||||
if (prop.ValueKind == JsonValueKind.False) return false;
|
||||
if (prop.ValueKind == JsonValueKind.Number && prop.TryGetInt32(out var n)) return n != 0;
|
||||
if (prop.ValueKind == JsonValueKind.String && TryParseBool(prop.GetString(), out var parsed)) return parsed;
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<string?> ReadStringValueAsync(params string[] keys)
|
||||
{
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var value = form[key].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(value)) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
var value = Request.Query[key].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(value)) return value;
|
||||
}
|
||||
|
||||
if ((Request.ContentLength ?? 0) <= 0) return null;
|
||||
try
|
||||
{
|
||||
using var doc = await JsonDocument.ParseAsync(Request.Body);
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.String)
|
||||
return doc.RootElement.GetString();
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return null;
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (!doc.RootElement.TryGetProperty(key, out var prop)) continue;
|
||||
if (prop.ValueKind == JsonValueKind.String) return prop.GetString();
|
||||
if (prop.ValueKind == JsonValueKind.Number || prop.ValueKind == JsonValueKind.True || prop.ValueKind == JsonValueKind.False)
|
||||
return prop.ToString();
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<JsonElement?> ReadJsonElementAsync(params string[] stringKeys)
|
||||
{
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
var dict = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var key in form.Keys)
|
||||
{
|
||||
var value = form[key].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(value)) dict[key] = value;
|
||||
}
|
||||
if (dict.Count == 0) return null;
|
||||
return JsonSerializer.SerializeToElement(dict);
|
||||
}
|
||||
|
||||
if ((Request.ContentLength ?? 0) > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = await JsonDocument.ParseAsync(Request.Body);
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var key in stringKeys)
|
||||
{
|
||||
var value = Request.Query[key].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(value)) return JsonSerializer.SerializeToElement(value);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryParseBool(string? raw, out bool value)
|
||||
{
|
||||
if (bool.TryParse(raw, out value)) return true;
|
||||
if (int.TryParse(raw, out var n))
|
||||
{
|
||||
value = n != 0;
|
||||
return true;
|
||||
}
|
||||
value = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<JsonElement> ReadJsonElementList(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return new List<JsonElement>();
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<JsonElement[]>(json)?.ToList() ?? new List<JsonElement>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return new List<JsonElement>();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> ReadStringList(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return new List<string>();
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<string[]>(json)?.Where(v => !string.IsNullOrWhiteSpace(v)).ToList()
|
||||
?? new List<string>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private static string SerializeLimited<T>(IEnumerable<T> values, int maxChars = 4096)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(values);
|
||||
return json.Length <= maxChars ? json : "[]";
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/cloning")]
|
||||
[HttpPut("rooms/{roomId:long}/cloning")]
|
||||
public async Task<IActionResult> BareCloning(long roomId, [FromBody] BareBoolRequest? body,
|
||||
@@ -569,6 +767,171 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
|
||||
if (body?.CustomRoomWarning is not null) r.CustomRoomWarning = body.CustomRoomWarning;
|
||||
});
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/creator")]
|
||||
[HttpPut("rooms/{roomId:long}/creator")]
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/creator")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/creator")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
public async Task<IActionResult> BareCreator(long roomId)
|
||||
{
|
||||
var creatorId = await ReadBareLongAsync(
|
||||
"creatorAccountId", "CreatorAccountId",
|
||||
"creatorPlayerId", "CreatorPlayerId",
|
||||
"accountId", "AccountId",
|
||||
"value", "Value");
|
||||
if (creatorId is null or <= 0) return BadRequest(new { error = "missing_creator" });
|
||||
var exists = await db.Players.AnyAsync(p => p.Id == creatorId.Value);
|
||||
if (!exists) return NotFound(new { error = "creator_not_found", creatorId });
|
||||
return await ApplyAndReturn(roomId, r => r.CreatorPlayerId = creatorId.Value);
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/allow_new_users")]
|
||||
[HttpPut("rooms/{roomId:long}/allow_new_users")]
|
||||
[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")]
|
||||
public async Task<IActionResult> BareAllowNewUsers(long roomId)
|
||||
{
|
||||
var value = await ReadBareBoolAsync("allowNewUsers", "AllowNewUsers", "value", "Value");
|
||||
return await ApplyAndReturn(roomId, r =>
|
||||
{
|
||||
if (value is bool v) r.AllowNewUsers = v;
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/min_level")]
|
||||
[HttpPut("rooms/{roomId:long}/min_level")]
|
||||
[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")]
|
||||
public async Task<IActionResult> BareMinLevel(long roomId)
|
||||
{
|
||||
var value = await ReadBareIntAsync("minLevel", "MinLevel", "level", "Level", "value", "Value");
|
||||
return await ApplyAndReturn(roomId, r =>
|
||||
{
|
||||
if (value is int v) r.MinLevel = Math.Max(0, v);
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/max_player_calculation_mode")]
|
||||
[HttpPut("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")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
public async Task<IActionResult> BareMaxPlayerCalculationMode(long roomId)
|
||||
{
|
||||
var value = await ReadBareIntAsync(
|
||||
"maxPlayerCalculationMode", "MaxPlayerCalculationMode",
|
||||
"mode", "Mode",
|
||||
"value", "Value");
|
||||
return await ApplyAndReturn(roomId, r =>
|
||||
{
|
||||
if (value is int v) r.MaxPlayerCalculationMode = Math.Max(0, v);
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/loadscreen")]
|
||||
[HttpPut("rooms/{roomId:long}/loadscreen")]
|
||||
[HttpPost("roomserver/rooms/{roomId:long}/loadscreen")]
|
||||
[HttpPut("roomserver/rooms/{roomId:long}/loadscreen")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
public async Task<IActionResult> BareLoadScreen(long roomId)
|
||||
{
|
||||
var payload = await ReadJsonElementAsync("loadScreen", "LoadScreen", "value", "Value");
|
||||
return await ApplyAndReturn(roomId, r =>
|
||||
{
|
||||
if (payload is JsonElement p)
|
||||
r.LoadScreensJson = p.ValueKind == JsonValueKind.Array
|
||||
? p.GetRawText()
|
||||
: SerializeLimited(new[] { p });
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/promo_images")]
|
||||
[HttpPut("rooms/{roomId:long}/promo_images")]
|
||||
[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")]
|
||||
public async Task<IActionResult> BarePromoImagesAdd(long roomId)
|
||||
{
|
||||
var image = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value");
|
||||
if (string.IsNullOrWhiteSpace(image)) return BadRequest(new { error = "missing_image" });
|
||||
return await ApplyAndReturn(roomId, r =>
|
||||
{
|
||||
var images = ReadStringList(r.PromoImagesJson);
|
||||
if (!images.Contains(image, StringComparer.OrdinalIgnoreCase))
|
||||
images.Add(image);
|
||||
r.PromoImagesJson = SerializeLimited(images);
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId:long}/promo_images")]
|
||||
[HttpDelete("roomserver/rooms/{roomId:long}/promo_images")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
public async Task<IActionResult> BarePromoImagesRemove(long roomId)
|
||||
{
|
||||
var image = await ReadStringValueAsync("imageName", "ImageName", "name", "Name", "value", "Value");
|
||||
if (string.IsNullOrWhiteSpace(image)) return BadRequest(new { error = "missing_image" });
|
||||
return await ApplyAndReturn(roomId, r =>
|
||||
{
|
||||
var images = ReadStringList(r.PromoImagesJson)
|
||||
.Where(v => !string.Equals(v, image, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
r.PromoImagesJson = SerializeLimited(images);
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId:long}/promo_external")]
|
||||
[HttpPut("rooms/{roomId:long}/promo_external")]
|
||||
[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")]
|
||||
public async Task<IActionResult> BarePromoExternalAdd(long roomId)
|
||||
{
|
||||
var payload = await ReadJsonElementAsync("url", "Url", "value", "Value");
|
||||
if (payload is null) return BadRequest(new { error = "missing_promo_external" });
|
||||
return await ApplyAndReturn(roomId, r =>
|
||||
{
|
||||
var items = ReadJsonElementList(r.PromoExternalContentJson);
|
||||
var raw = payload.Value.GetRawText();
|
||||
if (!items.Any(i => string.Equals(i.GetRawText(), raw, StringComparison.Ordinal)))
|
||||
items.Add(payload.Value);
|
||||
r.PromoExternalContentJson = SerializeLimited(items);
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId:long}/promo_external")]
|
||||
[HttpDelete("roomserver/rooms/{roomId:long}/promo_external")]
|
||||
[Consumes("application/x-www-form-urlencoded", "multipart/form-data", "application/json")]
|
||||
public async Task<IActionResult> BarePromoExternalRemove(long roomId)
|
||||
{
|
||||
var match = await ReadStringValueAsync("id", "Id", "url", "Url", "value", "Value");
|
||||
if (string.IsNullOrWhiteSpace(match)) return BadRequest(new { error = "missing_promo_external" });
|
||||
return await ApplyAndReturn(roomId, r =>
|
||||
{
|
||||
var items = ReadJsonElementList(r.PromoExternalContentJson)
|
||||
.Where(i => !PromoExternalMatches(i, match))
|
||||
.ToList();
|
||||
r.PromoExternalContentJson = SerializeLimited(items);
|
||||
});
|
||||
}
|
||||
|
||||
private static bool PromoExternalMatches(JsonElement item, string match)
|
||||
{
|
||||
if (string.Equals(item.GetRawText(), match, StringComparison.OrdinalIgnoreCase)) return true;
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
return string.Equals(item.GetString(), match, StringComparison.OrdinalIgnoreCase);
|
||||
if (item.ValueKind != JsonValueKind.Object) return false;
|
||||
foreach (var key in new[] { "Id", "id", "Url", "url", "Value", "value" })
|
||||
{
|
||||
if (item.TryGetProperty(key, out var prop) &&
|
||||
prop.ValueKind == JsonValueKind.String &&
|
||||
string.Equals(prop.GetString(), match, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary><c>rooms/{id}/comments</c> + <c>voice_chat_encryption</c>
|
||||
/// — fields not persisted in the 2020 RoomEntity. Owner-gated
|
||||
/// acknowledgement so the watch's settings panel doesn't surface
|
||||
@@ -639,10 +1002,19 @@ public class RoomsModerationController(DorkNetDbContext db) : ControllerBase
|
||||
SupportsWalkVR = source.SupportsWalkVR,
|
||||
SupportsTeleportVR = source.SupportsTeleportVR,
|
||||
AllowsJuniors = source.AllowsJuniors,
|
||||
AllowNewUsers = source.AllowNewUsers,
|
||||
MinLevel = source.MinLevel,
|
||||
MaxPlayerCalculationMode = source.MaxPlayerCalculationMode,
|
||||
LoadScreensJson = source.LoadScreensJson,
|
||||
PromoImagesJson = source.PromoImagesJson,
|
||||
PromoExternalContentJson = source.PromoExternalContentJson,
|
||||
RoomWarningMask = source.RoomWarningMask,
|
||||
CustomRoomWarning = source.CustomRoomWarning,
|
||||
DisableMicAutoMute = source.DisableMicAutoMute,
|
||||
LocationReplicationId = source.LocationReplicationId,
|
||||
IsStudioRoom = source.IsStudioRoom,
|
||||
IsRoomLinkedToRecRoomStudio = source.IsRoomLinkedToRecRoomStudio,
|
||||
StudioSessionId = source.StudioSessionId,
|
||||
TagsCsv = source.TagsCsv,
|
||||
CurrentDataBlobName = source.CurrentDataBlobName,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user