From 1084d2a063d59d68e5acc09b199553e65e56f905 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 13:19:46 -0400 Subject: [PATCH] Adds an indexer relay list --- .../vitorpamplona/amethyst/model/Account.kt | 11 +- .../amethyst/model/AccountSettings.kt | 2 + .../vitorpamplona/amethyst/model/Constants.kt | 4 + .../amethyst/model/LocalCache.kt | 8 ++ .../IndexerRelayListDecryptionCache.kt | 29 +++++ .../indexerRelays/IndexerRelayListState.kt | 88 +++++++++++++ .../MergedFollowPlusMineRelayListsState.kt | 5 + .../FilterAccountInfoAndListsFromKey.kt | 2 + .../FilterBasicAccountInfoFromKeys.kt | 2 + .../amethyst/ui/note/NoteCompose.kt | 3 + .../amethyst/ui/note/types/RelayList.kt | 21 +++ .../loggedIn/relays/AllRelayListScreen.kt | 39 ++++++ ...yListView.kt => BroadcastRelayListView.kt} | 0 .../relays/indexer/IndexerRelayListView.kt | 81 ++++++++++++ .../indexer/IndexerRelayListViewModel.kt | 34 +++++ .../loggedIn/threadview/ThreadFeedView.kt | 4 + amethyst/src/main/res/values/strings.xml | 6 +- .../com/vitorpamplona/quartz/EventFactory.kt | 2 + .../nip01Core/signers/NostrSignerSync.kt | 2 + .../relayLists/BroadcastRelayListEvent.kt | 5 +- .../relayLists/IndexerRelayListEvent.kt | 121 ++++++++++++++++++ .../relayLists/tags/TagArrayBuilderExt.kt | 3 + .../client/NostrSignerExternal.kt | 4 + 23 files changed, 471 insertions(+), 5 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/{TrustedRelayListView.kt => BroadcastRelayListView.kt} (100%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ad3c409d2..bbe916347 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecry import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache @@ -247,6 +249,9 @@ class Account( val broadcastRelayListDecryptionCache = BroadcastRelayListDecryptionCache(signer) val broadcastRelayList = BroadcastRelayListState(signer, cache, broadcastRelayListDecryptionCache, scope, settings) + val indexerRelayListDecryptionCache = IndexerRelayListDecryptionCache(signer) + val indexerRelayList = IndexerRelayListState(signer, cache, indexerRelayListDecryptionCache, scope, settings) + val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer) val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) @@ -293,7 +298,7 @@ class Account( // Follows Relays val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope) - val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, broadcastRelayList, scope) + val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, broadcastRelayList, indexerRelayList, scope) // keeps a cache of the outbox relays for each author val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow @@ -871,7 +876,7 @@ class Account( } fun sendLiterallyEverywhere(event: Event) { - client.send(event, outboxRelays.flow.value + client.relayStatusFlow().value.available) + client.send(event, outboxRelays.flow.value + indexerRelayList.flow.value + client.relayStatusFlow().value.available) cache.justConsumeMyOwnEvent(event) } @@ -1719,6 +1724,8 @@ class Account( suspend fun saveSearchRelayList(searchRelays: List) = sendMyPublicAndPrivateOutbox(searchRelayList.saveRelayList(searchRelays)) + suspend fun saveIndexerRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(indexerRelayList.saveRelayList(trustedRelays)) + suspend fun saveBroadcastRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(broadcastRelayList.saveRelayList(trustedRelays)) suspend fun saveTrustedRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 0c0b467fa..120281127 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -84,6 +84,8 @@ val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos) val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday) +val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Constants.userkinds) + val DefaultSignerPermissions = listOf( Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt index ec6a49a48..d34c7d42c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt @@ -43,6 +43,10 @@ object Constants { val auth = RelayUrlNormalizer.normalize("wss://auth.nostr1.com") val oxchat = RelayUrlNormalizer.normalize("wss://relay.0xchat.com") + val purplepages = RelayUrlNormalizer.normalize("wss://purplepag.es") + val coracle = RelayUrlNormalizer.normalize("wss://indexer.coracle.social") + val userkinds = RelayUrlNormalizer.normalize("wss://user.kindpag.es") + val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr, fmtwiz, bg) val defaultSearchRelaySet = setOf(band, wine, where) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 5ce63e658..3c68f850e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -139,6 +139,7 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -958,6 +959,12 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( + event: IndexerRelayListEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( event: BroadcastRelayListEvent, relay: NormalizedRelayUrl?, @@ -2760,6 +2767,7 @@ object LocalCache : ILocalCache { is GitRepositoryEvent -> consume(event, relay, wasVerified) is HashtagListEvent -> consume(event, relay, wasVerified) is HighlightEvent -> consume(event, relay, wasVerified) + is IndexerRelayListEvent -> consume(event, relay, wasVerified) is InteractiveStoryPrologueEvent -> consume(event, relay, wasVerified) is InteractiveStorySceneEvent -> consume(event, relay, wasVerified) is InteractiveStoryReadingStateEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt new file mode 100644 index 000000000..13d1ce9d4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt @@ -0,0 +1,29 @@ +/** + * 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.model.nip51Lists.indexerRelays + +import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent + +class IndexerRelayListDecryptionCache( + signer: NostrSigner, +) : GenericRelayListCache(signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt new file mode 100644 index 000000000..5fa08599a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt @@ -0,0 +1,88 @@ +/** + * 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.model.nip51Lists.indexerRelays + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class IndexerRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val decryptionCache: IndexerRelayListDecryptionCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getIndexerRelayListAddress() = IndexerRelayListEvent.createAddress(signer.pubKey) + + fun getIndexerRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getIndexerRelayListAddress()) + + fun getIndexerRelayListFlow(): StateFlow = getIndexerRelayListNote().flow().metadata.stateFlow + + fun getIndexerRelayList(): IndexerRelayListEvent? = getIndexerRelayListNote().event as? IndexerRelayListEvent + + suspend fun normalizeIndexerRelayListWithBackup(note: Note): Set { + val event = note.event as? IndexerRelayListEvent + return event?.let { decryptionCache.relays(it) } ?: emptySet() + } + + val flow = + getIndexerRelayListFlow() + .map { normalizeIndexerRelayListWithBackup(it.note) } + .onStart { emit(normalizeIndexerRelayListWithBackup(getIndexerRelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + suspend fun saveRelayList(indexerRelays: List): IndexerRelayListEvent { + val relayListForIndexer = getIndexerRelayList() + + return if (relayListForIndexer != null && relayListForIndexer.tags.isNotEmpty()) { + IndexerRelayListEvent.updateRelayList( + earlierVersion = relayListForIndexer, + relays = indexerRelays, + signer = signer, + ) + } else { + IndexerRelayListEvent.create( + relays = indexerRelays, + signer = signer, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index 1f2a6fa7c..da18f2185 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -43,6 +44,7 @@ class MergedFollowPlusMineRelayListsState( val localRelayList: LocalRelayListState, val trustedRelayList: TrustedRelayListState, val broadcastRelayList: BroadcastRelayListState, + val indexerRelayList: IndexerRelayListState, val scope: CoroutineScope, ) { fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } @@ -57,6 +59,7 @@ class MergedFollowPlusMineRelayListsState( localRelayList.flow, trustedRelayList.flow, broadcastRelayList.flow, + indexerRelayList.flow, ), ::mergeLists, ).onStart { @@ -70,6 +73,7 @@ class MergedFollowPlusMineRelayListsState( localRelayList.flow.value, trustedRelayList.flow.value, broadcastRelayList.flow.value, + indexerRelayList.flow.value, ), ), ) @@ -86,6 +90,7 @@ class MergedFollowPlusMineRelayListsState( localRelayList.flow.value, trustedRelayList.flow.value, broadcastRelayList.flow.value, + indexerRelayList.flow.value, ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 51a97c89c..099e3d677 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent @@ -57,6 +58,7 @@ val AccountInfoAndListsFromKeyKinds2 = BlockedRelayListEvent.KIND, TrustedRelayListEvent.KIND, BroadcastRelayListEvent.KIND, + IndexerRelayListEvent.KIND, ) val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt index d988c7b12..2861de76f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent @@ -51,6 +52,7 @@ val BasicAccountInfoKinds2 = BlockedRelayListEvent.KIND, TrustedRelayListEvent.KIND, BroadcastRelayListEvent.KIND, + IndexerRelayListEvent.KIND, ) fun filterBasicAccountInfoFromKeys( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 675860072..0f9de6c69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList +import com.vitorpamplona.amethyst.ui.note.types.DisplayIndexerRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet @@ -189,6 +190,7 @@ import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -656,6 +658,7 @@ private fun RenderNoteRow( is SearchRelayListEvent -> DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav) is BlockedRelayListEvent -> DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) is TrustedRelayListEvent -> DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) + is IndexerRelayListEvent -> DisplayIndexerRelayList(baseNote, backgroundColor, accountViewModel, nav) is BroadcastRelayListEvent -> DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav) is PinListEvent -> RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav) is EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index 2cb155e28..afe8555f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -234,6 +234,27 @@ fun DisplayTrustedRelayList( ) } +@Composable +fun DisplayIndexerRelayList( + baseNote: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val relays by accountViewModel.account.indexerRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle( + accountViewModel.account.indexerRelayListDecryptionCache.fastStartValueForRelayList(baseNote), + ) + + DisplayRelaySet( + relays, + stringRes(id = R.string.indexer_relays_title), + null, + backgroundColor, + accountViewModel, + nav, + ) +} + @Composable fun DisplayBroadcastRelayList( baseNote: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 35a189701..f668c96e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -41,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.DefaultDMRelayList +import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar @@ -54,6 +55,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedR import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.renderDMItems +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer.IndexerRelayListViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer.renderIndexerItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.LocalRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.renderLocalItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37.PrivateOutboxRelayListViewModel @@ -86,6 +89,7 @@ fun AllRelayListScreen( val localViewModel: LocalRelayListViewModel = viewModel() val connectedViewModel: ConnectedRelayListViewModel = viewModel() val broadcastViewModel: BroadcastRelayListViewModel = viewModel() + val indexerViewModel: IndexerRelayListViewModel = viewModel() dmViewModel.init(accountViewModel) nip65ViewModel.init(accountViewModel) @@ -96,6 +100,7 @@ fun AllRelayListScreen( blockedViewModel.init(accountViewModel) trustedViewModel.init(accountViewModel) broadcastViewModel.init(accountViewModel) + indexerViewModel.init(accountViewModel) LaunchedEffect(accountViewModel) { dmViewModel.load() @@ -107,6 +112,7 @@ fun AllRelayListScreen( blockedViewModel.load() trustedViewModel.load() broadcastViewModel.load() + indexerViewModel.load() } MappedAllRelayListView( @@ -119,6 +125,7 @@ fun AllRelayListScreen( blockedViewModel, trustedViewModel, broadcastViewModel, + indexerViewModel, accountViewModel, nav, ) @@ -136,6 +143,7 @@ fun MappedAllRelayListView( blockedViewModel: BlockedRelayListViewModel, trustedViewModel: TrustedRelayListViewModel, broadcastViewModel: BroadcastRelayListViewModel, + indexerViewModel: IndexerRelayListViewModel, accountViewModel: AccountViewModel, newNav: INav, ) { @@ -149,6 +157,7 @@ fun MappedAllRelayListView( val localFeedState by localViewModel.relays.collectAsStateWithLifecycle() val connectedRelays by connectedViewModel.relays.collectAsStateWithLifecycle() val broadcastRelays by broadcastViewModel.relays.collectAsStateWithLifecycle() + val indexerRelays by indexerViewModel.relays.collectAsStateWithLifecycle() Scaffold( topBar = { @@ -163,6 +172,7 @@ fun MappedAllRelayListView( trustedViewModel.clear() blockedViewModel.clear() broadcastViewModel.clear() + indexerViewModel.clear() newNav.popBack() }, onPost = { @@ -174,6 +184,7 @@ fun MappedAllRelayListView( trustedViewModel.create() blockedViewModel.create() broadcastViewModel.create() + indexerViewModel.create() newNav.popBack() }, ) @@ -240,6 +251,17 @@ fun MappedAllRelayListView( } renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, newNav) + item { + SettingsCategoryWithButton( + stringRes(R.string.indexer_section), + stringRes(R.string.indexer_section_explainer), + SettingsCategorySpacingModifier, + ) { + ResetIndexerRelays(indexerViewModel) + } + } + renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, newNav) + item { SettingsCategoryWithButton( stringRes(R.string.search_section), @@ -307,6 +329,23 @@ fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) { } } +@Composable +fun ResetIndexerRelays(postViewModel: IndexerRelayListViewModel) { + OutlinedButton( + onClick = { + postViewModel.deleteAll() + DefaultIndexerRelayList.forEach { + postViewModel.addRelay( + relaySetupInfoBuilder(it), + ) + } + postViewModel.loadRelayDocuments() + }, + ) { + Text(stringRes(R.string.default_relays)) + } +} + @Composable fun ResetDMRelays(postViewModel: DMRelayListViewModel) { OutlinedButton( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt similarity index 100% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt new file mode 100644 index 000000000..59c267127 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt @@ -0,0 +1,81 @@ +/** + * 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.ui.screen.loggedIn.relays.indexer + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer + +@Composable +fun IndexerRelayList( + postViewModel: IndexerRelayListViewModel, + accountViewModel: AccountViewModel, + onClose: () -> Unit, + nav: INav, +) { + val feedState by postViewModel.relays.collectAsStateWithLifecycle() + + val newNav = rememberExtendedNav(nav, onClose) + + Row(verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + renderIndexerItems(feedState, postViewModel, accountViewModel, newNav) + } + } +} + +fun LazyListScope.renderIndexerItems( + feedState: List, + postViewModel: IndexerRelayListViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + itemsIndexed(feedState, key = { _, item -> "Indexer" + item.relay }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + accountViewModel = accountViewModel, + nav, + ) + } + + item { + Spacer(modifier = StdVertSpacer) + RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt new file mode 100644 index 000000000..8bbb81fa2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt @@ -0,0 +1,34 @@ +/** + * 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.ui.screen.loggedIn.relays.indexer + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class IndexerRelayListViewModel : BasicRelaySetupInfoModel() { + override fun getRelayList(): List? = + account.indexerRelayList.flow.value + .toList() + + override suspend fun saveRelayList(urlList: List) { + account.saveIndexerRelayList(urlList) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index e22eb57ee..20dc7aebc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -122,6 +122,7 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList +import com.vitorpamplona.amethyst.ui.note.types.DisplayIndexerRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet @@ -210,6 +211,7 @@ import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -605,6 +607,8 @@ private fun FullBleedNoteCompose( DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is TrustedRelayListEvent) { DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is IndexerRelayListEvent) { + DisplayIndexerRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is BroadcastRelayListEvent) { DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is FhirResourceEvent) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 89d142a5f..82f1b3024 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1094,7 +1094,11 @@ Broadcast Relays Broadcast Relays - Relays that specialize in pushing your notes to all of the other relays. Amethyst will add this relay to all new events made by you + Relays that specialize in pushing your notes to all of the other relays, like sendit.nosflare.com. Amethyst will add this relay to all new events made by you + + Indexer Relays + Indexer Relays + Relays that specialize in hosting everyone\'s metadata and relay lists, like purplepag.es. Amethyst will use these relays to find users that are not in your lists. Blocked Relays Blocked Relays diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 6715c0368..8b73bfbc4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -83,6 +83,7 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -233,6 +234,7 @@ class EventFactory { HashtagListEvent.KIND -> HashtagListEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) + IndexerRelayListEvent.KIND -> IndexerRelayListEvent(id, pubKey, createdAt, tags, content, sig) InteractiveStoryPrologueEvent.KIND -> InteractiveStoryPrologueEvent(id, pubKey, createdAt, tags, content, sig) InteractiveStorySceneEvent.KIND -> InteractiveStorySceneEvent(id, pubKey, createdAt, tags, content, sig) InteractiveStoryReadingStateEvent.KIND -> InteractiveStoryReadingStateEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt index 25c9410c3..5969ca622 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt @@ -79,6 +79,7 @@ class NostrSignerSync( toPublicKey: HexKey, ): String { if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() + if (plaintext.isBlank()) return "" return Nip04.encrypt( plaintext, @@ -102,6 +103,7 @@ class NostrSignerSync( toPublicKey: HexKey, ): String { if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() + if (plaintext.isBlank()) return "" return Nip44 .encrypt( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt index f09930df0..05292c926 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt @@ -66,7 +66,7 @@ class BroadcastRelayListEvent( fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null) - fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "") + fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, "") suspend fun updateRelayList( earlierVersion: BroadcastRelayListEvent, @@ -75,8 +75,9 @@ class BroadcastRelayListEvent( createdAt: Long = TimeUtils.now(), ): BroadcastRelayListEvent { val newRelayList = relays.map { RelayTag.assemble(it) } + println("AABBCC 1 ${earlierVersion.content}") val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() - + println("AABBCC 2 ${privateTags.size}") val publicTags = earlierVersion.tags.remove(RelayTag::match) val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt new file mode 100644 index 000000000..ec057353d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt @@ -0,0 +1,121 @@ +/** + * 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.nip51Lists.relayLists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.indexerRelays +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.plus + +@Immutable +class IndexerRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRelays() = tags.relays() + + suspend fun decryptPrivateRelays(signer: NostrSigner) = privateTags(signer)?.relays() + + suspend fun decryptRelays(signer: NostrSigner): List = publicRelays() + (decryptPrivateRelays(signer) ?: emptyList()) + + companion object { + const val KIND = 10087 + const val ALT = "Broadcasting relays from this author" + val TAGS = arrayOf(AltTag.assemble(ALT)) + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") + + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null) + + fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "") + + suspend fun updateRelayList( + earlierVersion: IndexerRelayListEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): IndexerRelayListEvent { + val newRelayList = relays.map { RelayTag.assemble(it) } + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + val publicTags = earlierVersion.tags.remove(RelayTag::match) + val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) + + return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags) + } + + suspend fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): IndexerRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): IndexerRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun build( + publicRelays: List = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + indexerRelays(publicRelays) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt index c6cb3e2e4..f2d82f1cd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.tags.NameTag @@ -42,6 +43,8 @@ fun TagArrayBuilder.trustedRelays(relays: List.broadcastRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) +fun TagArrayBuilder.indexerRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) + fun TagArrayBuilder.privateRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) fun TagArrayBuilder.relaySet(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt index f6d23f26d..40cf1749c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt @@ -96,6 +96,8 @@ class NostrSignerExternal( plaintext: String, toPublicKey: HexKey, ): String { + if (plaintext.isBlank()) return "" + val result = backgroundQuery.nip04Encrypt(plaintext, toPublicKey) ?: foregroundQuery.nip04Encrypt(plaintext, toPublicKey) if (result is SignerResult.RequestAddressed.Successful) { @@ -124,6 +126,8 @@ class NostrSignerExternal( plaintext: String, toPublicKey: HexKey, ): String { + if (plaintext.isBlank()) return "" + val result = backgroundQuery.nip44Encrypt(plaintext, toPublicKey) ?: foregroundQuery.nip44Encrypt(plaintext, toPublicKey) if (result is SignerResult.RequestAddressed.Successful) {