Conversations are keyed by who is in them, and circles stay home
S8: a direct message joins a conversation only when its participants are exactly that conversation's members, found through a new DmGroup.ParticipantsKey (a hash of the sorted members). A remote context no longer decides anything: it let anyone who knew a conversation's context post into it, and joining by context while dropping a participant would have shown a reply to someone it was not addressed to. A context is kept only when it is on the author's origin. Sending a DM to the same people again reuses their conversation instead of opening a new one. S9: Group.Kind is Circle or Community. A circle is not a federated actor: its actor, collections, WebFinger and inbox answer 404, a remote Follow is refused, and posts in it are IsLocalOnly - never delivered, never in an outbox, never served. Communities keep today's behaviour until P4. Migration _003 makes every existing group a circle, marks their posts local-only and backfills the conversation keys. End-to-end inbox tests sign real deliveries from a fake peer: a context injection, a forged activity id, a note attributed to someone else, a cross-origin object, a bad signature, junk bodies and a Follow of a circle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
bf7c88ce71
commit
2eb2a63f1e
14 files changed
+367
-34
No files matched your search
@@ -65,6 +65,7 @@ namespace PrivaPub.Services
|
||||
return result.Invalidate(_localizer["Avatar not found."], StatusCodes.Status404NotFound);
|
||||
|
||||
LocalActor group = default;
|
||||
var isLocalOnly = false;
|
||||
if (!string.IsNullOrEmpty(form.GroupId))
|
||||
{
|
||||
var groupEntity = await _dbEntities.Groups.MatchID(form.GroupId).ExecuteFirstAsync(token);
|
||||
@@ -72,6 +73,7 @@ namespace PrivaPub.Services
|
||||
|| !groupEntity.Members.Any(m => !m.IsForeign && m.AvatarId == form.AvatarId))
|
||||
return result.Invalidate(_localizer["Group not found."], StatusCodes.Status404NotFound);
|
||||
group = _localActors.FromGroup(groupEntity);
|
||||
isLocalOnly = !group.IsFederated;
|
||||
}
|
||||
|
||||
var post = new PostEntity
|
||||
@@ -84,12 +86,19 @@ namespace PrivaPub.Services
|
||||
ContentFormat = ContentFormat.Markdown,
|
||||
HasContentWarning = form.HasContentWarning,
|
||||
AnsweringToPostId = form.AnsweringToPostId,
|
||||
IsLocalOnly = isLocalOnly,
|
||||
ActorURI = author.Uri
|
||||
};
|
||||
post.ID = (string)post.GenerateNewID();
|
||||
post.ObjectURI = author.PostUri(post.ID);
|
||||
await DB.Default.SaveAsync(post, token);
|
||||
|
||||
if (isLocalOnly)
|
||||
{
|
||||
result.Data = ToView(post);
|
||||
return result;
|
||||
}
|
||||
|
||||
var inReplyTo = await ReplyTarget(form.AnsweringToPostId, token);
|
||||
var note = ActivityPubRenderer.Note(post, author, group, inReplyTo);
|
||||
var create = ActivityPubRenderer.Create(author, note, $"create-{post.ID}");
|
||||
@@ -123,6 +132,8 @@ namespace PrivaPub.Services
|
||||
return result.Invalidate(_localizer["Post not found."], StatusCodes.Status404NotFound);
|
||||
|
||||
await DB.Default.DeleteAsync<PostEntity>(post.ID);
|
||||
if (post.IsLocalOnly)
|
||||
return result;
|
||||
|
||||
var delete = ActivityPubRenderer.Delete(author, post.ObjectURI, $"delete-{post.ID}",
|
||||
new JsonArray(ActivityPubRenderer.Public), new JsonArray(author.Followers));
|
||||
@@ -205,10 +216,15 @@ namespace PrivaPub.Services
|
||||
if (members.All(m => m.AvatarId != member.AvatarId))
|
||||
members.Add(member);
|
||||
}
|
||||
dmGroup = new DmGroup { Members = members };
|
||||
dmGroup.ID = (string)dmGroup.GenerateNewID();
|
||||
dmGroup.ConversationURI = $"{author.Uri}/conversations/{dmGroup.ID}";
|
||||
await DB.Default.SaveAsync(dmGroup, token);
|
||||
var key = DmGroup.KeyOf(members);
|
||||
dmGroup = await _dbEntities.DmGroups.Match(g => g.ParticipantsKey == key && !g.DeletionAt.HasValue).ExecuteFirstAsync(token);
|
||||
if (dmGroup == default)
|
||||
{
|
||||
dmGroup = new DmGroup { Members = members, ParticipantsKey = key };
|
||||
dmGroup.ID = (string)dmGroup.GenerateNewID();
|
||||
dmGroup.ConversationURI = $"{author.Uri}/conversations/{dmGroup.ID}";
|
||||
await DB.Default.SaveAsync(dmGroup, token);
|
||||
}
|
||||
}
|
||||
|
||||
var dm = new DmPostEntity
|
||||
|
||||
Reference in new issue
Block a user