Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+18
View File
@@ -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; }
}
}
+203
View File
@@ -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);
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace RadicadeServer.RecNet.Images
{
public class ImageCheerRequest
{
public bool Cheer { get; set; }
public long SavedImageId { get; set; }
}
}
+580
View File
@@ -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 });
}
}
}
+10
View File
@@ -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; }
}
}
+16
View File
@@ -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;
}
}
+51
View File
@@ -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;
}
}
}
+21
View File
@@ -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();
}
}