Brute forces the finding of events.

This commit is contained in:
Vitor Pamplona
2025-07-09 16:57:26 -04:00
parent 267d4c505a
commit accc8d4439
17 changed files with 345 additions and 145 deletions
@@ -95,6 +95,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.Entity
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
@@ -2928,7 +2929,7 @@ object LocalCache : ILocalCache {
}
getOrCreateUser(nip19.hex)
}
is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> {
is NNote -> {
getOrCreateNote(nip19.hex)
}
is NEvent -> {
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler
@@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
// This allows multiple screen to be listening to tags, even the same tag
class EventFinderQueryState(
val note: Note,
val account: Account,
)
class EventFinderFilterAssembler(
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -30,18 +31,19 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
fun EventFinderFilterAssemblerSubscription(
note: Note,
accountViewModel: AccountViewModel,
) = EventFinderFilterAssemblerSubscription(note, accountViewModel.dataSources().eventFinder)
) = EventFinderFilterAssemblerSubscription(note, accountViewModel.account, accountViewModel.dataSources().eventFinder)
@Composable
fun EventFinderFilterAssemblerSubscription(
note: Note,
account: Account,
dataSource: EventFinderFilterAssembler,
) {
// different screens get different states
// even if they are tracking the same tag.
val state =
remember(note) {
EventFinderQueryState(note)
remember(note, account) {
EventFinderQueryState(note, account)
}
KeyDataSourceSubscription(state, dataSource)
@@ -22,64 +22,113 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCache.relayHints
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.mapOfSet
fun filterMissingAddressables(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? {
val missingAddressables = mutableSetOf<Address>()
fun potentialRelaysToFindAddress(note: AddressableNote): Set<NormalizedRelayUrl> {
val set = mutableSetOf<NormalizedRelayUrl>()
keys.forEach {
if (it.note is AddressableNote && it.note.event == null) {
missingAddressables.add(it.note.address())
}
set.addAll(LocalCache.relayHints.hintsForAddress(note.idHex))
// loads threading that is event-based
it.note.replyTo?.forEach {
if (it is AddressableNote && it.event == null) {
missingAddressables.add(it.address())
}
val isInChannel = note.channelHex()
if (isInChannel != null) {
LocalCache.checkGetOrCreateChannel(isInChannel)?.relays()?.forEach {
set.add(it)
}
}
return filterMissingAddressables(missingAddressables)
note.replyTo?.map { parentNote ->
set.addAll(parentNote.relays)
parentNote
.channelHex()
?.let { LocalCache.checkGetOrCreateChannel(it) }
?.relays()
?.forEach { set.add(it) }
parentNote.author?.inboxRelays()?.let { set.addAll(it) }
}
note.replies.map { childNote ->
set.addAll(childNote.relays)
childNote
.channelHex()
?.let { LocalCache.checkGetOrCreateChannel(it) }
?.relays()
?.forEach { set.add(it) }
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
note.reactions.map { reactionType ->
reactionType.value.forEach { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
}
note.boosts.map { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
return set
}
fun filterMissingAddressables(missingAddressables: Set<Address>): List<RelayBasedFilter> {
fun filterMissingAddressables(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? {
val addressesPerRelay =
mapOfSet {
keys.forEach { key ->
val default = key.account.followPlusAllMine.flow.value
if (key.note is AddressableNote && key.note.event == null) {
potentialRelaysToFindAddress(key.note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, key.note.address)
}
}
// loads threading that is event-based
key.note.replyTo?.forEach { note ->
if (note is AddressableNote && note.event == null) {
potentialRelaysToFindAddress(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.address)
}
}
}
}
}
return filterMissingAddressables(addressesPerRelay)
}
fun filterMissingAddressables(missingAddressables: Map<NormalizedRelayUrl, Set<Address>>): List<RelayBasedFilter> {
if (missingAddressables.isEmpty()) return emptyList()
return missingAddressables.flatMap { aTag ->
val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(aTag.pubKeyHex)
val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent)
val authorHomeRelays =
authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null }
?: LocalCache.relayHints.hintsForKey(aTag.pubKeyHex).ifEmpty { null }
?: listOfNotNull(LocalCache.getUserIfExists(aTag.pubKeyHex)?.latestMetadataRelay)
val relayHints = LocalCache.relayHints.hintsForAddress(aTag.toValue())
(authorHomeRelays + relayHints).toSet().map {
if (aTag.kind < 25000 && aTag.dTag.isBlank()) {
return missingAddressables.flatMap { relayEntry ->
relayEntry.value.map { address ->
if (address.kind < 25000 && address.dTag.isBlank()) {
RelayBasedFilter(
relay = it,
relay = relayEntry.key,
filter =
Filter(
kinds = listOf(aTag.kind),
authors = listOf(aTag.pubKeyHex),
kinds = listOf(address.kind),
authors = listOf(address.pubKeyHex),
limit = 1,
),
)
} else {
RelayBasedFilter(
relay = it,
relay = relayEntry.key,
filter =
Filter(
kinds = listOf(aTag.kind),
tags = mapOf("d" to listOf(aTag.dTag)),
authors = listOf(aTag.pubKeyHex),
kinds = listOf(address.kind),
tags = mapOf("d" to listOf(address.dTag)),
authors = listOf(address.pubKeyHex),
limit = 1,
),
)
@@ -22,48 +22,101 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCache.relayHints
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.mapOfSet
fun filterMissingEvents(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? {
val missingEvents = mutableSetOf<String>()
fun potentialRelaysToFindEvent(note: Note): Set<NormalizedRelayUrl> {
val set = mutableSetOf<NormalizedRelayUrl>()
keys.forEach {
if (it.note !is AddressableNote && it.note.event == null) {
missingEvents.add(it.note.idHex)
}
set.addAll(LocalCache.relayHints.hintsForEvent(note.idHex))
// loads threading that is event-based
it.note.replyTo?.forEach {
if (it !is AddressableNote && it.event == null) {
missingEvents.add(it.idHex)
}
val isInChannel = note.channelHex()
if (isInChannel != null) {
LocalCache.checkGetOrCreateChannel(isInChannel)?.relays()?.forEach {
set.add(it)
}
}
return filterMissingEvents(missingEvents)
note.replyTo?.map { parentNote ->
set.addAll(parentNote.relays)
parentNote
.channelHex()
?.let { LocalCache.checkGetOrCreateChannel(it) }
?.relays()
?.forEach { set.add(it) }
parentNote.author?.inboxRelays()?.let { set.addAll(it) }
}
note.replies.map { childNote ->
set.addAll(childNote.relays)
childNote
.channelHex()
?.let { LocalCache.checkGetOrCreateChannel(it) }
?.relays()
?.forEach { set.add(it) }
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
note.reactions.map { reactionType ->
reactionType.value.forEach { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
}
note.boosts.map { childNote ->
set.addAll(childNote.relays)
childNote.author?.outboxRelays()?.let { set.addAll(it) }
}
return set
}
fun filterMissingEvents(missingEventIds: Set<HexKey>): List<RelayBasedFilter> {
if (missingEventIds.isEmpty()) return emptyList()
val relayHints =
fun filterMissingEvents(keys: List<EventFinderQueryState>): List<RelayBasedFilter>? {
val eventsPerRelay =
mapOfSet {
missingEventIds.forEach { eventId ->
LocalCache.relayHints.hintsForEvent(eventId).forEach { relayUrl ->
add(relayUrl, eventId)
keys.forEach { key ->
val default = key.account.followPlusAllMine.flow.value
if (key.note !is AddressableNote && key.note.event == null) {
potentialRelaysToFindEvent(key.note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, key.note.idHex)
}
}
// loads threading that is event-based
key.note.replyTo?.forEach { note ->
if (note !is AddressableNote && note.event == null) {
potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.idHex)
}
}
}
}
}
return relayHints.map {
RelayBasedFilter(
relay = it.key,
filter = Filter(ids = it.value.sorted()),
)
return filterMissingEvents(eventsPerRelay)
}
fun filterMissingEvents(missingEventIds: Map<NormalizedRelayUrl, Set<String>>): List<RelayBasedFilter> {
if (missingEventIds.isEmpty()) return emptyList()
return missingEventIds.mapNotNull {
if (it.value.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter = Filter(ids = it.value.sorted()),
)
} else {
null
}
}
}
@@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.forEach
@@ -36,35 +37,44 @@ val MetadataAndRelayListKinds =
AdvertisedRelayListEvent.KIND,
)
fun filterFindUserMetadataForKey(author: HexKey): List<RelayBasedFilter> =
fun filterFindUserMetadataForKey(
author: HexKey,
defaultRelays: Set<NormalizedRelayUrl>,
): List<RelayBasedFilter> =
LocalCache.checkGetOrCreateUser(author)?.let {
filterFindUserMetadataForKey(setOf(it))
filterFindUserMetadataForKey(setOf(it), defaultRelays)
} ?: emptyList()
fun filterFindUserMetadataForKey(authors: Set<User>): List<RelayBasedFilter> {
fun filterFindUserMetadataForKey(
authors: Set<User>,
defaultRelays: Set<NormalizedRelayUrl>,
): List<RelayBasedFilter> {
val perRelayKeys =
mapOfSet {
authors.forEach { key ->
val relays =
key.authorRelayList()?.writeRelaysNorm()
key.authorRelayList()?.writeRelaysNorm()?.ifEmpty { null }
?: LocalCache.relayHints.hintsForKey(key.pubkeyHex).ifEmpty { null }
?: key.relaysBeingUsed.keys
?: (key.relaysBeingUsed.keys + defaultRelays).toList()
relays.forEach {
add(it, key.pubkeyHex)
}
// TODO, DESPERATION: TRY ALL THE CONNECTED RELAYS.
}
}
return perRelayKeys.map {
RelayBasedFilter(
relay = it.key,
filter =
Filter(
kinds = MetadataAndRelayListKinds,
authors = it.value.sorted(),
),
)
return perRelayKeys.mapNotNull {
if (it.value.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter =
Filter(
kinds = MetadataAndRelayListKinds,
authors = it.value.sorted(),
),
)
} else {
null
}
}
}
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEo
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
class UserLoaderSubAssembler(
client: NostrClient,
@@ -42,7 +43,11 @@ class UserLoaderSubAssembler(
}
}
val defaultRelays = mutableSetOf<NormalizedRelayUrl>()
keys.mapTo(mutableSetOf()) { it.account }.forEach {
defaultRelays.addAll(it.followPlusAllMine.flow.value)
it.kind3FollowList.flow.value.authors.forEach {
val user = LocalCache.getOrCreateUser(it)
if (user.latestMetadata == null) {
@@ -55,7 +60,7 @@ class UserLoaderSubAssembler(
if (firstTimers.isEmpty()) return null
return filterFindUserMetadataForKey(firstTimers)
return filterFindUserMetadataForKey(firstTimers, defaultRelays)
}
override fun distinct(key: UserFinderQueryState) = key.user
@@ -20,17 +20,30 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.forEach
import kotlin.collections.ifEmpty
fun filterByAddress(address: NAddress) =
filterMissingAddressables(
setOf(
Address(
address.kind,
address.author,
address.dTag,
),
),
)
fun filterByAddress(
address: NAddress,
default: Set<NormalizedRelayUrl>,
): List<RelayBasedFilter> {
val note = LocalCache.getOrCreateAddressableNote(address.address())
val list =
mapOfSet {
if (note.event == null) {
potentialRelaysToFindAddress(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.address)
}
}
}
return filterMissingAddressables(list)
}
@@ -22,5 +22,9 @@ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassembli
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.filterFindUserMetadataForKey
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
fun filterByAuthor(pubKey: HexKey) = filterFindUserMetadataForKey(pubKey)
fun filterByAuthor(
pubKey: HexKey,
defaultRelays: Set<NormalizedRelayUrl>,
) = filterFindUserMetadataForKey(pubKey, defaultRelays)
@@ -20,7 +20,40 @@
*/
package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.forEach
import kotlin.collections.ifEmpty
fun filterByEvent(eventId: HexKey) = filterMissingEvents(setOf(eventId))
fun filterByEvent(
eventId: HexKey,
default: Set<NormalizedRelayUrl>,
): List<RelayBasedFilter> {
val note = LocalCache.checkGetOrCreateNote(eventId) ?: return emptyList()
val list =
mapOfSet {
if (note !is AddressableNote && note.event == null) {
potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.idHex)
}
}
// loads threading that is event-based
note.replyTo?.forEach { parentNote ->
if (parentNote !is AddressableNote && note.event == null) {
potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl ->
add(relayUrl, note.idHex)
}
}
}
}
return filterMissingEvents(list)
}
@@ -55,25 +55,27 @@ class SearchWatcherSubAssembler(
if (mySearchString.isBlank()) return null
val defaultRelays = key.account.followPlusAllMine.flow.value
val directFilters =
runCatching {
if (Hex.isHex(mySearchString)) {
val key = Hex.decode(mySearchString).toHexKey()
filterByAuthor(key) + filterByEvent(key)
val hexKey = Hex.decode(mySearchString).toHexKey()
filterByAuthor(hexKey, defaultRelays) + filterByEvent(hexKey, defaultRelays)
} else {
val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity
if (parsed != null) {
cache.consume(parsed)
when (parsed) {
is NSec -> filterByAuthor(parsed.toPubKeyHex())
is NPub -> filterByAuthor(parsed.hex)
is NProfile -> filterByAuthor(parsed.hex)
is NNote -> filterByEvent(parsed.hex)
is NEvent -> filterByEvent(parsed.hex)
is NSec -> filterByAuthor(parsed.toPubKeyHex(), defaultRelays)
is NPub -> filterByAuthor(parsed.hex, defaultRelays)
is NProfile -> filterByAuthor(parsed.hex, defaultRelays)
is NNote -> filterByEvent(parsed.hex, defaultRelays)
is NEvent -> filterByEvent(parsed.hex, defaultRelays)
is NEmbed -> emptyList()
is NRelay -> emptyList()
is NAddress -> filterByAddress(parsed)
is NAddress -> filterByAddress(parsed, defaultRelays)
else -> emptyList()
}
} else {
@@ -53,7 +53,7 @@ fun ThreadScreen(
)
WatchLifecycleAndUpdateModel(feedViewModel)
ThreadFilterAssemblerSubscription(noteId, accountViewModel.dataSources().thread)
ThreadFilterAssemblerSubscription(noteId, accountViewModel)
LoadNote(noteId, accountViewModel) {
if (it != null) {
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadEventLoaderSubAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadFilterSubAssembler
@@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
// This allows multiple screen to be listening to tags, even the same tag
class ThreadQueryState(
val eventId: HexKey,
val account: Account,
)
class ThreadFilterAssembler(
@@ -22,19 +22,32 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@Composable
fun ThreadFilterAssemblerSubscription(
eventId: HexKey,
accountViewModel: AccountViewModel,
) = ThreadFilterAssemblerSubscription(
eventId,
accountViewModel.account,
accountViewModel.dataSources().thread,
)
@Composable
fun ThreadFilterAssemblerSubscription(
eventId: HexKey,
account: Account,
filterAssembler: ThreadFilterAssembler,
) {
// different screens get different states
// even if they are tracking the same tag.
val state =
remember(eventId) {
ThreadQueryState(eventId)
ThreadQueryState(eventId, account)
}
KeyDataSourceSubscription(state, filterAssembler)
@@ -24,34 +24,54 @@ import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.ThreadAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.forEach
import kotlin.collections.ifEmpty
fun filterMissingEventsForThread(threadInfo: ThreadAssembler.ThreadInfo): List<RelayBasedFilter> {
val missingEvents = mutableSetOf<HexKey>()
val missingAddresses = mutableSetOf<Address>()
fun filterMissingEventsForThread(
threadInfo: ThreadAssembler.ThreadInfo,
defaultRelays: Set<NormalizedRelayUrl>,
): List<RelayBasedFilter> {
val missingEvents =
mapOfSet {
if (threadInfo.root.event == null && threadInfo.root !is AddressableNote) {
potentialRelaysToFindEvent(threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl ->
add(relayUrl, threadInfo.root.idHex)
}
}
if (threadInfo.root.event == null) {
if (threadInfo.root is AddressableNote) {
missingAddresses.add(threadInfo.root.address)
} else {
missingEvents.add(threadInfo.root.idHex)
}
}
threadInfo.allNotes.forEach {
if (it.event == null) {
if (it is AddressableNote) {
missingAddresses.add(it.address)
} else {
missingEvents.add(it.idHex)
threadInfo.allNotes.forEach {
if (it !is AddressableNote && it.event == null) {
potentialRelaysToFindEvent(it).ifEmpty { defaultRelays }.forEach { relayUrl ->
add(relayUrl, it.idHex)
}
}
}
}
}
val missingEventsFilter = filterMissingEvents(missingEvents) ?: emptyList()
val missingAddressFilter = filterMissingAddressables(missingAddresses) ?: emptyList()
val missingAddresses =
mapOfSet {
if (threadInfo.root.event == null && threadInfo.root is AddressableNote) {
potentialRelaysToFindEvent(threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl ->
add(relayUrl, threadInfo.root.address)
}
}
threadInfo.allNotes.forEach {
if (it is AddressableNote && it.event == null) {
potentialRelaysToFindAddress(it).ifEmpty { defaultRelays }.forEach { relayUrl ->
add(relayUrl, it.address)
}
}
}
}
val missingEventsFilter = filterMissingEvents(missingEvents)
val missingAddressFilter = filterMissingAddressables(missingAddresses)
return missingEventsFilter + missingAddressFilter
}
@@ -45,7 +45,8 @@ class ThreadEventLoaderSubAssembler(
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
val branches = ThreadAssembler().findThreadFor(key.eventId) ?: return null
return filterMissingEventsForThread(branches)
val defaultRelays = key.account.followPlusAllMine.flow.value
return filterMissingEventsForThread(branches, defaultRelays)
}
override fun id(key: ThreadQueryState) = key.eventId
@@ -21,7 +21,6 @@
package com.vitorpamplona.quartz.nip01Core.relay.filters
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
class Filter(
val ids: List<String>? = null,
@@ -48,22 +47,13 @@ class Filter(
search: String? = this.search,
) = Filter(ids, authors, kinds, tags, since, until, limit, search)
init {
// TODO: Remove this in production
ids?.forEach {
if (it.length != 64) throw IllegalArgumentException("Invalid id length $it on ${toJson()}")
}
authors?.forEach {
if (it.length != 64) throw IllegalArgumentException("Invalid author length $it on ${toJson()}")
}
tags?.get("#p")?.forEach {
if (it.length != 64) throw IllegalArgumentException("Invalid p-tag length $it on ${toJson()}")
}
tags?.get("#e")?.forEach {
if (it.length != 64) throw IllegalArgumentException("Invalid e-tag length $it on ${toJson()}")
}
tags?.get("#a")?.forEach {
if (Address.parse(it) != null) throw IllegalArgumentException("Invalid a-tag length $it on ${toJson()}")
}
}
fun isFilledFilter() =
(ids != null && ids.isNotEmpty()) ||
(authors != null && authors.isNotEmpty()) ||
(kinds != null && kinds.isNotEmpty()) ||
(tags != null && tags.isNotEmpty()) ||
(since != null) ||
(until != null) ||
(limit != null) ||
(search != null && search.isNotEmpty())
}