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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user