Separating Reports from Metadata watching

This commit is contained in:
Vitor Pamplona
2025-07-10 15:51:57 -04:00
parent 94a7d9cf14
commit a207d80d55
4 changed files with 146 additions and 22 deletions
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserLoaderSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserReportsSubAssembler
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserWatcherSubAssembler
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
@@ -40,6 +41,7 @@ class UserFinderFilterAssembler(
listOf(
UserLoaderSubAssembler(client, ::allKeys),
UserWatcherSubAssembler(client, ::allKeys),
UserReportsSubAssembler(client, ::allKeys),
)
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
@@ -35,17 +35,21 @@ fun filterReportsToKeysFromTrusted(
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
if (targets.isEmpty() || trustedAccounts.isEmpty()) return emptyList()
val sortedTargets = targets.sorted()
return trustedAccounts.map {
RelayBasedFilter(
relay = it.key,
filter =
Filter(
kinds = ReportKindList,
authors = it.value.sorted(),
tags = mapOf("p" to sortedTargets),
since = since?.get(it.key)?.time,
),
)
val sortedTargets = mapOf("p" to targets.sorted())
return trustedAccounts.mapNotNull {
if (it.value.isNotEmpty()) {
RelayBasedFilter(
relay = it.key,
filter =
Filter(
kinds = ReportKindList,
authors = it.value.sorted(),
tags = sortedTargets,
since = since?.get(it.key)?.time,
),
)
} else {
null
}
}
}
@@ -0,0 +1,127 @@
/**
* 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.service.relayClient.reqCommand.user.watchers
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.ammolite.relays.filters.MutableTime
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
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.flatten
class UserReportsSubAssembler(
client: NostrClient,
allKeys: () -> Set<UserFinderQueryState>,
) : SingleSubEoseManager<UserFinderQueryState>(client, allKeys) {
var lastUsersOnFilter: Set<User> = emptySet()
/**
* This assembler saves the EOSE per user key. That EOSE includes their metadata, etc
* and reports, but only from trusted accounts (follows of all logged in users).
*/
var latestEOSEs: EOSEAccountFast<User> = EOSEAccountFast<User>(2000)
override fun newEose(
relay: NormalizedRelayUrl,
time: Long,
) {
lastUsersOnFilter.forEach {
latestEOSEs.newEose(it, relay, time)
}
super.newEose(relay, time)
}
override fun updateFilter(
keys: List<UserFinderQueryState>,
since: SincePerRelayMap?,
): List<RelayBasedFilter>? {
if (keys.isEmpty()) return null
lastUsersOnFilter = keys.mapTo(mutableSetOf()) { it.user }
if (lastUsersOnFilter.isEmpty()) return null
val accounts = keys.mapTo(mutableSetOf()) { it.account }
val trustedAccounts =
mapOfSet {
accounts.map { it.followsPerRelay.value }.forEach {
add(it)
}
}
return groupByRelayPresence(lastUsersOnFilter, latestEOSEs, trustedAccounts.keys)
.map { group ->
val groupIds = group.map { it.pubkeyHex }.toSet()
if (groupIds.isNotEmpty()) {
val minEOSEs = findMinimumEOSEsForUsers(group, latestEOSEs)
filterReportsToKeysFromTrusted(groupIds, trustedAccounts, minEOSEs)
} else {
emptyList()
}
}.flatten()
}
fun groupByRelayPresence(
users: Iterable<User>,
eoseCache: EOSEAccountFast<User>,
inRelays: Set<NormalizedRelayUrl>,
): Collection<List<User>> =
users
.groupBy {
eoseCache
.since(it)
?.keys
?.intersect(inRelays)
?.hashCode()
}.values
.map {
// important to keep in order otherwise the Relay thinks the filter has changed and we REQ again
it.sortedBy { it.pubkeyHex }
}
fun findMinimumEOSEsForUsers(
users: List<User>,
eoseCache: EOSEAccountFast<User>,
): SincePerRelayMap {
val minLatestEOSEs = mutableMapOf<NormalizedRelayUrl, MutableTime>()
users.forEach {
eoseCache.since(it)?.forEach {
val minEose = minLatestEOSEs[it.key]
if (minEose == null) {
minLatestEOSEs.put(it.key, it.value.copy())
} else {
minEose.updateIfOlder(it.value.time)
}
}
}
return minLatestEOSEs
}
override fun distinct(key: UserFinderQueryState) = key.user
}
@@ -29,7 +29,6 @@ import com.vitorpamplona.ammolite.relays.filters.MutableTime
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
import com.vitorpamplona.quartz.utils.mapOfSet
import kotlin.collections.flatten
class UserWatcherSubAssembler(
@@ -67,20 +66,12 @@ class UserWatcherSubAssembler(
if (lastUsersOnFilter.isEmpty()) return null
val trustedAccounts =
mapOfSet {
keys.mapTo(mutableSetOf()) { it.account }.map { it.followsPerRelay.value }.forEach {
add(it)
}
}
return groupByRelayPresence(lastUsersOnFilter, latestEOSEs)
.map { group ->
val groupIds = group.map { it.pubkeyHex }.toSet()
if (groupIds.isNotEmpty()) {
val minEOSEs = findMinimumEOSEsForUsers(group, latestEOSEs)
filterUserMetadataForKey(groupIds, minEOSEs) +
filterReportsToKeysFromTrusted(groupIds, trustedAccounts, minEOSEs)
filterUserMetadataForKey(groupIds, minEOSEs)
} else {
emptyList()
}