Files
Swarm/DysonNetwork.Sphere/Post/PostServiceGrpc.cs

253 lines
9.1 KiB
C#

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;
}
}