diff --git a/DysonNetwork.Sphere/Account/AccountService.cs b/DysonNetwork.Sphere/Account/AccountService.cs index e0a3aff..63c056d 100644 --- a/DysonNetwork.Sphere/Account/AccountService.cs +++ b/DysonNetwork.Sphere/Account/AccountService.cs @@ -17,13 +17,13 @@ public class AccountService( { public async Task PurgeAccountCache(Account account) { - cache.Remove($"dyn_user_friends_{account.Id}"); + cache.Remove($"UserFriends_{account.Id}"); var sessions = await db.AuthSessions.Where(e => e.Account.Id == account.Id).Select(e => e.Id) .ToListAsync(); foreach (var session in sessions) { - cache.Remove($"dyn_auth_{session}"); + cache.Remove($"Auth_{session}"); } } diff --git a/DysonNetwork.Sphere/Account/RelationshipService.cs b/DysonNetwork.Sphere/Account/RelationshipService.cs index f5d745b..310dc97 100644 --- a/DysonNetwork.Sphere/Account/RelationshipService.cs +++ b/DysonNetwork.Sphere/Account/RelationshipService.cs @@ -51,8 +51,8 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa await db.SaveChangesAsync(); await ApplyRelationshipPermissions(relationship); - cache.Remove($"dyn_user_friends_{relationship.AccountId}"); - cache.Remove($"dyn_user_friends_{relationship.RelatedId}"); + cache.Remove($"UserFriends_{relationship.AccountId}"); + cache.Remove($"UserFriends_{relationship.RelatedId}"); return relationship; } @@ -105,8 +105,8 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa ApplyRelationshipPermissions(relationshipBackward) ); - cache.Remove($"dyn_user_friends_{relationship.AccountId}"); - cache.Remove($"dyn_user_friends_{relationship.RelatedId}"); + cache.Remove($"UserFriends_{relationship.AccountId}"); + cache.Remove($"UserFriends_{relationship.RelatedId}"); return relationshipBackward; } @@ -120,20 +120,20 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa db.Update(relationship); await db.SaveChangesAsync(); await ApplyRelationshipPermissions(relationship); - cache.Remove($"dyn_user_friends_{related.Id}"); + cache.Remove($"UserFriends_{related.Id}"); return relationship; } public async Task> ListAccountFriends(Account account) { - if (!cache.TryGetValue($"dyn_user_friends_{account.Id}", out List? friends)) + if (!cache.TryGetValue($"UserFriends_{account.Id}", out List? friends)) { friends = await db.AccountRelationships .Where(r => r.RelatedId == account.Id) .Where(r => r.Status == RelationshipStatus.Friends) .Select(r => r.AccountId) .ToListAsync(); - cache.Set($"dyn_user_friends_{account.Id}", friends, TimeSpan.FromHours(1)); + cache.Set($"UserFriends_{account.Id}", friends, TimeSpan.FromHours(1)); } return friends ?? []; diff --git a/DysonNetwork.Sphere/Auth/UserInfoMiddleware.cs b/DysonNetwork.Sphere/Auth/UserInfoMiddleware.cs index 8fffde6..6c949c4 100644 --- a/DysonNetwork.Sphere/Auth/UserInfoMiddleware.cs +++ b/DysonNetwork.Sphere/Auth/UserInfoMiddleware.cs @@ -10,7 +10,7 @@ public class UserInfoMiddleware(RequestDelegate next, IMemoryCache cache) var sessionIdClaim = context.User.FindFirst("session_id")?.Value; if (sessionIdClaim is not null && Guid.TryParse(sessionIdClaim, out var sessionId)) { - if (!cache.TryGetValue($"dyn_auth_{sessionId}", out Session? session)) + if (!cache.TryGetValue($"Auth_{sessionId}", out Session? session)) { session = await db.AuthSessions .Include(e => e.Challenge) @@ -21,7 +21,7 @@ public class UserInfoMiddleware(RequestDelegate next, IMemoryCache cache) if (session is not null) { - cache.Set($"dyn_auth_{sessionId}", session, TimeSpan.FromHours(1)); + cache.Set($"Auth_{sessionId}", session, TimeSpan.FromHours(1)); } } diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 45612f0..0486ae0 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -230,6 +230,8 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService .FirstOrDefaultAsync(); if (post is null) return NotFound(); + var isSelfReact = post.Publisher.AccountId is not null && post.Publisher.AccountId == currentUser.Id; + var isExistingReaction = await db.PostReactions .AnyAsync(r => r.PostId == post.Id && r.Symbol == request.Symbol && @@ -241,7 +243,7 @@ public class PostController(AppDatabase db, PostService ps, RelationshipService PostId = post.Id, AccountId = currentUser.Id }; - var isRemoving = await ps.ModifyPostVotes(post, reaction, isExistingReaction); + var isRemoving = await ps.ModifyPostVotes(post, reaction, isExistingReaction, isSelfReact); if (isRemoving) return NoContent(); return Ok(reaction); diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index c52a232..32c5ec6 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -174,7 +174,7 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act) /// Post that modifying /// The new / target reaction adding / removing /// Indicate this operation is adding / removing - public async Task ModifyPostVotes(Post post, PostReaction reaction, bool isRemoving) + public async Task ModifyPostVotes(Post post, PostReaction reaction, bool isRemoving, bool isSelfReact) { var isExistingReaction = await db.Set() .AnyAsync(r => r.PostId == post.Id && r.AccountId == reaction.AccountId); @@ -193,6 +193,12 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act) return isRemoving; } + if (isSelfReact) + { + await db.SaveChangesAsync(); + return isRemoving; + } + switch (reaction.Attitude) { case PostReactionAttitude.Positive: diff --git a/DysonNetwork.Sphere/Post/PublisherController.cs b/DysonNetwork.Sphere/Post/PublisherController.cs index 0fbef3b..b881c21 100644 --- a/DysonNetwork.Sphere/Post/PublisherController.cs +++ b/DysonNetwork.Sphere/Post/PublisherController.cs @@ -26,6 +26,14 @@ public class PublisherController(AppDatabase db, PublisherService ps, FileServic return Ok(publisher); } + [HttpGet("{name}/stats")] + public async Task> GetPublisherStats(string name) + { + var stats = await ps.GetPublisherStats(name); + if (stats is null) return NotFound(); + return Ok(stats); + } + [HttpGet] [Authorize] public async Task>> ListManagedPublishers() diff --git a/DysonNetwork.Sphere/Post/PublisherService.cs b/DysonNetwork.Sphere/Post/PublisherService.cs index 5ec48b1..00f4b9d 100644 --- a/DysonNetwork.Sphere/Post/PublisherService.cs +++ b/DysonNetwork.Sphere/Post/PublisherService.cs @@ -1,9 +1,11 @@ using DysonNetwork.Sphere.Storage; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using NodaTime; namespace DysonNetwork.Sphere.Post; -public class PublisherService(AppDatabase db, FileService fs) +public class PublisherService(AppDatabase db, FileService fs, IMemoryCache cache) { public async Task CreateIndividualPublisher( Account.Account account, @@ -44,4 +46,50 @@ public class PublisherService(AppDatabase db, FileService fs) } // TODO Able to create organizational publisher when the realm system is completed + + public class PublisherStats + { + public int PostsCreated { get; set; } + public int StickerPacksCreated { get; set; } + public int StickersCreated { get; set; } + public int UpvoteReceived { get; set; } + public int DownvoteReceived { get; set; } + } + + private const string PublisherStatsCacheKey = "PublisherStats_{0}"; + + public async Task GetPublisherStats(string name) + { + var cacheKey = string.Format(PublisherStatsCacheKey, name); + if (cache.TryGetValue(cacheKey, out PublisherStats? stats)) + return stats; + + var publisher = await db.Publishers.FirstOrDefaultAsync(e => e.Name == name); + if (publisher is null) return null; + + var postsCount = await db.Posts.Where(e => e.Publisher.Id == publisher.Id).CountAsync(); + var postsUpvotes = await db.PostReactions + .Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == PostReactionAttitude.Positive) + .CountAsync(); + var postsDownvotes = await db.PostReactions + .Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == PostReactionAttitude.Negative) + .CountAsync(); + + var stickerPacksId = await db.StickerPacks.Where(e => e.Publisher.Id == publisher.Id).Select(e => e.Id).ToListAsync(); + var stickerPacksCount = stickerPacksId.Count; + + var stickersCount = await db.Stickers.Where(e => stickerPacksId.Contains(e.PackId)).CountAsync(); + + stats = new PublisherStats + { + PostsCreated = postsCount, + StickerPacksCreated = stickerPacksCount, + StickersCreated = stickersCount, + UpvoteReceived = postsUpvotes, + DownvoteReceived = postsDownvotes + }; + + cache.Set(cacheKey, stats, TimeSpan.FromMinutes(5)); + return stats; + } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Sticker/StickerController.cs b/DysonNetwork.Sphere/Sticker/StickerController.cs index 8394c9f..f9d58b5 100644 --- a/DysonNetwork.Sphere/Sticker/StickerController.cs +++ b/DysonNetwork.Sphere/Sticker/StickerController.cs @@ -63,7 +63,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa } [HttpPost] - [RequiredPermission("global", "sticker.packs.create")] + [RequiredPermission("global", "stickers.packs.create")] public async Task> CreateStickerPack([FromBody] StickerPackRequest request) { if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) return Unauthorized(); @@ -104,16 +104,13 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa var pack = await db.StickerPacks .Include(p => p.Publisher) .FirstOrDefaultAsync(p => p.Id == id); - if (pack is null) return NotFound(); var member = await db.PublisherMembers .FirstOrDefaultAsync(m => m.AccountId == currentUser.Id && m.PublisherId == pack.PublisherId); - if (member is null) return StatusCode(403, "You are not a member of this publisher"); - if (member.Role < PublisherMemberRole.Editor) return StatusCode(403, "You need to be at least an editor to update sticker packs"); @@ -151,26 +148,17 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa await st.DeleteStickerPackAsync(pack); return NoContent(); } - - [HttpGet("{packId:guid}/stickers")] - public async Task>> ListStickers(Guid packId, [FromQuery] int offset = 0, - [FromQuery] int take = 20) + [HttpGet("{packId:guid}/content")] + public async Task>> ListStickers(Guid packId) { - var totalCount = await db.Stickers - .Where(s => s.Pack.Id == packId) - .CountAsync(); - var stickers = await db.Stickers .Where(s => s.Pack.Id == packId) .Include(e => e.Pack) .Include(e => e.Image) .OrderByDescending(e => e.CreatedAt) - .Skip(offset) - .Take(take) .ToListAsync(); - Response.Headers["X-Total"] = totalCount.ToString(); return Ok(stickers); } @@ -189,7 +177,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa return Ok(sticker); } - [HttpGet("{packId:guid}/stickers/{id:guid}")] + [HttpGet("{packId:guid}/content/{id:guid}")] public async Task> GetSticker(Guid packId, Guid id) { var sticker = await db.Stickers @@ -208,7 +196,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa public string? ImageId { get; set; } } - [HttpPatch("{packId:guid}/stickers/{id:guid}")] + [HttpPatch("{packId:guid}/content/{id:guid}")] public async Task UpdateSticker(Guid packId, Guid id, [FromBody] StickerRequest request) { if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) @@ -219,6 +207,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa return permissionCheck; var sticker = await db.Stickers + .Include(s => s.Image) .Include(s => s.Pack) .ThenInclude(p => p.Publisher) .FirstOrDefaultAsync(e => e.Id == id && e.Pack.Id == packId); @@ -242,7 +231,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa return Ok(sticker); } - [HttpDelete("{packId:guid}/stickers/{id:guid}")] + [HttpDelete("{packId:guid}/content/{id:guid}")] public async Task DeleteSticker(Guid packId, Guid id) { if (HttpContext.Items["CurrentUser"] is not Account.Account currentUser) @@ -253,6 +242,7 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa return permissionCheck; var sticker = await db.Stickers + .Include(s => s.Image) .Include(s => s.Pack) .ThenInclude(p => p.Publisher) .FirstOrDefaultAsync(e => e.Id == id && e.Pack.Id == packId); @@ -264,7 +254,9 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa return NoContent(); } - [HttpPost("{packId:guid}/stickers")] + public const int MaxStickersPerPack = 24; + + [HttpPost("{packId:guid}/content")] [RequiredPermission("global", "stickers.create")] public async Task CreateSticker(Guid packId, [FromBody] StickerRequest request) { @@ -283,9 +275,12 @@ public class StickerController(AppDatabase db, StickerService st) : ControllerBa var pack = await db.StickerPacks .Include(p => p.Publisher) .FirstOrDefaultAsync(e => e.Id == packId); - if (pack is null) return BadRequest("Sticker pack was not found."); + + var stickersCount = await db.Stickers.CountAsync(s => s.PackId == packId); + if (stickersCount >= MaxStickersPerPack) + return BadRequest($"Sticker pack has reached maximum capacity of {MaxStickersPerPack} stickers."); var image = await db.Files.FirstOrDefaultAsync(e => e.Id == request.ImageId); if (image is null)