Provide real user and posts data for the thinking

This commit is contained in:
2025-10-25 17:58:58 +08:00
parent 40325c6df5
commit 93f7dfd379
17 changed files with 898 additions and 72 deletions

View File

@@ -14,7 +14,7 @@ builder.ConfigureAppKestrel(builder.Configuration);
builder.Services.AddAppServices(builder.Configuration);
builder.Services.AddAppAuthentication();
builder.Services.AddDysonAuth();
builder.Services.AddPublisherService();
builder.Services.AddSphereService();
builder.Services.AddAccountService();
builder.Services.AddDriveService();

View File

@@ -187,7 +187,7 @@ public class FileController(
public class MarkFileRequest
{
public List<ContentSensitiveMark>? SensitiveMarks { get; set; }
public List<Shared.Models.ContentSensitiveMark>? SensitiveMarks { get; set; }
}
[Authorize]
@@ -298,7 +298,7 @@ public class FileController(
public string? Description { get; set; }
public Dictionary<string, object?>? UserMeta { get; set; }
public Dictionary<string, object?>? FileMeta { get; set; }
public List<ContentSensitiveMark>? SensitiveMarks { get; set; }
public List<Shared.Models.ContentSensitiveMark>? SensitiveMarks { get; set; }
public Guid PoolId { get; set; }
}

View File

@@ -122,9 +122,9 @@ var routes = specialRoutes.Concat(apiRoutes).Concat(swaggerRoutes).ToArray();
var clusters = serviceNames.Select(serviceName => new ClusterConfig
{
ClusterId = serviceName,
HealthCheck = new()
HealthCheck = new HealthCheckConfig
{
Active = new()
Active = new ActiveHealthCheckConfig
{
Enabled = true,
Interval = TimeSpan.FromSeconds(10),

View File

@@ -1,6 +1,7 @@
using DysonNetwork.Insight;
using DysonNetwork.Insight.Startup;
using DysonNetwork.Shared.Http;
using DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
@@ -10,10 +11,13 @@ builder.AddServiceDefaults();
builder.ConfigureAppKestrel(builder.Configuration);
builder.Services.AddControllers();
builder.Services.AddAppServices(builder.Configuration);
builder.Services.AddAppServices();
builder.Services.AddAppAuthentication();
builder.Services.AddAppFlushHandlers();
builder.Services.AddAppBusinessServices();
builder.Services.AddAccountService();
builder.Services.AddSphereService();
builder.Services.AddThinkingServices(builder.Configuration);
builder.AddSwaggerManifest(

View File

@@ -10,7 +10,7 @@ namespace DysonNetwork.Insight.Startup;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAppServices(this IServiceCollection services, IConfiguration configuration)
public static IServiceCollection AddAppServices(this IServiceCollection services)
{
services.AddDbContext<AppDatabase>();
services.AddSingleton<IClock>(SystemClock.Instance);
@@ -65,8 +65,7 @@ public static class ServiceCollectionExtensions
public static IServiceCollection AddThinkingServices(this IServiceCollection services, IConfiguration configuration)
{
var thinkingProvider = new ThinkingProvider(configuration);
services.AddSingleton(thinkingProvider);
services.AddSingleton<ThinkingProvider>();
return services;
}

View File

@@ -1,14 +1,34 @@
using DysonNetwork.Shared.Proto;
using Microsoft.SemanticKernel;
using Microsoft.Extensions.Configuration;
using System.Text.Json;
namespace DysonNetwork.Insight.Thinking;
public class ThinkingProvider
{
public readonly Kernel Kernel;
public readonly string? ModelProviderType;
public readonly string? ModelDefault;
private readonly Kernel _kernel;
private readonly PostService.PostServiceClient _postClient;
private readonly AccountService.AccountServiceClient _accountClient;
public ThinkingProvider(IConfiguration configuration)
public Kernel Kernel => _kernel;
public string? ModelProviderType { get; private set; }
public string? ModelDefault { get; private set; }
public ThinkingProvider(
IConfiguration configuration,
PostService.PostServiceClient postClient,
AccountService.AccountServiceClient accountClient
)
{
_postClient = postClient;
_accountClient = accountClient;
_kernel = InitializeThinkingProvider(configuration);
InitializeHelperFunctions();
}
private Kernel InitializeThinkingProvider(IConfiguration configuration)
{
var cfg = configuration.GetSection("Thinking");
ModelProviderType = cfg.GetValue<string>("Provider")?.ToLower();
@@ -26,23 +46,37 @@ public class ThinkingProvider
throw new IndexOutOfRangeException("Unknown thinking provider: " + ModelProviderType);
}
Kernel = builder.Build();
return builder.Build();
}
private void InitializeHelperFunctions()
{
// Add Solar Network tools plugin
Kernel.ImportPluginFromFunctions("helper_functions", [
_kernel.ImportPluginFromFunctions("helper_functions", [
KernelFunctionFactory.CreateFromMethod(async (string userId) =>
{
// MOCK: simulate fetching user profile
await Task.Delay(100);
return $"{{\"userId\":\"{userId}\",\"name\":\"MockUser\",\"bio\":\"Loves music and tech.\"}}";
var request = new GetAccountRequest { Id = userId };
var response = await _accountClient.GetAccountAsync(request);
return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions);
}, "get_user_profile", "Get a user profile from the Solar Network."),
KernelFunctionFactory.CreateFromMethod(async (string topic) =>
KernelFunctionFactory.CreateFromMethod(async (string postId) =>
{
// MOCK: simulate fetching recent posts
await Task.Delay(200);
return
$"[{{\"postId\":\"p1\",\"topic\":\"{topic}\",\"content\":\"Mock post content 1.\"}}, {{\"postId\":\"p2\",\"topic\":\"{topic}\",\"content\":\"Mock post content 2.\"}}]";
var request = new GetPostRequest { Id = postId };
var response = await _postClient.GetPostAsync(request);
return JsonSerializer.Serialize(response, GrpcTypeHelper.SerializerOptions);
}, "get_post", "Get a single post by ID from the Solar Network."),
KernelFunctionFactory.CreateFromMethod(async (string query) =>
{
var request = new SearchPostsRequest { Query = query, PageSize = 10 };
var response = await _postClient.SearchPostsAsync(request);
return JsonSerializer.Serialize(response.Posts, GrpcTypeHelper.SerializerOptions);
}, "search_posts", "Search posts by query from the Solar Network."),
KernelFunctionFactory.CreateFromMethod(async () =>
{
var request = new ListPostsRequest { PageSize = 10 };
var response = await _postClient.ListPostsAsync(request);
return JsonSerializer.Serialize(response.Posts, GrpcTypeHelper.SerializerOptions);
}, "get_recent_posts", "Get recent posts from the Solar Network.")
]);
}
}
}

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using DysonNetwork.Shared.Proto;
using NodaTime;
using NpgsqlTypes;
using Google.Protobuf.WellKnownTypes;
namespace DysonNetwork.Shared.Models;
@@ -85,6 +87,107 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
public string ResourceIdentifier => $"post:{Id}";
public Post ToProtoValue()
{
var proto = new Post
{
Id = Id.ToString(),
Title = Title ?? string.Empty,
Description = Description ?? string.Empty,
Slug = Slug ?? string.Empty,
Visibility = (Proto.PostVisibility)((int)Visibility + 1),
Type = (Proto.PostType)((int)Type + 1),
ViewsUnique = ViewsUnique,
ViewsTotal = ViewsTotal,
Upvotes = Upvotes,
Downvotes = Downvotes,
AwardedScore = (double)AwardedScore,
ReactionsCount = { ReactionsCount ?? new Dictionary<string, int>() },
RepliesCount = RepliesCount,
ReactionsMade = { ReactionsMade ?? new Dictionary<string, bool>() },
RepliedGone = RepliedGone,
ForwardedGone = ForwardedGone,
PublisherId = PublisherId.ToString(),
Publisher = Publisher.ToProto(),
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
};
if (EditedAt.HasValue)
{
proto.EditedAt = Timestamp.FromDateTimeOffset(EditedAt.Value.ToDateTimeOffset());
}
if (PublishedAt.HasValue)
{
proto.PublishedAt = Timestamp.FromDateTimeOffset(PublishedAt.Value.ToDateTimeOffset());
}
if (Content != null)
{
proto.Content = Content;
}
if (PinMode.HasValue)
{
proto.PinMode = (Proto.PostPinMode)((int)PinMode.Value);
}
if (Meta != null)
{
proto.Meta = GrpcTypeHelper.ConvertObjectToByteString(Meta);
}
if (SensitiveMarks != null)
{
proto.SensitiveMarks = GrpcTypeHelper.ConvertObjectToByteString(SensitiveMarks);
}
if (EmbedView != null)
{
proto.EmbedView = EmbedView.ToProtoValue();
}
if (RepliedPostId.HasValue)
{
proto.RepliedPostId = RepliedPostId.Value.ToString();
if (RepliedPost != null)
{
proto.RepliedPost = RepliedPost.ToProtoValue();
}
}
if (ForwardedPostId.HasValue)
{
proto.ForwardedPostId = ForwardedPostId.Value.ToString();
if (ForwardedPost != null)
{
proto.ForwardedPost = ForwardedPost.ToProtoValue();
}
}
if (RealmId.HasValue)
{
proto.RealmId = RealmId.Value.ToString();
if (Realm != null)
{
proto.Realm = Realm.ToProtoValue();
}
}
proto.Attachments.AddRange(Attachments.Select(a => a.ToProtoValue()));
proto.Awards.AddRange(Awards.Select(a => a.ToProto()));
proto.Reactions.AddRange(Reactions.Select(r => r.ToProtoValue()));
proto.Tags.AddRange(Tags.Select(t => t.ToProtoValue()));
proto.Categories.AddRange(Categories.Select(c => c.ToProtoValue()));
proto.FeaturedRecords.AddRange(FeaturedRecords.Select(f => f.ToProtoValue()));
if (DeletedAt.HasValue)
proto.DeletedAt = Timestamp.FromDateTimeOffset(DeletedAt.Value.ToDateTimeOffset());
return proto;
}
public SnActivity ToActivity()
{
return new SnActivity()
@@ -108,6 +211,30 @@ public class SnPostTag : ModelBase
[JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>();
[NotMapped] public int? Usage { get; set; }
public PostTag ToProtoValue()
{
return new PostTag
{
Id = Id.ToString(),
Slug = Slug,
Name = Name ?? string.Empty,
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
};
}
public static SnPostTag FromProtoValue(PostTag proto)
{
return new SnPostTag
{
Id = Guid.Parse(proto.Id),
Slug = proto.Slug,
Name = proto.Name != string.Empty ? proto.Name : null,
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset())
};
}
}
public class SnPostCategory : ModelBase
@@ -118,6 +245,30 @@ public class SnPostCategory : ModelBase
[JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>();
[NotMapped] public int? Usage { get; set; }
public PostCategory ToProtoValue()
{
return new PostCategory
{
Id = Id.ToString(),
Slug = Slug,
Name = Name ?? string.Empty,
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
};
}
public static SnPostCategory FromProtoValue(PostCategory proto)
{
return new SnPostCategory
{
Id = Guid.Parse(proto.Id),
Slug = proto.Slug,
Name = proto.Name != string.Empty ? proto.Name : null,
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset())
};
}
}
public class SnPostCategorySubscription : ModelBase
@@ -150,6 +301,23 @@ public class SnPostFeaturedRecord : ModelBase
[JsonIgnore] public SnPost Post { get; set; } = null!;
public Instant? FeaturedAt { get; set; }
public int SocialCredits { get; set; }
public PostFeaturedRecord ToProtoValue()
{
var proto = new PostFeaturedRecord
{
Id = Id.ToString(),
PostId = PostId.ToString(),
SocialCredits = SocialCredits,
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
};
if (FeaturedAt.HasValue)
{
proto.FeaturedAt = Timestamp.FromDateTimeOffset(FeaturedAt.Value.ToDateTimeOffset());
}
return proto;
}
}
public enum PostReactionAttitude
@@ -169,6 +337,40 @@ public class SnPostReaction : ModelBase
[JsonIgnore] public SnPost Post { get; set; } = null!;
public Guid AccountId { get; set; }
[NotMapped] public SnAccount? Account { get; set; }
public PostReaction ToProtoValue()
{
var proto = new PostReaction
{
Id = Id.ToString(),
Symbol = Symbol,
Attitude = (Proto.PostReactionAttitude)((int)Attitude + 1),
PostId = PostId.ToString(),
AccountId = AccountId.ToString(),
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
};
if (Account != null)
{
proto.Account = Account.ToProtoValue();
}
return proto;
}
public static SnPostReaction FromProtoValue(Proto.PostReaction proto)
{
return new SnPostReaction
{
Id = Guid.Parse(proto.Id),
Symbol = proto.Symbol,
Attitude = (PostReactionAttitude)((int)proto.Attitude - 1),
PostId = Guid.Parse(proto.PostId),
AccountId = Guid.Parse(proto.AccountId),
Account = proto.Account != null ? SnAccount.FromProtoValue(proto.Account) : null,
CreatedAt = Instant.FromDateTimeOffset(proto.CreatedAt.ToDateTimeOffset()),
UpdatedAt = Instant.FromDateTimeOffset(proto.UpdatedAt.ToDateTimeOffset())
};
}
}
public class SnPostAward : ModelBase
@@ -181,6 +383,25 @@ public class SnPostAward : ModelBase
public Guid PostId { get; set; }
[JsonIgnore] public SnPost Post { get; set; } = null!;
public Guid AccountId { get; set; }
public PostAward ToProto()
{
var proto = new PostAward
{
Id = Id.ToString(),
Amount = (double)Amount,
Attitude = (Proto.PostReactionAttitude)((int)Attitude + 1),
PostId = PostId.ToString(),
AccountId = AccountId.ToString(),
CreatedAt = Timestamp.FromDateTimeOffset(CreatedAt.ToDateTimeOffset()),
UpdatedAt = Timestamp.FromDateTimeOffset(UpdatedAt.ToDateTimeOffset())
};
if (Message != null)
{
proto.Message = Message;
}
return proto;
}
}
/// <summary>
@@ -193,6 +414,30 @@ public class PostEmbedView
public string Uri { get; set; } = null!;
public double? AspectRatio { get; set; }
public PostEmbedViewRenderer Renderer { get; set; } = PostEmbedViewRenderer.WebView;
public Proto.PostEmbedView ToProtoValue()
{
var proto = new Proto.PostEmbedView
{
Uri = Uri,
Renderer = (Proto.PostEmbedViewRenderer)(int)Renderer
};
if (AspectRatio.HasValue)
{
proto.AspectRatio = AspectRatio.Value;
}
return proto;
}
public static PostEmbedView FromProtoValue(Proto.PostEmbedView proto)
{
return new PostEmbedView
{
Uri = proto.Uri,
AspectRatio = proto.HasAspectRatio ? proto.AspectRatio : null,
Renderer = (PostEmbedViewRenderer)((int)proto.Renderer - 1)
};
}
}
public enum PostEmbedViewRenderer

View File

@@ -0,0 +1,283 @@
syntax = "proto3";
package proto;
option csharp_namespace = "DysonNetwork.Shared.Proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/struct.proto";
import "file.proto";
import "realm.proto";
import "publisher.proto";
// Enums
enum PostType {
POST_TYPE_UNSPECIFIED = 0;
MOMENT = 1;
ARTICLE = 2;
}
enum PostVisibility {
VISIBILITY_UNSPECIFIED = 0;
PUBLIC = 1;
FRIENDS = 2;
UNLISTED = 3;
PRIVATE = 4;
}
enum PostPinMode {
PIN_MODE_UNSPECIFIED = 0;
PUBLISHER_PAGE = 1;
REALM_PAGE = 2;
REPLY_PAGE = 3;
}
enum ContentSensitiveMark {
SENSITIVE_MARK_UNSPECIFIED = 0;
LANGUAGE = 1;
SEXUAL_CONTENT = 2;
VIOLENCE = 3;
PROFANITY = 4;
HATE_SPEECH = 5;
RACISM = 6;
ADULT_CONTENT = 7;
DRUG_ABUSE = 8;
ALCOHOL_ABUSE = 9;
GAMBLING = 10;
SELF_HARM = 11;
CHILD_ABUSE = 12;
OTHER = 13;
}
enum PostReactionAttitude {
ATTITUDE_UNSPECIFIED = 0;
POST_ATTITUDE_POSITIVE = 1;
POST_ATTITUDE_NEUTRAL = 2;
POST_ATTITUDE_NEGATIVE = 3;
}
enum PostEmbedViewRenderer {
RENDERER_UNSPECIFIED = 0;
WEBVIEW = 1;
}
// Messages
message PostEmbedView {
string uri = 1;
optional double aspect_ratio = 2;
PostEmbedViewRenderer renderer = 3;
}
message Post {
string id = 1;
string title = 2;
string description = 3;
string slug = 4;
optional google.protobuf.Timestamp edited_at = 5;
optional google.protobuf.Timestamp published_at = 6;
PostVisibility visibility = 7;
optional string content = 8;
PostType type = 9;
optional PostPinMode pin_mode = 10;
optional bytes meta = 11; // Dictionary<string, object>
optional bytes sensitive_marks = 12; // List<ContentSensitiveMark>
optional PostEmbedView embed_view = 13;
int32 views_unique = 14;
int32 views_total = 15;
int32 upvotes = 16;
int32 downvotes = 17;
double awarded_score = 18;
// Not mapped fields: handled client-side
map<string, int32> reactions_count = 19; // Dictionary<string, int>
int32 replies_count = 20;
map<string, bool> reactions_made = 21; // Dictionary<string, bool>
bool replied_gone = 22;
bool forwarded_gone = 23;
optional string replied_post_id = 24;
optional Post replied_post = 25; // full if populated
optional string forwarded_post_id = 26;
optional Post forwarded_post = 27; // full if populated
optional string realm_id = 28;
optional Realm realm = 29; // full if populated
repeated CloudFile attachments = 30; // List<SnCloudFileReferenceObject>
string publisher_id = 31;
Publisher publisher = 32;
repeated PostAward awards = 33;
repeated PostReaction reactions = 34;
repeated PostTag tags = 35;
repeated PostCategory categories = 36;
repeated PostFeaturedRecord featured_records = 37;
// Added for ToActivity
google.protobuf.Timestamp created_at = 38;
google.protobuf.Timestamp updated_at = 39;
optional google.protobuf.Timestamp deleted_at = 40;
}
message PostTag {
string id = 1;
string slug = 2;
string name = 3;
google.protobuf.Timestamp created_at = 4;
google.protobuf.Timestamp updated_at = 5;
}
message PostCategory {
string id = 1;
string slug = 2;
string name = 3;
google.protobuf.Timestamp created_at = 4;
google.protobuf.Timestamp updated_at = 5;
}
message PostCategorySubscription {
string id = 1;
string account_id = 2;
optional string category_id = 3;
optional PostCategory category = 4;
optional string tag_id = 5;
optional PostTag tag = 6;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
message PostCollection {
string id = 1;
string slug = 2;
optional google.protobuf.StringValue name = 3;
optional google.protobuf.StringValue description = 4;
Publisher publisher = 5;
optional string publisher_id = 6; // for cases where full publisher not needed
repeated Post posts = 7;
google.protobuf.Timestamp created_at = 8;
google.protobuf.Timestamp updated_at = 9;
}
message PostFeaturedRecord {
string id = 1;
string post_id = 2;
optional google.protobuf.Timestamp featured_at = 3;
int32 social_credits = 4;
google.protobuf.Timestamp created_at = 5;
google.protobuf.Timestamp updated_at = 6;
}
message PostReaction {
string id = 1;
string symbol = 2;
PostReactionAttitude attitude = 3;
string post_id = 4;
string account_id = 5;
optional Account account = 6; // optional full account
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
message PostAward {
string id = 1;
double amount = 2;
PostReactionAttitude attitude = 3;
optional google.protobuf.StringValue message = 4;
string post_id = 5;
string account_id = 6;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
// ====================================
// Request/Response Messages
// ====================================
message GetPostRequest {
string id = 1;
}
message GetPostBatchRequest {
repeated string ids = 1;
}
message GetPostBatchResponse {
repeated Post posts = 1;
}
message SearchPostsRequest {
string query = 1;
string publisher_id = 2;
string realm_id = 3;
int32 page_size = 4;
string page_token = 5;
string order_by = 6;
}
message SearchPostsResponse {
repeated Post posts = 1;
string next_page_token = 2;
int32 total_size = 3;
}
message ListPostsRequest {
string publisher_id = 1;
string realm_id = 2;
int32 page_size = 3;
string page_token = 4;
string order_by = 5;
repeated string categories = 6;
repeated string tags = 7;
string query = 8;
repeated PostType types = 9;
optional google.protobuf.Timestamp after = 10; // Filter posts created after this timestamp
optional google.protobuf.Timestamp before = 11; // Filter posts created before this timestamp
bool include_replies = 12; // Include reply posts
optional PostPinMode pinned = 13; // Filter by pinned mode (if present, null means not pinned)
bool only_media = 14; // Only return posts with attachments
bool shuffle = 15; // Random order
}
message ListPostsResponse {
repeated Post posts = 1;
string next_page_token = 2;
int32 total_size = 3;
}
// ====================================
// Service Definitions
// ====================================
service PostService {
// Get a single post by id
rpc GetPost(GetPostRequest) returns (Post);
// Get multiple posts by ids
rpc GetPostBatch(GetPostBatchRequest) returns (GetPostBatchResponse);
// Search posts
rpc SearchPosts(SearchPostsRequest) returns (SearchPostsResponse);
// List posts with filters
rpc ListPosts(ListPostsRequest) returns (ListPostsResponse);
}
import 'account.proto';

View File

@@ -59,7 +59,7 @@ public static class ServiceInjectionHelper
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
);
services
.AddGrpcClient<RealmService.RealmServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
@@ -69,7 +69,7 @@ public static class ServiceInjectionHelper
return services;
}
public static IServiceCollection AddDriveService(this IServiceCollection services)
{
services.AddGrpcClient<FileService.FileServiceClient>(o => o.Address = new Uri("https://_grpc.drive"))
@@ -86,8 +86,14 @@ public static class ServiceInjectionHelper
return services;
}
public static IServiceCollection AddPublisherService(this IServiceCollection services)
public static IServiceCollection AddSphereService(this IServiceCollection services)
{
services
.AddGrpcClient<PostService.PostServiceClient>(o => o.Address = new Uri("https://_grpc.sphere"))
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
);
services
.AddGrpcClient<PublisherService.PublisherServiceClient>(o => o.Address = new Uri("https://_grpc.sphere"))
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
@@ -107,4 +113,4 @@ public static class ServiceInjectionHelper
return services;
}
}
}

