Add remaining project files

This commit is contained in:
Marco Baldwin
2026-07-23 18:21:43 -07:00
parent c12d8ac35d
commit 6e15e89a9d
453 changed files with 64265 additions and 0 deletions
@@ -0,0 +1,291 @@
using DeluxeBackend.Models;
using DeluxeBackend.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Drawing;
using System.Linq.Expressions;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using static DeluxeBackend.Enums;
namespace DeluxeBackend.Controllers.Api
{
[Route("api/customAvatarItems")]
[ApiController]
[Authorize]
public class CustomAvatarItemsController(
ILiteDbService db,
IJwtService jwt,
DiscordBotService discord,
ICdnService cdn,
HttpClient httpClient) : ControllerBase
{
private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true };
[HttpGet("v1/isCreationEnabled")]
[HttpGet("v1/isRenderingEnabled")]
public IActionResult IsCreationEnabled() => Ok(true);
[HttpGet("v1/minPriceForPublicItem")]
public IActionResult MinPriceForPublicItem() => Ok(0);
[HttpGet("v1/isCreationAllowedForAccount")]
public async Task<IActionResult> IsCreationAllowedForAccount()
{
var account = await GetAuthenticatedAccountAsync();
if (account == null) return Unauthorized();
if (!account.DiscordId.HasValue)
return Ok(new { Success = false });
bool hasBoosted = await discord.HasUserBoostedGuild(account.DiscordId.Value);
return Ok(new { Success = hasBoosted });
}
[HttpGet("/econ/customAvatarItems/v1/owned")]
public async Task<IActionResult> Owned([FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var account = await GetAuthenticatedAccountAsync();
if (account == null) return Unauthorized();
const bool isCreator = true;
Expression<Func<CustomAvatarItem, bool>> filter = x =>
x.Creator.Id == account.Id && (isCreator || x.Accessibility == RoomAccessibility.Public);
return GetPagedItems(filter, skip, take);
}
[HttpGet("v2/fromCreator/{accId}")]
public async Task<IActionResult> FromCreator(long accId, [FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var account = await GetAuthenticatedAccountAsync();
if (account == null) return Unauthorized();
bool isCreator = account.Id == accId;
Expression<Func<CustomAvatarItem, bool>> filter = x =>
x.Creator.Id == accId && (isCreator || x.Accessibility == RoomAccessibility.Public);
return GetPagedItems(filter, skip, take);
}
[HttpPost("v1")]
public async Task<IActionResult> Create(
[FromForm(Name = "thumbnailImage"), Required] IFormFile thumbnailImage,
[FromForm(Name = "design"), Required] IFormFile designImage,
[FromForm(Name = "metadata"), Required] string metaJson)
{
var account = await GetAuthenticatedAccountAsync();
if (account == null) return Unauthorized();
if (!account.DiscordId.HasValue || !await discord.HasUserBoostedGuild(account.DiscordId.Value))
return StatusCode(StatusCodes.Status403Forbidden);
CustomAvatarItemMetaDTO? meta;
try
{
meta = JsonSerializer.Deserialize<CustomAvatarItemMetaDTO>(metaJson);
if (meta == null) return BadRequest("Invalid metadata format.");
}
catch
{
return BadRequest("Metadata is not valid JSON.");
}
using var thumbStream = thumbnailImage.OpenReadStream();
string? thumbRemotePath = await cdn.UploadFile(thumbStream, "img");
if (thumbRemotePath == null)
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to upload ThumbnailImage to the CDN.");
using var designStream = designImage.OpenReadStream();
string? designRemotePath = await cdn.UploadFile(designStream, "img");
if (designRemotePath == null)
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to upload DesignImage to the CDN.");
var customAvatar = new CustomAvatarItem
{
Creator = account,
Name = meta.Name,
Price = meta.Price,
BaseAvatarItemColor = ColorTranslator.FromHtml(meta.BaseAvatarItemColor),
ThumbnailImageFilename = thumbRemotePath,
DesignFilename = designRemotePath,
PreviewOrientation = meta.PreviewOrientation
};
db.CustomAvatarItems.Insert(customAvatar);
return Ok(new { Success = true, Value = customAvatar.ToDictionary() });
}
[HttpPost("v1/bulk")]
public async Task<IActionResult> Bulk([FromForm(Name = "customAvatarItemIds")] List<Guid> customAvatarItemIds)
{
var account = await GetAuthenticatedAccountAsync();
if (account == null) return Unauthorized();
var items = db.CustomAvatarItems.Find(x => customAvatarItemIds.Contains(x.Id)).ToList();
var foundIds = items.Select(x => x.Id).ToHashSet();
var missingIds = customAvatarItemIds.Where(id => !foundIds.Contains(id)).ToList();
if (missingIds.Any())
{
var importAccount = GetOrCreateImportAccount();
foreach (var missingId in missingIds)
{
var fallbackItem = await FetchItemFromRecNetAsync(missingId, importAccount);
if (fallbackItem != null)
{
items.Add(fallbackItem);
}
}
}
return Ok(items.Select(x => x.ToDictionary()).ToList());
}
[HttpGet("v1/{customAvatarItemId}")]
public async Task<IActionResult> Get(Guid customAvatarItemId)
{
var account = await GetAuthenticatedAccountAsync();
if (account == null) return Unauthorized();
var customAvatarItem = db.CustomAvatarItems.FindById(customAvatarItemId);
if (customAvatarItem == null)
{
var importAccount = GetOrCreateImportAccount();
customAvatarItem = await FetchItemFromRecNetAsync(customAvatarItemId, importAccount);
}
if (customAvatarItem == null)
return NotFound();
return Ok(customAvatarItem.ToDictionary());
}
#region Helper Methods
private async Task<Account?> GetAuthenticatedAccountAsync()
{
var login = await jwt.GetLoginWithInfo(User);
return login?.Account;
}
private IActionResult GetPagedItems(Expression<Func<CustomAvatarItem, bool>> filter, int skip, int take)
{
int totalCount = db.CustomAvatarItems.Count(filter);
var results = db.CustomAvatarItems
.Include(x => x.Creator)
.Find(filter)
.Skip(skip)
.Take(take)
.Select(x => x.ToDictionary());
return Ok(new
{
Results = results,
TotalResults = totalCount
});
}
private Account GetOrCreateImportAccount()
{
var importAccount = db.Accounts.FindOne(x => x.Username == "Import");
if (importAccount == null)
{
importAccount = new Account
{
Username = "Import",
DisplayName = "Import",
Birthday = DateOnly.MinValue,
ImageName = "DefaultProfileImage",
IsRecentHistoryVisible = false,
};
importAccount.Roles.AddRange(["developer", "keepsake", "livekeepsakeeventroomsaveoverride", "betaroomcurrencycreator", "multiinstanceevent"]);
db.Accounts.Insert(importAccount);
}
return importAccount;
}
private async Task<CustomAvatarItem?> FetchItemFromRecNetAsync(Guid itemId, Account importAccount)
{
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.rec.net/api/customAvatarItems/v1/{itemId}");
request.Headers.Add("Authorization", $"Bearer {ServerConfig.RRToken}");
using var response = await httpClient.SendAsync(request);
if (response.StatusCode == HttpStatusCode.OK)
{
string content = await response.Content.ReadAsStringAsync();
var recNetResult = JsonSerializer.Deserialize<CAvatarItemDto>(content, _jsonOptions);
if (recNetResult != null && !string.IsNullOrEmpty(recNetResult.DesignFilename))
{
var customAvatarItem = new CustomAvatarItem
{
Id = recNetResult.CustomAvatarItemId,
Accessibility=recNetResult.Accessibility,
Creator = importAccount,
Name = recNetResult.Name,
Description = recNetResult.Description,
Price = recNetResult.Price,
BaseAvatarItemId = recNetResult.BaseAvatarItemId,
BaseAvatarItemColor = ColorTranslator.FromHtml(recNetResult.BaseAvatarItemColor),
DesignFilename = recNetResult.DesignFilename,
ThumbnailImageFilename = recNetResult.ThumbnailImageFilename,
PreviewOrientation = recNetResult.PreviewOrientation
};
db.CustomAvatarItems.Insert(customAvatarItem);
return customAvatarItem;
}
}
}
catch
{
}
return null;
}
#endregion
#region DTOs
public class CustomAvatarItemMetaDTO
{
[Required] public required string Name { get; set; }
[Required] public required string Description { get; set; }
[Required] public required int Price { get; set; }
[Required] public required long BaseAvatarItemId { get; set; }
[Required] public required string BaseAvatarItemColor { get; set; }
[Required] public required RoomAccessibility Accessibility { get; set; }
[Required] public required PreviewOrientationType PreviewOrientation { get; set; }
}
public class CAvatarItemDto
{
public Guid CustomAvatarItemId { get; set; }
public long CreatorAccountId { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public int Price { get; set; }
public RoomAccessibility Accessibility { get; set; }
public bool IsFeatured { get; set; }
public string BaseAvatarItemColor { get; set; } = string.Empty;
public string DesignFilename { get; set; } = string.Empty;
public string ThumbnailImageFilename { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime ModifiedAt { get; set; }
public int? BaseAvatarItemId { get; set; }
public PreviewOrientationType PreviewOrientation { get; set; } = PreviewOrientationType.Front;
}
#endregion
}
}