mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
fix(concord,chat): address audit findings (mentions, re-index perf, auth coupling, cleanups)
Follows the code-review of the dual-reply + Concord work: 1. Public-chat/relay-group minichat replies no longer drop @-mentions, hashtags, URL references, quotes, custom emojis, attachments, content-warning or expiration: ChannelNewMessageViewModel.createTemplate's MINICHAT branch now runs after NewMessageTagger and builds the kind-1111 from tagger.message with the same enrichment the inline path uses (parent author is already tagged by replyBuilder). 2. refreshConcordChannelIndex no longer runs a full re-index + ban rescan on every ingested message: the revision collector is sample(500)-throttled, coalescing bursts into at most one pass per window. 3. Concord stream-key AUTH is no longer blocked behind the user-auth prompt: on a relay that hosts our planes, the ASK path is DISMISSed so the derived stream-key AUTHs return immediately instead of waiting on (or being dropped by) a dialog. 4. Account.sendMinichatReply evaluates chat.relays() once instead of twice. 5. AuthCoordinator caches the per-stream-key NostrSignerSync by secret, so the secp256k1 keypair isn't re-derived for every plane on every relay challenge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
@@ -329,6 +329,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
@@ -2015,10 +2016,10 @@ class Account(
|
||||
val rootEvent = rootNote.event ?: return false
|
||||
|
||||
gatherers?.firstNotNullOfOrNull { it as? PublicChatChannel }?.let { chat ->
|
||||
val relays = chat.relays().ifEmpty { outboxRelays.flow.value }
|
||||
val signed = signer.sign(CommentEvent.replyBuilder(text, EventHintBundle(rootEvent, chat.relays().firstOrNull())))
|
||||
val relays = chat.relays()
|
||||
val signed = signer.sign(CommentEvent.replyBuilder(text, EventHintBundle(rootEvent, relays.firstOrNull())))
|
||||
cache.justConsumeMyOwnEvent(signed)
|
||||
client.publish(signed, relays)
|
||||
client.publish(signed, relays.ifEmpty { outboxRelays.flow.value })
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -4804,9 +4805,12 @@ class Account(
|
||||
|
||||
// Keep Concord channel metadata (community name/icon, membership) live across the whole
|
||||
// app — not just the hub screen — so the Messages tab renders each channel's community
|
||||
// chip, and per-community bans apply, as soon as a Control Plane folds.
|
||||
// chip, and per-community bans apply, as soon as a Control Plane folds. The revision bumps
|
||||
// on every ingested message, so sample() coalesces bursts into at most one full re-index
|
||||
// per window instead of re-scanning every channel's notes per message.
|
||||
scope.launch {
|
||||
concordSessions.revision.collect { refreshConcordChannelIndex() }
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
concordSessions.revision.sample(500).collect { refreshConcordChannelIndex() }
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
|
||||
+14
-3
@@ -24,6 +24,8 @@ import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthContext
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.RelayAuthenticator
|
||||
@@ -33,6 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class ScreenAuthAccount(
|
||||
val account: Account,
|
||||
@@ -87,10 +90,13 @@ class AuthCoordinator(
|
||||
}
|
||||
val currentLedgers = relayLedgers
|
||||
// Ask the user (only in the ASK case) and fold every account's verdict into one
|
||||
// decision plus an optional per-relay override to remember.
|
||||
// decision plus an optional per-relay override to remember. When this relay hosts our
|
||||
// Concord planes, never block the derived stream-key AUTH behind a user prompt: skip
|
||||
// the user-auth ASK (DISMISS) so we return the stream AUTHs immediately instead of
|
||||
// waiting on — or being dropped by — a dialog the user may never answer.
|
||||
val outcome =
|
||||
AuthDecisionResolver.resolve(currentLedgers.map { it.decide(context) }) {
|
||||
promptBus.requestDecision(relayUrl, context.purposes)
|
||||
if (streamAuths.isNotEmpty()) UserAuthChoice.DISMISS else promptBus.requestDecision(relayUrl, context.purposes)
|
||||
}
|
||||
outcome.remember?.let { decision ->
|
||||
currentLedgers.firstOrNull()?.setDecision(relayUrl.url, decision)
|
||||
@@ -146,7 +152,9 @@ class AuthCoordinator(
|
||||
if (secrets.isEmpty()) return emptyList()
|
||||
return secrets.mapNotNull { secret ->
|
||||
try {
|
||||
NostrSignerSync(KeyPair(privKey = secret)).sign(authTemplate)
|
||||
// Cache the signer by secret so we don't re-derive the secp256k1 keypair for every
|
||||
// plane on every relay challenge/reconnect.
|
||||
streamSigners.getOrPut(secret.toHexKey()) { NostrSignerSync(KeyPair(privKey = secret)) }.sign(authTemplate)
|
||||
} catch (e: Exception) {
|
||||
Log.e("AuthCoordinator", "Failed to sign a Concord stream-key AUTH", e)
|
||||
null
|
||||
@@ -154,6 +162,9 @@ class AuthCoordinator(
|
||||
}
|
||||
}
|
||||
|
||||
// stream secret (hex) -> its local signer. Bounded by joined communities × channels.
|
||||
private val streamSigners = ConcurrentHashMap<HexKey, NostrSignerSync>()
|
||||
|
||||
fun destroy() {
|
||||
receiver.destroy()
|
||||
}
|
||||
|
||||
+19
-9
@@ -434,15 +434,6 @@ open class ChannelNewMessageViewModel :
|
||||
private suspend fun createTemplate(): EventTemplate<out Event>? {
|
||||
val channel = channel ?: return null
|
||||
|
||||
// A minichat reply is a kind-1111 thread comment rooted at the parent, independent of the
|
||||
// channel type; NIP-29 groups additionally carry the `h` tag so the relay scopes it.
|
||||
val minichatParent = replyTo.value?.takeIf { replyMode.value == ReplyMode.MINICHAT }?.event
|
||||
if (minichatParent != null) {
|
||||
return CommentEvent.replyBuilder(message.text.toString(), EventHintBundle(minichatParent, channel.relays().firstOrNull())) {
|
||||
if (channel is RelayGroupChannel) hTag(channel.groupId.id)
|
||||
}
|
||||
}
|
||||
|
||||
val messageText = message.text.toString()
|
||||
val tagger =
|
||||
NewMessageTagger(
|
||||
@@ -463,6 +454,25 @@ open class ChannelNewMessageViewModel :
|
||||
val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null
|
||||
val localExpirationDate = if (wantsExpirationDate) expirationDate else null
|
||||
|
||||
// A minichat reply is a kind-1111 thread comment rooted at the parent, independent of the
|
||||
// channel type (NIP-29 groups additionally carry the `h` tag). It carries the same mention/
|
||||
// hashtag/quote/emoji/attachment enrichment an inline message does — built from tagger.message,
|
||||
// not the raw text — so replying in a thread never silently drops any of them.
|
||||
val minichatParent = replyTo.value?.takeIf { replyMode.value == ReplyMode.MINICHAT }?.event
|
||||
if (minichatParent != null) {
|
||||
return CommentEvent.replyBuilder(tagger.message, EventHintBundle(minichatParent, channelRelays.firstOrNull())) {
|
||||
if (channel is RelayGroupChannel) hTag(channel.groupId.id)
|
||||
hashtags(findHashtags(tagger.message))
|
||||
references(findURLs(tagger.message))
|
||||
quotes(findNostrUris(tagger.message))
|
||||
contentWarningReason?.let { contentWarning(it) }
|
||||
localExpirationDate?.let { expiration(it) }
|
||||
geoHash?.let { geohash(it) }
|
||||
emojis(emojis)
|
||||
imetas(usedAttachments)
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
channel is PublicChatChannel -> {
|
||||
val replyingToEvent = replyTo.value?.toEventHint<ChannelMessageEvent>()
|
||||
|
||||
Reference in New Issue
Block a user