Following an account brings its earlier posts to its profile
Once a follow holds, OutboxBackfill reads the account's latest public posts from its outbox's first page (twenty at most, once a day) and keeps them as any fetched post: its profile shows them at once instead of only what it posts from then on. Homes still get only what arrives afterwards, as on Mastodon. Announces and other servers' objects in the outbox are left out. Checked live against Mastodon (scenarios/pins.sh, now 9 checks). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
b218768904
commit
b45321f28d
11 files changed
+203
-10
No files matched your search
@@ -0,0 +1,94 @@
|
||||
using MongoDB.Entities;
|
||||
|
||||
using PrivaPub.Federation.Inbox;
|
||||
using PrivaPub.Federation.Objects;
|
||||
using PrivaPub.Infrastructure.Http;
|
||||
using PrivaPub.Infrastructure.Jobs;
|
||||
using PrivaPub.Models.Jobs;
|
||||
using PrivaPub.StaticServices;
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PrivaPub.Federation.Actors
|
||||
{
|
||||
// The latest public posts of an account a persona has just followed, read from the first page of its outbox (at most
|
||||
// twenty, once a day) and kept as any fetched post: its profile shows them and threads find them at once, instead of
|
||||
// only what it posts from then on. Homes still get only what arrives from now on, as on Mastodon.
|
||||
public class OutboxBackfill : IJobHandler
|
||||
{
|
||||
public const int MaxPosts = 20;
|
||||
|
||||
readonly DbEntities _dbEntities;
|
||||
readonly IRemoteActorService _remoteActors;
|
||||
readonly IRemotePosts _remotePosts;
|
||||
|
||||
public OutboxBackfill(DbEntities dbEntities, IRemoteActorService remoteActors, IRemotePosts remotePosts)
|
||||
{
|
||||
_dbEntities = dbEntities;
|
||||
_remoteActors = remoteActors;
|
||||
_remotePosts = remotePosts;
|
||||
}
|
||||
|
||||
public JobKind Kind => JobKind.BackfillOutbox;
|
||||
public int Concurrency => 1;
|
||||
public int MaxAttempts => 2;
|
||||
public int PerHostLimit => 1;
|
||||
|
||||
public static string DedupeKey(string actorUri, DateTime day) => $"backfill|{actorUri}|{day:yyyyMMdd}";
|
||||
|
||||
public async Task<JobOutcome> Handle(Job job, CancellationToken token)
|
||||
{
|
||||
var actor = await _dbEntities.ForeignAvatars.Match(a => a.ActorURI == job.Payload && !a.DeletionAt.HasValue).ExecuteFirstAsync(token);
|
||||
if (actor == default || string.IsNullOrEmpty(actor.OutboxURL) || !Origin.Same(actor.OutboxURL, actor.ActorURI))
|
||||
return JobOutcome.Done;
|
||||
foreach (var objectUri in (await Items(actor, token)).Take(MaxPosts))
|
||||
await _remotePosts.StoreContext(objectUri, 0, token);
|
||||
return JobOutcome.Done;
|
||||
}
|
||||
|
||||
// the posts the outbox's first page creates, newest first: Create items name or hold them; Announces are left out,
|
||||
// being others' posts
|
||||
async Task<List<string>> Items(Models.User.ForeignAvatar actor, CancellationToken token)
|
||||
{
|
||||
using var scope = HttpScope.For("outbox");
|
||||
using var outbox = await _remoteActors.FetchObject(actor.OutboxURL, token);
|
||||
if (outbox == default || outbox.Root.ValueKind != JsonValueKind.Object)
|
||||
return new();
|
||||
var page = ItemsOf(outbox.Root, actor);
|
||||
if (page.Count > 0 || !outbox.Root.TryGetProperty("first", out var first))
|
||||
return page;
|
||||
if (first.ValueKind == JsonValueKind.Object)
|
||||
return ItemsOf(first, actor);
|
||||
var pageUri = first.ValueKind == JsonValueKind.String ? first.GetString() : default;
|
||||
if (pageUri == default || !Origin.Same(pageUri, actor.ActorURI))
|
||||
return page;
|
||||
using var fetched = await _remoteActors.FetchObject(pageUri, token);
|
||||
return fetched == default || fetched.Root.ValueKind != JsonValueKind.Object ? page : ItemsOf(fetched.Root, actor);
|
||||
}
|
||||
|
||||
static List<string> ItemsOf(JsonElement collection, Models.User.ForeignAvatar actor)
|
||||
{
|
||||
var posts = new List<string>();
|
||||
foreach (var name in new[] { "orderedItems", "items" })
|
||||
{
|
||||
if (!collection.TryGetProperty(name, out var list) || list.ValueKind != JsonValueKind.Array)
|
||||
continue;
|
||||
foreach (var item in list.EnumerateArray())
|
||||
{
|
||||
var objectUri = item.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object when Text(item, "type") == "Create" && item.TryGetProperty("object", out var created) =>
|
||||
created.ValueKind == JsonValueKind.String ? created.GetString() : created.ValueKind == JsonValueKind.Object ? Text(created, "id") : default,
|
||||
_ => default
|
||||
};
|
||||
if (objectUri != default && Origin.Same(objectUri, actor.ActorURI))
|
||||
posts.Add(objectUri);
|
||||
}
|
||||
}
|
||||
return posts;
|
||||
}
|
||||
|
||||
static string Text(JsonElement element, string property) =>
|
||||
element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() : default;
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,12 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
readonly IInteractionApprovals _approvals;
|
||||
readonly IParticipations _participations;
|
||||
readonly Relays.IRelays _relays;
|
||||
readonly Infrastructure.Jobs.IJobQueue _queue;
|
||||
|
||||
public AcceptHandler(DbEntities dbEntities, ILocalActorService localActors, IQuoteService quotes, IInteractionApprovals approvals = default,
|
||||
IParticipations participations = default, Relays.IRelays relays = default)
|
||||
IParticipations participations = default, Relays.IRelays relays = default, Infrastructure.Jobs.IJobQueue queue = default)
|
||||
{
|
||||
_queue = queue;
|
||||
_relays = relays;
|
||||
_participations = participations;
|
||||
_dbEntities = dbEntities;
|
||||
@@ -67,8 +69,14 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
await Apply(following, token);
|
||||
}
|
||||
|
||||
protected virtual Task Apply(Following following, CancellationToken token) =>
|
||||
DB.Default.Update<Following>().MatchID(following.ID).Modify(f => f.State, FollowState.Accepted).ExecuteAsync(token);
|
||||
// the follow holds: the account's latest public posts are read from its outbox (OutboxBackfill), once a day at most
|
||||
protected virtual async Task Apply(Following following, CancellationToken token)
|
||||
{
|
||||
await DB.Default.Update<Following>().MatchID(following.ID).Modify(f => f.State, FollowState.Accepted).ExecuteAsync(token);
|
||||
if (_queue != default)
|
||||
await _queue.Enqueue(Models.Jobs.JobKind.BackfillOutbox, following.TargetActorURI, new Uri(following.TargetActorURI).Host,
|
||||
Actors.OutboxBackfill.DedupeKey(following.TargetActorURI, DateTime.UtcNow), token);
|
||||
}
|
||||
|
||||
public static async Task<Following> FindFollowing(JsonNode follow, ForeignAvatar target, DbEntities dbEntities, ILocalActorService localActors,
|
||||
CancellationToken token)
|
||||
@@ -96,7 +104,7 @@ namespace PrivaPub.Federation.Inbox.Handlers
|
||||
{
|
||||
public RejectHandler(DbEntities dbEntities, ILocalActorService localActors, IQuoteService quotes, IInteractionApprovals approvals = default,
|
||||
IParticipations participations = default, Relays.IRelays relays = default)
|
||||
: base(dbEntities, localActors, quotes, approvals, participations, relays)
|
||||
: base(dbEntities, localActors, quotes, approvals, participations, relays, queue: default)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ namespace PrivaPub.Middleware
|
||||
.AddSingleton<IJobHandler, PollRefreshJob>()
|
||||
.AddSingleton<IJobHandler, RecoveryJob>()
|
||||
.AddSingleton<IJobHandler, Federation.Actors.AccountCountsJob>()
|
||||
.AddSingleton<IJobHandler, Federation.Actors.OutboxBackfill>()
|
||||
.AddSingleton<IJobHandler, PollCloseJob>()
|
||||
.AddSingleton<IJobHandler, Domain.Statuses.PublishScheduledJob>()
|
||||
.AddSingleton<InstanceDescriber>()
|
||||
|
||||
@@ -33,7 +33,8 @@ namespace PrivaPub.Models.Jobs
|
||||
SendRecovery,
|
||||
CountAccount,
|
||||
PublishScheduled,
|
||||
FetchReplies
|
||||
FetchReplies,
|
||||
BackfillOutbox
|
||||
}
|
||||
|
||||
public enum JobState
|
||||
|
||||
Reference in new issue
Block a user