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
@@ -117,7 +117,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
async Task<InboxResult> Follow(JsonNode follow, ForeignAvatar follower, CancellationToken token)
|
||||
{
|
||||
var target = await _localActors.FindByUri(Id(follow["object"]), token);
|
||||
if (target == default || target.Kind == LocalActorKind.Application)
|
||||
if (target is not { IsFederated: true } || target.Kind == LocalActorKind.Application)
|
||||
return new(StatusCodes.Status404NotFound, "no such local actor");
|
||||
|
||||
var existing = await _dbEntities.Followers
|
||||
@@ -197,7 +197,7 @@ namespace PrivaPub.Federation.Inbox
|
||||
foreach (var uri in addressed.Concat(new[] { Id(note["audience"]) }).Where(u => u != default).Distinct())
|
||||
{
|
||||
var local = await _localActors.FindByUri(uri, token);
|
||||
if (local != default && localTargets.All(l => l.Id != local.Id))
|
||||
if (local is { IsFederated: true } && localTargets.All(l => l.Id != local.Id))
|
||||
localTargets.Add(local);
|
||||
}
|
||||
|
||||
@@ -243,8 +243,12 @@ namespace PrivaPub.Federation.Inbox
|
||||
if (await _dbEntities.DmPosts.Match(p => p.ObjectURI == objectUri).ExecuteAnyAsync(token))
|
||||
return new(StatusCodes.Status202Accepted);
|
||||
|
||||
var participants = addressed.Where(a => a != ActivityPubRenderer.Public).Append(author.ActorURI).ToList();
|
||||
var dmGroup = await FindOrCreateDmGroup(participants, Value(note, "context") ?? Value(note, "conversation"), token);
|
||||
var participants = recipients.Select(r => r.Uri)
|
||||
.Concat(addressed.Where(a => a != ActivityPubRenderer.Public && !a.EndsWith("/followers") && !a.EndsWith("/following")))
|
||||
.Append(author.ActorURI)
|
||||
.ToList();
|
||||
var context = Value(note, "context") ?? Value(note, "conversation");
|
||||
var dmGroup = await FindOrCreateDmGroup(participants, Origin.Same(context, author.ActorURI) ? context : default, token);
|
||||
var dmHtml = ContentSanitizer.Html(Value(note, "content"));
|
||||
var dm = new DmPostEntity
|
||||
{
|
||||
@@ -349,27 +353,19 @@ namespace PrivaPub.Federation.Inbox
|
||||
foreach (var uri in participantUris.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
var local = await _localActors.FindByUri(uri, token);
|
||||
members.Add(local != default && local.Kind == LocalActorKind.Person
|
||||
var member = local != default && local.Kind == LocalActorKind.Person
|
||||
? new GroupMember { AvatarId = local.Id }
|
||||
: new GroupMember { AvatarId = uri, IsForeign = true });
|
||||
: new GroupMember { AvatarId = uri, IsForeign = true };
|
||||
if (members.All(m => m.AvatarId != member.AvatarId || m.IsForeign != member.IsForeign))
|
||||
members.Add(member);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(context))
|
||||
{
|
||||
var byContext = await _dbEntities.DmGroups.Match(g => g.ConversationURI == context).ExecuteFirstAsync(token);
|
||||
if (byContext != default)
|
||||
return byContext;
|
||||
}
|
||||
|
||||
var keys = members.Select(m => m.AvatarId).OrderBy(k => k, StringComparer.Ordinal).ToList();
|
||||
var candidates = await _dbEntities.DmGroups
|
||||
.Match(g => !g.DeletionAt.HasValue && g.Members.Count == keys.Count)
|
||||
.ExecuteAsync(token);
|
||||
var match = candidates.FirstOrDefault(g => g.Members.Select(m => m.AvatarId).OrderBy(k => k, StringComparer.Ordinal).SequenceEqual(keys));
|
||||
var key = DmGroup.KeyOf(members);
|
||||
var match = await _dbEntities.DmGroups.Match(g => g.ParticipantsKey == key && !g.DeletionAt.HasValue).ExecuteFirstAsync(token);
|
||||
if (match != default)
|
||||
return match;
|
||||
|
||||
var dmGroup = new DmGroup { Members = members, ConversationURI = context };
|
||||
var dmGroup = new DmGroup { Members = members, ConversationURI = context, ParticipantsKey = key };
|
||||
await DB.Default.SaveAsync(dmGroup, token);
|
||||
return dmGroup;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user