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:
1 parent
1265611f9e
commit
f1e743c331
14 files changed
+224
-3
No files matched your search
@@ -32,6 +32,7 @@
|
||||
public string HeaderDescription { get; set; } = string.Empty;
|
||||
public List<Field> Fields { get; set; } = new();
|
||||
public Source Source { get; set; }
|
||||
public Account Moved { get; set; }//the account it moved to, when that account names it among its aliases
|
||||
}
|
||||
|
||||
public class CustomEmojiEntity
|
||||
|
||||
@@ -166,8 +166,19 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
accounts[avatar.ID] = await Local(_localActors.FromAvatar(avatar), false, token);
|
||||
foreach (var group in await _dbEntities.Groups.Match(g => wanted.Contains(g.ID) && !g.DeletionAt.HasValue && g.Kind == GroupKind.Community).ExecuteAsync(token))
|
||||
accounts[group.ID] = await Local(_localActors.FromGroup(group), false, token);
|
||||
foreach (var foreign in await _dbEntities.ForeignAvatars.Match(f => wanted.Contains(f.ID) && !f.DeletionAt.HasValue).ExecuteAsync(token))
|
||||
var foreigns = await _dbEntities.ForeignAvatars.Match(f => wanted.Contains(f.ID) && !f.DeletionAt.HasValue).ExecuteAsync(token);
|
||||
foreach (var foreign in foreigns)
|
||||
accounts[foreign.ID] = Foreign(foreign);
|
||||
// an account that moved shows where to, once the new account names it as one of its own (as a Move is believed)
|
||||
var movedTo = foreigns.Where(f => !string.IsNullOrEmpty(f.MovedToURL)).Select(f => f.MovedToURL).Distinct().ToList();
|
||||
if (movedTo.Count > 0)
|
||||
{
|
||||
var targets = (await _dbEntities.ForeignAvatars.Match(f => movedTo.Contains(f.ActorURI) && !f.DeletionAt.HasValue).ExecuteAsync(token))
|
||||
.ToDictionary(f => f.ActorURI);
|
||||
foreach (var foreign in foreigns)
|
||||
if (foreign.MovedToURL != default && targets.TryGetValue(foreign.MovedToURL, out var target) && target.AlsoKnownAs.Contains(foreign.ActorURI))
|
||||
accounts[foreign.ID].Moved = Foreign(target);
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace PrivaPub.Federation.Actors
|
||||
public string IconDescription { get; init; }
|
||||
public string HeaderDescription { get; init; }
|
||||
public string MovedTo { get; init; }
|
||||
public List<string> AlsoKnownAs { get; init; } = new();//the accounts it says it also is (a Move's target names the moved one)
|
||||
public DateTime? Published { get; init; }
|
||||
public List<CustomEmoji> Emojis { get; init; } = new();
|
||||
public Dictionary<string, string> Fields { get; init; } = new();
|
||||
@@ -41,6 +42,14 @@ namespace PrivaPub.Federation.Actors
|
||||
|
||||
public ActorKey Key(string keyId) => Keys.FirstOrDefault(k => k.Id == keyId);
|
||||
|
||||
// a property of URIs: one, an array of them, or objects carrying an id
|
||||
static List<string> Uris(JsonElement value) => (value.ValueKind == JsonValueKind.Array ? value.EnumerateArray().ToList() : new List<JsonElement> { value })
|
||||
.Select(v => v.ValueKind == JsonValueKind.String ? v.GetString() : RemoteActorService.Text(v, "id"))
|
||||
.Where(v => Uri.TryCreate(v, UriKind.Absolute, out var uri) && uri.Scheme is "https" or "http")
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Take(20)
|
||||
.ToList();
|
||||
|
||||
public static ActorDocument Parse(JsonElement root)
|
||||
{
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
@@ -75,6 +84,7 @@ namespace PrivaPub.Federation.Actors
|
||||
IconDescription = root.TryGetProperty("icon", out var described) ? Alt(described) : default,
|
||||
HeaderDescription = root.TryGetProperty("image", out var headerImage) ? Alt(headerImage) : default,
|
||||
MovedTo = RemoteActorService.Text(root, "movedTo"),
|
||||
AlsoKnownAs = root.TryGetProperty("alsoKnownAs", out var aliases) ? Uris(aliases) : new(),
|
||||
Published = DateTimeOffset.TryParse(RemoteActorService.Text(root, "published"), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var published)
|
||||
? published.UtcDateTime
|
||||
: default(DateTime?),
|
||||
|
||||
@@ -214,6 +214,7 @@ namespace PrivaPub.Federation.Actors
|
||||
.Modify(a => a.IsLocked, actor.Locked)
|
||||
.Modify(a => a.IsMemorial, actor.Memorial)
|
||||
.Modify(a => a.MovedToURL, Origin.Of(actor.MovedTo) == default ? default : actor.MovedTo)
|
||||
.Modify(a => a.AlsoKnownAs, actor.AlsoKnownAs)
|
||||
.Modify(a => a.Published, actor.Published)
|
||||
.Modify(a => a.Emojis, actor.Emojis)
|
||||
.Modify(a => a.Fields, actor.Fields)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ namespace PrivaPub.Middleware
|
||||
.AddSingleton<IActivityHandler, FlagHandler>()
|
||||
.AddSingleton<IActivityHandler, BlockHandler>()
|
||||
.AddSingleton<IActivityHandler, LockHandler>()
|
||||
.AddSingleton<IActivityHandler, MoveHandler>()
|
||||
.AddSingleton<IActivityHandler, CreateHandler>()
|
||||
.AddSingleton<IActivityHandler, DeleteHandler>()
|
||||
.AddSingleton<IActivityHandler, UpdateHandler>()
|
||||
|
||||
@@ -76,6 +76,7 @@ namespace PrivaPub.Models.User
|
||||
public string InboxURL { get; set; }
|
||||
public string OutboxURL { get; set; }
|
||||
public string MovedToURL { get; set; }
|
||||
public List<string> AlsoKnownAs { get; set; } = new();//alsoKnownAs: the accounts it says it also is
|
||||
public string PictureURL { get; set; }//icon
|
||||
public string ThumbnailURL { get; set; }//image
|
||||
public AvatarType AvatarType { get; set; } = AvatarType.Person;
|
||||
|
||||
Reference in new issue
Block a user