mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 15:09:26 -07:00
492 lines
14 KiB
C#
492 lines
14 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using RadicadeServer.API.Notifications;
|
|
using RadicadeServer.DB;
|
|
using RadicadeServer.Extensions;
|
|
using RadicadeServer.RecNet.Avatar;
|
|
using RadicadeServer.RecNet.Responses;
|
|
|
|
namespace RadicadeServer.API.Avatars
|
|
{
|
|
[Route("api/customAvatarItems")]
|
|
[Route("econ/customAvatarItems")]
|
|
[Route("econ/econ/customAvatarItems")]
|
|
public class CustomAvatarItemsAPI : Controller
|
|
{
|
|
readonly RecDBContext db;
|
|
|
|
public CustomAvatarItemsAPI(RecDBContext context)
|
|
{
|
|
this.db = context;
|
|
}
|
|
|
|
private static Task AnnounceCustomItemChange(long ownerId) => Notify.SendAccountUpdate(ownerId);
|
|
|
|
/// <summary>
|
|
/// Whether this account may draw a custom item at all. The client reads the answer off the
|
|
/// two obfuscated members this route's own response type carries, so a PascalCase "success"
|
|
/// body left every option on the drawing screen disabled - creating and recolouring both
|
|
/// greyed out - even though nothing on the account was wrong.
|
|
/// </summary>
|
|
[HttpGet("v1/isCreationAllowedForAccount")]
|
|
public IActionResult IsCreationAllowedForAccount()
|
|
{
|
|
return Ok(ClientAction.Ok());
|
|
}
|
|
|
|
[HttpGet("v1/isCreationEnabled")]
|
|
[HttpGet("v1/isRenderingEnabled")]
|
|
public IActionResult IsEnabled()
|
|
{
|
|
return Ok(true);
|
|
}
|
|
|
|
[HttpGet("v1/minPriceForPublicItem")]
|
|
public IActionResult MinPriceForPublicItem()
|
|
{
|
|
return Ok(0);
|
|
}
|
|
|
|
[HttpGet("v1/itemOwnershipLimit")]
|
|
public IActionResult ItemOwnershipLimit()
|
|
{
|
|
return Ok(100);
|
|
}
|
|
|
|
|
|
private static object ToItem(CustomAvatarItemDB item) => new
|
|
{
|
|
CustomAvatarItemId = item.CustomAvatarItemId.ToString(),
|
|
CreatorAccountId = item.CreatorAccountId,
|
|
Name = item.Name,
|
|
Description = item.Description,
|
|
Price = item.Price,
|
|
BaseAvatarItemId = item.BaseAvatarItemId,
|
|
BaseAvatarItemColor = item.BaseAvatarItemColor,
|
|
Accessibility = item.Accessibility,
|
|
IsFeatured = false,
|
|
Permission = 0,
|
|
ThumbnailImageFilename = item.ImageName,
|
|
ImageName = item.ImageName,
|
|
DesignFilename = item.DesignFileName,
|
|
CreatedAt = item.CreatedAt,
|
|
ModifiedAt = item.ModifiedAt,
|
|
};
|
|
|
|
private static bool Visible(CustomAvatarItemDB item, long? viewer) =>
|
|
item.IsPublished || (viewer != null && item.CreatorAccountId == viewer);
|
|
|
|
|
|
[HttpGet("v1/featured")]
|
|
[HttpGet("v1/hot")]
|
|
public async Task<IActionResult> Featured()
|
|
{
|
|
var viewer = User.GetUserId() ?? 0;
|
|
var rows = await db.CustomAvatarItems
|
|
.Where(x => x.IsPublished || x.CreatorAccountId == viewer)
|
|
.OrderByDescending(x => x.CreatedAt)
|
|
.Take(50)
|
|
.ToListAsync();
|
|
|
|
return Ok(rows.Select(ToItem).ToList());
|
|
}
|
|
|
|
[HttpGet("v1/owned")]
|
|
public async Task<IActionResult> Owned([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
|
{
|
|
return await Page(x => x.CreatorAccountId == User.GetUserId(), skip, take);
|
|
}
|
|
|
|
[HttpGet("v1")]
|
|
[HttpGet("v1/me")]
|
|
public async Task<IActionResult> Items([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
|
{
|
|
return await Page(x => x.CreatorAccountId == User.GetUserId(), skip, take);
|
|
}
|
|
|
|
private static object ToDesign(CustomAvatarItemDB item) => new
|
|
{
|
|
CustomAvatarItemId = item.CustomAvatarItemId.ToString(),
|
|
CustomAvatarItemColor = item.BaseAvatarItemColor,
|
|
DesignFilename = item.DesignFileName,
|
|
ImageName = item.ImageName,
|
|
};
|
|
|
|
[HttpGet("v1/design")]
|
|
public async Task<IActionResult> Design()
|
|
{
|
|
var viewer = User.GetUserId() ?? 0;
|
|
var itemId = Guid.TryParse(Request.Query["customAvatarItemId"], out var parsed) ? parsed : (Guid?)null;
|
|
var color = Request.Query["customAvatarItemColor"].ToString();
|
|
|
|
var query = db.CustomAvatarItems.Where(x => x.DesignFileName != null && (x.IsPublished || x.CreatorAccountId == viewer));
|
|
if (itemId != null)
|
|
query = query.Where(x => x.CustomAvatarItemId == itemId);
|
|
if (!string.IsNullOrEmpty(color))
|
|
query = query.Where(x => x.BaseAvatarItemColor == color);
|
|
|
|
var rows = await query.OrderByDescending(x => x.CreatedAt).ToListAsync();
|
|
|
|
return Ok(rows.Select(ToDesign).ToList());
|
|
}
|
|
|
|
[HttpPost("v1/design")]
|
|
public async Task<IActionResult> SaveDesign()
|
|
{
|
|
var ownerId = User.GetUserId();
|
|
if (ownerId == null)
|
|
{
|
|
return Unauthorized();
|
|
}
|
|
|
|
IFormFile? uploaded = null;
|
|
string? itemIdRaw = Request.Query["customAvatarItemId"].ToString();
|
|
string? color = Request.Query["customAvatarItemColor"].ToString();
|
|
|
|
if (Request.HasFormContentType)
|
|
{
|
|
var form = await Request.ReadFormAsync();
|
|
uploaded = form.Files.FirstOrDefault(f => f.Length > 0);
|
|
if (string.IsNullOrEmpty(itemIdRaw))
|
|
itemIdRaw = form["customAvatarItemId"].ToString();
|
|
if (string.IsNullOrEmpty(color))
|
|
color = form["customAvatarItemColor"].ToString();
|
|
}
|
|
|
|
var item = Guid.TryParse(itemIdRaw, out var itemId)
|
|
? await db.CustomAvatarItems.FirstOrDefaultAsync(x => x.CustomAvatarItemId == itemId)
|
|
: null;
|
|
|
|
item ??= await db.CustomAvatarItems
|
|
.Where(x => x.CreatorAccountId == ownerId)
|
|
.OrderByDescending(x => x.CreatedAt)
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (item == null)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
if (uploaded != null)
|
|
item.DesignFileName = await SaveUpload(uploaded, DBConfig.CDNImages, ".png");
|
|
if (!string.IsNullOrEmpty(color))
|
|
item.BaseAvatarItemColor = color;
|
|
|
|
item.ModifiedAt = DateTime.UtcNow;
|
|
await db.SaveChangesAsync();
|
|
|
|
await AnnounceCustomItemChange(ownerId.Value);
|
|
return Ok(ToDesign(item));
|
|
}
|
|
|
|
[HttpGet("v2/fromCreator/{accountId:long}")]
|
|
public async Task<IActionResult> FromCreator(long accountId, [FromQuery] int skip = 0, [FromQuery] int take = 100)
|
|
{
|
|
return await Page(x => x.CreatorAccountId == accountId, skip, take);
|
|
}
|
|
|
|
[HttpGet("v1/{customAvatarItemId:guid}")]
|
|
public async Task<IActionResult> Item(Guid customAvatarItemId)
|
|
{
|
|
var item = await db.CustomAvatarItems.FirstOrDefaultAsync(x => x.CustomAvatarItemId == customAvatarItemId);
|
|
if (item == null || !Visible(item, User.GetUserId()))
|
|
return NotFound();
|
|
|
|
return Ok(ToItem(item));
|
|
}
|
|
|
|
[HttpPut("v1/{customAvatarItemId:guid}")]
|
|
public async Task<IActionResult> Update(Guid customAvatarItemId)
|
|
{
|
|
var ownerId = User.GetUserId();
|
|
if (ownerId == null) return Unauthorized();
|
|
|
|
var item = await db.CustomAvatarItems.FirstOrDefaultAsync(x => x.CustomAvatarItemId == customAvatarItemId);
|
|
if (item == null) return NotFound();
|
|
if (item.CreatorAccountId != ownerId.Value)
|
|
{
|
|
return StatusCode(StatusCodes.Status403Forbidden);
|
|
}
|
|
|
|
var meta = await ReadMetadataAsync();
|
|
if (ReadString(meta, "Name") is { } name && !string.IsNullOrWhiteSpace(name))
|
|
item.Name = name;
|
|
if (ReadString(meta, "Description") is { } description)
|
|
item.Description = description;
|
|
if (TryReadInt(meta, "Price", out var price))
|
|
item.Price = price;
|
|
if (TryReadInt(meta, "BaseAvatarItemId", out var baseItemId))
|
|
item.BaseAvatarItemId = baseItemId;
|
|
if (ReadString(meta, "BaseAvatarItemColor") is { } color)
|
|
item.BaseAvatarItemColor = color;
|
|
if (TryReadInt(meta, "Accessibility", out var accessibility))
|
|
{
|
|
item.Accessibility = accessibility;
|
|
item.IsPublished = accessibility != 0;
|
|
}
|
|
|
|
item.ModifiedAt = DateTime.UtcNow;
|
|
await db.SaveChangesAsync();
|
|
|
|
await AnnounceCustomItemChange(ownerId.Value);
|
|
|
|
// The route answers with the two obfuscated members, not the item: the client refetches
|
|
// what it needs from the account update above, and an item body here read as a failed save.
|
|
return Ok(ClientAction.Ok());
|
|
}
|
|
|
|
[HttpDelete("v1/{customAvatarItemId:guid}")]
|
|
public async Task<IActionResult> Delete(Guid customAvatarItemId)
|
|
{
|
|
var ownerId = User.GetUserId();
|
|
if (ownerId == null) return Unauthorized();
|
|
|
|
var item = await db.CustomAvatarItems.FirstOrDefaultAsync(x => x.CustomAvatarItemId == customAvatarItemId);
|
|
if (item == null) return NotFound();
|
|
if (item.CreatorAccountId != ownerId.Value)
|
|
{
|
|
return StatusCode(StatusCodes.Status403Forbidden);
|
|
}
|
|
|
|
db.CustomAvatarItems.Remove(item);
|
|
await db.SaveChangesAsync();
|
|
|
|
await AnnounceCustomItemChange(ownerId.Value);
|
|
return Ok(ClientAction.Ok());
|
|
}
|
|
|
|
[HttpGet("v1/search")]
|
|
public async Task<IActionResult> Search(
|
|
[FromQuery] string? query = null,
|
|
[FromQuery] string? searchTerm = null,
|
|
[FromQuery] int skip = 0,
|
|
[FromQuery] int take = 100)
|
|
{
|
|
var viewer = User.GetUserId() ?? 0;
|
|
var term = (query ?? searchTerm ?? string.Empty).Trim();
|
|
return await Page(x => (x.IsPublished || x.CreatorAccountId == viewer)
|
|
&& (term.Length == 0 || (x.Name ?? string.Empty).Contains(term) || (x.Description ?? string.Empty).Contains(term)), skip, take);
|
|
}
|
|
|
|
[HttpPost("v1/bulk")]
|
|
public async Task<IActionResult> Bulk()
|
|
{
|
|
var ids = new List<Guid>();
|
|
if (Request.HasFormContentType)
|
|
{
|
|
var form = await Request.ReadFormAsync();
|
|
foreach (var value in form["customAvatarItemIds"])
|
|
if (Guid.TryParse(value, out var id))
|
|
ids.Add(id);
|
|
}
|
|
|
|
var viewer = User.GetUserId();
|
|
var rows = await db.CustomAvatarItems.Where(x => ids.Contains(x.CustomAvatarItemId)).ToListAsync();
|
|
|
|
return Ok(rows.Where(x => Visible(x, viewer)).Select(ToItem).ToList());
|
|
}
|
|
|
|
[HttpPost("v1")]
|
|
[HttpPost("v1/me")]
|
|
public async Task<IActionResult> Create()
|
|
{
|
|
var ownerId = User.GetUserId();
|
|
if (ownerId == null)
|
|
{
|
|
return Unauthorized();
|
|
}
|
|
|
|
string? rawMeta = null;
|
|
var files = new List<string>();
|
|
IFormFile? thumbnail = null;
|
|
IFormFile? design = null;
|
|
|
|
if (Request.HasFormContentType)
|
|
{
|
|
var form = await Request.ReadFormAsync();
|
|
rawMeta = form["metadata"].ToString();
|
|
|
|
foreach (var file in form.Files)
|
|
{
|
|
files.Add($"{file.Name}:{file.ContentType}:{file.Length}");
|
|
if (file.Length <= 0)
|
|
continue;
|
|
|
|
var isThumbnail = file.Name.Contains("thumbnail", StringComparison.OrdinalIgnoreCase);
|
|
var isDesign = file.Name.Contains("design", StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (isThumbnail && thumbnail == null)
|
|
thumbnail = file;
|
|
else if (isDesign && design == null)
|
|
design = file;
|
|
else if (thumbnail == null)
|
|
thumbnail = file;
|
|
else if (design == null)
|
|
design = file;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Request.EnableBuffering();
|
|
Request.Body.Position = 0;
|
|
var buffer = new byte[1024];
|
|
var read = await Request.Body.ReadAsync(buffer, 0, buffer.Length);
|
|
Request.Body.Position = 0;
|
|
rawMeta = Encoding.UTF8.GetString(buffer, 0, read);
|
|
}
|
|
|
|
using var meta = ParseMetadata(rawMeta);
|
|
|
|
var now = DateTime.UtcNow;
|
|
var item = new CustomAvatarItemDB
|
|
{
|
|
CustomAvatarItemId = Guid.NewGuid(),
|
|
CreatorAccountId = (long)ownerId,
|
|
Name = ReadString(meta, "Name") ?? "Custom Avatar Item",
|
|
Description = ReadString(meta, "Description") ?? string.Empty,
|
|
Price = ReadInt(meta, "Price"),
|
|
BaseAvatarItemId = ReadInt(meta, "BaseAvatarItemId"),
|
|
BaseAvatarItemColor = ReadString(meta, "BaseAvatarItemColor"),
|
|
Accessibility = ReadInt(meta, "Accessibility"),
|
|
CreatedAt = now,
|
|
ModifiedAt = now,
|
|
};
|
|
|
|
item.IsPublished = item.Accessibility != 0;
|
|
|
|
if (thumbnail != null)
|
|
item.ImageName = await SaveUpload(thumbnail, DBConfig.CDNImages, ".png");
|
|
if (design != null)
|
|
item.DesignFileName = await SaveUpload(design, DBConfig.CDNImages, ".png");
|
|
|
|
db.CustomAvatarItems.Add(item);
|
|
await db.SaveChangesAsync();
|
|
|
|
await AnnounceCustomItemChange(ownerId.Value);
|
|
var body = ToItem(item);
|
|
|
|
return Ok(body);
|
|
}
|
|
|
|
[HttpPost("GetCustomAvatarItemCurrentSavesForLegacyAvatarItems")]
|
|
public IActionResult CurrentSavesForLegacyAvatarItems()
|
|
{
|
|
return Ok(new { customAvatarItemSavesByAvatarItemDesc = new Dictionary<string, object>() });
|
|
}
|
|
|
|
|
|
private async Task<IActionResult> Page(System.Linq.Expressions.Expression<Func<CustomAvatarItemDB, bool>> filter, int skip, int take)
|
|
{
|
|
take = take <= 0 ? 100 : Math.Min(take, 100);
|
|
skip = Math.Max(skip, 0);
|
|
|
|
var query = db.CustomAvatarItems.Where(filter);
|
|
var total = await query.CountAsync();
|
|
var rows = await query
|
|
.OrderByDescending(x => x.CreatedAt)
|
|
.Skip(skip)
|
|
.Take(take)
|
|
.ToListAsync();
|
|
|
|
return Ok(new { Results = rows.Select(ToItem).ToList(), TotalResults = total });
|
|
}
|
|
|
|
private async Task<JsonDocument> ReadMetadataAsync()
|
|
{
|
|
if (Request.HasFormContentType)
|
|
{
|
|
var form = await Request.ReadFormAsync();
|
|
return ParseMetadata(form["metadata"].ToString());
|
|
}
|
|
|
|
if (Request.ContentLength is not > 0)
|
|
return ParseMetadata("{}");
|
|
|
|
Request.EnableBuffering();
|
|
Request.Body.Position = 0;
|
|
string raw;
|
|
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
|
|
raw = await reader.ReadToEndAsync();
|
|
Request.Body.Position = 0;
|
|
return ParseMetadata(raw);
|
|
}
|
|
|
|
private static bool TryReadInt(JsonDocument meta, string key, out int value)
|
|
{
|
|
value = 0;
|
|
if (!TryGet(meta, key, out var element)) return false;
|
|
if (element.ValueKind == JsonValueKind.Number && element.TryGetInt32(out value)) return true;
|
|
return int.TryParse(element.ToString(), out value);
|
|
}
|
|
|
|
private static JsonDocument ParseMetadata(string raw)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
return JsonDocument.Parse("{}");
|
|
|
|
try
|
|
{
|
|
return JsonDocument.Parse(raw);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return JsonDocument.Parse("{}");
|
|
}
|
|
}
|
|
|
|
private static string? ReadString(JsonDocument meta, string key)
|
|
{
|
|
if (!TryGet(meta, key, out var value))
|
|
return null;
|
|
|
|
return value.ValueKind == JsonValueKind.String ? value.GetString() : value.ToString();
|
|
}
|
|
|
|
private static int ReadInt(JsonDocument meta, string key)
|
|
{
|
|
if (!TryGet(meta, key, out var value) || value.ValueKind != JsonValueKind.Number)
|
|
return 0;
|
|
|
|
return value.GetInt32();
|
|
}
|
|
|
|
private static bool TryGet(JsonDocument meta, string key, out JsonElement value)
|
|
{
|
|
value = default;
|
|
if (meta == null)
|
|
return false;
|
|
|
|
if (meta.RootElement.TryGetProperty(key, out value))
|
|
return true;
|
|
|
|
foreach (var property in meta.RootElement.EnumerateObject())
|
|
{
|
|
if (string.Equals(property.Name, key, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
value = property.Value;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static async Task<string> SaveUpload(IFormFile file, string directory, string extension)
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
|
|
var name = Guid.NewGuid().ToString() + extension;
|
|
var path = Path.Join(directory, name);
|
|
|
|
using (var stream = new FileStream(path, FileMode.Create))
|
|
{
|
|
await file.CopyToAsync(stream);
|
|
}
|
|
|
|
return name;
|
|
}
|
|
}
|
|
}
|