An account that moves shows where it went

PrivaPub dropped Move as an unknown type and showed no `moved` on accounts.
Now a Move is believed as Mastodon believes it: the moving account sends it
about itself, and the new account, read again from its own server, names it
in alsoKnownAs (now kept on remote accounts). The old account then shows the
new one as `moved` in the Mastodon API. The personas following it keep
following it: following the new account on their behalf would tell another
server about them, so that waits for the owner.

Checked live against GoToSocial (scenarios/moves.sh: an alias, a move, 6
checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 18:39:46 +02:00
1 parent 1265611f9e
commit f1e743c331
14 files changed
+224 -3

No files matched your search

@@ -0,0 +1,50 @@
using MongoDB.Entities;
using PrivaPub.Federation.Actors;
using PrivaPub.Models.User;
using System.Text.Json.Nodes;
using static PrivaPub.Federation.Objects.ActivityJson;
namespace PrivaPub.Federation.Inbox.Handlers
{
// An account moving to another (Mastodon's Move{object: itself, target: the new account}), believed as Mastodon
// believes it: the moving account sends it about itself, and the new account, read again from its own server, names
// it among its alsoKnownAs. The old account then shows where it went (`moved`). The personas following it keep
// following it: following the new one is theirs to do.
public class MoveHandler : IActivityHandler
{
readonly IRemoteActorService _remoteActors;
public MoveHandler(IRemoteActorService remoteActors)
{
_remoteActors = remoteActors;
}
public string Type => "Move";
public async Task Handle(JsonNode activity, ForeignAvatar actor, CancellationToken token)
{
var target = Id(activity["target"]);
if (Id(activity["object"]) != actor.ActorURI || string.IsNullOrEmpty(target) || target == actor.ActorURI)
{
Arrival.Drop("not-self");
return;
}
var moved = await _remoteActors.GetActor(target, refresh: true, token);
if (moved == default)
{
Arrival.Drop("target-unavailable");
return;
}
if (!moved.AlsoKnownAs.Contains(actor.ActorURI))
{
Arrival.Drop("not-aliased");
return;
}
await DB.Default.Update<ForeignAvatar>().MatchID(actor.ID).Modify(a => a.MovedToURL, target).ExecuteAsync(token);
Arrival.Accept("moved");
}
}
}