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.AddAppServices(builder.Configuration);
builder.Services.AddAppAuthentication(); builder.Services.AddAppAuthentication();
builder.Services.AddDysonAuth(); builder.Services.AddDysonAuth();
builder.Services.AddPublisherService(); builder.Services.AddSphereService();
builder.Services.AddAccountService(); builder.Services.AddAccountService();
builder.Services.AddDriveService(); builder.Services.AddDriveService();

View File

@@ -187,7 +187,7 @@ public class FileController(
public class MarkFileRequest public class MarkFileRequest
{ {
public List<ContentSensitiveMark>? SensitiveMarks { get; set; } public List<Shared.Models.ContentSensitiveMark>? SensitiveMarks { get; set; }
} }
[Authorize] [Authorize]
@@ -298,7 +298,7 @@ public class FileController(
public string? Description { get; set; } public string? Description { get; set; }
public Dictionary<string, object?>? UserMeta { get; set; } public Dictionary<string, object?>? UserMeta { get; set; }
public Dictionary<string, object?>? FileMeta { 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; } 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 var clusters = serviceNames.Select(serviceName => new ClusterConfig
{ {
ClusterId = serviceName, ClusterId = serviceName,
HealthCheck = new() HealthCheck = new HealthCheckConfig
{ {
Active = new() Active = new ActiveHealthCheckConfig
{ {
Enabled = true, Enabled = true,
Interval = TimeSpan.FromSeconds(10), Interval = TimeSpan.FromSeconds(10),

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using DysonNetwork.Shared.Proto;
using NodaTime; using NodaTime;
using NpgsqlTypes; using NpgsqlTypes;
using Google.Protobuf.WellKnownTypes;
namespace DysonNetwork.Shared.Models; namespace DysonNetwork.Shared.Models;
@@ -85,6 +87,107 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
public string ResourceIdentifier => $"post:{Id}"; 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() public SnActivity ToActivity()
{ {
return new SnActivity() return new SnActivity()
@@ -108,6 +211,30 @@ public class SnPostTag : ModelBase
[JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>(); [JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>();
[NotMapped] public int? Usage { get; set; } [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 public class SnPostCategory : ModelBase
@@ -118,6 +245,30 @@ public class SnPostCategory : ModelBase
[JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>(); [JsonIgnore] public List<SnPost> Posts { get; set; } = new List<SnPost>();
[NotMapped] public int? Usage { get; set; } [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 public class SnPostCategorySubscription : ModelBase
@@ -150,6 +301,23 @@ public class SnPostFeaturedRecord : ModelBase
[JsonIgnore] public SnPost Post { get; set; } = null!; [JsonIgnore] public SnPost Post { get; set; } = null!;
public Instant? FeaturedAt { get; set; } public Instant? FeaturedAt { get; set; }
public int SocialCredits { 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 public enum PostReactionAttitude
@@ -169,6 +337,40 @@ public class SnPostReaction : ModelBase
[JsonIgnore] public SnPost Post { get; set; } = null!; [JsonIgnore] public SnPost Post { get; set; } = null!;
public Guid AccountId { get; set; } public Guid AccountId { get; set; }
[NotMapped] public SnAccount? Account { 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 public class SnPostAward : ModelBase
@@ -181,6 +383,25 @@ public class SnPostAward : ModelBase
public Guid PostId { get; set; } public Guid PostId { get; set; }
[JsonIgnore] public SnPost Post { get; set; } = null!; [JsonIgnore] public SnPost Post { get; set; } = null!;
public Guid AccountId { get; set; } 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> /// <summary>
@@ -193,6 +414,30 @@ public class PostEmbedView
public string Uri { get; set; } = null!; public string Uri { get; set; } = null!;
public double? AspectRatio { get; set; } public double? AspectRatio { get; set; }
public PostEmbedViewRenderer Renderer { get; set; } = PostEmbedViewRenderer.WebView; 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 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() .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
{ ServerCertificateCustomValidationCallback = (_, _, _, _) => true } { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }
); );
services services
.AddGrpcClient<RealmService.RealmServiceClient>(o => o.Address = new Uri("https://_grpc.pass")) .AddGrpcClient<RealmService.RealmServiceClient>(o => o.Address = new Uri("https://_grpc.pass"))
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler() .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
@@ -69,7 +69,7 @@ public static class ServiceInjectionHelper
return services; return services;
} }
public static IServiceCollection AddDriveService(this IServiceCollection services) public static IServiceCollection AddDriveService(this IServiceCollection services)
{ {
services.AddGrpcClient<FileService.FileServiceClient>(o => o.Address = new Uri("https://_grpc.drive")) services.AddGrpcClient<FileService.FileServiceClient>(o => o.Address = new Uri("https://_grpc.drive"))
@@ -86,8 +86,14 @@ public static class ServiceInjectionHelper
return services; 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 services
.AddGrpcClient<PublisherService.PublisherServiceClient>(o => o.Address = new Uri("https://_grpc.sphere")) .AddGrpcClient<PublisherService.PublisherServiceClient>(o => o.Address = new Uri("https://_grpc.sphere"))
.ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler() .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler()
@@ -107,4 +113,4 @@ public static class ServiceInjectionHelper
return services; return services;
} }
} }

View File

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

View File

@@ -125,7 +125,7 @@ public class PostController(
if (realm != null) if (realm != null)
query = query.Where(p => p.RealmId == realm.Id); query = query.Where(p => p.RealmId == realm.Id);
if (type != null) 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 }) if (categories is { Count: > 0 })
query = query.Where(p => p.Categories.Any(c => categories.Contains(c.Slug))); query = query.Where(p => p.Categories.Any(c => categories.Contains(c.Slug)));
if (tags is { Count: > 0 }) if (tags is { Count: > 0 })
@@ -139,10 +139,10 @@ public class PostController(
switch (pinned) switch (pinned)
{ {
case true when realm != null: case true when realm != null:
query = query.Where(p => p.PinMode == PostPinMode.RealmPage); query = query.Where(p => p.PinMode == Shared.Models.PostPinMode.RealmPage);
break; break;
case true when publisher != null: case true when publisher != null:
query = query.Where(p => p.PinMode == PostPinMode.PublisherPage); query = query.Where(p => p.PinMode == Shared.Models.PostPinMode.PublisherPage);
break; break;
case true: case true:
return BadRequest( return BadRequest(
@@ -360,7 +360,7 @@ public class PostController(
var now = SystemClock.Instance.GetCurrentInstant(); var now = SystemClock.Instance.GetCurrentInstant();
var posts = await db.Posts 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) .OrderByDescending(p => p.CreatedAt)
.FilterWithVisibility(currentUser, userFriends, userPublishers) .FilterWithVisibility(currentUser, userFriends, userPublishers)
.ToListAsync(); .ToListAsync();
@@ -425,9 +425,9 @@ public class PostController(
[MaxLength(4096)] public string? Description { get; set; } [MaxLength(4096)] public string? Description { get; set; }
[MaxLength(1024)] public string? Slug { get; set; } [MaxLength(1024)] public string? Slug { get; set; }
public string? Content { get; set; } public string? Content { get; set; }
public PostVisibility? Visibility { get; set; } = PostVisibility.Public; public Shared.Models.PostVisibility? Visibility { get; set; } = Shared.Models.PostVisibility.Public;
public PostType? Type { get; set; } public Shared.Models.PostType? Type { get; set; }
public PostEmbedView? EmbedView { get; set; } public Shared.Models.PostEmbedView? EmbedView { get; set; }
[MaxLength(16)] public List<string>? Tags { get; set; } [MaxLength(16)] public List<string>? Tags { get; set; }
[MaxLength(8)] public List<string>? Categories { get; set; } [MaxLength(8)] public List<string>? Categories { get; set; }
[MaxLength(32)] public List<string>? Attachments { get; set; } [MaxLength(32)] public List<string>? Attachments { get; set; }
@@ -477,9 +477,9 @@ public class PostController(
Description = request.Description, Description = request.Description,
Slug = request.Slug, Slug = request.Slug,
Content = request.Content, Content = request.Content,
Visibility = request.Visibility ?? PostVisibility.Public, Visibility = request.Visibility ?? Shared.Models.PostVisibility.Public,
PublishedAt = request.PublishedAt, PublishedAt = request.PublishedAt,
Type = request.Type ?? PostType.Moment, Type = request.Type ?? Shared.Models.PostType.Moment,
Meta = request.Meta, Meta = request.Meta,
EmbedView = request.EmbedView, EmbedView = request.EmbedView,
Publisher = publisher, Publisher = publisher,
@@ -565,7 +565,7 @@ public class PostController(
public class PostReactionRequest public class PostReactionRequest
{ {
[MaxLength(256)] public string Symbol { get; set; } = null!; [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 = public static readonly List<string> ReactionsAllowedDefault =
@@ -638,7 +638,7 @@ public class PostController(
public class PostAwardRequest public class PostAwardRequest
{ {
public decimal Amount { get; set; } public decimal Amount { get; set; }
public PostReactionAttitude Attitude { get; set; } public Shared.Models.PostReactionAttitude Attitude { get; set; }
[MaxLength(4096)] public string? Message { 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) 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) if (request.Attitude == Shared.Models.PostReactionAttitude.Neutral)
return BadRequest("You cannot create a neutral post award"); return BadRequest("You cannot create a neutral post award");
var friendsResponse = var friendsResponse =
@@ -714,7 +714,7 @@ public class PostController(
public class PostPinRequest public class PostPinRequest
{ {
[Required] public PostPinMode Mode { get; set; } [Required] public Shared.Models.PostPinMode Mode { get; set; }
} }
[HttpPost("{id:guid}/pin")] [HttpPost("{id:guid}/pin")]
@@ -734,7 +734,7 @@ public class PostController(
if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor)) if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor))
return StatusCode(403, "You are not an editor of this publisher"); 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 })) if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, new List<int> { RealmMemberRole.Moderator }))
return StatusCode(403, "You are not a moderator of this realm"); 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)) if (!await pub.IsMemberWithRole(post.PublisherId, accountId, PublisherMemberRole.Editor))
return StatusCode(403, "You are not an editor of this publisher"); 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 })) if (!await rs.IsMemberWithRole(post.RealmId.Value, accountId, new List<int> { RealmMemberRole.Moderator }))
return StatusCode(403, "You are not a moderator of this realm"); 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) 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 // Find all URLs in the content
var matches = GetLinkRegex().Matches(item.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); var accountId = Guid.Parse(currentUser.Id);
if (post.RepliedPostId != null) 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."); 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));
@@ -516,11 +516,11 @@ public partial class PostService(
switch (reaction.Attitude) switch (reaction.Attitude)
{ {
case PostReactionAttitude.Positive: case Shared.Models.PostReactionAttitude.Positive:
if (isRemoving) post.Upvotes--; if (isRemoving) post.Upvotes--;
else post.Upvotes++; else post.Upvotes++;
break; break;
case PostReactionAttitude.Negative: case Shared.Models.PostReactionAttitude.Negative:
if (isRemoving) post.Downvotes--; if (isRemoving) post.Downvotes--;
else post.Downvotes++; else post.Downvotes++;
break; break;
@@ -771,7 +771,7 @@ public partial class PostService(
if (currentUser is null) if (currentUser is null)
{ {
// Anonymous user can only view public posts that are published // 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 // Check publication status - either published or user is member
@@ -781,10 +781,10 @@ public partial class PostService(
return false; return false;
// Check visibility // Check visibility
if (post.Visibility == PostVisibility.Private && !isMember) if (post.Visibility == Shared.Models.PostVisibility.Private && !isMember)
return false; 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)) !(post.Publisher.AccountId.HasValue && userFriends.Contains(post.Publisher.AccountId.Value) || isMember))
return false; return false;
@@ -843,7 +843,7 @@ public partial class PostService(
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 == Shared.Models.PostVisibility.Public)
.Where(e => e.CreatedAt >= periodStart && e.CreatedAt < periodEnd) .Where(e => e.CreatedAt >= periodStart && e.CreatedAt < periodEnd)
.Select(e => e.Id) .Select(e => e.Id)
.ToListAsync(); .ToListAsync();
@@ -854,7 +854,7 @@ public partial class PostService(
.Select(e => new .Select(e => new
{ {
PostId = e.Key, 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); .ToDictionaryAsync(e => e.PostId, e => e.Score);
@@ -928,7 +928,7 @@ public partial class PostService(
Guid postId, Guid postId,
Guid accountId, Guid accountId,
decimal amount, decimal amount,
PostReactionAttitude attitude, Shared.Models.PostReactionAttitude attitude,
string? message string? message
) )
{ {
@@ -947,7 +947,7 @@ public partial class PostService(
db.PostAwards.Add(award); db.PostAwards.Add(award);
await db.SaveChangesAsync(); 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) await db.Posts.Where(p => p.Id == postId)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.AwardedScore, p => p.AwardedScore + delta)); .ExecuteUpdateAsync(s => s.SetProperty(p => p.AwardedScore, p => p.AwardedScore + delta));
@@ -1017,20 +1017,20 @@ public static class PostQueryExtensions
source = isListing switch source = isListing switch
{ {
true when currentUser is not null => source.Where(e => true when currentUser is not null => source.Where(e =>
e.Visibility != PostVisibility.Unlisted || publishersId.Contains(e.PublisherId)), e.Visibility != Shared.Models.PostVisibility.Unlisted || publishersId.Contains(e.PublisherId)),
true => source.Where(e => e.Visibility != PostVisibility.Unlisted), true => source.Where(e => e.Visibility != Shared.Models.PostVisibility.Unlisted),
_ => source _ => source
}; };
if (currentUser is null) if (currentUser is null)
return source return source
.Where(e => e.PublishedAt != null && now >= e.PublishedAt) .Where(e => e.PublishedAt != null && now >= e.PublishedAt)
.Where(e => e.Visibility == PostVisibility.Public); .Where(e => e.Visibility == Shared.Models.PostVisibility.Public);
return source return source
.Where(e => (e.PublishedAt != null && now >= e.PublishedAt) || publishersId.Contains(e.PublisherId)) .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 != Shared.Models.PostVisibility.Private || publishersId.Contains(e.PublisherId))
.Where(e => e.Visibility != PostVisibility.Friends || .Where(e => e.Visibility != Shared.Models.PostVisibility.Friends ||
(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));
} }

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 DysonNetwork.Shared.Registry;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NodaTime; using NodaTime;
using PublisherMemberRole = DysonNetwork.Shared.Models.PublisherMemberRole;
using PublisherType = DysonNetwork.Shared.Models.PublisherType; using PublisherType = DysonNetwork.Shared.Models.PublisherType;
namespace DysonNetwork.Sphere.Publisher; namespace DysonNetwork.Sphere.Publisher;
@@ -161,7 +162,7 @@ public class PublisherService(
{ {
var publisher = new SnPublisher var publisher = new SnPublisher
{ {
Type = Shared.Models.PublisherType.Individual, Type = PublisherType.Individual,
Name = name ?? account.Name, Name = name ?? account.Name,
Nick = nick ?? account.Nick, Nick = nick ?? account.Nick,
Bio = bio ?? account.Profile.Bio, Bio = bio ?? account.Profile.Bio,
@@ -177,7 +178,7 @@ public class PublisherService(
new() new()
{ {
AccountId = Guid.Parse(account.Id), AccountId = Guid.Parse(account.Id),
Role = Shared.Models.PublisherMemberRole.Owner, Role = PublisherMemberRole.Owner,
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow) JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow)
} }
] ]
@@ -214,7 +215,7 @@ public class PublisherService(
} }
public async Task<SnPublisher> CreateOrganizationPublisher( public async Task<SnPublisher> CreateOrganizationPublisher(
Shared.Models.SnRealm realm, SnRealm realm,
Account account, Account account,
string? name, string? name,
string? nick, string? nick,
@@ -225,7 +226,7 @@ public class PublisherService(
{ {
var publisher = new SnPublisher var publisher = new SnPublisher
{ {
Type = Shared.Models.PublisherType.Organizational, Type = PublisherType.Organizational,
Name = name ?? realm.Slug, Name = name ?? realm.Slug,
Nick = nick ?? realm.Name, Nick = nick ?? realm.Name,
Bio = bio ?? realm.Description, Bio = bio ?? realm.Description,
@@ -237,7 +238,7 @@ public class PublisherService(
new() new()
{ {
AccountId = Guid.Parse(account.Id), AccountId = Guid.Parse(account.Id),
Role = Shared.Models.PublisherMemberRole.Owner, Role = PublisherMemberRole.Owner,
JoinedAt = Instant.FromDateTimeUtc(DateTime.UtcNow) 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 postsCount = await db.Posts.Where(e => e.Publisher.Id == publisher.Id).CountAsync();
var postsUpvotes = await db.PostReactions 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(); .CountAsync();
var postsDownvotes = await db.PostReactions 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(); .CountAsync();
var stickerPacksId = await db.StickerPacks.Where(e => e.Publisher.Id == publisher.Id).Select(e => e.Id) 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( public class PublisherSubscriptionService(
AppDatabase db, AppDatabase db,
PostService ps, Post.PostService ps,
IStringLocalizer<NotificationResource> localizer, IStringLocalizer<NotificationResource> localizer,
ICacheService cache, ICacheService cache,
RingService.RingServiceClient pusher, RingService.RingServiceClient pusher,
@@ -54,7 +54,7 @@ public class PublisherSubscriptionService(
{ {
if (post.RepliedPostId is not null) if (post.RepliedPostId is not null)
return 0; return 0;
if (post.Visibility != PostVisibility.Public) if (post.Visibility != Shared.Models.PostVisibility.Public)
return 0; return 0;
// Create notification data // Create notification data

View File

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

View File

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