mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RadicadeServer.RecNet.Images
|
||||
{
|
||||
public class CheeredImage
|
||||
{
|
||||
public long SavedImageId { get; set; }
|
||||
public bool IsCheered { get; set; }
|
||||
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), JsonIgnore]
|
||||
public long InteractionId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public long PlayerId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
|
||||
namespace RadicadeServer.Images
|
||||
{
|
||||
[Route("images")]
|
||||
public class ImageAPI : Controller
|
||||
{
|
||||
private const string SignatureData =
|
||||
"IWwe/pZ5vWWqNSkSM/54isgDxlZkdrP0sUrppKCbNktO2yCOTjq746xWiiLsueGuVcAGQqkjeRTimxolHckS/YXSYkEJxtiCXbLlsRia2DyAqtWVkGWsfczzFhp/56U66FVzolTspPCvjScOVlGO7dDIK7sJ+ndcRauWjsQsC6g3e7rUc6uwY099a6gy7sw6xr5BFZQSz8wg+fqyHYD/Sc4nQQVOTFZNNASqbJYhpNhEMXRnafCMuLl8a3mkGwvy3t4q2D/7SM48xrGZjEV47qNx1A91KCe28XVToFh4BzwEUU8nZ0d+KwV79MGarLo1cY8igc8FcoThKcovI4ClOg==";
|
||||
|
||||
public const string PatchedKeyId = "KEY:RSA:p1.radicade.internal";
|
||||
|
||||
public const string StockKeyId = "KEY:RSA:p1.rec.net";
|
||||
|
||||
public static readonly string SignatureString = BuildSignature(PatchedKeyId);
|
||||
|
||||
public static readonly string StockSignatureString = BuildSignature(StockKeyId);
|
||||
|
||||
public const string KeyIdQueryParameter = "rcsig";
|
||||
|
||||
public static readonly string BareRouteKeyId =
|
||||
IsWellFormedKeyId(Environment.GetEnvironmentVariable("RADICADE_IMAGE_BARE_KEYID") ?? string.Empty)
|
||||
? Environment.GetEnvironmentVariable("RADICADE_IMAGE_BARE_KEYID") ?? StockKeyId
|
||||
: StockKeyId;
|
||||
|
||||
private static string BuildSignature(string keyId) => $"key-id={keyId}; data={SignatureData}";
|
||||
|
||||
[HttpGet("img/{imageName}")]
|
||||
public async Task<IActionResult> GetImageFile(string imageName, [FromQuery] int? width, [FromQuery] int? cropSquare, [FromQuery] string sig,
|
||||
[FromQuery(Name = KeyIdQueryParameter)] string clientKeyId)
|
||||
{
|
||||
return await ProcessImage(imageName, width, cropSquare, false, SignatureFor(clientKeyId, "img", PatchedKeyId));
|
||||
}
|
||||
|
||||
[HttpGet("{imageName}")]
|
||||
public async Task<IActionResult> GetImageFileAlt(string imageName, [FromQuery] int? width, [FromQuery] int? cropSquare, [FromQuery] string sig,
|
||||
[FromQuery(Name = KeyIdQueryParameter)] string clientKeyId)
|
||||
{
|
||||
return await ProcessImage(imageName, width, cropSquare, false, SignatureFor(clientKeyId, "bare", BareRouteKeyId));
|
||||
}
|
||||
|
||||
[HttpGet("default")]
|
||||
public async Task<IActionResult> GetDefaultImage([FromQuery] int? width, [FromQuery] int? cropSquare, [FromQuery] string sig,
|
||||
[FromQuery(Name = KeyIdQueryParameter)] string clientKeyId)
|
||||
{
|
||||
return await ProcessImage(DBConfig.DefaultRoomImage, width, cropSquare, false, SignatureFor(clientKeyId, "default", BareRouteKeyId));
|
||||
}
|
||||
|
||||
public static string SignatureFor(string clientKeyId, string route, string routeDefaultKeyId)
|
||||
{
|
||||
var keyId = IsWellFormedKeyId(clientKeyId) ? clientKeyId : routeDefaultKeyId;
|
||||
return BuildSignature(keyId);
|
||||
}
|
||||
|
||||
public static bool IsWellFormedKeyId(string keyId)
|
||||
{
|
||||
const string prefix = "KEY:RSA:p1.";
|
||||
if (string.IsNullOrEmpty(keyId) || keyId.Length > 96 || !keyId.StartsWith(prefix, StringComparison.Ordinal))
|
||||
return false;
|
||||
var tail = keyId.Substring(prefix.Length);
|
||||
if (tail.Length == 0 || tail.Length > 64)
|
||||
return false;
|
||||
foreach (var c in tail)
|
||||
{
|
||||
if (!char.IsLetterOrDigit(c) && c != '.' && c != '-' && c != '_')
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string[] AssetRoots() => new[] { DBConfig.CDNImages, DBConfig.CDNCustom, DBConfig.CDNStorage };
|
||||
|
||||
private static readonly string[] ExtensionVariants = { ".png", ".jpg", ".jpeg" };
|
||||
|
||||
public static string? ResolveAsset(string imageName) => ResolveAsset(imageName, AssetRoots());
|
||||
|
||||
public static string? ResolveAsset(string imageName, IEnumerable<string> roots)
|
||||
{
|
||||
var name = Path.GetFileName(imageName);
|
||||
if (string.IsNullOrWhiteSpace(name) || name == "." || name == "..")
|
||||
return null;
|
||||
|
||||
foreach (var candidate in CandidateNames(name))
|
||||
{
|
||||
foreach (var root in roots)
|
||||
{
|
||||
var path = Path.Join(root, candidate);
|
||||
if (WithinRoot(root, path) && System.IO.File.Exists(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> CandidateNames(string name)
|
||||
{
|
||||
yield return name;
|
||||
|
||||
var existing = Path.GetExtension(name);
|
||||
|
||||
foreach (var variant in ExtensionVariants)
|
||||
{
|
||||
if (existing.Length == 0)
|
||||
{
|
||||
yield return name + variant;
|
||||
}
|
||||
else if (!string.Equals(existing, variant, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
yield return string.Concat(name.AsSpan(0, name.Length - existing.Length), variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool WithinRoot(string root, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fullRoot = Path.GetFullPath(root) + Path.DirectorySeparatorChar;
|
||||
return Path.GetFullPath(path).StartsWith(fullRoot, StringComparison.Ordinal);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ProcessImage(string imageName, int? width, int? cropSquare, bool isFallback = false, string? signature = null)
|
||||
{
|
||||
Response.Headers["Content-Signature"] = signature ?? SignatureString;
|
||||
|
||||
var fullPath = ResolveAsset(imageName);
|
||||
|
||||
if (fullPath == null)
|
||||
{
|
||||
if (!isFallback)
|
||||
{
|
||||
var fallback = ResolveAsset(DBConfig.DefaultRoomImage);
|
||||
if (fallback != null)
|
||||
{
|
||||
return await ServeImageFile(fallback, width, cropSquare);
|
||||
}
|
||||
}
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await ServeImageFile(fullPath, width, cropSquare);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> ServeImageFile(string fullPath, int? width, int? cropSquare)
|
||||
{
|
||||
if (!width.HasValue)
|
||||
{
|
||||
return PhysicalFile(fullPath, "image/jpeg");
|
||||
}
|
||||
|
||||
Image image;
|
||||
try
|
||||
{
|
||||
image = await Image.LoadAsync(fullPath);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return PhysicalFile(fullPath, "image/jpeg");
|
||||
}
|
||||
|
||||
using (image)
|
||||
{
|
||||
if (cropSquare == 1)
|
||||
{
|
||||
int size = Math.Min(image.Width, image.Height);
|
||||
|
||||
var cropRect = new Rectangle((image.Width - size) / 2, (image.Height - size) / 2, size, size);
|
||||
|
||||
image.Mutate(x =>
|
||||
{
|
||||
x.Crop(cropRect);
|
||||
x.Resize(width.Value, width.Value);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
image.Mutate(x => x.Resize(width.Value, 0));
|
||||
}
|
||||
|
||||
IImageFormat format = await Image.DetectFormatAsync(fullPath);
|
||||
|
||||
var ms = new MemoryStream();
|
||||
|
||||
await image.SaveAsync(ms, format);
|
||||
|
||||
ms.Position = 0;
|
||||
|
||||
return File(ms, format.DefaultMimeType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RadicadeServer.RecNet.Images
|
||||
{
|
||||
public class ImageCheerRequest
|
||||
{
|
||||
public bool Cheer { get; set; }
|
||||
public long SavedImageId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,580 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace RadicadeServer.RecNet.Images
|
||||
{
|
||||
public class NamedImage
|
||||
{
|
||||
public string FriendlyImageName { get; set; } = null!;
|
||||
public string ImageName { get; set; } = null!;
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime EndTime { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using RadicadeServer.RecNet.Auth;
|
||||
using RadicadeServer.RecNet.CDN;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
|
||||
namespace RadicadeServer.Radicade.Public
|
||||
{
|
||||
public class PublicPhotoData
|
||||
{
|
||||
public string ImageCdnUrl { get; set; } = DBConfig.DefaultRoomImage;
|
||||
|
||||
public string OwnerName { get; set; } = "@Noone";
|
||||
public string LocationName { get; set; } = "^Nowhere";
|
||||
|
||||
public int Cheers { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Auth;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.Radicade.Public;
|
||||
|
||||
namespace RadicadeServer.API.Public
|
||||
{
|
||||
[Route("p/photos")]
|
||||
public class PublicPhotosAPI(RecDBContext db) : Controller
|
||||
{
|
||||
[HttpGet("latest")]
|
||||
public async Task<ActionResult<List<PublicPhotoData>>> GetLatestPhotos([FromQuery] int? amount)
|
||||
{
|
||||
if (!amount.HasValue) amount = 3;
|
||||
if (amount.Value > 6) return BadRequest("Can't go above 6 pictures.");
|
||||
|
||||
var images = await db.Images.AsNoTracking().Where(x => x.Accessibility == RecNet.CDN.SavedImageAccessibility.Public).OrderByDescending(x => x.CreatedAt).Take(amount.Value).ToListAsync();
|
||||
List<PublicPhotoData> converted = new();
|
||||
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (image == null) continue;
|
||||
|
||||
RoomData? roomData = null;
|
||||
Profile? profile = null;
|
||||
int cheers = 0;
|
||||
|
||||
if (image.RoomId.HasValue)
|
||||
roomData = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == image.RoomId && x.Accessibility >= RecNet.Enums.Accessibility.Public);
|
||||
profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == image.OwnerId);
|
||||
cheers = await db.ImageInteractions.AsNoTracking().Where(x => x.SavedImageId == image.Id).CountAsync();
|
||||
|
||||
if (profile == null) continue;
|
||||
|
||||
PublicPhotoData data = new PublicPhotoData
|
||||
{
|
||||
Cheers = cheers,
|
||||
ImageCdnUrl = $"{DBConfig.NameServer.Images}/img/{image.Name}?width=512",
|
||||
LocationName = (roomData == null) ? "[PRIVATE]" : "^" + roomData.Name,
|
||||
OwnerName = "@" + profile.username
|
||||
};
|
||||
|
||||
converted.Add(data);
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using RadicadeServer.RecNet.CDN;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
|
||||
namespace RadicadeServer.RecNet.Images
|
||||
{
|
||||
public class SharedImage
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public SavedImageType Type { get; set; }
|
||||
public SavedImageAccessibility Accessibility { get; set; }
|
||||
public bool AccessibilityLocked { get; set; } = false;
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
public long PlayerId { get; set; }
|
||||
public long? RoomId { get; set; } = null;
|
||||
public long? PlayerEventId { get; set; } = null;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public int CheerCount { get; set; }
|
||||
public int CommentCount { get; set; }
|
||||
public List<long> TaggedPlayerIds { get; set; } = new();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user