✨ Award post
This commit is contained in:
		| @@ -1,4 +1,5 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.Globalization; | ||||
| using DysonNetwork.Shared.Auth; | ||||
| using DysonNetwork.Shared.Content; | ||||
| using DysonNetwork.Shared.Data; | ||||
| @@ -24,6 +25,7 @@ public class PostController( | ||||
|     PublisherService pub, | ||||
|     AccountService.AccountServiceClient accounts, | ||||
|     ActionLogService.ActionLogServiceClient als, | ||||
|     PaymentService.PaymentServiceClient payments, | ||||
|     PollService polls, | ||||
|     RealmService rs | ||||
| ) | ||||
| @@ -324,7 +326,6 @@ public class PostController( | ||||
|             .OrderByDescending(p => p.CreatedAt) | ||||
|             .FilterWithVisibility(currentUser, userFriends, userPublishers) | ||||
|             .ToListAsync(); | ||||
|         if (posts is null) return NotFound(); | ||||
|         posts = await ps.LoadPostInfo(posts, currentUser); | ||||
|  | ||||
|         return Ok(posts); | ||||
| @@ -582,14 +583,22 @@ public class PostController( | ||||
|     public class PostAwardRequest | ||||
|     { | ||||
|         public decimal Amount { get; set; } | ||||
|         public PostReactionAttitude Attitude { get; set; } | ||||
|         [MaxLength(4096)] public string? Message { get; set; } | ||||
|     } | ||||
|  | ||||
|     public class PostAwardResponse | ||||
|     { | ||||
|         public Guid OrderId { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{id:guid}/awards")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<PostAward>> AwardPost(Guid id, [FromBody] PostAwardRequest request) | ||||
|     public async Task<ActionResult<PostAwardResponse>> AwardPost(Guid id, [FromBody] PostAwardRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         if (request.Attitude == PostReactionAttitude.Neutral) | ||||
|             return BadRequest("You cannot create a neutral post award"); | ||||
|  | ||||
|         var friendsResponse = | ||||
|             await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest | ||||
| @@ -605,10 +614,27 @@ public class PostController( | ||||
|         if (post is null) return NotFound(); | ||||
|  | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|          | ||||
|         // TODO Make payment, add record | ||||
|  | ||||
|         return Ok(); | ||||
|         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}", | ||||
|             Meta = GrpcTypeHelper.ConvertObjectToByteString(new Dictionary<string, object?>() | ||||
|             { | ||||
|                 ["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), | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     public class PostPinRequest | ||||
|   | ||||
| @@ -424,10 +424,12 @@ public partial class PostService( | ||||
|         var accountId = Guid.Parse(currentUser.Id); | ||||
|         if (post.RepliedPostId != null) | ||||
|         { | ||||
|             if (pinMode != PostPinMode.ReplyPage) throw new InvalidOperationException("Replies can only be pinned in the reply page."); | ||||
|             if (pinMode != PostPinMode.ReplyPage) | ||||
|                 throw new InvalidOperationException("Replies can only be pinned in the reply page."); | ||||
|             if (post.RepliedPost == null) throw new ArgumentNullException(nameof(post.RepliedPost)); | ||||
|  | ||||
|             if (!await ps.IsMemberWithRole(post.RepliedPost.PublisherId, accountId, Publisher.PublisherMemberRole.Editor)) | ||||
|             if (!await ps.IsMemberWithRole(post.RepliedPost.PublisherId, accountId, | ||||
|                     Publisher.PublisherMemberRole.Editor)) | ||||
|                 throw new InvalidOperationException("Only editors of original post can pin replies."); | ||||
|  | ||||
|             post.PinMode = pinMode; | ||||
| @@ -453,7 +455,8 @@ public partial class PostService( | ||||
|         { | ||||
|             if (post.RepliedPost == null) throw new ArgumentNullException(nameof(post.RepliedPost)); | ||||
|  | ||||
|             if (!await ps.IsMemberWithRole(post.RepliedPost.PublisherId, accountId, Publisher.PublisherMemberRole.Editor)) | ||||
|             if (!await ps.IsMemberWithRole(post.RepliedPost.PublisherId, accountId, | ||||
|                     Publisher.PublisherMemberRole.Editor)) | ||||
|                 throw new InvalidOperationException("Only editors of original post can unpin replies."); | ||||
|         } | ||||
|         else | ||||
| @@ -815,9 +818,10 @@ public partial class PostService( | ||||
|         { | ||||
|             // The previous day highest rated posts | ||||
|             var today = SystemClock.Instance.GetCurrentInstant(); | ||||
|             var periodStart = today.InUtc().Date.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant().Minus(Duration.FromDays(1)); | ||||
|             var periodStart = today.InUtc().Date.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant() | ||||
|                 .Minus(Duration.FromDays(1)); | ||||
|             var periodEnd = today.InUtc().Date.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant(); | ||||
|              | ||||
|  | ||||
|             var postsInPeriod = await db.Posts | ||||
|                 .Where(e => e.Visibility == PostVisibility.Public) | ||||
|                 .Where(e => e.CreatedAt >= periodStart && e.CreatedAt < periodEnd) | ||||
| @@ -890,6 +894,34 @@ public partial class PostService( | ||||
|  | ||||
|         return posts; | ||||
|     } | ||||
|  | ||||
|     public async Task<PostAward> AwardPost( | ||||
|         Guid postId, | ||||
|         Guid accountId, | ||||
|         decimal amount, | ||||
|         PostReactionAttitude attitude, | ||||
|         string? message | ||||
|     ) | ||||
|     { | ||||
|         var award = new PostAward | ||||
|         { | ||||
|             Amount = amount, | ||||
|             Attitude = attitude, | ||||
|             Message = message, | ||||
|             PostId = postId, | ||||
|             AccountId = accountId | ||||
|         }; | ||||
|  | ||||
|         db.PostAwards.Add(award); | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         var delta = award.Attitude == PostReactionAttitude.Positive ? amount : -amount; | ||||
|  | ||||
|         await db.Posts.Where(p => p.Id == postId) | ||||
|             .ExecuteUpdateAsync(s => s.SetProperty(p => p.AwardedScore, p => p.AwardedScore + delta)); | ||||
|  | ||||
|         return award; | ||||
|     } | ||||
| } | ||||
|  | ||||
| public static class PostQueryExtensions | ||||
| @@ -925,4 +957,4 @@ public static class PostQueryExtensions | ||||
|                         (e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) || | ||||
|                         publishersId.Contains(e.PublisherId)); | ||||
|     } | ||||
| } | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Shared.Stream; | ||||
| using DysonNetwork.Sphere.Post; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NATS.Client.Core; | ||||
|  | ||||
| @@ -13,7 +14,84 @@ public class BroadcastEventHandler( | ||||
| { | ||||
|     protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||||
|     { | ||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>(AccountDeletedEvent.Type, cancellationToken: stoppingToken)) | ||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>(PaymentOrderEvent.Type, cancellationToken: stoppingToken)) | ||||
|         { | ||||
|             PaymentOrderEvent? evt = null; | ||||
|             try | ||||
|             { | ||||
|                 evt = JsonSerializer.Deserialize<PaymentOrderEvent>(msg.Data); | ||||
|  | ||||
|                 // Every order goes into the MQ is already paid, so we skipped the status validation | ||||
|  | ||||
|                 if (evt?.ProductIdentifier is null) | ||||
|                     continue; | ||||
|  | ||||
|                 switch (evt.ProductIdentifier) | ||||
|                 { | ||||
|                     case "posts.award": | ||||
|                     { | ||||
|                         logger.LogInformation("Handling post award order: {OrderId}", evt.OrderId); | ||||
|  | ||||
|                         if (!evt.Meta.TryGetValue("account_id", out var accountIdObj) || | ||||
|                             accountIdObj is not string accountIdStr || | ||||
|                             !Guid.TryParse(accountIdStr, out var accountId)) | ||||
|                         { | ||||
|                             logger.LogWarning("Post award order {OrderId} missing or invalid account_id", evt.OrderId); | ||||
|                             break; | ||||
|                         } | ||||
|  | ||||
|                         if (!evt.Meta.TryGetValue("post_id", out var postIdObj) || | ||||
|                             postIdObj is not string postIdStr || | ||||
|                             !Guid.TryParse(postIdStr, out var postId)) | ||||
|                         { | ||||
|                             logger.LogWarning("Post award order {OrderId} missing or invalid post_id", evt.OrderId); | ||||
|                             break; | ||||
|                         } | ||||
|  | ||||
|                         if (!evt.Meta.TryGetValue("amount", out var amountObj) || | ||||
|                             amountObj is not string amountStr || | ||||
|                             !decimal.TryParse(amountStr, out var amount)) | ||||
|                         { | ||||
|                             logger.LogWarning("Post award order {OrderId} missing or invalid amount", evt.OrderId); | ||||
|                             break; | ||||
|                         } | ||||
|  | ||||
|                         if (!evt.Meta.TryGetValue("attitude", out var attitudeObj) || | ||||
|                             attitudeObj is not string attitudeStr || | ||||
|                             !int.TryParse(attitudeStr, out var attitudeInt) || | ||||
|                             !Enum.IsDefined(typeof(PostReactionAttitude), attitudeInt)) | ||||
|                         { | ||||
|                             logger.LogWarning("Post award order {OrderId} missing or invalid attitude", evt.OrderId); | ||||
|                             break; | ||||
|                         } | ||||
|                         var attitude = (PostReactionAttitude)attitudeInt; | ||||
|  | ||||
|                         string? message = null; | ||||
|                         if (evt.Meta.TryGetValue("message", out var messageObj) && | ||||
|                             messageObj is string messageStr) | ||||
|                         { | ||||
|                             message = messageStr; | ||||
|                         } | ||||
|  | ||||
|                         await using var scope = serviceProvider.CreateAsyncScope(); | ||||
|                         var ps = scope.ServiceProvider.GetRequiredService<PostService>(); | ||||
|  | ||||
|                         await ps.AwardPost(postId, accountId, amount, attitude, message); | ||||
|  | ||||
|                         logger.LogInformation("Post award for order {OrderId} handled successfully.", evt.OrderId); | ||||
|  | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             catch (Exception ex) | ||||
|             { | ||||
|                 logger.LogError(ex, "Error processing payment order event for order {OrderId}", evt?.OrderId); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         await foreach (var msg in nats.SubscribeAsync<byte[]>(AccountDeletedEvent.Type, | ||||
|                            cancellationToken: stoppingToken)) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
| @@ -21,14 +99,14 @@ public class BroadcastEventHandler( | ||||
|                 if (evt == null) continue; | ||||
|  | ||||
|                 logger.LogInformation("Account deleted: {AccountId}", evt.AccountId); | ||||
|                  | ||||
|  | ||||
|                 using var scope = serviceProvider.CreateScope(); | ||||
|                 var db = scope.ServiceProvider.GetRequiredService<AppDatabase>(); | ||||
|  | ||||
|                 await db.ChatMembers | ||||
|                     .Where(m => m.AccountId == evt.AccountId) | ||||
|                     .ExecuteDeleteAsync(cancellationToken: stoppingToken); | ||||
|                  | ||||
|  | ||||
|                 await db.RealmMembers | ||||
|                     .Where(m => m.AccountId == evt.AccountId) | ||||
|                     .ExecuteDeleteAsync(cancellationToken: stoppingToken); | ||||
| @@ -44,7 +122,7 @@ public class BroadcastEventHandler( | ||||
|                         await db.Posts | ||||
|                             .Where(p => p.PublisherId == publisher.Id) | ||||
|                             .ExecuteDeleteAsync(cancellationToken: stoppingToken); | ||||
|                      | ||||
|  | ||||
|                     var publisherIds = publishers.Select(p => p.Id).ToList(); | ||||
|                     await db.Publishers | ||||
|                         .Where(p => publisherIds.Contains(p.Id)) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user