Files
SocialPub/PrivaPub/Federation/Actors/OutboxBackfill.cs
T

94 lines
3.8 KiB
C#
Raw Normal View History

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;
}
}