From b8fa5f5f24d16139bcfeb86b98fd922b34af075b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 2 Nov 2025 23:08:44 +0800 Subject: [PATCH] :sparkles: More filters available in list post --- DysonNetwork.Sphere/Post/PostController.cs | 682 ++++++++++++++------- 1 file changed, 446 insertions(+), 236 deletions(-) diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 163a441..71e7ccb 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -29,8 +29,7 @@ public class PostController( PaymentService.PaymentServiceClient payments, PollService polls, RemoteRealmService rs -) - : ControllerBase +) : ControllerBase { [HttpGet("featured")] public async Task>> ListFeaturedPosts() @@ -73,7 +72,11 @@ public class PostController( OperationId = "ListPosts", Tags = ["Posts"] )] - [SwaggerResponse(StatusCodes.Status200OK, "Successfully retrieved the list of posts", typeof(List))] + [SwaggerResponse( + StatusCodes.Status200OK, + "Successfully retrieved the list of posts", + typeof(List) + )] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request parameters")] public async Task>> ListPosts( [FromQuery] int offset = 0, @@ -87,17 +90,29 @@ public class PostController( [FromQuery(Name = "media")] bool onlyMedia = false, [FromQuery(Name = "shuffle")] bool shuffle = false, [FromQuery(Name = "replies")] bool? includeReplies = null, - [FromQuery(Name = "pinned")] bool? pinned = null + [FromQuery(Name = "pinned")] bool? pinned = null, + [FromQuery(Name = "order")] string? order = null, + [FromQuery(Name = "orderDesc")] bool orderDesc = true, + [FromQuery(Name = "periodStart")] int? periodStartTime = null, + [FromQuery(Name = "periodEnd")] int? periodEndTime = null ) { HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); var currentUser = currentUserValue as Account; + Instant? periodStart = periodStartTime.HasValue + ? Instant.FromUnixTimeSeconds(periodStartTime.Value) + : null; + Instant? periodEnd = periodEndTime.HasValue + ? Instant.FromUnixTimeSeconds(periodEndTime.Value) + : null; + List userFriends = []; if (currentUser != null) { - var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest - { AccountId = currentUser.Id }); + var friendsResponse = await accounts.ListFriendsAsync( + new ListRelationshipSimpleRequest { AccountId = currentUser.Id } + ); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); } @@ -108,11 +123,14 @@ public class PostController( var publicRealmIds = publicRealms.Select(r => r.Id).ToList(); var visibleRealmIds = userRealms.Concat(publicRealmIds).Distinct().ToList(); - var publisher = pubName == null ? null : await db.Publishers.FirstOrDefaultAsync(p => p.Name == pubName); + var publisher = + pubName == null + ? null + : await db.Publishers.FirstOrDefaultAsync(p => p.Name == pubName); var realm = realmName == null ? null : (await rs.GetRealmBySlug(realmName)); - var query = db.Posts - .Include(e => e.Categories) + var query = db + .Posts.Include(e => e.Categories) .Include(e => e.Tags) .Include(e => e.RepliedPost) .Include(e => e.ForwardedPost) @@ -120,8 +138,6 @@ public class PostController( .AsQueryable(); if (publisher != null) query = query.Where(p => p.PublisherId == publisher.Id); - if (realm != null) - query = query.Where(p => p.RealmId == realm.Id); if (type != null) query = query.Where(p => p.Type == (Shared.Models.PostType)type); if (categories is { Count: > 0 }) @@ -131,8 +147,17 @@ public class PostController( if (onlyMedia) query = query.Where(e => e.Attachments.Count > 0); - if (realm == null) - query = query.Where(p => p.RealmId == null || visibleRealmIds.Contains(p.RealmId.Value)); + if (realm != null) + query = query.Where(p => p.RealmId == realm.Id); + else + query = query.Where(p => + p.RealmId == null || visibleRealmIds.Contains(p.RealmId.Value) + ); + + if (periodStart != null) + query = query.Where(p => (p.PublishedAt ?? p.CreatedAt) >= periodStart); + if (periodEnd != null) + query = query.Where(p => (p.PublishedAt ?? p.CreatedAt) <= periodEnd); switch (pinned) { @@ -144,7 +169,8 @@ public class PostController( break; case true: return BadRequest( - "You need pass extra realm or publisher params in order to filter with pinned posts."); + "You need pass extra realm or publisher params in order to filter with pinned posts." + ); case false: query = query.Where(p => p.PinMode == null); break; @@ -154,32 +180,45 @@ public class PostController( { false => query.Where(e => e.RepliedPostId == null), true => query.Where(e => e.RepliedPostId != null), - _ => query + _ => query, }; if (!string.IsNullOrWhiteSpace(queryTerm)) { query = query.Where(p => - (p.Title != null && EF.Functions.ILike(p.Title, $"%{queryTerm}%")) || - (p.Description != null && EF.Functions.ILike(p.Description, $"%{queryTerm}%")) || - (p.Content != null && EF.Functions.ILike(p.Content, $"%{queryTerm}%")) + (p.Title != null && EF.Functions.ILike(p.Title, $"%{queryTerm}%")) + || (p.Description != null && EF.Functions.ILike(p.Description, $"%{queryTerm}%")) + || (p.Content != null && EF.Functions.ILike(p.Content, $"%{queryTerm}%")) ); } - query = query - .FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true); + query = query.FilterWithVisibility( + currentUser, + userFriends, + userPublishers, + isListing: true + ); - var totalCount = await query - .CountAsync(); + if (shuffle) + { + query = query.OrderBy(e => EF.Functions.Random()); + } + else + { + query = order switch + { + "popularity" => orderDesc + ? query.OrderByDescending(e => e.Upvotes * 10 - e.Downvotes * 10 + e.AwardedScore) + : query.OrderBy(e => e.Upvotes * 10 - e.Downvotes * 10 + e.AwardedScore), + _ => orderDesc + ? query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt) + : query.OrderBy(e => e.PublishedAt ?? e.CreatedAt) + }; + } - query = shuffle - ? query.OrderBy(e => EF.Functions.Random()) - : query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt); + var totalCount = await query.CountAsync(); - var posts = await query - .Skip(offset) - .Take(take) - .ToListAsync(); + var posts = await query.Skip(offset).Take(take).ToListAsync(); posts = await ps.LoadPostInfo(posts, currentUser, true); // Load realm data for posts that have realm @@ -192,8 +231,13 @@ public class PostController( private static async Task LoadPostsRealmsAsync(List posts, RemoteRealmService rs) { - var postRealmIds = posts.Where(p => p.RealmId != null).Select(p => p.RealmId!.Value).Distinct().ToList(); - if (!postRealmIds.Any()) return; + var postRealmIds = posts + .Where(p => p.RealmId != null) + .Select(p => p.RealmId!.Value) + .Distinct() + .ToList(); + if (!postRealmIds.Any()) + return; var realms = await rs.GetRealmBatch(postRealmIds.Select(id => id.ToString()).ToList()); var realmDict = realms.GroupBy(r => r.Id).ToDictionary(g => g.Key, g => g.FirstOrDefault()); @@ -215,15 +259,18 @@ public class PostController( List userFriends = []; if (currentUser != null) { - var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest - { AccountId = currentUser.Id }); + var friendsResponse = await accounts.ListFriendsAsync( + new ListRelationshipSimpleRequest { AccountId = currentUser.Id } + ); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); } - var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); + var userPublishers = currentUser is null + ? [] + : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); - var post = await db.Posts - .Include(e => e.Publisher) + var post = await db + .Posts.Include(e => e.Publisher) .Where(e => e.Slug == slug && e.Publisher.Name == publisherName) .Include(e => e.Tags) .Include(e => e.Categories) @@ -232,7 +279,8 @@ public class PostController( .Include(e => e.FeaturedRecords) .FilterWithVisibility(currentUser, userFriends, userPublishers) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (post is null) + return NotFound(); post = await ps.LoadPostInfo(post, currentUser); if (post.RealmId != null) post.Realm = await rs.GetRealm(post.RealmId.Value.ToString()); @@ -248,15 +296,18 @@ public class PostController( List userFriends = []; if (currentUser != null) { - var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest - { AccountId = currentUser.Id }); + var friendsResponse = await accounts.ListFriendsAsync( + new ListRelationshipSimpleRequest { AccountId = currentUser.Id } + ); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); } - var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); + var userPublishers = currentUser is null + ? [] + : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); - var post = await db.Posts - .Where(e => e.Id == id) + var post = await db + .Posts.Where(e => e.Id == id) .Include(e => e.Publisher) .Include(e => e.Tags) .Include(e => e.Categories) @@ -265,7 +316,8 @@ public class PostController( .Include(e => e.FeaturedRecords) .FilterWithVisibility(currentUser, userFriends, userPublishers) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (post is null) + return NotFound(); post = await ps.LoadPostInfo(post, currentUser); if (post.RealmId != null) { @@ -283,9 +335,9 @@ public class PostController( [FromQuery] int take = 20 ) { - var query = db.PostReactions - .Where(e => e.PostId == id); - if (symbol is not null) query = query.Where(e => e.Symbol == symbol); + var query = db.PostReactions.Where(e => e.PostId == id); + if (symbol is not null) + query = query.Where(e => e.Symbol == symbol); var totalCount = await query.CountAsync(); Response.Headers.Append("X-Total", totalCount.ToString()); @@ -297,8 +349,13 @@ public class PostController( .Skip(offset) .ToListAsync(); - var accountsProto = await remoteAccountsHelper.GetAccountBatch(reactions.Select(r => r.AccountId).ToList()); - var accounts = accountsProto.ToDictionary(a => Guid.Parse(a.Id), a => SnAccount.FromProtoValue(a)); + var accountsProto = await remoteAccountsHelper.GetAccountBatch( + reactions.Select(r => r.AccountId).ToList() + ); + var accounts = accountsProto.ToDictionary( + a => Guid.Parse(a.Id), + a => SnAccount.FromProtoValue(a) + ); foreach (var reaction in reactions) if (accounts.TryGetValue(reaction.AccountId, out var account)) @@ -315,24 +372,26 @@ public class PostController( List userFriends = []; if (currentUser != null) { - var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest - { AccountId = currentUser.Id }); + var friendsResponse = await accounts.ListFriendsAsync( + new ListRelationshipSimpleRequest { AccountId = currentUser.Id } + ); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); } - var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); + var userPublishers = currentUser is null + ? [] + : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); var now = SystemClock.Instance.GetCurrentInstant(); - var post = await db.Posts - .Where(e => e.RepliedPostId == id) + var post = await db + .Posts.Where(e => e.RepliedPostId == id) .OrderByDescending(p => - p.Upvotes * 2 - - p.Downvotes + - ((p.CreatedAt - now).TotalMinutes < 60 ? 5 : 0) + p.Upvotes * 2 - p.Downvotes + ((p.CreatedAt - now).TotalMinutes < 60 ? 5 : 0) ) .FilterWithVisibility(currentUser, userFriends, userPublishers) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (post is null) + return NotFound(); post = await ps.LoadPostInfo(post, currentUser, true); return Ok(post); @@ -346,16 +405,21 @@ public class PostController( List userFriends = []; if (currentUser != null) { - var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest - { AccountId = currentUser.Id }); + var friendsResponse = await accounts.ListFriendsAsync( + new ListRelationshipSimpleRequest { AccountId = currentUser.Id } + ); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); } - var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); + var userPublishers = currentUser is null + ? [] + : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); var now = SystemClock.Instance.GetCurrentInstant(); - var posts = await db.Posts - .Where(e => e.RepliedPostId == id && e.PinMode == Shared.Models.PostPinMode.ReplyPage) + var posts = await db + .Posts.Where(e => + e.RepliedPostId == id && e.PinMode == Shared.Models.PostPinMode.ReplyPage + ) .OrderByDescending(p => p.CreatedAt) .FilterWithVisibility(currentUser, userFriends, userPublishers) .ToListAsync(); @@ -365,8 +429,11 @@ public class PostController( } [HttpGet("{id:guid}/replies")] - public async Task>> ListReplies(Guid id, [FromQuery] int offset = 0, - [FromQuery] int take = 20) + public async Task>> ListReplies( + Guid id, + [FromQuery] int offset = 0, + [FromQuery] int take = 20 + ) { HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue); var currentUser = currentUserValue as Account; @@ -374,24 +441,26 @@ public class PostController( List userFriends = []; if (currentUser != null) { - var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest - { AccountId = currentUser.Id }); + var friendsResponse = await accounts.ListFriendsAsync( + new ListRelationshipSimpleRequest { AccountId = currentUser.Id } + ); userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); } - var userPublishers = currentUser is null ? [] : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); + var userPublishers = currentUser is null + ? [] + : await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); - var parent = await db.Posts - .Where(e => e.Id == id) - .FirstOrDefaultAsync(); - if (parent is null) return NotFound(); + var parent = await db.Posts.Where(e => e.Id == id).FirstOrDefaultAsync(); + if (parent is null) + return NotFound(); - var totalCount = await db.Posts - .Where(e => e.RepliedPostId == parent.Id) + var totalCount = await db + .Posts.Where(e => e.RepliedPostId == parent.Id) .FilterWithVisibility(currentUser, userFriends, userPublishers, isListing: true) .CountAsync(); - var posts = await db.Posts - .Where(e => e.RepliedPostId == id) + var posts = await db + .Posts.Where(e => e.RepliedPostId == id) .Include(e => e.ForwardedPost) .Include(e => e.Categories) .Include(e => e.Tags) @@ -406,8 +475,9 @@ public class PostController( var postsId = posts.Select(e => e.Id).ToList(); var reactionMaps = await ps.GetPostReactionMapBatch(postsId); foreach (var post in posts) - post.ReactionsCount = - reactionMaps.TryGetValue(post.Id, out var count) ? count : new Dictionary(); + post.ReactionsCount = reactionMaps.TryGetValue(post.Id, out var count) + ? count + : new Dictionary(); Response.Headers["X-Total"] = totalCount.ToString(); @@ -417,14 +487,22 @@ public class PostController( public class PostRequest { [MaxLength(1024)] public string? Title { get; set; } + [MaxLength(4096)] public string? Description { get; set; } + [MaxLength(1024)] public string? Slug { get; set; } public string? Content { get; set; } - public Shared.Models.PostVisibility? Visibility { get; set; } = Shared.Models.PostVisibility.Public; + + public Shared.Models.PostVisibility? Visibility { get; set; } = + Shared.Models.PostVisibility.Public; + public Shared.Models.PostType? Type { get; set; } public Shared.Models.PostEmbedView? EmbedView { get; set; } + [MaxLength(16)] public List? Tags { get; set; } + [MaxLength(8)] public List? Categories { get; set; } + [MaxLength(32)] public List? Attachments { get; set; } public Dictionary? Meta { get; set; } public Instant? PublishedAt { get; set; } @@ -445,7 +523,8 @@ public class PostController( request.Content = TextSanitizer.Sanitize(request.Content); if (string.IsNullOrWhiteSpace(request.Content) && request.Attachments is { Count: 0 }) return BadRequest("Content is required."); - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); var accountId = Guid.Parse(currentUser.Id); @@ -454,17 +533,20 @@ public class PostController( { // Use the first personal publisher publisher = await db.Publishers.FirstOrDefaultAsync(e => - e.AccountId == accountId && e.Type == Shared.Models.PublisherType.Individual); + e.AccountId == accountId && e.Type == Shared.Models.PublisherType.Individual + ); } else { publisher = await pub.GetPublisherByName(pubName); - if (publisher is null) return BadRequest("Publisher was not found."); + if (publisher is null) + return BadRequest("Publisher was not found."); if (!await pub.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor)) return StatusCode(403, "You need at least be an editor to post as this publisher."); } - if (publisher is null) return BadRequest("Publisher was not found."); + if (publisher is null) + return BadRequest("Publisher was not found."); var post = new SnPost { @@ -482,22 +564,24 @@ public class PostController( if (request.RepliedPostId is not null) { - var repliedPost = await db.Posts - .Where(p => p.Id == request.RepliedPostId.Value) + var repliedPost = await db + .Posts.Where(p => p.Id == request.RepliedPostId.Value) .Include(p => p.Publisher) .FirstOrDefaultAsync(); - if (repliedPost is null) return BadRequest("Post replying to was not found."); + if (repliedPost is null) + return BadRequest("Post replying to was not found."); post.RepliedPost = repliedPost; post.RepliedPostId = repliedPost.Id; } if (request.ForwardedPostId is not null) { - var forwardedPost = await db.Posts - .Where(p => p.Id == request.ForwardedPostId.Value) + var forwardedPost = await db + .Posts.Where(p => p.Id == request.ForwardedPostId.Value) .Include(p => p.Publisher) .FirstOrDefaultAsync(); - if (forwardedPost is null) return BadRequest("Forwarded post was not found."); + if (forwardedPost is null) + return BadRequest("Forwarded post was not found."); post.ForwardedPost = forwardedPost; post.ForwardedPostId = forwardedPost.Id; } @@ -505,7 +589,13 @@ public class PostController( if (request.RealmId is not null) { var realm = await rs.GetRealm(request.RealmId.Value.ToString()); - if (!await rs.IsMemberWithRole(realm.Id, accountId, new List { RealmMemberRole.Normal })) + if ( + !await rs.IsMemberWithRole( + realm.Id, + accountId, + new List { RealmMemberRole.Normal } + ) + ) return StatusCode(403, "You are not a member of this realm."); post.RealmId = realm.Id; } @@ -516,8 +606,10 @@ public class PostController( { var pollEmbed = await polls.MakePollEmbed(request.PollId.Value); post.Meta ??= new Dictionary(); - if (!post.Meta.TryGetValue("embeds", out var existingEmbeds) || - existingEmbeds is not List) + if ( + !post.Meta.TryGetValue("embeds", out var existingEmbeds) + || existingEmbeds is not List + ) post.Meta["embeds"] = new List>(); var embeds = (List>)post.Meta["embeds"]; embeds.Add(EmbeddableBase.ToDictionary(pollEmbed)); @@ -543,14 +635,22 @@ public class PostController( return BadRequest(err.Message); } - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = ActionLogType.PostCreate, - Meta = { { "post_id", Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) } }, - AccountId = currentUser.Id.ToString(), - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); + _ = als.CreateActionLogAsync( + new CreateActionLogRequest + { + Action = ActionLogType.PostCreate, + Meta = + { + { + "post_id", + Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) + }, + }, + AccountId = currentUser.Id.ToString(), + UserAgent = Request.Headers.UserAgent, + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString(), + } + ); post.Publisher = publisher; @@ -564,18 +664,34 @@ public class PostController( } public static readonly List ReactionsAllowedDefault = - ["thumb_up", "thumb_down", "just_okay", "cry", "confuse", "clap", "laugh", "angry", "party", "pray", "heart"]; + [ + "thumb_up", + "thumb_down", + "just_okay", + "cry", + "confuse", + "clap", + "laugh", + "angry", + "party", + "pray", + "heart", + ]; [HttpPost("{id:guid}/reactions")] [Authorize] [RequiredPermission("global", "posts.react")] - public async Task> ReactPost(Guid id, [FromBody] PostReactionRequest request) + public async Task> ReactPost( + Guid id, + [FromBody] PostReactionRequest request + ) { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); - var friendsResponse = - await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest - { AccountId = currentUser.Id.ToString() }); + var friendsResponse = await accounts.ListFriendsAsync( + new ListRelationshipSimpleRequest { AccountId = currentUser.Id.ToString() } + ); var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); @@ -583,26 +699,27 @@ public class PostController( if (currentUser.PerkSubscription is null) return BadRequest("You need subscription to send custom reactions"); - var post = await db.Posts - .Where(e => e.Id == id) + var post = await db + .Posts.Where(e => e.Id == id) .Include(e => e.Publisher) .FilterWithVisibility(currentUser, userFriends, userPublishers) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (post is null) + return NotFound(); var accountId = Guid.Parse(currentUser.Id); - var isSelfReact = post.Publisher.AccountId is not null && post.Publisher.AccountId == accountId; + var isSelfReact = + post.Publisher.AccountId is not null && post.Publisher.AccountId == accountId; - var isExistingReaction = await db.PostReactions - .AnyAsync(r => r.PostId == post.Id && - r.Symbol == request.Symbol && - r.AccountId == accountId); + var isExistingReaction = await db.PostReactions.AnyAsync(r => + r.PostId == post.Id && r.Symbol == request.Symbol && r.AccountId == accountId + ); var reaction = new SnPostReaction { Symbol = request.Symbol, Attitude = request.Attitude, PostId = post.Id, - AccountId = accountId + AccountId = accountId, }; var isRemoving = await ps.ModifyPostVotes( post, @@ -612,20 +729,26 @@ public class PostController( isSelfReact ); - if (isRemoving) return NoContent(); + if (isRemoving) + return NoContent(); - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = ActionLogType.PostReact, - Meta = + _ = als.CreateActionLogAsync( + new CreateActionLogRequest { - { "post_id", Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) }, - { "reaction", Google.Protobuf.WellKnownTypes.Value.ForString(request.Symbol) } - }, - AccountId = currentUser.Id.ToString(), - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); + Action = ActionLogType.PostReact, + Meta = + { + { + "post_id", + Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) + }, + { "reaction", Google.Protobuf.WellKnownTypes.Value.ForString(request.Symbol) }, + }, + AccountId = currentUser.Id.ToString(), + UserAgent = Request.Headers.UserAgent, + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString(), + } + ); return Ok(reaction); } @@ -634,24 +757,23 @@ public class PostController( { public decimal Amount { get; set; } public Shared.Models.PostReactionAttitude Attitude { get; set; } + [MaxLength(4096)] public string? Message { get; set; } } [HttpGet("{id:guid}/awards")] - public async Task> GetPostAwards(Guid id, [FromQuery] int offset = 0, - [FromQuery] int take = 20) + public async Task> GetPostAwards( + Guid id, + [FromQuery] int offset = 0, + [FromQuery] int take = 20 + ) { - var queryable = db.PostAwards - .Where(a => a.PostId == id) - .AsQueryable(); + var queryable = db.PostAwards.Where(a => a.PostId == id).AsQueryable(); var totalCount = await queryable.CountAsync(); Response.Headers.Append("X-Total", totalCount.ToString()); - var awards = await queryable - .Take(take) - .Skip(offset) - .ToListAsync(); + var awards = await queryable.Take(take).Skip(offset).ToListAsync(); return Ok(awards); } @@ -663,48 +785,56 @@ public class PostController( [HttpPost("{id:guid}/awards")] [Authorize] - public async Task> AwardPost(Guid id, [FromBody] PostAwardRequest request) + public async Task> AwardPost( + Guid id, + [FromBody] PostAwardRequest request + ) { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); if (request.Attitude == Shared.Models.PostReactionAttitude.Neutral) return BadRequest("You cannot create a neutral post award"); - var friendsResponse = - await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest - { AccountId = currentUser.Id.ToString() }); + var friendsResponse = await accounts.ListFriendsAsync( + new ListRelationshipSimpleRequest { AccountId = currentUser.Id.ToString() } + ); var userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); var userPublishers = await pub.GetUserPublishers(Guid.Parse(currentUser.Id)); - var post = await db.Posts - .Where(e => e.Id == id) + var post = await db + .Posts.Where(e => e.Id == id) .Include(e => e.Publisher) .FilterWithVisibility(currentUser, userFriends, userPublishers) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (post is null) + return NotFound(); var accountId = Guid.Parse(currentUser.Id); - var orderRemark = string.IsNullOrWhiteSpace(post.Title) ? "from @" + post.Publisher.Name : post.Title; - var order = await payments.CreateOrderAsync(new CreateOrderRequest - { - ProductIdentifier = "posts.award", - Currency = "points", // NSP - Source Points - Remarks = $"Award post {orderRemark}", - Amount = request.Amount.ToString(CultureInfo.InvariantCulture), - Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary + var orderRemark = string.IsNullOrWhiteSpace(post.Title) + ? "from @" + post.Publisher.Name + : post.Title; + var order = await payments.CreateOrderAsync( + new CreateOrderRequest { - ["account_id"] = accountId, - ["post_id"] = post.Id, - ["amount"] = request.Amount.ToString(CultureInfo.InvariantCulture), - ["message"] = request.Message, - ["attitude"] = request.Attitude, - }) - }); + ProductIdentifier = "posts.award", + Currency = "points", // NSP - Source Points + Remarks = $"Award post {orderRemark}", + Amount = request.Amount.ToString(CultureInfo.InvariantCulture), + Meta = GrpcTypeHelper.ConvertObjectToByteString( + new Dictionary + { + ["account_id"] = accountId, + ["post_id"] = post.Id, + ["amount"] = request.Amount.ToString(CultureInfo.InvariantCulture), + ["message"] = request.Message, + ["attitude"] = request.Attitude, + } + ), + } + ); - return Ok(new PostAwardResponse() - { - OrderId = Guid.Parse(order.Id), - }); + return Ok(new PostAwardResponse() { OrderId = Guid.Parse(order.Id) }); } public class PostPinRequest @@ -716,14 +846,16 @@ public class PostController( [Authorize] public async Task> PinPost(Guid id, [FromBody] PostPinRequest request) { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); - var post = await db.Posts - .Where(e => e.Id == id) + var post = await db + .Posts.Where(e => e.Id == id) .Include(e => e.Publisher) .Include(e => e.RepliedPost) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (post is null) + return NotFound(); var accountId = Guid.Parse(currentUser.Id); if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor)) @@ -731,7 +863,13 @@ public class PostController( if (request.Mode == Shared.Models.PostPinMode.RealmPage && post.RealmId != null) { - if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, new List { RealmMemberRole.Moderator })) + if ( + !await rs.IsMemberWithRole( + post.RealmId.Value, + accountId, + new List { RealmMemberRole.Moderator } + ) + ) return StatusCode(403, "You are not a moderator of this realm"); } @@ -744,18 +882,26 @@ public class PostController( return BadRequest(err.Message); } - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = ActionLogType.PostPin, - Meta = + _ = als.CreateActionLogAsync( + new CreateActionLogRequest { - { "post_id", Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) }, - { "mode", Google.Protobuf.WellKnownTypes.Value.ForString(request.Mode.ToString()) } - }, - AccountId = currentUser.Id.ToString(), - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); + Action = ActionLogType.PostPin, + Meta = + { + { + "post_id", + Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) + }, + { + "mode", + Google.Protobuf.WellKnownTypes.Value.ForString(request.Mode.ToString()) + }, + }, + AccountId = currentUser.Id.ToString(), + UserAgent = Request.Headers.UserAgent, + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString(), + } + ); return Ok(post); } @@ -764,14 +910,16 @@ public class PostController( [Authorize] public async Task> UnpinPost(Guid id) { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); - var post = await db.Posts - .Where(e => e.Id == id) + var post = await db + .Posts.Where(e => e.Id == id) .Include(e => e.Publisher) .Include(e => e.RepliedPost) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (post is null) + return NotFound(); var accountId = Guid.Parse(currentUser.Id); if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor)) @@ -779,7 +927,13 @@ public class PostController( if (post is { PinMode: Shared.Models.PostPinMode.RealmPage, RealmId: not null }) { - if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, new List { RealmMemberRole.Moderator })) + if ( + !await rs.IsMemberWithRole( + post.RealmId.Value, + accountId, + new List { RealmMemberRole.Moderator } + ) + ) return StatusCode(403, "You are not a moderator of this realm"); } @@ -792,17 +946,22 @@ public class PostController( return BadRequest(err.Message); } - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = ActionLogType.PostUnpin, - Meta = + _ = als.CreateActionLogAsync( + new CreateActionLogRequest { - { "post_id", Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) } - }, - AccountId = currentUser.Id.ToString(), - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); + Action = ActionLogType.PostUnpin, + Meta = + { + { + "post_id", + Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) + }, + }, + AccountId = currentUser.Id.ToString(), + UserAgent = Request.Headers.UserAgent, + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString(), + } + ); return Ok(post); } @@ -817,16 +976,18 @@ public class PostController( request.Content = TextSanitizer.Sanitize(request.Content); if (string.IsNullOrWhiteSpace(request.Content) && request.Attachments is { Count: 0 }) return BadRequest("Content is required."); - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); - var post = await db.Posts - .Where(e => e.Id == id) + var post = await db + .Posts.Where(e => e.Id == id) .Include(e => e.Publisher) .Include(e => e.Categories) .Include(e => e.Tags) .Include(e => e.FeaturedRecords) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (post is null) + return NotFound(); var accountId = Guid.Parse(currentUser.Id); if (!await pub.IsMemberWithRole(post.Publisher.Id, accountId, PublisherMemberRole.Editor)) @@ -835,20 +996,31 @@ public class PostController( if (pubName is not null) { var publisher = await pub.GetPublisherByName(pubName); - if (publisher is null) return NotFound(); + if (publisher is null) + return NotFound(); if (!await pub.IsMemberWithRole(publisher.Id, accountId, PublisherMemberRole.Editor)) - return StatusCode(403, "You need at least be an editor to transfer this post to this publisher."); + return StatusCode( + 403, + "You need at least be an editor to transfer this post to this publisher." + ); post.PublisherId = publisher.Id; post.Publisher = publisher; } - if (request.Title is not null) post.Title = request.Title; - if (request.Description is not null) post.Description = request.Description; - if (request.Slug is not null) post.Slug = request.Slug; - if (request.Content is not null) post.Content = request.Content; - if (request.Visibility is not null) post.Visibility = request.Visibility.Value; - if (request.Type is not null) post.Type = request.Type.Value; - if (request.Meta is not null) post.Meta = request.Meta; + if (request.Title is not null) + post.Title = request.Title; + if (request.Description is not null) + post.Description = request.Description; + if (request.Slug is not null) + post.Slug = request.Slug; + if (request.Content is not null) + post.Content = request.Content; + if (request.Visibility is not null) + post.Visibility = request.Visibility.Value; + if (request.Type is not null) + post.Type = request.Type.Value; + if (request.Meta is not null) + post.Meta = request.Meta; // The same, this field can be null, so update it anyway. post.EmbedView = request.EmbedView; @@ -861,12 +1033,16 @@ public class PostController( { var pollEmbed = await polls.MakePollEmbed(request.PollId.Value); post.Meta ??= new Dictionary(); - if (!post.Meta.TryGetValue("embeds", out var existingEmbeds) || - existingEmbeds is not List) + if ( + !post.Meta.TryGetValue("embeds", out var existingEmbeds) + || existingEmbeds is not List + ) post.Meta["embeds"] = new List>(); var embeds = (List>)post.Meta["embeds"]; // Remove all old poll embeds - embeds.RemoveAll(e => e.TryGetValue("type", out var type) && type.ToString() == "poll"); + embeds.RemoveAll(e => + e.TryGetValue("type", out var type) && type.ToString() == "poll" + ); embeds.Add(EmbeddableBase.ToDictionary(pollEmbed)); post.Meta["embeds"] = embeds; } @@ -878,8 +1054,10 @@ public class PostController( else { post.Meta ??= new Dictionary(); - if (!post.Meta.TryGetValue("embeds", out var existingEmbeds) || - existingEmbeds is not List) + if ( + !post.Meta.TryGetValue("embeds", out var existingEmbeds) + || existingEmbeds is not List + ) post.Meta["embeds"] = new List>(); var embeds = (List>)post.Meta["embeds"]; // Remove all old poll embeds @@ -890,7 +1068,13 @@ public class PostController( if (request.RealmId is not null) { var realm = await rs.GetRealm(request.RealmId.Value.ToString()); - if (!await rs.IsMemberWithRole(realm.Id, accountId, new List { RealmMemberRole.Normal })) + if ( + !await rs.IsMemberWithRole( + realm.Id, + accountId, + new List { RealmMemberRole.Normal } + ) + ) return StatusCode(403, "You are not a member of this realm."); post.RealmId = realm.Id; } @@ -914,14 +1098,22 @@ public class PostController( return BadRequest(err.Message); } - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = ActionLogType.PostUpdate, - Meta = { { "post_id", Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) } }, - AccountId = currentUser.Id.ToString(), - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); + _ = als.CreateActionLogAsync( + new CreateActionLogRequest + { + Action = ActionLogType.PostUpdate, + Meta = + { + { + "post_id", + Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) + }, + }, + AccountId = currentUser.Id.ToString(), + UserAgent = Request.Headers.UserAgent, + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString(), + } + ); return Ok(post); } @@ -929,29 +1121,47 @@ public class PostController( [HttpDelete("{id:guid}")] public async Task> DeletePost(Guid id) { - if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); + if (HttpContext.Items["CurrentUser"] is not Account currentUser) + return Unauthorized(); - var post = await db.Posts - .Where(e => e.Id == id) + var post = await db + .Posts.Where(e => e.Id == id) .Include(e => e.Publisher) .FirstOrDefaultAsync(); - if (post is null) return NotFound(); + if (post is null) + return NotFound(); - if (!await pub.IsMemberWithRole(post.Publisher.Id, Guid.Parse(currentUser.Id), - PublisherMemberRole.Editor)) - return StatusCode(403, "You need at least be an editor to delete the publisher's post."); + if ( + !await pub.IsMemberWithRole( + post.Publisher.Id, + Guid.Parse(currentUser.Id), + PublisherMemberRole.Editor + ) + ) + return StatusCode( + 403, + "You need at least be an editor to delete the publisher's post." + ); await ps.DeletePostAsync(post); - _ = als.CreateActionLogAsync(new CreateActionLogRequest - { - Action = ActionLogType.PostDelete, - Meta = { { "post_id", Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) } }, - AccountId = currentUser.Id.ToString(), - UserAgent = Request.Headers.UserAgent, - IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString() - }); + _ = als.CreateActionLogAsync( + new CreateActionLogRequest + { + Action = ActionLogType.PostDelete, + Meta = + { + { + "post_id", + Google.Protobuf.WellKnownTypes.Value.ForString(post.Id.ToString()) + }, + }, + AccountId = currentUser.Id.ToString(), + UserAgent = Request.Headers.UserAgent, + IpAddress = Request.HttpContext.Connection.RemoteIpAddress?.ToString(), + } + ); return NoContent(); } -} \ No newline at end of file +}