🛂 Stricter post visibility check
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user