🛂 Stricter post visibility check

This commit is contained in:
2025-10-24 00:02:27 +08:00
parent 266b9e36e2
commit 4ba6206c9d
3 changed files with 77 additions and 64 deletions

View File

@@ -73,11 +73,12 @@ public class SnPost : ModelBase, IIdentifiedResource, IActivity
public Guid PublisherId { get; set; } public Guid PublisherId { get; set; }
public SnPublisher Publisher { get; set; } = null!; public SnPublisher Publisher { get; set; } = null!;
public ICollection<SnPostAward> Awards { get; set; } = null!; public List<SnPostAward> Awards { get; set; } = null!;
[JsonIgnore] public ICollection<SnPostReaction> Reactions { get; set; } = new List<SnPostReaction>(); [JsonIgnore] public List<SnPostReaction> Reactions { get; set; } = [];
public ICollection<SnPostTag> Tags { get; set; } = new List<SnPostTag>(); public List<SnPostTag> Tags { get; set; } = [];
public ICollection<SnPostCategory> Categories { get; set; } = new List<SnPostCategory>(); public List<SnPostCategory> Categories { get; set; } = [];
[JsonIgnore] public ICollection<SnPostCollection> Collections { get; set; } = new List<SnPostCollection>(); [JsonIgnore] public List<SnPostCollection> Collections { get; set; } = [];
public List<SnPostFeaturedRecord> FeaturedRecords { get; set; } = [];
[JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null; [JsonIgnore] public bool Empty => Content == null && Attachments.Count == 0 && ForwardedPostId == null;
[NotMapped] public bool IsTruncated { get; set; } = false; [NotMapped] public bool IsTruncated { get; set; } = false;
@@ -104,7 +105,7 @@ public class SnPostTag : ModelBase
public Guid Id { get; set; } public Guid Id { get; set; }
[MaxLength(128)] public string Slug { get; set; } = null!; [MaxLength(128)] public string Slug { get; set; } = null!;
[MaxLength(256)] public string? Name { get; set; } [MaxLength(256)] public string? Name { get; set; }
[JsonIgnore] public ICollection<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; }
} }
@@ -114,7 +115,7 @@ public class SnPostCategory : ModelBase
public Guid Id { get; set; } public Guid Id { get; set; }
[MaxLength(128)] public string Slug { get; set; } = null!; [MaxLength(128)] public string Slug { get; set; } = null!;
[MaxLength(256)] public string? Name { get; set; } [MaxLength(256)] public string? Name { get; set; }
[JsonIgnore] public ICollection<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; }
} }
@@ -139,7 +140,7 @@ public class SnPostCollection : ModelBase
public SnPublisher Publisher { get; set; } = null!; public SnPublisher Publisher { get; set; } = null!;
public ICollection<SnPost> Posts { get; set; } = new List<SnPost>(); public List<SnPost> Posts { get; set; } = new List<SnPost>();
} }
public class SnPostFeaturedRecord : ModelBase public class SnPostFeaturedRecord : ModelBase

View File

@@ -106,7 +106,7 @@ public class ActivityService(
var userRealms = await rs.GetUserRealms(Guid.Parse(currentUser.Id)); 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); var postsQuery = BuildPostsQuery(cursor, filteredPublishersId, userRealms);
// Apply visibility filtering and execute // Apply visibility filtering and execute
@@ -122,12 +122,9 @@ public class ActivityService(
var posts = await GetAndProcessPosts( var posts = await GetAndProcessPosts(
postsQuery, postsQuery,
currentUser, currentUser,
userFriends,
userPublishers,
trackViews: true); trackViews: true);
if (currentUser != null) await LoadPostsRealmsAsync(posts, rs);
await LoadPostsRealmsAsync(posts, rs);
posts = RankPosts(posts, take); posts = RankPosts(posts, take);
@@ -283,8 +280,6 @@ public class ActivityService(
private async Task<List<SnPost>> GetAndProcessPosts( private async Task<List<SnPost>> GetAndProcessPosts(
IQueryable<SnPost> baseQuery, IQueryable<SnPost> baseQuery,
Account? currentUser = null, Account? currentUser = null,
List<Guid>? userFriends = null,
List<Shared.Models.SnPublisher>? userPublishers = null,
bool trackViews = true) bool trackViews = true)
{ {
var posts = await baseQuery.ToListAsync(); var posts = await baseQuery.ToListAsync();

View File

@@ -25,9 +25,9 @@ public partial class PostService(
ILogger<PostService> logger, ILogger<PostService> logger,
FileService.FileServiceClient files, FileService.FileServiceClient files,
FileReferenceService.FileReferenceServiceClient fileRefs, FileReferenceService.FileReferenceServiceClient fileRefs,
PollService polls,
Publisher.PublisherService ps, Publisher.PublisherService ps,
WebReaderService reader WebReaderService reader,
AccountService.AccountServiceClient accounts
) )
{ {
private const string PostFileUsageIdentifier = "post"; private const string PostFileUsageIdentifier = "post";
@@ -702,6 +702,16 @@ public partial class PostService(
: new Dictionary<Guid, Dictionary<string, bool>>(); : new Dictionary<Guid, Dictionary<string, bool>>();
var repliesCountMap = await GetPostRepliesCountBatch(postsId); var repliesCountMap = await GetPostRepliesCountBatch(postsId);
// Load user friends if the current user exists
List<SnPublisher> publishers = [];
List<Guid> 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) foreach (var post in posts)
{ {
// Set reaction count // Set reaction count
@@ -719,6 +729,26 @@ public partial class PostService(
? repliesCount ? repliesCount
: 0; : 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 // Track view for each post in the list
if (currentUser != null) if (currentUser != null)
await IncreaseViewCount(post.Id, currentUser.Id); await IncreaseViewCount(post.Id, currentUser.Id);
@@ -729,6 +759,39 @@ public partial class PostService(
return posts; return posts;
} }
private bool CanViewPost(SnPost post, Account? currentUser, List<SnPublisher> publishers, List<Guid> 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<Dictionary<Guid, int>> GetPostRepliesCountBatch(List<Guid> postIds) private async Task<Dictionary<Guid, int>> GetPostRepliesCountBatch(List<Guid> postIds)
{ {
return await db.Posts return await db.Posts
@@ -739,47 +802,7 @@ public partial class PostService(
g => g.Count() 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<List<Dictionary<string, object>>>(),
_ => 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<List<SnPost>> LoadPostInfo( public async Task<List<SnPost>> LoadPostInfo(
List<SnPost> posts, List<SnPost> posts,
Account? currentUser = null, Account? currentUser = null,
@@ -791,12 +814,6 @@ public partial class PostService(
posts = await LoadPublishers(posts); posts = await LoadPublishers(posts);
posts = await LoadInteractive(posts, currentUser); 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) if (truncate)
posts = TruncatePostContent(posts); posts = TruncatePostContent(posts);