mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
fix: keep group thread replies scoped to the group
Replying to a NIP-29 group thread went through the shared NIP-22 comment
composer, which built a plain kind-1111 comment with no group `h` tag and
broadcast it to the author's outbox. Such a reply is not group content: the
host relay rejects it and no other member — or other NIP-29 client like
Flotilla — ever sees it, and for a private/closed group it leaks to
unrelated relays.
Both fixes are tightly guarded on the replied-to event being group-scoped,
so ordinary comments are untouched:
- CommentPostViewModel inherits the group's `h` tag from the event being
replied to (covers replies to the kind-11 root and to nested 1111
comments — both route here).
- The reply is published only to the group's host relay (the relay the
thread was seen on) via signAndSendPrivatelyOrBroadcast, instead of the
outbox-computing broadcast — so it reaches the group and never leaks.
Verified: quartz test builds the reply as the composer does
(CommentEvent.replyBuilder { hTag } over a kind-11 root) and asserts it is a
1111 carrying the group `h` tag and referencing the thread.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
+21
@@ -91,6 +91,9 @@ import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuoteIds
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip22Comments.notify
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.groupId
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.hTag
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.isGroupScoped
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
|
||||
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
|
||||
@@ -520,8 +523,20 @@ open class CommentPostViewModel :
|
||||
val anonymous = wantsAnonymousPost
|
||||
cancel()
|
||||
|
||||
// A reply within a NIP-29 group is group content: pin it to the group's
|
||||
// host relay (the relay the thread was seen on) instead of the author's
|
||||
// outbox, so it reaches the group and — for a private/closed group — is
|
||||
// never leaked to unrelated relays.
|
||||
val groupHostRelays =
|
||||
replyingTo
|
||||
?.takeIf { it.event?.isGroupScoped() == true }
|
||||
?.relays
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
if (anonymous) {
|
||||
accountViewModel.account.signAnonymouslyAndBroadcast(template, extraNotesToBroadcast, anonymousSigner())
|
||||
} else if (groupHostRelays != null) {
|
||||
accountViewModel.account.signAndSendPrivatelyOrBroadcast(template) { groupHostRelays }
|
||||
} else {
|
||||
accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast)
|
||||
}
|
||||
@@ -577,6 +592,12 @@ open class CommentPostViewModel :
|
||||
msg = tagger.message,
|
||||
replyingTo = eventHint,
|
||||
) {
|
||||
// A reply inside a NIP-29 group thread must carry the group's `h`
|
||||
// tag, or the host relay won't accept it and no other member (or
|
||||
// other NIP-29 client, e.g. Flotilla) will see it. Inherit it from
|
||||
// the event being replied to; a no-op for non-group comments.
|
||||
replyingToEvent?.groupId()?.let { hTag(it) }
|
||||
|
||||
val notifyPTags = tagger.pTags?.let { pTagList -> pTagList.map { it.toPTag() } } ?: emptyList()
|
||||
|
||||
val extraNotificationAuthors =
|
||||
|
||||
+23
@@ -21,6 +21,8 @@
|
||||
package com.vitorpamplona.quartz.nip29RelayGroups
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupAdminsEvent
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.metadata.GroupMembersEvent
|
||||
@@ -332,6 +334,27 @@ class Nip29ArmadaInteropTest {
|
||||
assertFalse(put.any { it[0] == "previous" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildsGroupThreadReplyWithHTagAndRootReference() {
|
||||
// A reply to a kind-11 group thread must be a 1111 comment that BOTH roots
|
||||
// at the thread AND carries the group `h` tag — the exact composition the
|
||||
// Android composer builds (CommentEvent.replyBuilder { hTag(...) }).
|
||||
val thread =
|
||||
parse(
|
||||
ThreadEvent.KIND,
|
||||
arrayOf(arrayOf("h", gid), arrayOf("title", "T")),
|
||||
content = "body",
|
||||
pubKey = alice,
|
||||
) as ThreadEvent
|
||||
|
||||
val reply = CommentEvent.replyBuilder("nice", EventHintBundle(thread)) { hTag(gid) }
|
||||
|
||||
assertEquals(CommentEvent.KIND, reply.kind)
|
||||
assertEquals(gid, reply.tags.hTag())
|
||||
// References the thread it replies to (NIP-22 root scope carries the id).
|
||||
assertTrue(reply.tags.any { it.size > 1 && it[1] == thread.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildsJoinLeaveAndGroupScopedChat() {
|
||||
val join = JoinRequestEvent.build(gid, reason = "hi", inviteCode = "inv9")
|
||||
|
||||
Reference in New Issue
Block a user