A remote account's searchableBy, and a post's own, outrank indexable in status search: Public lets anyone find a public post, the author's followers only those who follow it, anything else nobody but those it already reaches (their own, named, favourited, bookmarked or boosted posts). Read on actors and posts, kept through edits; PrivaPub does not emit it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
d4b1788a4f
commit
fb4949b511
11 files changed
+111
-5
No files matched your search
@@ -105,15 +105,30 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
mine.UnionWith((await _dbEntities.Posts.Match(p => p.AuthorAccountId == MyId && ids.Contains(p.ReblogOfPostId) && !p.DeletedAt.HasValue).ExecuteAsync(token))
|
||||
.Select(p => p.ReblogOfPostId));
|
||||
var authors = candidates.Select(p => p.AuthorAccountId ?? p.GroupUserId).Where(a => a != default).Distinct().ToList();
|
||||
var indexable = (await _dbEntities.Avatars.Match(a => authors.Contains(a.ID) && a.Settings.IsIndexable).ExecuteAsync(token)).Select(a => a.ID)
|
||||
.Concat((await _dbEntities.ForeignAvatars.Match(f => authors.Contains(f.ID) && f.IsIndexable).ExecuteAsync(token)).Select(f => f.ID))
|
||||
.ToHashSet();
|
||||
var indexable = (await _dbEntities.Avatars.Match(a => authors.Contains(a.ID) && a.Settings.IsIndexable).ExecuteAsync(token)).Select(a => a.ID).ToHashSet();
|
||||
// a remote author's searchableBy (FEP-268d) outranks its indexable, and a post's own outranks both
|
||||
var remoteAuthors = (await _dbEntities.ForeignAvatars.Match(f => authors.Contains(f.ID)).ExecuteAsync(token)).ToDictionary(f => f.ID);
|
||||
var stating = remoteAuthors.Values.Where(f => f.SearchableBy != default).Select(f => f.ActorURI)
|
||||
.Concat(candidates.Where(p => p.SearchableBy != default).Select(p => p.ActorURI))
|
||||
.Where(uri => uri != default).Distinct().ToList();
|
||||
var followed = stating.Count == 0
|
||||
? new HashSet<string>()
|
||||
: (await _dbEntities.Followings.Match(f => f.AvatarId == MyId && stating.Contains(f.TargetActorURI) && f.State == Models.Social.FollowState.Accepted)
|
||||
.ExecuteAsync(token)).Select(f => f.TargetActorURI).ToHashSet();
|
||||
bool Searchable(PostEntity post, string author)
|
||||
{
|
||||
var remote = remoteAuthors.GetValueOrDefault(author);
|
||||
var rule = post.SearchableBy ?? remote?.SearchableBy;
|
||||
if (rule != default)
|
||||
return Federation.Objects.ObjectShapes.Searchable(rule, remote?.FollowersURL, followed.Contains(post.ActorURI ?? string.Empty));
|
||||
return remote?.IsIndexable == true || indexable.Contains(author);
|
||||
}
|
||||
var found = new List<PostEntity>();
|
||||
foreach (var post in candidates)
|
||||
{
|
||||
var author = post.AuthorAccountId ?? post.GroupUserId;
|
||||
var reachable = author == MyId && !post.IsFederatedCopy || mine.Contains(post.ID) || post.Mentions.Any(m => m.AccountId == MyId)
|
||||
|| post.Visibility == PostVisibility.Public && indexable.Contains(author);
|
||||
|| post.Visibility == PostVisibility.Public && Searchable(post, author);
|
||||
if (reachable && await VisibilityPolicy.CanSee(post, MyId, token))
|
||||
found.Add(post);
|
||||
if (found.Count >= offset + limit)
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace PrivaPub.Federation.Actors
|
||||
public string MovedTo { get; init; }
|
||||
public string Moderators { get; init; }
|
||||
public List<PostFlair> Flairs { get; init; } = new();
|
||||
public List<string> SearchableBy { get; init; }
|
||||
public List<string> AlsoKnownAs { get; init; } = new();//the accounts it says it also is (a Move's target names the moved one)
|
||||
public DateTime? Published { get; init; }
|
||||
public List<CustomEmoji> Emojis { get; init; } = new();
|
||||
@@ -96,6 +97,7 @@ namespace PrivaPub.Federation.Actors
|
||||
MovedTo = RemoteActorService.Text(root, "movedTo"),
|
||||
Flairs = type != "Group" ? new() : ObjectShapes.Flairs(root.TryGetProperty("tag", out var labels) ? JsonNode.Parse(labels.GetRawText()) : default,
|
||||
root.TryGetProperty("lemmy:tagsForPosts", out var older) ? JsonNode.Parse(older.GetRawText()) : default),
|
||||
SearchableBy = root.TryGetProperty("searchableBy", out var searchable) ? ObjectShapes.SearchableBy(JsonNode.Parse(searchable.GetRawText())) : default,
|
||||
Moderators = type == "Group" ? RemoteActorService.Text(root, "attributedTo") ?? RemoteActorService.Text(root, "moderators") : default,
|
||||
AlsoKnownAs = root.TryGetProperty("alsoKnownAs", out var aliases) ? Uris(aliases) : new(),
|
||||
Published = DateTimeOffset.TryParse(RemoteActorService.Text(root, "published"), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var published)
|
||||
|
||||
@@ -236,6 +236,7 @@ namespace PrivaPub.Federation.Actors
|
||||
.Modify(a => a.MovedToURL, Origin.Of(actor.MovedTo) == default ? default : actor.MovedTo)
|
||||
.Modify(a => a.ModeratorsURL, Origin.Same(actor.Moderators, actor.Id) ? actor.Moderators : default)
|
||||
.Modify(a => a.Flairs, actor.Flairs)
|
||||
.Modify(a => a.SearchableBy, actor.SearchableBy)
|
||||
.Modify(a => a.AlsoKnownAs, actor.AlsoKnownAs)
|
||||
.Modify(a => a.Published, actor.Published)
|
||||
.Modify(a => a.Emojis, actor.Emojis)
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
post.Event = note.Event ?? post.Event;
|
||||
post.Place = note.Place;
|
||||
post.Flairs = await RemotePosts.Flairs(note, post.AudienceURI, token);
|
||||
post.SearchableBy = note.SearchableBy;
|
||||
if (note.CommentsEnabled is { } enabled)
|
||||
post.LockedAt = enabled ? null : post.LockedAt ?? DateTime.UtcNow;
|
||||
if (!IsEdit(note, post))
|
||||
|
||||
@@ -106,6 +106,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
Excerpt = note.Excerpt,
|
||||
Source = note.Source,
|
||||
Poll = note.Poll,
|
||||
SearchableBy = note.SearchableBy,
|
||||
QuotePolicy = note.QuotePolicy,
|
||||
ReplyPolicy = note.ReplyPolicy,
|
||||
LikePolicy = note.LikePolicy,
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace PrivaPub.Federation.Objects
|
||||
public PostSource Source { get; init; }
|
||||
public PostPoll Poll { get; init; }
|
||||
public List<PostFlair> Flairs { get; init; } = new();
|
||||
public List<string> SearchableBy { get; init; }
|
||||
public InteractionRule QuotePolicy { get; init; }
|
||||
public InteractionRule ReplyPolicy { get; init; }
|
||||
public InteractionRule LikePolicy { get; init; }
|
||||
@@ -121,6 +122,7 @@ namespace PrivaPub.Federation.Objects
|
||||
Source = ObjectShapes.Source(note),
|
||||
Poll = ObjectShapes.Poll(note),
|
||||
Flairs = ObjectShapes.Flairs(note["tag"]),
|
||||
SearchableBy = ObjectShapes.SearchableBy(note["searchableBy"]),
|
||||
QuotePolicy = ObjectShapes.QuotePolicy(note),
|
||||
ReplyPolicy = ObjectShapes.Policy(note, "canReply"),
|
||||
LikePolicy = ObjectShapes.Policy(note, "canLike"),
|
||||
|
||||
@@ -100,6 +100,18 @@ namespace PrivaPub.Federation.Objects
|
||||
|
||||
const int MaxFlairs = 20;
|
||||
|
||||
// FEP-268d: who may find this in a search (Public, the author's followers, nobody else); null when not stated
|
||||
public static List<string> SearchableBy(JsonNode node) => node switch
|
||||
{
|
||||
null => default,
|
||||
JsonArray array => array.Select(Id).Where(id => id != default).Take(MaxAudience).ToList(),
|
||||
_ when Id(node) is { } single => new List<string> { single },
|
||||
_ => new List<string>()
|
||||
};
|
||||
|
||||
public static bool Searchable(List<string> searchableBy, string followers, bool viewerFollows) =>
|
||||
searchableBy.Any(Addressing.IsPublic) || viewerFollows && searchableBy.Any(w => w == followers || w.EndsWith("/followers", StringComparison.Ordinal));
|
||||
|
||||
// a post's flairs, or a community's (its `tag`, and PieFed's older `lemmy:tagsForPosts`), in both dialects
|
||||
public static List<PostFlair> Flairs(params JsonNode[] lists) =>
|
||||
lists.SelectMany(Objects)
|
||||
|
||||
@@ -48,6 +48,8 @@ namespace PrivaPub.Models.Post
|
||||
public PostPoll Poll { get; set; }
|
||||
[BsonIgnoreIfNull]
|
||||
public List<PostFlair> Flairs { get; set; }
|
||||
[BsonIgnoreIfNull]
|
||||
public List<string> SearchableBy { get; set; }//FEP-268d on the post itself: outranks its author's
|
||||
public List<float> Location { get; set; } = new();
|
||||
public float RangeKm { get; set; } = 5.0f;
|
||||
[BsonIgnoreIfNull]
|
||||
|
||||
@@ -78,6 +78,7 @@ namespace PrivaPub.Models.User
|
||||
public string OutboxURL { get; set; }
|
||||
public string FeaturedURL { get; set; }//featured: the posts it pins
|
||||
public List<PrivaPub.Models.Post.PostFlair> Flairs { get; set; } = new();//a community's labels for its posts
|
||||
public List<string> SearchableBy { get; set; }//FEP-268d, when the account states it: outranks IsIndexable
|
||||
public string ModeratorsURL { get; set; }//a community's attributedTo (Lemmy, PieFed, Mbin): the collection of its moderators
|
||||
public List<AssertionKey> AssertionKeys { get; set; } = new();//its Ed25519 keys (FEP-521a), which prove what it sends (FEP-8b32)
|
||||
public string WallURL { get; set; }//sm:wall (FEP-400e): where others write to it, Smithereen's walls
|
||||
|
||||
Reference in new issue
Block a user