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:
thepraandClaude Opus 5.5 committed 2026-10-01 10:57:01 +02:00
1 parent bf7c88ce71
commit 2eb2a63f1e
14 files changed
+367 -34

No files matched your search

+8
View File
@@ -1,5 +1,8 @@
using MongoDB.Entities;
using System.Security.Cryptography;
using System.Text;
namespace PrivaPub.Models.Group
{
public class DmGroup : Entity
@@ -7,9 +10,14 @@ namespace PrivaPub.Models.Group
public string Description { get; set; }
public List<GroupMember> Members { get; set; } = new();
public string ConversationURI { get; set; }//context of the federated thread
public string ParticipantsKey { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletionAt { get; set; }
public static string KeyOf(IEnumerable<GroupMember> members) =>
Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("\n",
members.Select(m => (m.IsForeign ? "remote:" : "local:") + m.AvatarId).Distinct().Order(StringComparer.Ordinal)))));
}
}
+7
View File
@@ -17,6 +17,7 @@ namespace PrivaPub.Models.Group
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public GroupKind Kind { get; set; }
public bool IsDiscoverable { get; set; } = true;
public bool ManuallyApprovesMembers { get; set; }
public string OwnerAvatarId { get; set; }
@@ -38,6 +39,12 @@ namespace PrivaPub.Models.Group
public DateTime JoinedAt { get; set; } = DateTime.UtcNow;
}
public enum GroupKind
{
Circle,
Community
}
public enum GroupRole
{
Member,