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 ParseTaggedPlayerIds(string json) { try { return string.IsNullOrWhiteSpace(json) ? new List() : (json.FromJson>() ?? new List()); } catch { return new List(); } } private async Task> CheerCountsAsync(IEnumerable imageIds) { var ids = imageIds.Distinct().ToList(); if (ids.Count == 0) return new Dictionary(); 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> 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> GetImagesFromPlayerV4(long id, [FromQuery] int skip = 0, [FromQuery] int take = 100) => GetImagesFromPlayer(id, 0, skip, take); [HttpGet("v4/room/{id}")] public async Task> GetImagesFromRoom(long? id, [FromQuery] int sort = 0, [FromQuery] int filter = 0, [FromQuery] int skip = 0, [FromQuery] int take = 100) { var result = new List(); 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> 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 OrderImages(IEnumerable images, int sort, Dictionary 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> 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 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> GetBulkImages([FromQuery] string ids) { var result = new List(); 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 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> GetBulkImageMetadata([FromQuery] string[]? ids) { var result = new List(); var parsed = (ids ?? Array.Empty()) .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 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 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> 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 GetNamedImages() { var config = db.ServerConfig.FirstOrDefault(x => x.Key == "NamedImages"); return config?.Value.FromJson>() ?? new List(); } private async Task> CheerLookup(List ids) { var userId = User.GetUserId(); if (userId == null) return new List(); ids ??= new List(); 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> GetBulkImageCheers([FromQuery] long[] id) => CheerLookup((id ?? Array.Empty()).ToList()); [HttpPost("v5/cheered/bulk"), Authorize] public async Task> PostBulkImageCheers() { var ids = new List(); 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 ReportImage(long imageId) { await Task.CompletedTask; return Ok(new { success = true }); } [HttpPost("v1/{imageId:long}/player/{playerId:long}/untag"), Authorize] public async Task 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> 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()) .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 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 }); } } }