An organiser's edits to a group's event follow its server

Mobilizon's organiser sends the Create, Update and Delete of an event attributed to the group, which announces the
Event itself. PrivaPub refused the organiser's activities as misattributed (400) and kept the event through the
group's Announce, so an edit was lost and a deletion left the event in place. An object attributed to another account
of the actor's own server is now that server's to vouch for: created or edited as the server has it, under the account
it is attributed to, and deleted once the server answers 404 or 410. Attributed to an account elsewhere, it is still
refused. Checked against Mobilizon 5.2.4 in the pasture.

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 10:54:32 +02:00
1 parent 7f6837ccb1
commit 2d293a6148
8 files changed
+102 -3

No files matched your search

@@ -72,6 +72,23 @@ namespace PrivaPub.Federation.Inbox.Handlers
Arrival.Drop("fetch-failed");
}
var note = NoteParser.Parse(node);
// made by another account of the author's own server (Mobilizon's organiser creates the group's event): taken
// as that server has it, under the account it is attributed to
if (note != default && note.AttributedTo != author.ActorURI && Origin.Same(note.AttributedTo, author.ActorURI))
{
if (!refetched)
{
using var fetched = await _remoteActors.FetchObject(note.Id, token);
note = fetched == default ? default : NoteParser.Parse(JsonNode.Parse(fetched.Root.GetRawText()));
refetched = true;
}
author = note == default ? default : await _remoteActors.GetActor(note.AttributedTo, refresh: false, token);
if (author == default)
{
Arrival.Drop(note == default ? "fetch-failed" : "author-unavailable");
return;
}
}
if (note == default || note.AttributedTo != author.ActorURI)
{
Arrival.Drop(note == default ? "unparseable" : "misattributed");
@@ -77,6 +77,11 @@ namespace PrivaPub.Federation.Inbox.Handlers
await RemoteDeletes.Tombstone(objectUri, token);
var post = await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri && p.ActorURI == actor.ActorURI).ExecuteFirstAsync(token);
// deleted by another account of its author's server (Mobilizon's organiser deletes the group's event): once
// that server says it is gone
if (post == default && await _dbEntities.Posts.Match(p => p.ObjectURI == objectUri).ExecuteFirstAsync(token) is { } held
&& held.ActorURI != actor.ActorURI && Origin.Same(held.ActorURI, actor.ActorURI) && await _remoteActors.IsGone(objectUri, token))
post = held;
if (post == default)
{
Arrival.Accept("tombstone-only");
@@ -55,6 +55,25 @@ namespace PrivaPub.Federation.Inbox.Handlers
return;
}
var note = NoteParser.Parse(inner);
// edited by another account of the author's own server (Mobilizon's organiser edits the group's event): applied
// as that server has it now
if (note != default && note.AttributedTo != actor.ActorURI && Origin.Same(note.AttributedTo, actor.ActorURI))
{
using var fetched = await _remoteActors.FetchObject(note.Id, token);
var current = fetched == default ? default : NoteParser.Parse(JsonNode.Parse(fetched.Root.GetRawText()));
if (current == default || current.Id != note.Id || current.AttributedTo != note.AttributedTo)
{
Arrival.Drop("fetch-failed");
return;
}
note = current;
actor = await _remoteActors.GetActor(note.AttributedTo, refresh: false, token);
if (actor == default)
{
Arrival.Drop("author-unavailable");
return;
}
}
if (note == default || note.AttributedTo != actor.ActorURI)
{
Arrival.Drop(note == default ? "unparseable" : "misattributed");
+3 -1
View File
@@ -235,8 +235,10 @@ namespace PrivaPub.Federation.Inbox
break;
case "Undo" when inner is JsonObject && Id(inner["actor"]) != actorUri:
return new(StatusCodes.Status400BadRequest, "an actor can only undo its own activities", Reason: "undo-foreign");
// (attributed to another account of the actor's own server, as Mobilizon's organiser creates a group's
// event: the handlers read it from that server, Origin.Same)
case "Create" or "Update" when inner is JsonObject && Origin.Same(Id(inner), actorUri)
&& inner["attributedTo"] != default && Id(inner["attributedTo"]) != actorUri
&& inner["attributedTo"] != default && !Origin.Same(Id(inner["attributedTo"]), actorUri)
&& Value(inner, "type") is not ("Person" or "Service" or "Application" or "Group" or "Organization"):
return new(StatusCodes.Status400BadRequest, "the object is not attributed to the actor", Reason: "misattributed");
}