diff --git a/DysonNetwork.Shared/Models/Post.cs b/DysonNetwork.Shared/Models/Post.cs index 10b0d1e..50c8506 100644 --- a/DysonNetwork.Shared/Models/Post.cs +++ b/DysonNetwork.Shared/Models/Post.cs @@ -73,11 +73,12 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity public Guid PublisherId { get; set; } public SnPublisher Publisher { get; set; } = null!; - public ICollection Awards { get; set; } = null!; - [JsonIgnore] public ICollection Reactions { get; set; } = new List(); - public ICollection Tags { get; set; } = new List(); - public ICollection Categories { get; set; } = new List(); - [JsonIgnore] public ICollection Collections { get; set; } = new List(); + public List Awards { get; set; } = null!; + [JsonIgnore] public List Reactions { get; set; } = []; + public List Tags { get; set; } = []; + public List Categories { get; set; } = []; + [JsonIgnore] public List Collections { get; set; } = []; + public List FeaturedRecords { get; set; } = []; [JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null; [NotMapped] public bool IsTruncated { get; set; } = false; @@ -104,7 +105,7 @@ public class SnPostTag : ModelBase public Guid Id { get; set; } [MaxLength(128)] public string Slug { get; set; } = null!; [MaxLength(256)] public string? Name { get; set; } - [JsonIgnore] public ICollection Posts { get; set; } = new List(); + [JsonIgnore] public List Posts { get; set; } = new List(); [NotMapped] public int? Usage { get; set; } } @@ -114,7 +115,7 @@ public class SnPostCategory : ModelBase public Guid Id { get; set; } [MaxLength(128)] public string Slug { get; set; } = null!; [MaxLength(256)] public string? Name { get; set; } - [JsonIgnore] public ICollection Posts { get; set; } = new List(); + [JsonIgnore] public List Posts { get; set; } = new List(); [NotMapped] public int? Usage { get; set; } } @@ -139,7 +140,7 @@ public class SnPostCollection : ModelBase public SnPublisher Publisher { get; set; } = null!; - public ICollection Posts { get; set; } = new List(); + public List Posts { get; set; } = new List(); } public class SnPostFeaturedRecord : ModelBase diff --git a/DysonNetwork.Sphere/Activity/ActivityService.cs b/DysonNetwork.Sphere/Activity/ActivityService.cs index a67527a..d3edbee 100644 --- a/DysonNetwork.Sphere/Activity/ActivityService.cs +++ b/DysonNetwork.Sphere/Activity/ActivityService.cs @@ -106,7 +106,7 @@ public class ActivityService( var userRealms = await rs.GetUserRealms(Guid.Parse(currentUser.Id)); - // Build and execute the posts query + // Build and execute the post query var postsQuery = BuildPostsQuery(cursor, filteredPublishersId, userRealms); // Apply visibility filtering and execute @@ -122,12 +122,9 @@ public class ActivityService( var posts = await GetAndProcessPosts( postsQuery, currentUser, - userFriends, - userPublishers, trackViews: true); - if (currentUser != null) - await LoadPostsRealmsAsync(posts, rs); + await LoadPostsRealmsAsync(posts, rs); posts = RankPosts(posts, take); @@ -283,8 +280,6 @@ public class ActivityService( private async Task> GetAndProcessPosts( IQueryable baseQuery, Account? currentUser = null, - List? userFriends = null, - List? userPublishers = null, bool trackViews = true) { var posts = await baseQuery.ToListAsync(); diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index b5e19fd..aee384a 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -25,9 +25,9 @@ public partial class PostService( ILogger logger, FileService.FileServiceClient files, FileReferenceService.FileReferenceServiceClient fileRefs, - PollService polls, Publisher.PublisherService ps, - WebReaderService reader + WebReaderService reader, + AccountService.AccountServiceClient accounts ) { private const string PostFileUsageIdentifier = "post"; @@ -702,6 +702,16 @@ public partial class PostService( : new Dictionary>(); var repliesCountMap = await GetPostRepliesCountBatch(postsId); + // Load user friends if the current user exists + List publishers = []; + List userFriends = []; + if (currentUser is not null) + { + var friendsResponse = await accounts.ListFriendsAsync(new ListRelationshipSimpleRequest { AccountId = currentUser.Id }); + userFriends = friendsResponse.AccountsId.Select(Guid.Parse).ToList(); + publishers = await ps.GetUserPublishers(Guid.Parse(currentUser.Id)); + } + foreach (var post in posts) { // Set reaction count @@ -719,6 +729,26 @@ public partial class PostService( ? repliesCount : 0; + // Check visibility for replied post + if (post.RepliedPost != null) + { + if (!CanViewPost(post.RepliedPost, currentUser, publishers, userFriends)) + { + post.RepliedPost = null; + post.RepliedGone = true; + } + } + + // Check visibility for forwarded post + if (post.ForwardedPost != null) + { + if (!CanViewPost(post.ForwardedPost, currentUser, publishers, userFriends)) + { + post.ForwardedPost = null; + post.ForwardedGone = true; + } + } + // Track view for each post in the list if (currentUser != null) await IncreaseViewCount(post.Id, currentUser.Id); @@ -729,6 +759,39 @@ public partial class PostService( return posts; } + private bool CanViewPost(SnPost post, Account? currentUser, List publishers, List userFriends) + { + var now = SystemClock.Instance.GetCurrentInstant(); + var publishersId = publishers.Select(e => e.Id).ToList(); + + // Check if post is deleted + if (post.DeletedAt != null) + return false; + + 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; + } + + // Check publication status - either published or user is member + var isPublished = post.PublishedAt != null && now >= post.PublishedAt; + var isMember = publishersId.Contains(post.PublisherId); + if (!isPublished && !isMember) + return false; + + // Check visibility + if (post.Visibility == PostVisibility.Private && !isMember) + return false; + + if (post.Visibility == PostVisibility.Friends && + !(post.Publisher.AccountId.HasValue && userFriends.Contains(post.Publisher.AccountId.Value) || isMember)) + return false; + + // Public and Unlisted are allowed + return true; + } + private async Task> GetPostRepliesCountBatch(List postIds) { return await db.Posts @@ -739,47 +802,7 @@ public partial class PostService( g => g.Count() ); } - - private async Task LoadPostEmbed(SnPost post, Account? currentUser) - { - if (!post.Meta!.TryGetValue("embeds", out var value)) - return; - - var embeds = value switch - { - JsonElement e => e.Deserialize>>(), - _ => null - }; - if (embeds is null) - return; - - // Find the index of the poll embed first - var pollIndex = embeds.FindIndex(e => - e.ContainsKey("type") && ((JsonElement)e["type"]).ToString() == "poll" - ); - - if (pollIndex < 0) - { - post.Meta["embeds"] = embeds; - return; - } - - var pollEmbed = embeds[pollIndex]; - try - { - var pollId = Guid.Parse(((JsonElement)pollEmbed["id"]).ToString()); - - Guid? currentUserId = currentUser is not null ? Guid.Parse(currentUser.Id) : null; - var updatedPoll = await polls.LoadPollEmbed(pollId, currentUserId); - embeds[pollIndex] = EmbeddableBase.ToDictionary(updatedPoll); - post.Meta["embeds"] = embeds; - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to load poll embed for post {PostId}", post.Id); - } - } - + public async Task> LoadPostInfo( List posts, Account? currentUser = null, @@ -791,12 +814,6 @@ public partial class PostService( posts = await LoadPublishers(posts); posts = await LoadInteractive(posts, currentUser); - foreach ( - var post in posts - .Where(e => e.Meta is not null && e.Meta.ContainsKey("embeds")) - ) - await LoadPostEmbed(post, currentUser); - if (truncate) posts = TruncatePostContent(posts);