mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 07:29:00 -07:00
171 lines
7.1 KiB
C#
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);
|