diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/Nip46ConsentInfoBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/Nip46ConsentInfoBuilder.kt index c8eebe886a..fdcc046cd8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/Nip46ConsentInfoBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/Nip46ConsentInfoBuilder.kt @@ -24,8 +24,12 @@ import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAut import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46PermissionAuthorizer.Companion.toNarrowSignerOp import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent +import com.vitorpamplona.quartz.nip42RelayAuth.authRelays import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign import com.vitorpamplona.quartz.utils.Log @@ -91,10 +95,7 @@ object Nip46ConsentInfoBuilder { val preview = when { - request is BunkerRequestSign -> - request.event.content - .take(PREVIEW_MAX_CHARS) - .trim() + request is BunkerRequestSign -> signPreview(request.event.kind, request.event.tags, request.event.content) plaintext != null -> plaintext.take(PREVIEW_MAX_CHARS).trim() else -> "" } @@ -132,6 +133,22 @@ object Nip46ConsentInfoBuilder { ) } + /** + * The one-line preview of an event an app asks to sign. Usually its content; a NIP-42 relay + * login has none, so it names the relay(s) instead — otherwise a batched row reads as a blank + * "sign Relay Auth" with nothing to say where the user is logging in. + */ + fun signPreview( + kind: Int, + tags: TagArray, + content: String, + ): String = + if (kind == RelayAuthEvent.KIND) { + tags.authRelays().joinToString(", ") { it.displayUrl() } + } else { + content.take(PREVIEW_MAX_CHARS).trim() + } + /** * Decrypts the message the app asked to read. Never throws and never hangs: a signer that fails, * refuses, returns nothing, or takes too long yields [failureText], because a request whose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt index 1d46b6ece6..75a9bb4fb4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt @@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_fallback_title import com.vitorpamplona.amethyst.commons.resources.napplet_op_decrypt import com.vitorpamplona.amethyst.commons.resources.napplet_op_decrypt_from import com.vitorpamplona.amethyst.commons.resources.napplet_op_encrypt +import com.vitorpamplona.amethyst.commons.resources.napplet_op_relay_login import com.vitorpamplona.amethyst.commons.resources.napplet_op_sign_kind_named import com.vitorpamplona.amethyst.commons.resources.nip46_signer_allow_always_for import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.relays.kindNameFor +import com.vitorpamplona.amethyst.connectedApps.consent.Nip46ConsentInfoBuilder import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectInfo import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentInfo import com.vitorpamplona.quartz.nip01Core.core.Event @@ -46,12 +48,19 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.TimeUtils /** Human-readable label for a [NostrSignerOp]. */ suspend fun NostrSignerOp.label(context: Context): String = when (this) { - is NostrSignerOp.SignKind -> loadStringRes(Res.string.napplet_op_sign_kind_named, kindNameFor(kind), kind) + is NostrSignerOp.SignKind -> + if (kind == RelayAuthEvent.KIND) { + // A relay login is not "signing" anything the user would recognise; say what it does. + loadStringRes(Res.string.napplet_op_relay_login) + } else { + loadStringRes(Res.string.napplet_op_sign_kind_named, kindNameFor(kind), kind) + } NostrSignerOp.Encrypt -> loadStringRes(Res.string.napplet_op_encrypt) NostrSignerOp.Decrypt -> loadStringRes(Res.string.napplet_op_decrypt) is NostrSignerOp.DecryptFrom -> loadStringRes(Res.string.napplet_op_decrypt_from, counterpartyLabel(counterparty)) @@ -96,8 +105,8 @@ suspend fun buildSignerConsentInfo( val summary = (narrowOp ?: op).label(context) val preview = when (request) { - is NappletRequest.Publish -> request.content.take(160).trim() - is NappletRequest.SignEvent -> request.content.take(160).trim() + is NappletRequest.Publish -> Nip46ConsentInfoBuilder.signPreview(request.kind, request.tags, request.content) + is NappletRequest.SignEvent -> Nip46ConsentInfoBuilder.signPreview(request.kind, request.tags, request.content) is NappletRequest.PublishEncrypted -> request.content.take(160).trim() // Encryption shows the plaintext the page wants sealed; decryption has only ciphertext, // which tells the user nothing, so its preview stays empty and the counterparty in diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt index cf52314cdb..16ebfbc601 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/napplets/ConnectedAppDetailScreen.kt @@ -105,6 +105,7 @@ import com.vitorpamplona.amethyst.commons.resources.napplet_decision_deny import com.vitorpamplona.amethyst.commons.resources.napplet_op_decrypt import com.vitorpamplona.amethyst.commons.resources.napplet_op_decrypt_from import com.vitorpamplona.amethyst.commons.resources.napplet_op_encrypt +import com.vitorpamplona.amethyst.commons.resources.napplet_op_relay_login import com.vitorpamplona.amethyst.commons.resources.napplet_op_sign_kind import com.vitorpamplona.amethyst.commons.resources.napplet_permissions_ask_each_time import com.vitorpamplona.amethyst.commons.resources.napplet_policy_full_trust @@ -141,6 +142,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.nip46AppOnli import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.nip46.nip46ClientSubtitle import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -838,7 +840,12 @@ private fun GrantOption( @Composable private fun NostrSignerOp.opLabel(): String = when (this) { - is NostrSignerOp.SignKind -> stringRes(Res.string.napplet_op_sign_kind, kind) + is NostrSignerOp.SignKind -> + if (kind == RelayAuthEvent.KIND) { + stringRes(Res.string.napplet_op_relay_login) + } else { + stringRes(Res.string.napplet_op_sign_kind, kind) + } NostrSignerOp.Encrypt -> stringRes(Res.string.napplet_op_encrypt) NostrSignerOp.Decrypt -> stringRes(Res.string.napplet_op_decrypt) is NostrSignerOp.DecryptFrom -> stringRes(Res.string.napplet_op_decrypt_from, counterpartyLabel(counterparty)) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/connectedApps/consent/Nip46ConsentInfoBuilderTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/connectedApps/consent/Nip46ConsentInfoBuilderTest.kt index 59cc601902..015a380df2 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/connectedApps/consent/Nip46ConsentInfoBuilderTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/connectedApps/consent/Nip46ConsentInfoBuilderTest.kt @@ -22,7 +22,9 @@ package com.vitorpamplona.amethyst.connectedApps.consent import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt @@ -173,4 +175,24 @@ class Nip46ConsentInfoBuilderTest { assertNull(info.narrowOp) assertNotNull(info.previewTemplate) } + + @Test + fun aRelayLoginPreviewNamesTheRelayInsteadOfItsEmptyContent() = + runTest { + val auth = RelayAuthEvent.build(RelayUrlNormalizer.normalize("wss://relay.damus.io"), "chal-123") + val template = EventTemplate(auth.createdAt, auth.kind, auth.tags, auth.content) + val info = build(BunkerRequestSign(event = template), op = NostrSignerOp.SignKind(RelayAuthEvent.KIND)) + + assertEquals("relay.damus.io", info.contentPreview) + assertEquals(template, info.previewTemplate) + } + + @Test + fun aRegularPostPreviewIsStillItsContent() = + runTest { + val template = EventTemplate(1L, 1, emptyArray(), " hello world ") + val info = build(BunkerRequestSign(event = template), op = NostrSignerOp.SignKind(1)) + + assertEquals("hello world", info.contentPreview) + } } diff --git a/commonsUI/src/commonMain/composeResources/values/strings.xml b/commonsUI/src/commonMain/composeResources/values/strings.xml index 99b2ba83b9..82e11bc2a9 100644 --- a/commonsUI/src/commonMain/composeResources/values/strings.xml +++ b/commonsUI/src/commonMain/composeResources/values/strings.xml @@ -1134,6 +1134,10 @@ Just for now Logged in until you restart Amethyst Forget this login + Relay login + Proves to the relay that you control this account. Nothing is posted. + No relay named in this login + Challenge: %1$s %1$s will ask again the next time it needs you. Not restored. “Never log in” is on, so Amethyst won't log in to %1$s. Blocked by your block list @@ -4505,6 +4509,7 @@ read your private messages read your private messages with %1$s encrypt a message + log in to relays as you sign for %1$s (kind: %2$d) Connected Apps nApplets diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCompose.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCompose.kt index 55adb2159f..bca367d13f 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCompose.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCompose.kt @@ -221,6 +221,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPublicationIndex import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPublicationSection import com.vitorpamplona.amethyst.commons.ui.note.types.RenderReaction import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayAddMember +import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayAuth import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayDiscovery import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayGroupMessage import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayJoinRequest @@ -372,6 +373,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip40Expiration.expiration +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent import com.vitorpamplona.quartz.nip43RelayMembers.joinRequest.RelayJoinRequestEvent import com.vitorpamplona.quartz.nip43RelayMembers.leaveRequest.RelayLeaveRequestEvent @@ -1307,6 +1309,10 @@ private fun RenderNoteRow( RenderRelayLeaveRequest(baseNote, accountViewModel, nav) } + is RelayAuthEvent -> { + RenderRelayAuth(baseNote, accountViewModel, nav) + } + is PinListEvent -> { RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav) } diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/RelayAuth.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/RelayAuth.kt new file mode 100644 index 0000000000..4771cdbf46 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/types/RelayAuth.kt @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.note.types + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.resources.Res +import com.vitorpamplona.amethyst.commons.resources.relay_auth_event_challenge +import com.vitorpamplona.amethyst.commons.resources.relay_auth_event_explainer +import com.vitorpamplona.amethyst.commons.resources.relay_auth_event_no_relay +import com.vitorpamplona.amethyst.commons.resources.relay_auth_event_title +import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.commons.ui.note.RenderRelay +import com.vitorpamplona.amethyst.commons.ui.note.RenderRelayIcon +import com.vitorpamplona.amethyst.commons.ui.stringRes +import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent + +/** + * NIP-42 kind 22242: a login to a relay. It is not a post — its content is empty and nobody but + * the relay ever sees it — so it renders as "log in to " instead of falling through to the + * text-note layout. Mostly met in signer consent prompts, where an app asks to sign one. + */ +@Composable +fun RenderRelayAuth( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = baseNote.event as? RelayAuthEvent ?: return + val relays = remember(noteEvent) { noteEvent.relays() } + val challenge = remember(noteEvent) { noteEvent.challenge() } + + RelayAuthCard(relays, challenge) { relay -> + RenderRelay(relay, accountViewModel, nav) + } +} + +@Composable +fun RelayAuthCard( + relays: List, + challenge: String?, + relayIcon: @Composable (NormalizedRelayUrl) -> Unit, +) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + symbol = MaterialSymbols.Key, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Column { + Text( + text = stringRes(Res.string.relay_auth_event_title), + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.bodyLarge, + ) + Text( + text = stringRes(Res.string.relay_auth_event_explainer), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + if (relays.isEmpty()) { + Text( + text = stringRes(Res.string.relay_auth_event_no_relay), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(start = 32.dp), + ) + } + + relays.forEach { relay -> + Row( + modifier = Modifier.padding(start = 32.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + relayIcon(relay) + Text( + text = relay.displayUrl(), + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + + challenge?.let { + Text( + text = stringRes(Res.string.relay_auth_event_challenge, it), + style = MaterialTheme.typography.labelSmall.copy(fontFamily = FontFamily.Monospace), + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(start = 32.dp), + ) + } + } +} + +@Preview +@Composable +private fun RelayAuthCardPreview() { + ThemeComparisonColumn { + RelayAuthCard( + relays = listOf(NormalizedRelayUrl("wss://relay.damus.io/")), + challenge = "4f2c9a1e-7b3d-4c8e-a6f0-2d9b1e3c5a7f", + ) { relay -> + RenderRelayIcon( + displayUrl = relay.url, + iconUrl = null, + loadProfilePicture = false, + loadRobohash = true, + pingInMs = 0, + ) + } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadFeedView.kt index ca61da19fc..63a74cbada 100644 --- a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -223,6 +223,7 @@ import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPublicMessage import com.vitorpamplona.amethyst.commons.ui.note.types.RenderPublicationSection import com.vitorpamplona.amethyst.commons.ui.note.types.RenderReaction import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayAddMember +import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayAuth import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayDiscovery import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayJoinRequest import com.vitorpamplona.amethyst.commons.ui.note.types.RenderRelayLeaveRequest @@ -341,6 +342,7 @@ import com.vitorpamplona.quartz.nip34Git.status.GitStatusEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent import com.vitorpamplona.quartz.nip43RelayMembers.joinRequest.RelayJoinRequestEvent import com.vitorpamplona.quartz.nip43RelayMembers.leaveRequest.RelayLeaveRequestEvent @@ -1119,6 +1121,8 @@ private fun FullBleedNoteCompose( RenderRelayJoinRequest(baseNote, accountViewModel, nav) } else if (noteEvent is RelayLeaveRequestEvent) { RenderRelayLeaveRequest(baseNote, accountViewModel, nav) + } else if (noteEvent is RelayAuthEvent) { + RenderRelayAuth(baseNote, accountViewModel, nav) } else if (noteEvent is TextNoteModificationEvent) { RenderTextModificationEvent( note = baseNote, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt index e030bafc20..8505a1caa0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt @@ -42,7 +42,9 @@ class RelayAuthEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { fun relay() = tags.firstNotNullOfOrNull(RelayTag::parse) - fun challenge() = tags.firstNotNullOfOrNull(ChallengeTag::parse) + fun relays() = tags.authRelays() + + fun challenge() = tags.authChallenge() companion object { const val KIND = 22242 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip42RelayAuth/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip42RelayAuth/TagArrayExt.kt new file mode 100644 index 0000000000..a60c94ef3f --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip42RelayAuth/TagArrayExt.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip42RelayAuth + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull +import com.vitorpamplona.quartz.nip42RelayAuth.tags.ChallengeTag +import com.vitorpamplona.quartz.nip42RelayAuth.tags.RelayTag + +/** Every relay a NIP-42 auth event (or its unsigned template) logs into. Usually just one. */ +fun TagArray.authRelays() = mapNotNull(RelayTag::parse) + +fun TagArray.authChallenge() = fastFirstNotNullOfOrNull(ChallengeTag::parse)