- Separates a relay failure tracker to a module
- Updates finding the outbox relay set to include the set of connected relays before all of the relays.
This commit is contained in:
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.tor.TorManager
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.stats.RelayReqStats
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||
@@ -192,14 +193,26 @@ class AppModules(
|
||||
// Tries to verify new OTS events when they arrive.
|
||||
val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationDefaultScope)
|
||||
|
||||
// Tracks if it is possible to connect to relays.
|
||||
val failureTracker = RelayOfflineTracker(client)
|
||||
|
||||
// Captures statistics about relays
|
||||
val relayStats = RelayStats(client)
|
||||
|
||||
// Logs debug messages when needed
|
||||
val detailedLogger = if (isDebug) RelayLogger(client, true, false) else null
|
||||
val relayReqStats = if (isDebug) RelayReqStats(client) else null
|
||||
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||
|
||||
// Coordinates all subscriptions for the Nostr Client
|
||||
val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, authCoordinator.receiver, applicationDefaultScope)
|
||||
val sources: RelaySubscriptionsCoordinator =
|
||||
RelaySubscriptionsCoordinator(
|
||||
LocalCache,
|
||||
client,
|
||||
authCoordinator.receiver,
|
||||
failureTracker,
|
||||
applicationDefaultScope,
|
||||
)
|
||||
|
||||
// keeps all accounts live
|
||||
val accountsCache =
|
||||
|
||||
+4
-2
@@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProf
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -47,10 +48,11 @@ class RelaySubscriptionsCoordinator(
|
||||
cache: LocalCache,
|
||||
client: INostrClient,
|
||||
authenticator: IAuthStatus,
|
||||
failureTracker: RelayOfflineTracker,
|
||||
scope: CoroutineScope,
|
||||
) {
|
||||
// main one: notifications, dms and account settings
|
||||
val account = AccountFilterAssembler(client, cache, authenticator, scope)
|
||||
val account = AccountFilterAssembler(client, cache, authenticator, failureTracker, scope)
|
||||
|
||||
// always running, feed assemblers.
|
||||
val home = HomeFilterAssembler(client)
|
||||
@@ -62,7 +64,7 @@ class RelaySubscriptionsCoordinator(
|
||||
// they are active when looking at events, users, channels.
|
||||
val channelFinder = ChannelFinderFilterAssemblyGroup(client)
|
||||
val eventFinder = EventFinderFilterAssembler(client)
|
||||
val userFinder = UserFinderFilterAssembler(client, cache)
|
||||
val userFinder = UserFinderFilterAssembler(client, cache, failureTracker)
|
||||
|
||||
// active when searching or tagging users.
|
||||
val search = SearchFilterAssembler(client, scope, cache)
|
||||
|
||||
+3
-1
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59Gi
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
@@ -49,12 +50,13 @@ class AccountFilterAssembler(
|
||||
client: INostrClient,
|
||||
cache: LocalCache,
|
||||
authenticator: IAuthStatus,
|
||||
failureTracker: RelayOfflineTracker,
|
||||
scope: CoroutineScope,
|
||||
) : ComposeSubscriptionManager<AccountQueryState>() {
|
||||
val group =
|
||||
listOf(
|
||||
AccountMetadataEoseManager(client, ::allKeys),
|
||||
AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, ::allKeys),
|
||||
AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, failureTracker, ::allKeys),
|
||||
AccountGiftWrapsEoseManager(client, ::allKeys),
|
||||
AccountDraftsEoseManager(client, ::allKeys),
|
||||
AccountNotificationsEoseFromInboxRelaysManager(client, ::allKeys),
|
||||
|
||||
+11
-42
@@ -22,8 +22,6 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follow
|
||||
|
||||
import com.vitorpamplona.amethyst.isDebug
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
|
||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.IEoseManager
|
||||
@@ -32,6 +30,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
|
||||
import com.vitorpamplona.ammolite.relays.BundledUpdate
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.IAuthStatus
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
|
||||
@@ -51,7 +50,6 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.collections.ifEmpty
|
||||
|
||||
/**
|
||||
* This downloads the outbox events for all Follows of all users.
|
||||
@@ -62,13 +60,12 @@ class AccountFollowsLoaderSubAssembler(
|
||||
val cache: LocalCache,
|
||||
val scope: CoroutineScope,
|
||||
val authStatus: IAuthStatus,
|
||||
val failureTracker: RelayOfflineTracker,
|
||||
val allKeys: () -> Set<AccountQueryState>,
|
||||
) : IEoseManager {
|
||||
private val logTag = "AccountFollowsLoaderSubAssembler"
|
||||
private val orchestrator = SubscriptionController(client)
|
||||
|
||||
private val cannotConnectRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
|
||||
// Refreshes observers in batches of 500ms
|
||||
private val bundler = BundledUpdate(500, Dispatchers.Default)
|
||||
|
||||
@@ -141,56 +138,28 @@ class AccountFollowsLoaderSubAssembler(
|
||||
newEose(TimeUtils.now(), relay, forFilters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCannotConnect(
|
||||
relay: NormalizedRelayUrl,
|
||||
message: String,
|
||||
forFilters: List<Filter>?,
|
||||
) {
|
||||
cannotConnectRelays.add(relay)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun updateFilterForAllAccounts(accounts: Collection<Account>): List<RelayBasedFilter>? {
|
||||
val noOutboxList = mutableSetOf<User>()
|
||||
|
||||
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
val homeRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
val searchRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
val commonRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
|
||||
val users = mutableSetOf<User>()
|
||||
accounts.forEach { key ->
|
||||
key.kind3FollowList.userList.value.forEach { user ->
|
||||
if (user.authorRelayList() == null) {
|
||||
noOutboxList.add(user)
|
||||
users.add(user)
|
||||
}
|
||||
}
|
||||
|
||||
indexRelays.addAll(
|
||||
key.indexerRelayList.flow.value
|
||||
.ifEmpty { DefaultIndexerRelayList },
|
||||
)
|
||||
|
||||
homeRelays.addAll(key.nip65RelayList.allFlowNoDefaults.value)
|
||||
homeRelays.addAll(key.privateStorageRelayList.flow.value)
|
||||
homeRelays.addAll(key.localRelayList.flow.value)
|
||||
|
||||
searchRelays.addAll(key.trustedRelayList.flow.value)
|
||||
searchRelays.addAll(
|
||||
key.searchRelayList.flow.value
|
||||
.ifEmpty { DefaultSearchRelayList },
|
||||
)
|
||||
|
||||
// uses followShared to ignore personal relays when finding users.
|
||||
commonRelays.addAll(key.followSharedOutboxesOrProxy.flow.value - cannotConnectRelays)
|
||||
}
|
||||
|
||||
if (noOutboxList.isEmpty()) return null
|
||||
if (users.isEmpty()) return null
|
||||
|
||||
val perRelay = pickRelaysToLoadUsers(noOutboxList, indexRelays, homeRelays, searchRelays, commonRelays, hasTried)
|
||||
println("AccountFollowNeeds ${users.size}")
|
||||
|
||||
hasTried.removeEveryoneBut(noOutboxList)
|
||||
val connectedRelays = client.relayStatusFlow().value.connected
|
||||
|
||||
val perRelay = pickRelaysToLoadUsers(users, accounts, connectedRelays, failureTracker.cannotConnectRelays, hasTried)
|
||||
|
||||
hasTried.removeEveryoneBut(users)
|
||||
|
||||
return perRelay.mapNotNull { (relay, users) ->
|
||||
if (users.isNotEmpty()) {
|
||||
|
||||
+64
-55
@@ -20,71 +20,62 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Constants
|
||||
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
|
||||
import com.vitorpamplona.amethyst.model.DefaultSearchRelayList
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
|
||||
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.ifEmpty
|
||||
|
||||
val MetadataOnlyKinds = listOf(MetadataEvent.KIND)
|
||||
val OutboxOnlyKinds = listOf(AdvertisedRelayListEvent.KIND)
|
||||
val BothKinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND)
|
||||
|
||||
fun filterFindFollowMetadataForKey(
|
||||
loadMetadata: Set<User>,
|
||||
loadOutbox: Set<User>,
|
||||
connectedRelays: Set<NormalizedRelayUrl>,
|
||||
indexRelays: Set<NormalizedRelayUrl>,
|
||||
searchRelays: Set<NormalizedRelayUrl>,
|
||||
allRelays: Set<NormalizedRelayUrl>,
|
||||
fun pickRelaysToLoadUsers(
|
||||
users: Set<User>,
|
||||
accounts: Collection<Account>,
|
||||
connected: Set<NormalizedRelayUrl>,
|
||||
cannotConnectRelays: Set<NormalizedRelayUrl>,
|
||||
hasTried: EOSEAccountFast<User>,
|
||||
): List<RelayBasedFilter> {
|
||||
val both = loadMetadata.intersect(loadOutbox)
|
||||
val onlyMetadata = loadMetadata - both
|
||||
val onlyOutbox = loadOutbox - both
|
||||
): Map<NormalizedRelayUrl, Set<HexKey>> {
|
||||
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
val homeRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
val searchRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
val commonRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
|
||||
val perRelayKeysBoth = pickRelaysToLoadUsers(both, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
|
||||
val perRelayKeysOnlyMetadata = pickRelaysToLoadUsers(onlyMetadata, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
|
||||
val perRelayKeysOnlyOutbox = pickRelaysToLoadUsers(onlyOutbox, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
|
||||
accounts.forEach { key ->
|
||||
indexRelays.addAll(
|
||||
key.indexerRelayList.flow.value
|
||||
.ifEmpty { DefaultIndexerRelayList },
|
||||
)
|
||||
|
||||
return perRelayKeysBoth.mapNotNull {
|
||||
val sortedUsers = it.value.sorted()
|
||||
if (sortedUsers.isNotEmpty()) {
|
||||
RelayBasedFilter(
|
||||
relay = it.key,
|
||||
filter = Filter(kinds = BothKinds, authors = sortedUsers),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} +
|
||||
perRelayKeysOnlyMetadata.mapNotNull {
|
||||
val sortedUsers = it.value.sorted()
|
||||
if (sortedUsers.isNotEmpty()) {
|
||||
RelayBasedFilter(
|
||||
relay = it.key,
|
||||
filter = Filter(kinds = MetadataOnlyKinds, authors = sortedUsers),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} +
|
||||
perRelayKeysOnlyOutbox.mapNotNull {
|
||||
val sortedUsers = it.value.sorted()
|
||||
if (sortedUsers.isNotEmpty()) {
|
||||
RelayBasedFilter(
|
||||
relay = it.key,
|
||||
filter = Filter(kinds = OutboxOnlyKinds, authors = sortedUsers),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
homeRelays.addAll(key.nip65RelayList.allFlowNoDefaults.value)
|
||||
homeRelays.addAll(key.privateStorageRelayList.flow.value)
|
||||
homeRelays.addAll(key.localRelayList.flow.value)
|
||||
|
||||
searchRelays.addAll(key.trustedRelayList.flow.value)
|
||||
searchRelays.addAll(
|
||||
key.searchRelayList.flow.value
|
||||
.ifEmpty { DefaultSearchRelayList },
|
||||
)
|
||||
|
||||
// uses followShared to ignore personal relays when finding users.
|
||||
commonRelays.addAll(
|
||||
key.followSharedOutboxesOrProxy.flow.value
|
||||
.ifEmpty { Constants.eventFinderRelays },
|
||||
)
|
||||
}
|
||||
|
||||
return pickRelaysToLoadUsers(
|
||||
users,
|
||||
indexRelays - cannotConnectRelays,
|
||||
homeRelays - cannotConnectRelays,
|
||||
searchRelays - cannotConnectRelays,
|
||||
connected,
|
||||
commonRelays - cannotConnectRelays,
|
||||
hasTried,
|
||||
)
|
||||
}
|
||||
|
||||
fun pickRelaysToLoadUsers(
|
||||
@@ -92,6 +83,7 @@ fun pickRelaysToLoadUsers(
|
||||
indexRelays: Set<NormalizedRelayUrl>,
|
||||
homeRelays: Set<NormalizedRelayUrl>,
|
||||
searchRelays: Set<NormalizedRelayUrl>,
|
||||
connected: Set<NormalizedRelayUrl>,
|
||||
commonRelays: Set<NormalizedRelayUrl>,
|
||||
hasTried: EOSEAccountFast<User>,
|
||||
): Map<NormalizedRelayUrl, Set<HexKey>> =
|
||||
@@ -166,6 +158,23 @@ fun pickRelaysToLoadUsers(
|
||||
add(it, key.pubkeyHex)
|
||||
}
|
||||
|
||||
val connectedRelaysLeftToTry =
|
||||
(connected - tried)
|
||||
.sortedBy { relay ->
|
||||
key.pubkeyHex.hashCode() xor relay.url.hashCode()
|
||||
}.take(100)
|
||||
|
||||
// picks one at random to avoid overloading these relays
|
||||
if (users.size > 300) {
|
||||
connectedRelaysLeftToTry.take(20).forEach {
|
||||
add(it, key.pubkeyHex)
|
||||
}
|
||||
} else {
|
||||
connectedRelaysLeftToTry.forEach {
|
||||
add(it, key.pubkeyHex)
|
||||
}
|
||||
}
|
||||
|
||||
if (searchRelaysLeftToTry.size < 2) {
|
||||
// This creates a pre-deterministic order of the array such that
|
||||
// if this function is called twice, it returns the same arrays
|
||||
|
||||
+3
-1
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.Us
|
||||
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.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
|
||||
// This allows multiple screen to be listening to tags, even the same tag
|
||||
class UserFinderQueryState(
|
||||
@@ -38,10 +39,11 @@ class UserFinderQueryState(
|
||||
class UserFinderFilterAssembler(
|
||||
client: INostrClient,
|
||||
cache: LocalCache,
|
||||
failureTracker: RelayOfflineTracker,
|
||||
) : ComposeSubscriptionManager<UserFinderQueryState>() {
|
||||
val group =
|
||||
listOf(
|
||||
UserOutboxFinderSubAssembler(client, cache, ::allKeys),
|
||||
UserOutboxFinderSubAssembler(client, cache, failureTracker, ::allKeys),
|
||||
UserWatcherSubAssembler(client, cache, ::allKeys),
|
||||
UserReportsSubAssembler(client, ::allKeys),
|
||||
)
|
||||
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
/**
|
||||
* 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.loaders
|
||||
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
|
||||
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
|
||||
|
||||
val RelayListKinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND)
|
||||
|
||||
fun findFindUserOutBox(
|
||||
authors: Set<User>,
|
||||
connectedRelays: Set<NormalizedRelayUrl>,
|
||||
indexRelays: Set<NormalizedRelayUrl>,
|
||||
searchRelays: Set<NormalizedRelayUrl>,
|
||||
allRelays: Set<NormalizedRelayUrl>,
|
||||
hasTried: EOSEAccountFast<User>,
|
||||
): List<RelayBasedFilter> {
|
||||
val perRelayKeysBoth = pickRelaysToLoadUsers(authors, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
|
||||
|
||||
return perRelayKeysBoth.mapNotNull {
|
||||
val sortedUsers = it.value.sorted()
|
||||
if (sortedUsers.isNotEmpty()) {
|
||||
RelayBasedFilter(
|
||||
relay = it.key,
|
||||
filter = Filter(kinds = RelayListKinds, authors = sortedUsers),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-28
@@ -20,27 +20,32 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Constants
|
||||
import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows.pickRelaysToLoadUsers
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||
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.TimeUtils
|
||||
import kotlin.collections.ifEmpty
|
||||
|
||||
class UserOutboxFinderSubAssembler(
|
||||
client: INostrClient,
|
||||
val cache: LocalCache,
|
||||
val failureTracker: RelayOfflineTracker,
|
||||
allKeys: () -> Set<UserFinderQueryState>,
|
||||
) : BaseEoseManager<UserFinderQueryState>(client, allKeys) {
|
||||
val relayListKinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND)
|
||||
|
||||
/**
|
||||
* 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).
|
||||
@@ -91,35 +96,28 @@ class UserOutboxFinderSubAssembler(
|
||||
|
||||
if (noOutboxList.isEmpty()) return null
|
||||
|
||||
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
val searchRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
val allRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
val accounts = keys.mapTo(mutableSetOf()) { it.account }
|
||||
val connectedRelays = client.relayStatusFlow().value.connected
|
||||
|
||||
keys.mapTo(mutableSetOf()) { it.account }.forEach { acc ->
|
||||
// broadens the search as it goes.
|
||||
indexRelays.addAll(
|
||||
acc.indexerRelayList.flow.value
|
||||
.ifEmpty { DefaultIndexerRelayList },
|
||||
val perRelayKeysBoth =
|
||||
pickRelaysToLoadUsers(
|
||||
noOutboxList,
|
||||
accounts,
|
||||
connectedRelays,
|
||||
failureTracker.cannotConnectRelays,
|
||||
hasTried,
|
||||
)
|
||||
indexRelays.addAll(acc.proxyRelayList.flow.value)
|
||||
|
||||
searchRelays.addAll(acc.nip65RelayList.allFlowNoDefaults.value)
|
||||
searchRelays.addAll(acc.privateStorageRelayList.flow.value)
|
||||
searchRelays.addAll(acc.localRelayList.flow.value)
|
||||
searchRelays.addAll(acc.searchRelayList.flow.value)
|
||||
searchRelays.addAll(acc.trustedRelayList.flow.value)
|
||||
|
||||
allRelays.addAll(acc.followOutboxesOrProxy.flow.value)
|
||||
return perRelayKeysBoth.mapNotNull {
|
||||
val sortedUsers = it.value.sorted()
|
||||
if (sortedUsers.isNotEmpty()) {
|
||||
RelayBasedFilter(
|
||||
relay = it.key,
|
||||
filter = Filter(kinds = relayListKinds, authors = sortedUsers),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
allRelays.addAll(Constants.eventFinderRelays)
|
||||
|
||||
return findFindUserOutBox(
|
||||
noOutboxList,
|
||||
client.relayStatusFlow().value.connected,
|
||||
indexRelays,
|
||||
searchRelays,
|
||||
allRelays,
|
||||
hasTried,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -98,6 +98,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.auth.EmptyIAuthStatus
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
@@ -1678,6 +1679,7 @@ fun mockAccountViewModel(): AccountViewModel {
|
||||
val authenticator = EmptyIAuthStatus
|
||||
|
||||
val nwcFilters = NWCPaymentFilterAssembler(client)
|
||||
val failureTracker = RelayOfflineTracker(client)
|
||||
|
||||
val account =
|
||||
Account(
|
||||
@@ -1696,7 +1698,7 @@ fun mockAccountViewModel(): AccountViewModel {
|
||||
settings = uiState,
|
||||
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
||||
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, scope),
|
||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, failureTracker, scope),
|
||||
).also {
|
||||
mockedCache = it
|
||||
}
|
||||
@@ -1727,6 +1729,7 @@ fun mockVitorAccountViewModel(): AccountViewModel {
|
||||
val authenticator = EmptyIAuthStatus
|
||||
|
||||
val nwcFilters = NWCPaymentFilterAssembler(client)
|
||||
val failureTracker = RelayOfflineTracker(client)
|
||||
|
||||
val account =
|
||||
Account(
|
||||
@@ -1745,7 +1748,7 @@ fun mockVitorAccountViewModel(): AccountViewModel {
|
||||
settings = uiState,
|
||||
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
||||
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, scope),
|
||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, failureTracker, scope),
|
||||
).also {
|
||||
vitorCache = it
|
||||
}
|
||||
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 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.nip01Core.relay.client.accessories
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NotifyMessage
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
/**
|
||||
* Listens to NostrClient's onNotify messages from the relay
|
||||
*/
|
||||
class RelayOfflineTracker(
|
||||
val client: INostrClient,
|
||||
) {
|
||||
companion object {
|
||||
const val TAG = "RelayOfflineTracker"
|
||||
}
|
||||
|
||||
val cannotConnectRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||
|
||||
private val clientListener =
|
||||
object : IRelayClientListener {
|
||||
override fun onConnected(
|
||||
relay: IRelayClient,
|
||||
pingMillis: Int,
|
||||
compressed: Boolean,
|
||||
) {
|
||||
cannotConnectRelays.remove(relay.url)
|
||||
}
|
||||
|
||||
override fun onCannotConnect(
|
||||
relay: IRelayClient,
|
||||
errorMessage: String,
|
||||
) {
|
||||
cannotConnectRelays.add(relay.url)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.d(TAG, "Init, Subscribe")
|
||||
client.subscribe(clientListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
// makes sure to run
|
||||
Log.d(TAG, "Destroy, Unsubscribe")
|
||||
client.unsubscribe(clientListener)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user