✨ Award post
This commit is contained in:
@@ -27,6 +27,7 @@ public class PaymentService(
|
|||||||
Duration? expiration = null,
|
Duration? expiration = null,
|
||||||
string? appIdentifier = null,
|
string? appIdentifier = null,
|
||||||
string? productIdentifier = null,
|
string? productIdentifier = null,
|
||||||
|
string? remarks = null,
|
||||||
Dictionary<string, object>? meta = null,
|
Dictionary<string, object>? meta = null,
|
||||||
bool reuseable = true
|
bool reuseable = true
|
||||||
)
|
)
|
||||||
@@ -65,6 +66,7 @@ public class PaymentService(
|
|||||||
ExpiredAt = SystemClock.Instance.GetCurrentInstant().Plus(expiration ?? Duration.FromHours(24)),
|
ExpiredAt = SystemClock.Instance.GetCurrentInstant().Plus(expiration ?? Duration.FromHours(24)),
|
||||||
AppIdentifier = appIdentifier,
|
AppIdentifier = appIdentifier,
|
||||||
ProductIdentifier = productIdentifier,
|
ProductIdentifier = productIdentifier,
|
||||||
|
Remarks = remarks,
|
||||||
Meta = meta
|
Meta = meta
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ public class PaymentServiceGrpc(PaymentService paymentService) : Shared.Proto.Pa
|
|||||||
request.Expiration is not null ? Duration.FromSeconds(request.Expiration.Seconds) : null,
|
request.Expiration is not null ? Duration.FromSeconds(request.Expiration.Seconds) : null,
|
||||||
request.HasAppIdentifier ? request.AppIdentifier : Order.InternalAppIdentifier,
|
request.HasAppIdentifier ? request.AppIdentifier : Order.InternalAppIdentifier,
|
||||||
request.HasProductIdentifier ? request.ProductIdentifier : null,
|
request.HasProductIdentifier ? request.ProductIdentifier : null,
|
||||||
|
request.HasRemarks ? request.Remarks : null,
|
||||||
request.HasMeta
|
request.HasMeta
|
||||||
? GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>(request.Meta)
|
? GrpcTypeHelper.ConvertByteStringToObject<Dictionary<string, object>>(request.Meta)
|
||||||
: null,
|
: null,
|
||||||
|
@@ -97,7 +97,19 @@ public static class GrpcClientHelper
|
|||||||
return new PermissionService.PermissionServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
return new PermissionService.PermissionServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
clientCertPassword));
|
clientCertPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<PaymentService.PaymentServiceClient> CreatePaymentServiceClient(
|
||||||
|
IEtcdClient etcdClient,
|
||||||
|
string clientCertPath,
|
||||||
|
string clientKeyPath,
|
||||||
|
string? clientCertPassword = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var url = await GetServiceUrlFromEtcd(etcdClient, "DysonNetwork.Pass");
|
||||||
|
return new PaymentService.PaymentServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
|
||||||
|
clientCertPassword));
|
||||||
|
}
|
||||||
|
|
||||||
public static async Task<PusherService.PusherServiceClient> CreatePusherServiceClient(
|
public static async Task<PusherService.PusherServiceClient> CreatePusherServiceClient(
|
||||||
IEtcdClient etcdClient,
|
IEtcdClient etcdClient,
|
||||||
string clientCertPath,
|
string clientCertPath,
|
||||||
|
@@ -126,6 +126,7 @@ message CreateOrderRequest {
|
|||||||
optional string product_identifier = 8;
|
optional string product_identifier = 8;
|
||||||
// Using bytes for meta to represent JSON.
|
// Using bytes for meta to represent JSON.
|
||||||
optional bytes meta = 6;
|
optional bytes meta = 6;
|
||||||
|
optional string remarks = 9;
|
||||||
bool reuseable = 7;
|
bool reuseable = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -71,6 +71,20 @@ public static class ServiceInjectionHelper
|
|||||||
.GetResult();
|
.GetResult();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.AddSingleton<PaymentService.PaymentServiceClient>(sp =>
|
||||||
|
{
|
||||||
|
var etcdClient = sp.GetRequiredService<IEtcdClient>();
|
||||||
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
|
var clientCertPath = config["Service:ClientCert"]!;
|
||||||
|
var clientKeyPath = config["Service:ClientKey"]!;
|
||||||
|
var clientCertPassword = config["Service:CertPassword"];
|
||||||
|
|
||||||
|
return GrpcClientHelper
|
||||||
|
.CreatePaymentServiceClient(etcdClient, clientCertPath, clientKeyPath, clientCertPassword)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Globalization;
|
||||||
using DysonNetwork.Shared.Auth;
|
using DysonNetwork.Shared.Auth;
|
||||||
using DysonNetwork.Shared.Content;
|
using DysonNetwork.Shared.Content;
|
||||||
using DysonNetwork.Shared.Data;
|
using DysonNetwork.Shared.Data;
|
||||||
@@ -24,6 +25,7 @@ public class PostController(
|
|||||||
PublisherService pub,
|
PublisherService pub,
|
||||||
AccountService.AccountServiceClient accounts,
|
AccountService.AccountServiceClient accounts,
|
||||||
ActionLogService.ActionLogServiceClient als,
|
ActionLogService.ActionLogServiceClient als,
|
||||||
|
PaymentService.PaymentServiceClient payments,
|
||||||
PollService polls,
|
PollService polls,
|
||||||
RealmService rs
|
RealmService rs
|
||||||
)
|
)
|
||||||
@@ -324,7 +326,6 @@ public class PostController(
|
|||||||
.OrderByDescending(p => p.CreatedAt)
|
.OrderByDescending(p => p.CreatedAt)
|
||||||
.FilterWithVisibility(currentUser, userFriends, userPublishers)
|
.FilterWithVisibility(currentUser, userFriends, userPublishers)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
if (posts is null) return NotFound();
|
|
||||||
posts = await ps.LoadPostInfo(posts, currentUser);
|
posts = await ps.LoadPostInfo(posts, currentUser);
|
||||||
|
|
||||||
return Ok(posts);
|
return Ok(posts);
|
||||||
@@ -582,14 +583,22 @@ public class PostController(
|
|||||||
public class PostAwardRequest
|
public class PostAwardRequest
|
||||||
{
|
{
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
|
public PostReactionAttitude Attitude { get; set; }
|
||||||
[MaxLength(4096)] public string? Message { get; set; }
|
[MaxLength(4096)] public string? Message { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PostAwardResponse
|
||||||
|
{
|
||||||
|
public Guid OrderId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{id:guid}/awards")]
|
[HttpPost("{id:guid}/awards")]
|
||||||
[Authorize]
|
[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 (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 =
|
var friendsResponse =
|
||||||
await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest
|
||||||
@@ -605,10 +614,27 @@ public class PostController(
|
|||||||
if (post is null) return NotFound();
|
if (post is null) return NotFound();
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
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
|
public class PostPinRequest
|
||||||
|
@@ -424,10 +424,12 @@ public partial class PostService(
|
|||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
if (post.RepliedPostId != null)
|
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 (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.");
|
throw new InvalidOperationException("Only editors of original post can pin replies.");
|
||||||
|
|
||||||
post.PinMode = pinMode;
|
post.PinMode = pinMode;
|
||||||
@@ -453,7 +455,8 @@ public partial class PostService(
|
|||||||
{
|
{
|
||||||
if (post.RepliedPost == null) throw new ArgumentNullException(nameof(post.RepliedPost));
|
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.");
|
throw new InvalidOperationException("Only editors of original post can unpin replies.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -815,9 +818,10 @@ public partial class PostService(
|
|||||||
{
|
{
|
||||||
// The previous day highest rated posts
|
// The previous day highest rated posts
|
||||||
var today = SystemClock.Instance.GetCurrentInstant();
|
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 periodEnd = today.InUtc().Date.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
|
||||||
|
|
||||||
var postsInPeriod = await db.Posts
|
var postsInPeriod = await db.Posts
|
||||||
.Where(e => e.Visibility == PostVisibility.Public)
|
.Where(e => e.Visibility == PostVisibility.Public)
|
||||||
.Where(e => e.CreatedAt >= periodStart && e.CreatedAt < periodEnd)
|
.Where(e => e.CreatedAt >= periodStart && e.CreatedAt < periodEnd)
|
||||||
@@ -890,6 +894,34 @@ public partial class PostService(
|
|||||||
|
|
||||||
return posts;
|
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
|
public static class PostQueryExtensions
|
||||||
@@ -925,4 +957,4 @@ public static class PostQueryExtensions
|
|||||||
(e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) ||
|
(e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) ||
|
||||||
publishersId.Contains(e.PublisherId));
|
publishersId.Contains(e.PublisherId));
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using DysonNetwork.Shared.Stream;
|
using DysonNetwork.Shared.Stream;
|
||||||
|
using DysonNetwork.Sphere.Post;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NATS.Client.Core;
|
using NATS.Client.Core;
|
||||||
|
|
||||||
@@ -13,7 +14,84 @@ public class BroadcastEventHandler(
|
|||||||
{
|
{
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -21,14 +99,14 @@ public class BroadcastEventHandler(
|
|||||||
if (evt == null) continue;
|
if (evt == null) continue;
|
||||||
|
|
||||||
logger.LogInformation("Account deleted: {AccountId}", evt.AccountId);
|
logger.LogInformation("Account deleted: {AccountId}", evt.AccountId);
|
||||||
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
using var scope = serviceProvider.CreateScope();
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||||
|
|
||||||
await db.ChatMembers
|
await db.ChatMembers
|
||||||
.Where(m => m.AccountId == evt.AccountId)
|
.Where(m => m.AccountId == evt.AccountId)
|
||||||
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
||||||
|
|
||||||
await db.RealmMembers
|
await db.RealmMembers
|
||||||
.Where(m => m.AccountId == evt.AccountId)
|
.Where(m => m.AccountId == evt.AccountId)
|
||||||
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
||||||
@@ -44,7 +122,7 @@ public class BroadcastEventHandler(
|
|||||||
await db.Posts
|
await db.Posts
|
||||||
.Where(p => p.PublisherId == publisher.Id)
|
.Where(p => p.PublisherId == publisher.Id)
|
||||||
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
.ExecuteDeleteAsync(cancellationToken: stoppingToken);
|
||||||
|
|
||||||
var publisherIds = publishers.Select(p => p.Id).ToList();
|
var publisherIds = publishers.Select(p => p.Id).ToList();
|
||||||
await db.Publishers
|
await db.Publishers
|
||||||
.Where(p => publisherIds.Contains(p.Id))
|
.Where(p => publisherIds.Contains(p.Id))
|
||||||
|
Reference in New Issue
Block a user