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 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 Owned([FromQuery] int skip = 0, [FromQuery] int take = 100) { var account = await GetAuthenticatedAccountAsync(); if (account == null) return Unauthorized(); const bool isCreator = true; Expression> filter = x => x.Creator.Id == account.Id && (isCreator || x.Accessibility == RoomAccessibility.Public); return GetPagedItems(filter, skip, take); } [HttpGet("v2/fromCreator/{accId}")] public async Task 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> filter = x => x.Creator.Id == accId && (isCreator || x.Accessibility == RoomAccessibility.Public); return GetPagedItems(filter, skip, take); } [HttpPost("v1")] public async Task 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(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 Bulk([FromForm(Name = "customAvatarItemIds")] List 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 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 GetAuthenticatedAccountAsync() { var login = await jwt.GetLoginWithInfo(User); return login?.Account; } private IActionResult GetPagedItems(Expression> 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 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(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 } }