Files
2026-10-16 23:33:13 -07:00

171 lines
7.1 KiB
C#

using System.Collections.Concurrent;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.Enums.Rooms;
using RadicadeServer.Utils;
namespace RadicadeServer.API.Studio
{
[Route("studio")]
public class StudioAPI : Controller
{
private sealed record StoredCloudBuild(long RoomId, long SubRoomId, StudioCloudBuild Build);
private static readonly ConcurrentDictionary<string, StoredCloudBuild> CloudBuilds = new(StringComparer.Ordinal);
private readonly ILogger<StudioAPI> logger;
private readonly StudioBuildWorker worker;
private readonly IDbContextFactory<RecDBContext> dbFactory;
public StudioAPI(ILogger<StudioAPI> logger, StudioBuildWorker worker, IDbContextFactory<RecDBContext> dbFactory)
{
this.logger = logger;
this.worker = worker;
this.dbFactory = dbFactory;
}
[HttpGet("cloud-builds/for-room"), Authorize]
public async Task<IActionResult> CloudBuildsForRoom(
[FromQuery] long roomId,
[FromQuery] long subRoomId,
[FromQuery] int skip = 0,
[FromQuery] int take = 30)
{
if (roomId <= 0 || subRoomId <= 0) return BadRequest();
var accountId = CurrentAccountId();
if (accountId == null) return Unauthorized();
if (!await CanBuildInRoomAsync(roomId, accountId.Value, HttpContext.RequestAborted)) return Forbid();
logger.LogInformation("Studio cloud builds requested: room={RoomId}, subRoom={SubRoomId}, skip={Skip}, take={Take}.",
roomId, subRoomId, skip, take);
return Ok(BuildPage(roomId, subRoomId, skip, take));
}
private static object BuildPage(long roomId, long subRoomId, int skip, int take)
{
var builds = CloudBuilds.Values
.Where(x => (roomId == 0 || x.RoomId == roomId) && (subRoomId == 0 || x.SubRoomId == subRoomId))
.OrderByDescending(x => x.Build.StartedAt)
.ToList();
var page = builds.Skip(Math.Max(0, skip)).Take(Math.Clamp(take, 0, 100)).Select(x => x.Build).ToList();
return new { Results = page, TotalResults = builds.Count, TotalCount = builds.Count };
}
[HttpPost("cloud-builds/create"), Authorize]
public async Task<IActionResult> CreateCloudBuild(
[FromQuery] long? roomId = null,
[FromQuery] long? subRoomId = null,
[FromQuery] long? subRoomDataSaveId = null,
[FromQuery] bool? autoPublish = null,
[FromQuery] string? buildBranch = null)
{
var form = Request.HasFormContentType
? await Request.ReadFormAsync(HttpContext.RequestAborted)
: null;
long? FormLong(string key) => long.TryParse(form?[key].ToString(), System.Globalization.NumberStyles.Integer,
System.Globalization.CultureInfo.InvariantCulture, out var value) ? value : null;
bool? FormBool(string key) => bool.TryParse(form?[key].ToString(), out var value) ? value : null;
roomId ??= FormLong("roomId");
subRoomId ??= FormLong("subRoomId");
subRoomDataSaveId ??= FormLong("subRoomDataSaveId");
autoPublish ??= FormBool("autoPublish");
buildBranch ??= form?["buildBranch"].ToString();
if (roomId is not > 0 || subRoomId is not > 0 || subRoomDataSaveId is not > 0)
return BadRequest(new { Success = false, Error = "roomId, subRoomId and subRoomDataSaveId are required", Value = (object?)null });
var resolvedRoomId = roomId.Value;
var resolvedSubRoomId = subRoomId.Value;
var resolvedSaveId = subRoomDataSaveId.Value;
var resolvedAutoPublish = autoPublish ?? false;
var resolvedBuildBranch = buildBranch;
var accountId = CurrentAccountId();
if (accountId == null || accountId > int.MaxValue)
return Unauthorized(new { Success = false, Error = "The Studio account could not be resolved.", Value = (object?)null });
await using (var db = await dbFactory.CreateDbContextAsync(HttpContext.RequestAborted))
{
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == resolvedRoomId, HttpContext.RequestAborted);
var canBuild = room != null && (room.CreatorAccountId == accountId.Value ||
await db.RoomRoles.AsNoTracking().AnyAsync(x => x.RoomId == resolvedRoomId && x.AccountId == accountId.Value && x.Role >= RoleType.CoOwner,
HttpContext.RequestAborted));
if (!canBuild)
return Forbid();
}
var now = DateTime.UtcNow;
var build = new StudioCloudBuild(
Guid.NewGuid().ToString("N"), now, null,
null, string.Empty, (int)accountId);
CloudBuilds[build.CloudBuildId] = new StoredCloudBuild(resolvedRoomId, resolvedSubRoomId, build);
var job = new StudioBuildWorker.BuildJob(build.CloudBuildId, resolvedRoomId, resolvedSubRoomId,
resolvedSaveId, accountId.Value, dbFactory, build, updated =>
CloudBuilds.AddOrUpdate(build.CloudBuildId,
_ => new StoredCloudBuild(resolvedRoomId, resolvedSubRoomId, updated),
(_, existing) => existing with { Build = updated }));
worker.Enqueue(job);
logger.LogInformation("Queued local Studio Windows build: room={RoomId}, subRoom={SubRoomId}, save={SaveId}, autoPublish={AutoPublish}, branch={BuildBranch}, cloudBuild={CloudBuildId}.",
resolvedRoomId, resolvedSubRoomId, resolvedSaveId, resolvedAutoPublish, resolvedBuildBranch, build.CloudBuildId);
return Ok(new { Success = true, Error = (string?)null, Value = build });
}
[HttpGet("cloud-builds"), Authorize]
public async Task<IActionResult> CloudBuild([FromQuery] string cloudBuildId)
{
if (!CloudBuilds.TryGetValue(cloudBuildId, out var stored)) return NotFound();
var accountId = CurrentAccountId();
if (accountId == null) return Unauthorized();
if (!await CanBuildInRoomAsync(stored.RoomId, accountId.Value, HttpContext.RequestAborted)) return Forbid();
return Ok(stored.Build);
}
private long? CurrentAccountId()
{
if (long.TryParse(User.FindFirstValue("userId"), out var userId) && userId > 0) return userId;
if (long.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var identifier) && identifier > 0) return identifier;
return null;
}
private async Task<bool> CanBuildInRoomAsync(long roomId, long accountId, CancellationToken cancellationToken)
{
await using var db = await dbFactory.CreateDbContextAsync(cancellationToken);
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId, cancellationToken);
return room != null && (room.CreatorAccountId == accountId ||
await db.RoomRoles.AsNoTracking().AnyAsync(x => x.RoomId == roomId && x.AccountId == accountId && x.Role >= RoleType.CoOwner,
cancellationToken));
}
[HttpGet("collaboration/owners-in-room")]
public IActionResult OwnersInRoom([FromQuery] long roomId)
{
logger.LogInformation("Studio owners-in-room requested: room={RoomId}; answering an empty owner list.", roomId);
return Ok(new { Success = true, Error = (string?)null, Value = Array.Empty<int>() });
}
[HttpGet("analytics/rudderstack-api-key")]
public IActionResult RudderstackApiKey()
{
return Ok(new { key = string.Empty, value = string.Empty });
}
}
}
public sealed record StudioCloudBuild(
string CloudBuildId,
DateTime StartedAt,
DateTime? CompletedAt,
string? Error,
string UnityAssetId,
int CreatedByAccountId);