291 lines
12 KiB
C#
291 lines
12 KiB
C#
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
|
|
}
|
|
} |