An import reads the archive's followers once, finds threads in linear time, and imports a located post once
A post's visibility no longer parses actor.json again for every outbox item; a reply's conversation is its thread's first post's, found from what the first pass read instead of searching every planned post (quadratic on a large archive); and a located post always records where it came from, so importing a persona's own archive twice made it twice. 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
44a5895b0c
commit
163f7e4e2b
2 files changed
+36
-17
No files matched your search
@@ -65,15 +65,18 @@ namespace PrivaPub.Domain.Portability
|
||||
DedupeKey = $"import|{avatarId}|{DateTime.UtcNow.Ticks}"
|
||||
};
|
||||
|
||||
sealed class Run(PersonaArchive state, LocalActor me, SafeArchive archive, string archiveActor, bool sameActor, IServiceProvider services)
|
||||
sealed class Run(PersonaArchive state, LocalActor me, SafeArchive archive, string archiveActor, string archiveFollowers, bool sameActor,
|
||||
IServiceProvider services)
|
||||
{
|
||||
public PersonaArchive State { get; } = state;
|
||||
public string ArchiveFollowers { get; } = archiveFollowers;
|
||||
public LocalActor Me { get; } = me;
|
||||
public SafeArchive Archive { get; } = archive;
|
||||
public string ArchiveActor { get; } = archiveActor;
|
||||
public bool SameActor { get; } = sameActor;
|
||||
public IServiceProvider Services { get; } = services;
|
||||
public Dictionary<string, (string Id, string Uri)> Posts { get; } = new(StringComparer.Ordinal);//archive object id → here
|
||||
public Dictionary<string, string> Parents { get; } = new(StringComparer.Ordinal);//archive object id → what it answers
|
||||
public DateTime LastSaved { get; set; }
|
||||
|
||||
public void Count(string what, int by = 1) => State.ImportCounts[what] = State.ImportCounts.GetValueOrDefault(what) + by;
|
||||
@@ -109,7 +112,8 @@ namespace PrivaPub.Domain.Portability
|
||||
return JobOutcome.Done;
|
||||
}
|
||||
using var scope = scopes.CreateScope();
|
||||
var run = new Run(state, me, archive, actorId, actorId == me.Uri, scope.ServiceProvider);
|
||||
var run = new Run(state, me, archive, actorId, Text(actor["followers"]) ?? actorId.TrimEnd('/') + "/followers", actorId == me.Uri,
|
||||
scope.ServiceProvider);
|
||||
state.ImportState = "running";
|
||||
state.ImportFrom = actorId;
|
||||
state.ImportSameActor = run.SameActor;
|
||||
@@ -224,10 +228,7 @@ namespace PrivaPub.Domain.Portability
|
||||
return PostVisibility.Public;
|
||||
if (cc.Any(Public.Contains))
|
||||
return PostVisibility.Unlisted;
|
||||
var followers = run.ArchiveActor.TrimEnd('/') is var actor && run.Archive.Json("actor.json") is JsonObject document
|
||||
? Text(document["followers"]) ?? actor + "/followers"
|
||||
: default;
|
||||
if (followers != default && to.Concat(cc).Contains(followers) && Text(note["audience"]) == default)
|
||||
if (to.Concat(cc).Contains(run.ArchiveFollowers) && Text(note["audience"]) == default)
|
||||
return PostVisibility.FollowersOnly;
|
||||
return default;
|
||||
}
|
||||
@@ -260,6 +261,8 @@ namespace PrivaPub.Domain.Portability
|
||||
continue;
|
||||
var original = Text(note["id"]);
|
||||
var published = Date(note["published"]) ?? DateTime.UtcNow;
|
||||
if (Id(note["inReplyTo"]) is { } answers)
|
||||
run.Parents[original] = answers;
|
||||
if (run.SameActor)
|
||||
{
|
||||
var id = original.TrimEnd('/').Split('/')[^1];
|
||||
@@ -363,10 +366,9 @@ namespace PrivaPub.Domain.Portability
|
||||
}
|
||||
if (Text(note["type"]) == "Question")
|
||||
post.Poll = Poll(note);
|
||||
if (visibility is PostVisibility.Public or PostVisibility.Unlisted)
|
||||
post.ContextURI = post.AnsweringToPostId != default && run.Posts.ContainsValue((post.AnsweringToPostId, post.InReplyToURI))
|
||||
? post.InReplyToURI + "/context"
|
||||
: post.ObjectURI + "/context";
|
||||
// the conversation (FEP-7888) of its thread's first post, when the thread starts in the archive
|
||||
if (visibility is PostVisibility.Public or PostVisibility.Unlisted && Root(run, original) is { } root && run.Posts.TryGetValue(root, out var first))
|
||||
post.ContextURI = first.Uri + "/context";
|
||||
post.Media = await Media(run, note["attachment"] as JsonArray, id, token);
|
||||
var rendered = ActivityPubRenderer.Note(post, me, default, post.InReplyToURI);
|
||||
post.To = Strings(rendered["to"]);
|
||||
@@ -376,6 +378,19 @@ namespace PrivaPub.Domain.Portability
|
||||
await DB.Default.Update<PostEntity>().MatchID(post.AnsweringToPostId).Modify(b => b.Inc(p => p.RepliesCount, 1)).ExecuteAsync(token);
|
||||
}
|
||||
|
||||
// the first post of an archive post's thread, when the thread starts in the archive
|
||||
static string Root(Run run, string original)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
while (run.Parents.TryGetValue(original, out var parent) && seen.Add(original))
|
||||
{
|
||||
if (!run.Posts.ContainsKey(parent))
|
||||
return default;
|
||||
original = parent;
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
static PostPoll Poll(JsonObject note)
|
||||
{
|
||||
var multiple = note["anyOf"] is JsonArray;
|
||||
@@ -490,7 +505,7 @@ namespace PrivaPub.Domain.Portability
|
||||
RangeKm = (float)Math.Clamp(located["rangeKm"]?.GetValue<double>() ?? 5, 1, 50),
|
||||
ObjectURI = run.Me.PostUri(id),
|
||||
Url = run.Me.PostHtmlUrl(id),
|
||||
ImportedFromURI = original == run.Me.PostUri(original?.Split('/')[^1] ?? string.Empty) ? default : original,
|
||||
ImportedFromURI = original,
|
||||
CreationDate = Clamp(Date(located["published"]) ?? DateTime.UtcNow)
|
||||
};
|
||||
var files = (located["media"] as JsonArray ?? []).OfType<JsonObject>()
|
||||
|
||||
Reference in new issue
Block a user