Files
Radicade/Server/Images/ImagesAPI.cs
T
2026-10-16 23:33:13 -07:00

581 lines
19 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using RadicadeServer.DB;
using RadicadeServer.Extensions;
using RadicadeServer.RecNet.CDN;
using RadicadeServer.RecNet.Enums;
using RadicadeServer.RecNet.Images;
using RadicadeServer.RecNet.Responses;
using RadicadeServer.Radicade.Discord.Templates;
namespace RadicadeServer.API
{
[Route("api/images")]
public class ImagesAPI : Controller
{
RecDBContext db;
public ImagesAPI(RecDBContext context)
{
this.db = context;
}
private static object ToSavedImage(ImageDB image, int cheerCount) => new
{
Id = image.Id,
SavedImageId = image.Id,
Type = (int)image.ImageType,
SavedImageType = (int)image.ImageType,
Accessibility = (int)image.Accessibility,
AccessibilityLocked = false,
ImageName = image.Name,
Description = (string?)null,
PlayerId = image.OwnerId,
TaggedPlayerIds = ParseTaggedPlayerIds(image.PlayerIds),
RoomId = image.RoomId,
PlayerEventId = image.EventId,
CreatedAt = image.CreatedAt,
CheerCount = cheerCount,
CommentCount = 0
};
private static object ToImagesPlayer(ImageDB image, int cheerCount) => new
{
SavedImageId = image.Id,
Id = image.Id,
SavedImageType = (int)image.ImageType,
Type = (int)image.ImageType,
Accessibility = (int)image.Accessibility,
AccessibilityLocked = false,
CheerCount = cheerCount,
CommentCount = 0,
CreatedAt = image.CreatedAt,
Description = (string?)null,
ImageName = image.Name,
PlayerEventId = image.EventId,
PlayerId = image.OwnerId,
RoomId = image.RoomId
};
private static List<long> ParseTaggedPlayerIds(string json)
{
try
{
return string.IsNullOrWhiteSpace(json)
? new List<long>()
: (json.FromJson<List<long>>() ?? new List<long>());
}
catch
{
return new List<long>();
}
}
private async Task<Dictionary<long, int>> CheerCountsAsync(IEnumerable<long> imageIds)
{
var ids = imageIds.Distinct().ToList();
if (ids.Count == 0)
return new Dictionary<long, int>();
return await db.ImageInteractions.AsNoTracking()
.Where(c => c.IsCheered && ids.Contains(c.SavedImageId))
.GroupBy(c => c.SavedImageId)
.Select(g => new { g.Key, Count = g.Count() })
.ToDictionaryAsync(g => g.Key, g => g.Count);
}
private static string[] TagMatchPatterns(long playerId) => new[]
{
$"[{playerId},", $",{playerId},", $",{playerId}]", $"[{playerId}]"
};
[HttpGet("v5/player/{id:long}")]
public async Task<List<object>> GetImagesFromPlayer(long id, [FromQuery] int sort = 0, [FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var images = await db.Images.AsNoTracking()
.Where(image => image.OwnerId == id && image.Accessibility == SavedImageAccessibility.Public)
.ToListAsync();
var cheers = await CheerCountsAsync(images.Select(image => image.Id));
var ordered = OrderImages(images, sort, cheers)
.Skip(skip).Take(take)
.ToList();
return ordered.Select(image => ToImagesPlayer(image, cheers.GetValueOrDefault(image.Id))).ToList();
}
[HttpGet("v4/player/{id:long}")]
public Task<List<object>> GetImagesFromPlayerV4(long id, [FromQuery] int skip = 0, [FromQuery] int take = 100)
=> GetImagesFromPlayer(id, 0, skip, take);
[HttpGet("v4/room/{id}")]
public async Task<List<object>> GetImagesFromRoom(long? id, [FromQuery] int sort = 0, [FromQuery] int filter = 0, [FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var result = new List<object>();
if (id == null)
return result;
var images = await db.Images.AsNoTracking()
.Where(x => x.RoomId == id && x.Accessibility == SavedImageAccessibility.Public)
.ToListAsync();
if (filter > 0)
images = images.Where(x => (int)x.ImageType == filter).ToList();
var cheers = await CheerCountsAsync(images.Select(x => x.Id));
var ordered = OrderImages(images, sort, cheers)
.Skip(skip).Take(take)
.ToList();
return ordered.Select(image => ToSavedImage(image, cheers.GetValueOrDefault(image.Id))).ToList();
}
[HttpGet("v1/playerevent/{eventId}")]
public async Task<List<object>> GetImagesFromEvent(long eventId, [FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var images = await db.Images.AsNoTracking()
.Where(x => x.EventId == eventId && x.Accessibility == SavedImageAccessibility.Public)
.ToListAsync();
var cheers = await CheerCountsAsync(images.Select(x => x.Id));
var ordered = OrderImages(images, 0, cheers)
.Skip(skip).Take(take)
.ToList();
return ordered.Select(image => ToSavedImage(image, cheers.GetValueOrDefault(image.Id))).ToList();
}
private static IEnumerable<ImageDB> OrderImages(IEnumerable<ImageDB> images, int sort, Dictionary<long, int> cheers)
{
return sort == 1
? images.OrderByDescending(x => cheers.GetValueOrDefault(x.Id))
.ThenByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)
: images.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id);
}
[HttpGet("v3/feed/player/{id:long}")]
public async Task<List<object>> GetPlayerFeed(long id, [FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var patterns = TagMatchPatterns(id);
var images = await db.Images.AsNoTracking()
.Where(x => x.Accessibility == SavedImageAccessibility.Public &&
(x.OwnerId == id ||
x.PlayerIds.Contains(patterns[0]) ||
x.PlayerIds.Contains(patterns[1]) ||
x.PlayerIds.Contains(patterns[2]) ||
x.PlayerIds.Contains(patterns[3])))
.ToListAsync();
var cheers = await CheerCountsAsync(images.Select(x => x.Id));
var ordered = OrderImages(images, 0, cheers)
.Skip(skip).Take(take)
.ToList();
return ordered.Select(image => ToImagesPlayer(image, cheers.GetValueOrDefault(image.Id))).ToList();
}
[HttpGet("v3/feed/global")]
public async Task<object> GetGlobalFeed([FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var images = await db.Images.AsNoTracking()
.Where(x => x.Accessibility == SavedImageAccessibility.Public &&
x.ImageType == SavedImageType.ShareCamera)
.OrderByDescending(x => x.CreatedAt).ThenByDescending(x => x.Id)
.Skip(skip).Take(Math.Clamp(take, 1, 100))
.ToListAsync();
var ownerIds = images.Select(x => x.OwnerId).Distinct().ToList();
var roomIds = images.Where(x => x.RoomId != null).Select(x => x.RoomId!.Value).Distinct().ToList();
var usernames = await db.Profiles.AsNoTracking()
.Where(p => ownerIds.Contains(p.accountId))
.ToDictionaryAsync(p => p.accountId, p => p.username);
var roomNames = await db.Rooms.AsNoTracking()
.Where(r => roomIds.Contains(r.RoomId))
.ToDictionaryAsync(r => r.RoomId, r => r.Name);
var entries = images.Select(image => new
{
SavedImageId = image.Id,
ImageName = image.Name,
Username = usernames.TryGetValue(image.OwnerId, out var username) ? username : $"Player{image.OwnerId}",
RoomName = image.RoomId != null && roomNames.TryGetValue(image.RoomId.Value, out var roomName) ? roomName : (string?)null,
RoomId = image.RoomId,
SavedImageType = (int)image.ImageType,
PlayerEventId = image.EventId,
Accessibility = (int)image.Accessibility,
PlayerIds = ParseTaggedPlayerIds(image.PlayerIds)
}).ToList();
return new { Images = entries, ValidTill = DateTime.UtcNow.AddMinutes(2) };
}
[HttpGet("v3/bulk")]
public async Task<List<object>> GetBulkImages([FromQuery] string ids)
{
var result = new List<object>();
if (string.IsNullOrWhiteSpace(ids))
return result;
var parsed = ids
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(part => long.TryParse(part, out var value) ? value : -1)
.Where(value => value > 0)
.ToList();
if (parsed.Count == 0)
return result;
var images = await db.Images.AsNoTracking()
.Where(x => parsed.Contains(x.Id) && x.Accessibility == SavedImageAccessibility.Public)
.ToListAsync();
var byId = images.ToDictionary(x => x.Id);
var cheers = await CheerCountsAsync(images.Select(x => x.Id));
foreach (var id in parsed)
{
if (byId.TryGetValue(id, out var image))
result.Add(ToSavedImage(image, cheers.GetValueOrDefault(image.Id)));
}
return result;
}
[HttpGet("v4/{photoId:long}")]
public async Task<IActionResult> GetImageById(long photoId)
{
var image = await db.Images.AsNoTracking().FirstOrDefaultAsync(x => x.Id == photoId);
if (image == null)
return NotFound();
if (image.Accessibility == SavedImageAccessibility.Private)
return NotFound();
return Ok(ToSavedImage(image, await db.ImageInteractions.AsNoTracking()
.CountAsync(c => c.SavedImageId == image.Id && c.IsCheered)));
}
[HttpGet("v5/bulk")]
public async Task<List<object>> GetBulkImageMetadata([FromQuery] string[]? ids)
{
var result = new List<object>();
var parsed = (ids ?? Array.Empty<string>())
.SelectMany(raw => (raw ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
.Select(part => long.TryParse(part, out var value) ? value : -1)
.Where(value => value > 0)
.Distinct()
.ToList();
if (parsed.Count == 0)
return result;
var images = await db.Images.AsNoTracking()
.Where(x => parsed.Contains(x.Id) && x.Accessibility == SavedImageAccessibility.Public)
.ToListAsync();
var byId = images.ToDictionary(x => x.Id);
var cheers = await CheerCountsAsync(images.Select(x => x.Id));
foreach (var id in parsed)
{
if (byId.TryGetValue(id, out var image))
result.Add(ToBulkSavedImage(image, cheers.GetValueOrDefault(image.Id)));
}
return result;
}
private static object ToBulkSavedImage(ImageDB image, int cheerCount) => new
{
Id = image.Id,
Type = (int)image.ImageType,
Accessibility = (int)image.Accessibility,
AccessibilityLocked = false,
ImageName = image.Name,
Description = (string?)null,
PlayerId = image.OwnerId,
TaggedPlayerIds = ParseTaggedPlayerIds(image.PlayerIds),
RoomId = image.RoomId,
PlayerEventId = image.EventId,
CreatedAt = image.CreatedAt,
CheerCount = cheerCount,
CommentCount = 0,
SavedImageId = image.Id,
SavedImageType = (int)image.ImageType,
ClubId = 0
};
[HttpGet("v6")]
public async Task<IActionResult> GetImageByName([FromQuery] string name)
{
if (string.IsNullOrWhiteSpace(name))
return BadRequest(new { error = "name is required" });
var image = await db.Images.AsNoTracking().FirstOrDefaultAsync(x => x.Name == name);
if (image == null)
return NotFound();
if (image.Accessibility == SavedImageAccessibility.Private)
return NotFound();
var viewerId = User.GetUserId() ?? Request.Headers.Authorization.ToString().GetUserId();
if (viewerId != null)
RecentImageReferences.Remember(viewerId.Value, image.Id);
return Ok(new
{
SavedImageId = image.Id,
ImageName = image.Name,
PlayerId = image.OwnerId,
RoomId = image.RoomId ?? 0,
PlayerEventId = image.EventId ?? 0,
ClubId = 0,
Description = "",
Accessibility = (int)image.Accessibility,
AccessibilityLocked = false,
SavedImageType = (int)image.ImageType,
CreatedAt = image.CreatedAt,
CheerCount = await db.ImageInteractions.AsNoTracking()
.CountAsync(c => c.SavedImageId == image.Id && c.IsCheered),
CommentCount = 0
});
}
[HttpPost("v1/cheer"), Authorize]
public async Task<Success> CheerImage([FromBody] ImageCheerRequest req)
{
var userId = User.GetUserId();
if (userId == null)
return new Success(false, "Not signed in", null);
var existingInteraction = await db.ImageInteractions.FirstOrDefaultAsync(x =>
x.PlayerId == userId && x.SavedImageId == req.SavedImageId);
if (existingInteraction != null)
existingInteraction.IsCheered = req.Cheer;
else
await db.ImageInteractions.AddAsync(
new CheeredImage { IsCheered = req.Cheer, PlayerId = (long)userId, SavedImageId = req.SavedImageId }
);
await db.SaveChangesAsync();
return new Success(true, null, null);
}
[HttpGet("v1/{imageId:long}/cheers")]
public async Task<List<long>> GetImageCheers(long imageId)
{
return await db.ImageInteractions.AsNoTracking()
.Where(x => x.SavedImageId == imageId && x.IsCheered)
.Select(x => x.PlayerId)
.ToListAsync();
}
[HttpGet("v2/named")]
public List<NamedImage> GetNamedImages()
{
var config = db.ServerConfig.FirstOrDefault(x => x.Key == "NamedImages");
return config?.Value.FromJson<List<NamedImage>>() ?? new List<NamedImage>();
}
private async Task<List<object>> CheerLookup(List<long> ids)
{
var userId = User.GetUserId();
if (userId == null)
return new List<object>();
ids ??= new List<long>();
var cheeredIds = await db.ImageInteractions.AsNoTracking()
.Where(x => x.PlayerId == userId && x.IsCheered && ids.Contains(x.SavedImageId))
.Select(x => x.SavedImageId)
.ToListAsync();
var cheeredSet = cheeredIds.ToHashSet();
return ids.Select(id => (object)new { SavedImageId = id, IsCheered = cheeredSet.Contains(id) }).ToList();
}
[HttpGet("v5/cheered/bulk"), Authorize]
public Task<List<object>> GetBulkImageCheers([FromQuery] long[] id)
=> CheerLookup((id ?? Array.Empty<long>()).ToList());
[HttpPost("v5/cheered/bulk"), Authorize]
public async Task<List<object>> PostBulkImageCheers()
{
var ids = new List<long>();
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
foreach (var value in form["id"])
{
if (long.TryParse(value, out var parsed))
ids.Add(parsed);
}
}
return await CheerLookup(ids);
}
[HttpPost("v1/{imageId:long}/report"), Authorize]
public async Task<IActionResult> ReportImage(long imageId)
{
await Task.CompletedTask;
return Ok(new { success = true });
}
[HttpPost("v1/{imageId:long}/player/{playerId:long}/untag"), Authorize]
public async Task<IActionResult> UntagFromImage(long imageId, long playerId)
{
var userId = User.GetUserId();
var image = await db.Images.FirstOrDefaultAsync(x => x.Id == imageId);
if (image == null)
return NotFound();
if (userId != playerId && userId != image.OwnerId)
return StatusCode(403);
var tagged = ParseTaggedPlayerIds(image.PlayerIds);
if (!tagged.Remove(playerId))
return Ok(new { success = true });
image.PlayerIds = tagged.ToJson();
await db.SaveChangesAsync();
return Ok(new { success = true });
}
[HttpPost("v4/uploadsaved"), Authorize]
public async Task<ActionResult<ImageNameDTO?>> UploadSavedImage([FromForm] IFormFile? image, [FromForm] SavedImageMetaDTO? imgMeta)
{
if (imgMeta == null)
return BadRequest();
if (image == null || image.Length <= 0 || image.Length >= 2 * 1024 * 1024)
return StatusCode(413);
var userId = User.GetUserId();
if (userId == null)
return Unauthorized();
var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId);
var roomInstance = presence != null
? await db.RoomInstances.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance)
: null;
string imageName = Guid.NewGuid().ToString() + ".png";
string path = Path.Join(DBConfig.CDNImages, imageName);
using (var stream = new FileStream(path, FileMode.Create))
{
await image.CopyToAsync(stream);
}
SavedImageAccessibility accessibility = imgMeta.accessibility;
if (imgMeta.savedImageType == SavedImageType.InventionThumbnail ||
imgMeta.savedImageType == SavedImageType.OutfitThumbnail ||
imgMeta.savedImageType == SavedImageType.ProfileThumbnail ||
imgMeta.savedImageType == SavedImageType.RoomThumbnail)
{
accessibility = SavedImageAccessibility.Private;
}
var taggedPlayerIds = (imgMeta.playerIds ?? new List<long>())
.Where(id => id > 0)
.Distinct()
.ToList();
var savedImage = new ImageDB { CreatedAt = DateTime.Now, Name = imageName, OwnerId = (long)userId, Accessibility = accessibility, ImageType = imgMeta.savedImageType, RoomId = imgMeta.roomId ?? roomInstance?.roomId, EventId = imgMeta.playerEventId, PlayerIds = taggedPlayerIds.ToJson() };
await db.Images.AddAsync(savedImage);
await db.SaveChangesAsync();
RecentImageReferences.Remember((long)userId, savedImage.Id);
if (imgMeta.savedImageType == SavedImageType.ProfileThumbnail)
{
var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == userId);
if (profile != null)
{
profile.profileImage = imageName;
await db.SaveChangesAsync();
}
}
if (accessibility == SavedImageAccessibility.Public || imgMeta.savedImageType == SavedImageType.ShareCamera)
{
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == imgMeta.roomId);
var ownerProfile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == userId);
string? roomName = null;
if (ownerProfile != null)
{
if (room != null)
{
if (room.Accessibility >= Accessibility.Public)
roomName = "^" + room.Name;
else if (room.IsDorm && roomInstance != null)
roomName = roomInstance?.name;
else
roomName = "[PRIVATE]";
}
await WebhookImageUpload.Send($"{DBConfig.NameServer.Images}/img/{imageName}", ownerProfile.username, ownerProfile.displayName, $"{DBConfig.NameServer.Images}/img/{ownerProfile.profileImage}", roomName);
}
}
return new ImageNameDTO { ImageName = imageName };
}
[HttpDelete("v1/deletesaved"), Authorize]
[HttpPost("v1/deletesaved"), Authorize]
public async Task<IActionResult> DeleteSavedImage()
{
var userId = User.GetUserId();
string? imageName = null;
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
imageName = form["ImageName"].ToString();
if (string.IsNullOrWhiteSpace(imageName))
imageName = form["imageName"].ToString();
}
else
{
try
{
using var doc = await System.Text.Json.JsonDocument.ParseAsync(Request.Body);
if (doc.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object &&
doc.RootElement.TryGetProperty("ImageName", out var prop) &&
prop.ValueKind == System.Text.Json.JsonValueKind.String)
imageName = prop.GetString();
}
catch (System.Text.Json.JsonException) { }
}
if (string.IsNullOrWhiteSpace(imageName))
return BadRequest(new { error = "ImageName is required" });
var image = await db.Images.FirstOrDefaultAsync(x => x.Name == imageName);
if (image == null)
return NotFound();
if (image.OwnerId != userId)
return StatusCode(403, new { error = "Not your image" });
var cheers = db.ImageInteractions.Where(x => x.SavedImageId == image.Id);
db.ImageInteractions.RemoveRange(cheers);
db.Images.Remove(image);
await db.SaveChangesAsync();
var fullPath = Path.Join(DBConfig.CDNImages, imageName);
if (System.IO.File.Exists(fullPath))
System.IO.File.Delete(fullPath);
return Ok(new { success = true });
}
}
}