View File

@@ -12,7 +12,7 @@ namespace DysonNetwork.Sphere.Activity;
public class ActivityService(
AppDatabase db,
Publisher.PublisherService pub,
PostService ps,
Post.PostService ps,
RemoteRealmService rs,
DiscoveryService ds,
AccountService.AccountServiceClient accounts

View File

@@ -125,7 +125,7 @@ public class PostController(
if (realm != null)
query = query.Where(p => p.RealmId == realm.Id);
if (type != null)
query = query.Where(p => p.Type == (PostType)type);
query = query.Where(p => p.Type == (Shared.Models.PostType)type);
if (categories is { Count: > 0 })
query = query.Where(p => p.Categories.Any(c => categories.Contains(c.Slug)));
if (tags is { Count: > 0 })
@@ -139,10 +139,10 @@ public class PostController(
switch (pinned)
{
case true when realm != null:
query = query.Where(p => p.PinMode == PostPinMode.RealmPage);
query = query.Where(p => p.PinMode == Shared.Models.PostPinMode.RealmPage);
break;
case true when publisher != null:
query = query.Where(p => p.PinMode == PostPinMode.PublisherPage);
query = query.Where(p => p.PinMode == Shared.Models.PostPinMode.PublisherPage);
break;
case true:
return BadRequest(
@@ -360,7 +360,7 @@ public class PostController(
var now = SystemClock.Instance.GetCurrentInstant();
var posts = await db.Posts
.Where(e => e.RepliedPostId == id && e.PinMode == PostPinMode.ReplyPage)
.Where(e => e.RepliedPostId == id && e.PinMode == Shared.Models.PostPinMode.ReplyPage)
.OrderByDescending(p => p.CreatedAt)
.FilterWithVisibility(currentUser, userFriends, userPublishers)
.ToListAsync();
@@ -425,9 +425,9 @@ public class PostController(
[MaxLength(4096)] public string? Description { get; set; }
[MaxLength(1024)] public string? Slug { get; set; }
public string? Content { get; set; }
public PostVisibility? Visibility { get; set; } = PostVisibility.Public;
public PostType? Type { get; set; }
public PostEmbedView? EmbedView { get; set; }
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<string>? Tags { get; set; }
[MaxLength(8)] public List<string>? Categories { get; set; }
[MaxLength(32)] public List<string>? Attachments { get; set; }
@@ -477,9 +477,9 @@ public class PostController(
Description = request.Description,
Slug = request.Slug,
Content = request.Content,
Visibility = request.Visibility ?? PostVisibility.Public,
Visibility = request.Visibility ?? Shared.Models.PostVisibility.Public,
PublishedAt = request.PublishedAt,
Type = request.Type ?? PostType.Moment,
Type = request.Type ?? Shared.Models.PostType.Moment,
Meta = request.Meta,
EmbedView = request.EmbedView,
Publisher = publisher,
@@ -565,7 +565,7 @@ public class PostController(
public class PostReactionRequest
{
[MaxLength(256)] public string Symbol { get; set; } = null!;
public PostReactionAttitude Attitude { get; set; }
public Shared.Models.PostReactionAttitude Attitude { get; set; }
}
public static readonly List<string> ReactionsAllowedDefault =
@@ -638,7 +638,7 @@ public class PostController(
public class PostAwardRequest
{
public decimal Amount { get; set; }
public PostReactionAttitude Attitude { get; set; }
public Shared.Models.PostReactionAttitude Attitude { get; set; }
[MaxLength(4096)] public string? Message { get; set; }
}
@@ -671,7 +671,7 @@ public class PostController(
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)
if (request.Attitude == Shared.Models.PostReactionAttitude.Neutral)
return BadRequest("You cannot create a neutral post award");
var friendsResponse =
@@ -714,7 +714,7 @@ public class PostController(
public class PostPinRequest
{
[Required] public PostPinMode Mode { get; set; }
[Required] public Shared.Models.PostPinMode Mode { get; set; }
}
[HttpPost("{id:guid}/pin")]
@@ -734,7 +734,7 @@ public class PostController(
if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor))
return StatusCode(403, "You are not an editor of this publisher");
if (request.Mode == PostPinMode.RealmPage && post.RealmId != null)
if (request.Mode == Shared.Models.PostPinMode.RealmPage && post.RealmId != null)
{
if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, new List<int> { RealmMemberRole.Moderator }))
return StatusCode(403, "You are not a moderator of this realm");
@@ -782,7 +782,7 @@ public class PostController(
if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor))
return StatusCode(403, "You are not an editor of this publisher");
if (post is { PinMode: PostPinMode.RealmPage, RealmId: not null })
if (post is { PinMode: Shared.Models.PostPinMode.RealmPage, RealmId: not null })
{
if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, new List<int> { RealmMemberRole.Moderator }))
return StatusCode(403, "You are not a moderator of this realm");

View File

@@ -290,7 +290,7 @@ public partial class PostService(
public async Task<SnPost> PreviewPostLinkAsync(SnPost item)
{
if (item.Type != PostType.Moment || string.IsNullOrEmpty(item.Content)) return item;
if (item.Type != Shared.Models.PostType.Moment || string.IsNullOrEmpty(item.Content)) return item;
// Find all URLs in the content
var matches = GetLinkRegex().Matches(item.Content);
@@ -420,12 +420,12 @@ public partial class PostService(
}
}
public async Task<SnPost> PinPostAsync(SnPost post, Account currentUser, PostPinMode pinMode)
public async Task<SnPost> PinPostAsync(SnPost post, Account currentUser, Shared.Models.PostPinMode pinMode)
{
var accountId = Guid.Parse(currentUser.Id);
if (post.RepliedPostId != null)
{
if (pinMode != PostPinMode.ReplyPage)
if (pinMode != Shared.Models.PostPinMode.ReplyPage)
throw new InvalidOperationException("Replies can only be pinned in the reply page.");
if (post.RepliedPost == null) throw new ArgumentNullException(nameof(post.RepliedPost));
@@ -516,11 +516,11 @@ public partial class PostService(
switch (reaction.Attitude)
{
case PostReactionAttitude.Positive:
case Shared.Models.PostReactionAttitude.Positive:
if (isRemoving) post.Upvotes--;
else post.Upvotes++;
break;
case PostReactionAttitude.Negative:
case Shared.Models.PostReactionAttitude.Negative:
if (isRemoving) post.Downvotes--;
else post.Downvotes++;
break;
@@ -771,7 +771,7 @@ public partial class PostService(
if (currentUser is null)
{
// Anonymous user can only view public posts that are published
return post.PublishedAt != null && now >= post.PublishedAt && post.Visibility == PostVisibility.Public;
return post.PublishedAt != null && now >= post.PublishedAt && post.Visibility == Shared.Models.PostVisibility.Public;
}
// Check publication status - either published or user is member
@@ -781,10 +781,10 @@ public partial class PostService(
return false;
// Check visibility
if (post.Visibility == PostVisibility.Private && !isMember)
if (post.Visibility == Shared.Models.PostVisibility.Private && !isMember)
return false;
if (post.Visibility == PostVisibility.Friends &&
if (post.Visibility == Shared.Models.PostVisibility.Friends &&
!(post.Publisher.AccountId.HasValue && userFriends.Contains(post.Publisher.AccountId.Value) || isMember))
return false;
@@ -843,7 +843,7 @@ public partial class PostService(
var periodEnd = today.InUtc().Date.AtStartOfDayInZone(DateTimeZone.Utc).ToInstant();
var postsInPeriod = await db.Posts
.Where(e => e.Visibility == PostVisibility.Public)
.Where(e => e.Visibility == Shared.Models.PostVisibility.Public)
.Where(e => e.CreatedAt >= periodStart && e.CreatedAt < periodEnd)
.Select(e => e.Id)
.ToListAsync();
@@ -854,7 +854,7 @@ public partial class PostService(
.Select(e => new
{
PostId = e.Key,
Score = e.Sum(r => r.Attitude == PostReactionAttitude.Positive ? 1 : -1)
Score = e.Sum(r => r.Attitude == Shared.Models.PostReactionAttitude.Positive ? 1 : -1)
})
.ToDictionaryAsync(e => e.PostId, e => e.Score);
@@ -928,7 +928,7 @@ public partial class PostService(
Guid postId,
Guid accountId,
decimal amount,
PostReactionAttitude attitude,
Shared.Models.PostReactionAttitude attitude,
string? message
)
{
@@ -947,7 +947,7 @@ public partial class PostService(
db.PostAwards.Add(award);
await db.SaveChangesAsync();
var delta = award.Attitude == PostReactionAttitude.Positive ? amount : -amount;
var delta = award.Attitude == Shared.Models.PostReactionAttitude.Positive ? amount : -amount;
await db.Posts.Where(p => p.Id == postId)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.AwardedScore, p => p.AwardedScore + delta));
@@ -1017,20 +1017,20 @@ public static class PostQueryExtensions
source = isListing switch
{
true when currentUser is not null => source.Where(e =>
e.Visibility != PostVisibility.Unlisted || publishersId.Contains(e.PublisherId)),
true => source.Where(e => e.Visibility != PostVisibility.Unlisted),
e.Visibility != Shared.Models.PostVisibility.Unlisted || publishersId.Contains(e.PublisherId)),
true => source.Where(e => e.Visibility != Shared.Models.PostVisibility.Unlisted),
_ => source
};
if (currentUser is null)
return source
.Where(e => e.PublishedAt != null && now >= e.PublishedAt)
.Where(e => e.Visibility == PostVisibility.Public);
.Where(e => e.Visibility == Shared.Models.PostVisibility.Public);
return source
.Where(e => (e.PublishedAt != null && now >= e.PublishedAt) || publishersId.Contains(e.PublisherId))
.Where(e => e.Visibility != PostVisibility.Private || publishersId.Contains(e.PublisherId))
.Where(e => e.Visibility != PostVisibility.Friends ||
.Where(e => e.Visibility != Shared.Models.PostVisibility.Private || publishersId.Contains(e.PublisherId))
.Where(e => e.Visibility != Shared.Models.PostVisibility.Friends ||
(e.Publisher.AccountId != null && userFriends.Contains(e.Publisher.AccountId.Value)) ||
publishersId.Contains(e.PublisherId));
}

View File

@@ -0,0 +1,252 @@
using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Models;
using Grpc.Core;
using Microsoft.EntityFrameworkCore;
namespace DysonNetwork.Sphere.Post;
public class PostServiceGrpc(AppDatabase db, PostService ps) : Shared.Proto.PostService.PostServiceBase
{
public override async Task<Shared.Proto.Post> GetPost(GetPostRequest request, ServerCallContext context)
{
if (!Guid.TryParse(request.Id, out var id))
throw new RpcException(new Status(StatusCode.InvalidArgument, "invalid post id"));
var post = await db.Posts
.Include(p => p.Publisher)
.Include(p => p.Tags)
.Include(p => p.Categories)
.Include(p => p.RepliedPost)
.Include(p => p.ForwardedPost)
.Include(p => p.FeaturedRecords)
.FilterWithVisibility(null, [], [])
.FirstOrDefaultAsync(p => p.Id == id);
if (post == null) throw new RpcException(new Status(StatusCode.NotFound, "post not found"));
post = await ps.LoadPostInfo(post);
return post.ToProtoValue();
}
public override async Task<GetPostBatchResponse> GetPostBatch(GetPostBatchRequest request, ServerCallContext context)
{
var ids = request.Ids
.Where(s => !string.IsNullOrWhiteSpace(s) && Guid.TryParse(s, out _))
.Select(Guid.Parse)
.ToList();
if (ids.Count == 0) return new GetPostBatchResponse();
var posts = await db.Posts
.Include(p => p.Publisher)
.Include(p => p.Tags)
.Include(p => p.Categories)
.Include(p => p.RepliedPost)
.Include(p => p.ForwardedPost)
.Include(p => p.FeaturedRecords)
.Include(p => p.Awards)
.Where(p => ids.Contains(p.Id))
.FilterWithVisibility(null, [], [])
.ToListAsync();
posts = await ps.LoadPostInfo(posts, null);
var resp = new GetPostBatchResponse();
resp.Posts.AddRange(posts.Select(p => p.ToProtoValue()));
return resp;
}
public override async Task<SearchPostsResponse> SearchPosts(SearchPostsRequest request, ServerCallContext context)
{
var query = db.Posts
.Include(p => p.Publisher)
.Include(p => p.Tags)
.Include(p => p.Categories)
.Include(p => p.RepliedPost)
.Include(p => p.ForwardedPost)
.Include(p => p.Attachments)
.Include(p => p.Awards)
.Include(p => p.Reactions)
.Include(p => p.FeaturedRecords)
.Where(p => p.DeletedAt == null) // Only active posts
.AsQueryable();
if (!string.IsNullOrWhiteSpace(request.Query))
{
// Simple search, assuming full-text search or title/content contains
query = query.Where(p =>
EF.Functions.ILike(p.Title, $"%{request.Query}%") ||
EF.Functions.ILike(p.Content, $"%{request.Query}%") ||
EF.Functions.ILike(p.Description, $"%{request.Query}%"));
}
if (!string.IsNullOrWhiteSpace(request.PublisherId) && Guid.TryParse(request.PublisherId, out var pid))
{
query = query.Where(p => p.PublisherId == pid);
}
if (!string.IsNullOrWhiteSpace(request.RealmId) && Guid.TryParse(request.RealmId, out var rid))
{
query = query.Where(p => p.RealmId == rid);
}
query = query.FilterWithVisibility(null, [], []);
var totalSize = await query.CountAsync();
// Apply pagination
var pageSize = request.PageSize > 0 ? request.PageSize : 20;
var pageToken = request.PageToken;
var offset = string.IsNullOrEmpty(pageToken) ? 0 : int.Parse(pageToken);
var posts = await query
.OrderByDescending(p => p.PublishedAt ?? p.CreatedAt)
.Skip(offset)
.Take(pageSize)
.ToListAsync();
posts = await ps.LoadPostInfo(posts, null, true);
var nextToken = offset + pageSize < totalSize ? (offset + pageSize).ToString() : string.Empty;
var resp = new SearchPostsResponse();
resp.Posts.AddRange(posts.Select(p => p.ToProtoValue()));
resp.NextPageToken = nextToken;
resp.TotalSize = totalSize;
return resp;
}
public override async Task<ListPostsResponse> ListPosts(ListPostsRequest request, ServerCallContext context)
{
var query = db.Posts
.Include(p => p.Publisher)
.Include(p => p.Tags)
.Include(p => p.Categories)
.Include(p => p.RepliedPost)
.Include(p => p.ForwardedPost)
.Include(p => p.Awards)
.Include(p => p.FeaturedRecords)
.Where(p => p.DeletedAt == null)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(request.PublisherId) && Guid.TryParse(request.PublisherId, out var pid))
{
query = query.Where(p => p.PublisherId == pid);
}
if (!string.IsNullOrWhiteSpace(request.RealmId) && Guid.TryParse(request.RealmId, out var rid))
{
query = query.Where(p => p.RealmId == rid);
}
if (request.Categories.Count > 0)
{
query = query.Where(p => p.Categories.Any(c => request.Categories.Contains(c.Slug)));
}
if (request.Tags.Count > 0)
{
query = query.Where(p => p.Tags.Any(c => request.Tags.Contains(c.Slug)));
}
// TODO: Add types filtering when proto is regenerated
// if (request.Types.Count > 0)
// {
// var types = request.Types.Select(t => (Shared.Models.PostType)t).Distinct();
// query = query.Where(p => types.Contains(p.Type));
// }
if (request.OnlyMedia)
{
query = query.Where(e => e.Attachments.Count > 0);
}
// Pinned filtering
switch (request.Pinned)
{
case Shared.Proto.PostPinMode.RealmPage when !string.IsNullOrWhiteSpace(request.RealmId):
query = query.Where(p => p.PinMode == Shared.Models.PostPinMode.RealmPage);
break;
case Shared.Proto.PostPinMode.PublisherPage when !string.IsNullOrWhiteSpace(request.PublisherId):
query = query.Where(p => p.PinMode == Shared.Models.PostPinMode.PublisherPage);
break;
case Shared.Proto.PostPinMode.ReplyPage:
query = query.Where(p => p.PinMode == Shared.Models.PostPinMode.ReplyPage);
break;
default:
if (request.Pinned != null)
{
// Specific pinned mode but conditions not met, or unknown mode
query = query.Where(p => p.PinMode == (Shared.Models.PostPinMode)request.Pinned);
}
else
{
query = query.Where(p => p.PinMode == null);
}
break;
}
// Include/exclude replies
if (request.IncludeReplies)
{
// Include both root and reply posts
}
else
{
// Exclude reply posts, only root posts
query = query.Where(e => e.RepliedPostId == null);
}
// TODO: Time range filtering when proto fields are available
// if (request.After != null)
// {
// var afterTime = request.After.ToDateTimeOffset();
// query = query.Where(p => (p.CreatedAt >= afterTime) || (p.PublishedAt >= afterTime));
// }
// if (request.Before != null)
// {
// var beforeTime = request.Before.ToDateTimeOffset();
// query = query.Where(p => (p.CreatedAt <= beforeTime) || (p.PublishedAt <= beforeTime));
// }
// TODO: Query text search when proto field is available
// if (!string.IsNullOrWhiteSpace(request.Query))
// {
// query = query.Where(p =>
// EF.Functions.ILike(p.Title, $"%{request.Query}%") ||
// EF.Functions.ILike(p.Content, $"%{request.Query}%") ||
// EF.Functions.ILike(p.Description, $"%{request.Query}%"));
// }
// Visibility filter (simplified for grpc - no user context)
query = query.FilterWithVisibility(null, [], []);
var totalSize = await query.CountAsync();
var pageSize = request.PageSize > 0 ? request.PageSize : 20;
var pageToken = request.PageToken;
var offset = string.IsNullOrEmpty(pageToken) ? 0 : int.Parse(pageToken);
// Ordering - TODO: Add shuffle when proto field is available
var orderedQuery = query.OrderByDescending(e => e.PublishedAt ?? e.CreatedAt);
var posts = await orderedQuery
.Skip(offset)
.Take(pageSize)
.ToListAsync();
posts = await ps.LoadPostInfo(posts, null, true);
var nextToken = offset + pageSize < totalSize ? (offset + pageSize).ToString() : string.Empty;
var resp = new ListPostsResponse();
resp.Posts.AddRange(posts.Select(p => p.ToProtoValue()));
resp.NextPageToken = nextToken;
resp.TotalSize = totalSize;
return resp;
}
}

View File

@@ -4,6 +4,7 @@ using DysonNetwork.Shared.Proto;
using DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore;
using NodaTime;
using PublisherMemberRole = DysonNetwork.Shared.Models.PublisherMemberRole;
using PublisherType = DysonNetwork.Shared.Models.PublisherType;
namespace DysonNetwork.Sphere.Publisher;
@@ -161,7 +162,7 @@ public class PublisherService(
{
var publisher = new SnPublisher
{
Type = Shared.Models.PublisherType.Individual,
Type = PublisherType.Individual,
Name = name ?? account.Name,
Nick = nick ?? account.Nick,
Bio = bio ?? account.Profile.Bio,
@@ -177,7 +178,7 @@ public class PublisherService(
new()
{
AccountId = Guid.Parse(account.Id),
Role = Shared.Models.PublisherMemberRole.Owner,
Role = PublisherMemberRole.Owner,
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
}
]
@@ -214,7 +215,7 @@ public class PublisherService(
}
public async Task<SnPublisher> CreateOrganizationPublisher(
Shared.Models.SnRealm realm,
SnRealm realm,
Account account,
string? name,
string? nick,
@@ -225,7 +226,7 @@ public class PublisherService(
{
var publisher = new SnPublisher
{
Type = Shared.Models.PublisherType.Organizational,
Type = PublisherType.Organizational,
Name = name ?? realm.Slug,
Nick = nick ?? realm.Name,
Bio = bio ?? realm.Description,
@@ -237,7 +238,7 @@ public class PublisherService(
new()
{
AccountId = Guid.Parse(account.Id),
Role = Shared.Models.PublisherMemberRole.Owner,
Role = PublisherMemberRole.Owner,
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
}
}
@@ -299,10 +300,10 @@ public class PublisherService(
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)
.Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == Shared.Models.PostReactionAttitude.Positive)
.CountAsync();
var postsDownvotes = await db.PostReactions
.Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == PostReactionAttitude.Negative)
.Where(r => r.Post.Publisher.Id == publisher.Id && r.Attitude == Shared.Models.PostReactionAttitude.Negative)
.CountAsync();
var stickerPacksId = await db.StickerPacks.Where(e => e.Publisher.Id == publisher.Id).Select(e => e.Id)

View File

@@ -11,7 +11,7 @@ namespace DysonNetwork.Sphere.Publisher;
public class PublisherSubscriptionService(
AppDatabase db,
PostService ps,
Post.PostService ps,
IStringLocalizer<NotificationResource> localizer,
ICacheService cache,
RingService.RingServiceClient pusher,
@@ -54,7 +54,7 @@ public class PublisherSubscriptionService(
{
if (post.RepliedPostId is not null)
return 0;
if (post.Visibility != PostVisibility.Public)
if (post.Visibility != Shared.Models.PostVisibility.Public)
return 0;
// Create notification data

View File

@@ -1,5 +1,6 @@
using DysonNetwork.Shared.Auth;
using DysonNetwork.Shared.Http;
using DysonNetwork.Sphere.Post;
using DysonNetwork.Sphere.Publisher;
namespace DysonNetwork.Sphere.Startup;
@@ -20,6 +21,7 @@ public static class ApplicationConfiguration
app.MapControllers();
// Map gRPC services
app.MapGrpcService<PostServiceGrpc>();
app.MapGrpcService<PublisherServiceGrpc>();
return app;

View File

@@ -24,7 +24,7 @@ public class PaymentOrderAwardMeta
[JsonPropertyName("account_id")] public Guid AccountId { get; set; }
[JsonPropertyName("post_id")] public Guid PostId { get; set; }
[JsonPropertyName("amount")] public string Amount { get; set; } = null!;
[JsonPropertyName("attitude")] public PostReactionAttitude Attitude { get; set; }
[JsonPropertyName("attitude")] public Shared.Models.PostReactionAttitude Attitude { get; set; }
[JsonPropertyName("message")] public string? Message { get; set; }
}
@@ -82,7 +82,7 @@ public class BroadcastEventHandler(
logger.LogInformation("Handling post award order: {OrderId}", evt.OrderId);
await using var scope = serviceProvider.CreateAsyncScope();
var ps = scope.ServiceProvider.GetRequiredService<PostService>();
var ps = scope.ServiceProvider.GetRequiredService<Post.PostService>();
var amountNum = decimal.Parse(meta.Amount);