- 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.INostrClient
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
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.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.reqs.stats.RelayReqStats
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
||||||
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache
|
||||||
@@ -192,14 +193,26 @@ class AppModules(
|
|||||||
// Tries to verify new OTS events when they arrive.
|
// Tries to verify new OTS events when they arrive.
|
||||||
val otsEventVerifier = IncomingOtsEventVerifier(otsVerifCache, cache, applicationDefaultScope)
|
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)
|
val relayStats = RelayStats(client)
|
||||||
|
|
||||||
|
// Logs debug messages when needed
|
||||||
val detailedLogger = if (isDebug) RelayLogger(client, true, false) else null
|
val detailedLogger = if (isDebug) RelayLogger(client, true, false) else null
|
||||||
val relayReqStats = if (isDebug) RelayReqStats(client) else null
|
val relayReqStats = if (isDebug) RelayReqStats(client) else null
|
||||||
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
val logger = if (isDebug) RelaySpeedLogger(client) else null
|
||||||
|
|
||||||
// Coordinates all subscriptions for the Nostr Client
|
// 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
|
// keeps all accounts live
|
||||||
val accountsCache =
|
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.threadview.datasources.ThreadFilterAssembler
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler
|
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.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.auth.IAuthStatus
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
@@ -47,10 +48,11 @@ class RelaySubscriptionsCoordinator(
|
|||||||
cache: LocalCache,
|
cache: LocalCache,
|
||||||
client: INostrClient,
|
client: INostrClient,
|
||||||
authenticator: IAuthStatus,
|
authenticator: IAuthStatus,
|
||||||
|
failureTracker: RelayOfflineTracker,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
) {
|
) {
|
||||||
// main one: notifications, dms and account settings
|
// 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.
|
// always running, feed assemblers.
|
||||||
val home = HomeFilterAssembler(client)
|
val home = HomeFilterAssembler(client)
|
||||||
@@ -62,7 +64,7 @@ class RelaySubscriptionsCoordinator(
|
|||||||
// they are active when looking at events, users, channels.
|
// they are active when looking at events, users, channels.
|
||||||
val channelFinder = ChannelFinderFilterAssemblyGroup(client)
|
val channelFinder = ChannelFinderFilterAssemblyGroup(client)
|
||||||
val eventFinder = EventFinderFilterAssembler(client)
|
val eventFinder = EventFinderFilterAssembler(client)
|
||||||
val userFinder = UserFinderFilterAssembler(client, cache)
|
val userFinder = UserFinderFilterAssembler(client, cache, failureTracker)
|
||||||
|
|
||||||
// active when searching or tagging users.
|
// active when searching or tagging users.
|
||||||
val search = SearchFilterAssembler(client, scope, cache)
|
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.amethyst.ui.screen.loggedIn.AccountFeedContentStates
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
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.auth.IAuthStatus
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
|
||||||
@@ -49,12 +50,13 @@ class AccountFilterAssembler(
|
|||||||
client: INostrClient,
|
client: INostrClient,
|
||||||
cache: LocalCache,
|
cache: LocalCache,
|
||||||
authenticator: IAuthStatus,
|
authenticator: IAuthStatus,
|
||||||
|
failureTracker: RelayOfflineTracker,
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
) : ComposeSubscriptionManager<AccountQueryState>() {
|
) : ComposeSubscriptionManager<AccountQueryState>() {
|
||||||
val group =
|
val group =
|
||||||
listOf(
|
listOf(
|
||||||
AccountMetadataEoseManager(client, ::allKeys),
|
AccountMetadataEoseManager(client, ::allKeys),
|
||||||
AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, ::allKeys),
|
AccountFollowsLoaderSubAssembler(client, cache, scope, authenticator, failureTracker, ::allKeys),
|
||||||
AccountGiftWrapsEoseManager(client, ::allKeys),
|
AccountGiftWrapsEoseManager(client, ::allKeys),
|
||||||
AccountDraftsEoseManager(client, ::allKeys),
|
AccountDraftsEoseManager(client, ::allKeys),
|
||||||
AccountNotificationsEoseFromInboxRelaysManager(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.isDebug
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
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.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.IEoseManager
|
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.ammolite.relays.BundledUpdate
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
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.auth.IAuthStatus
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
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.pool.groupByRelay
|
||||||
@@ -51,7 +50,6 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.collections.forEach
|
import kotlin.collections.forEach
|
||||||
import kotlin.collections.ifEmpty
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This downloads the outbox events for all Follows of all users.
|
* This downloads the outbox events for all Follows of all users.
|
||||||
@@ -62,13 +60,12 @@ class AccountFollowsLoaderSubAssembler(
|
|||||||
val cache: LocalCache,
|
val cache: LocalCache,
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
val authStatus: IAuthStatus,
|
val authStatus: IAuthStatus,
|
||||||
|
val failureTracker: RelayOfflineTracker,
|
||||||
val allKeys: () -> Set<AccountQueryState>,
|
val allKeys: () -> Set<AccountQueryState>,
|
||||||
) : IEoseManager {
|
) : IEoseManager {
|
||||||
private val logTag = "AccountFollowsLoaderSubAssembler"
|
private val logTag = "AccountFollowsLoaderSubAssembler"
|
||||||
private val orchestrator = SubscriptionController(client)
|
private val orchestrator = SubscriptionController(client)
|
||||||
|
|
||||||
private val cannotConnectRelays = mutableSetOf<NormalizedRelayUrl>()
|
|
||||||
|
|
||||||
// Refreshes observers in batches of 500ms
|
// Refreshes observers in batches of 500ms
|
||||||
private val bundler = BundledUpdate(500, Dispatchers.Default)
|
private val bundler = BundledUpdate(500, Dispatchers.Default)
|
||||||
|
|
||||||
@@ -141,56 +138,28 @@ class AccountFollowsLoaderSubAssembler(
|
|||||||
newEose(TimeUtils.now(), relay, forFilters)
|
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>? {
|
fun updateFilterForAllAccounts(accounts: Collection<Account>): List<RelayBasedFilter>? {
|
||||||
val noOutboxList = mutableSetOf<User>()
|
val users = mutableSetOf<User>()
|
||||||
|
|
||||||
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
|
|
||||||
val homeRelays = mutableSetOf<NormalizedRelayUrl>()
|
|
||||||
val searchRelays = mutableSetOf<NormalizedRelayUrl>()
|
|
||||||
val commonRelays = mutableSetOf<NormalizedRelayUrl>()
|
|
||||||
|
|
||||||
accounts.forEach { key ->
|
accounts.forEach { key ->
|
||||||
key.kind3FollowList.userList.value.forEach { user ->
|
key.kind3FollowList.userList.value.forEach { user ->
|
||||||
if (user.authorRelayList() == null) {
|
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) ->
|
return perRelay.mapNotNull { (relay, users) ->
|
||||||
if (users.isNotEmpty()) {
|
if (users.isNotEmpty()) {
|
||||||
|
|||||||
+64
-55
@@ -20,71 +20,62 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.follows
|
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.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
|
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
|
||||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
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.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
|
||||||
import com.vitorpamplona.quartz.utils.mapOfSet
|
import com.vitorpamplona.quartz.utils.mapOfSet
|
||||||
|
import kotlin.collections.ifEmpty
|
||||||
|
|
||||||
val MetadataOnlyKinds = listOf(MetadataEvent.KIND)
|
fun pickRelaysToLoadUsers(
|
||||||
val OutboxOnlyKinds = listOf(AdvertisedRelayListEvent.KIND)
|
users: Set<User>,
|
||||||
val BothKinds = listOf(MetadataEvent.KIND, AdvertisedRelayListEvent.KIND)
|
accounts: Collection<Account>,
|
||||||
|
connected: Set<NormalizedRelayUrl>,
|
||||||
fun filterFindFollowMetadataForKey(
|
cannotConnectRelays: Set<NormalizedRelayUrl>,
|
||||||
loadMetadata: Set<User>,
|
|
||||||
loadOutbox: Set<User>,
|
|
||||||
connectedRelays: Set<NormalizedRelayUrl>,
|
|
||||||
indexRelays: Set<NormalizedRelayUrl>,
|
|
||||||
searchRelays: Set<NormalizedRelayUrl>,
|
|
||||||
allRelays: Set<NormalizedRelayUrl>,
|
|
||||||
hasTried: EOSEAccountFast<User>,
|
hasTried: EOSEAccountFast<User>,
|
||||||
): List<RelayBasedFilter> {
|
): Map<NormalizedRelayUrl, Set<HexKey>> {
|
||||||
val both = loadMetadata.intersect(loadOutbox)
|
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||||
val onlyMetadata = loadMetadata - both
|
val homeRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||||
val onlyOutbox = loadOutbox - both
|
val searchRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||||
|
val commonRelays = mutableSetOf<NormalizedRelayUrl>()
|
||||||
|
|
||||||
val perRelayKeysBoth = pickRelaysToLoadUsers(both, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
|
accounts.forEach { key ->
|
||||||
val perRelayKeysOnlyMetadata = pickRelaysToLoadUsers(onlyMetadata, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
|
indexRelays.addAll(
|
||||||
val perRelayKeysOnlyOutbox = pickRelaysToLoadUsers(onlyOutbox, connectedRelays, indexRelays, searchRelays, allRelays, hasTried)
|
key.indexerRelayList.flow.value
|
||||||
|
.ifEmpty { DefaultIndexerRelayList },
|
||||||
|
)
|
||||||
|
|
||||||
return perRelayKeysBoth.mapNotNull {
|
homeRelays.addAll(key.nip65RelayList.allFlowNoDefaults.value)
|
||||||
val sortedUsers = it.value.sorted()
|
homeRelays.addAll(key.privateStorageRelayList.flow.value)
|
||||||
if (sortedUsers.isNotEmpty()) {
|
homeRelays.addAll(key.localRelayList.flow.value)
|
||||||
RelayBasedFilter(
|
|
||||||
relay = it.key,
|
searchRelays.addAll(key.trustedRelayList.flow.value)
|
||||||
filter = Filter(kinds = BothKinds, authors = sortedUsers),
|
searchRelays.addAll(
|
||||||
)
|
key.searchRelayList.flow.value
|
||||||
} else {
|
.ifEmpty { DefaultSearchRelayList },
|
||||||
null
|
)
|
||||||
}
|
|
||||||
} +
|
// uses followShared to ignore personal relays when finding users.
|
||||||
perRelayKeysOnlyMetadata.mapNotNull {
|
commonRelays.addAll(
|
||||||
val sortedUsers = it.value.sorted()
|
key.followSharedOutboxesOrProxy.flow.value
|
||||||
if (sortedUsers.isNotEmpty()) {
|
.ifEmpty { Constants.eventFinderRelays },
|
||||||
RelayBasedFilter(
|
)
|
||||||
relay = it.key,
|
}
|
||||||
filter = Filter(kinds = MetadataOnlyKinds, authors = sortedUsers),
|
|
||||||
)
|
return pickRelaysToLoadUsers(
|
||||||
} else {
|
users,
|
||||||
null
|
indexRelays - cannotConnectRelays,
|
||||||
}
|
homeRelays - cannotConnectRelays,
|
||||||
} +
|
searchRelays - cannotConnectRelays,
|
||||||
perRelayKeysOnlyOutbox.mapNotNull {
|
connected,
|
||||||
val sortedUsers = it.value.sorted()
|
commonRelays - cannotConnectRelays,
|
||||||
if (sortedUsers.isNotEmpty()) {
|
hasTried,
|
||||||
RelayBasedFilter(
|
)
|
||||||
relay = it.key,
|
|
||||||
filter = Filter(kinds = OutboxOnlyKinds, authors = sortedUsers),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pickRelaysToLoadUsers(
|
fun pickRelaysToLoadUsers(
|
||||||
@@ -92,6 +83,7 @@ fun pickRelaysToLoadUsers(
|
|||||||
indexRelays: Set<NormalizedRelayUrl>,
|
indexRelays: Set<NormalizedRelayUrl>,
|
||||||
homeRelays: Set<NormalizedRelayUrl>,
|
homeRelays: Set<NormalizedRelayUrl>,
|
||||||
searchRelays: Set<NormalizedRelayUrl>,
|
searchRelays: Set<NormalizedRelayUrl>,
|
||||||
|
connected: Set<NormalizedRelayUrl>,
|
||||||
commonRelays: Set<NormalizedRelayUrl>,
|
commonRelays: Set<NormalizedRelayUrl>,
|
||||||
hasTried: EOSEAccountFast<User>,
|
hasTried: EOSEAccountFast<User>,
|
||||||
): Map<NormalizedRelayUrl, Set<HexKey>> =
|
): Map<NormalizedRelayUrl, Set<HexKey>> =
|
||||||
@@ -166,6 +158,23 @@ fun pickRelaysToLoadUsers(
|
|||||||
add(it, key.pubkeyHex)
|
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) {
|
if (searchRelaysLeftToTry.size < 2) {
|
||||||
// This creates a pre-deterministic order of the array such that
|
// This creates a pre-deterministic order of the array such that
|
||||||
// if this function is called twice, it returns the same arrays
|
// 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.UserReportsSubAssembler
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserWatcherSubAssembler
|
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.INostrClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayOfflineTracker
|
||||||
|
|
||||||
// This allows multiple screen to be listening to tags, even the same tag
|
// This allows multiple screen to be listening to tags, even the same tag
|
||||||
class UserFinderQueryState(
|
class UserFinderQueryState(
|
||||||
@@ -38,10 +39,11 @@ class UserFinderQueryState(
|
|||||||
class UserFinderFilterAssembler(
|
class UserFinderFilterAssembler(
|
||||||
client: INostrClient,
|
client: INostrClient,
|
||||||
cache: LocalCache,
|
cache: LocalCache,
|
||||||
|
failureTracker: RelayOfflineTracker,
|
||||||
) : ComposeSubscriptionManager<UserFinderQueryState>() {
|
) : ComposeSubscriptionManager<UserFinderQueryState>() {
|
||||||
val group =
|
val group =
|
||||||
listOf(
|
listOf(
|
||||||
UserOutboxFinderSubAssembler(client, cache, ::allKeys),
|
UserOutboxFinderSubAssembler(client, cache, failureTracker, ::allKeys),
|
||||||
UserWatcherSubAssembler(client, cache, ::allKeys),
|
UserWatcherSubAssembler(client, cache, ::allKeys),
|
||||||
UserReportsSubAssembler(client, ::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
|
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.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.BaseEoseManager
|
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.relayClient.reqCommand.user.UserFinderQueryState
|
||||||
import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast
|
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.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.RelayBasedFilter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
|
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlin.collections.ifEmpty
|
import kotlin.collections.ifEmpty
|
||||||
|
|
||||||
class UserOutboxFinderSubAssembler(
|
class UserOutboxFinderSubAssembler(
|
||||||
client: INostrClient,
|
client: INostrClient,
|
||||||
val cache: LocalCache,
|
val cache: LocalCache,
|
||||||
|
val failureTracker: RelayOfflineTracker,
|
||||||
allKeys: () -> Set<UserFinderQueryState>,
|
allKeys: () -> Set<UserFinderQueryState>,
|
||||||
) : BaseEoseManager<UserFinderQueryState>(client, allKeys) {
|
) : 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
|
* 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).
|
* and reports, but only from trusted accounts (follows of all logged in users).
|
||||||
@@ -91,35 +96,28 @@ class UserOutboxFinderSubAssembler(
|
|||||||
|
|
||||||
if (noOutboxList.isEmpty()) return null
|
if (noOutboxList.isEmpty()) return null
|
||||||
|
|
||||||
val indexRelays = mutableSetOf<NormalizedRelayUrl>()
|
val accounts = keys.mapTo(mutableSetOf()) { it.account }
|
||||||
val searchRelays = mutableSetOf<NormalizedRelayUrl>()
|
val connectedRelays = client.relayStatusFlow().value.connected
|
||||||
val allRelays = mutableSetOf<NormalizedRelayUrl>()
|
|
||||||
|
|
||||||
keys.mapTo(mutableSetOf()) { it.account }.forEach { acc ->
|
val perRelayKeysBoth =
|
||||||
// broadens the search as it goes.
|
pickRelaysToLoadUsers(
|
||||||
indexRelays.addAll(
|
noOutboxList,
|
||||||
acc.indexerRelayList.flow.value
|
accounts,
|
||||||
.ifEmpty { DefaultIndexerRelayList },
|
connectedRelays,
|
||||||
|
failureTracker.cannotConnectRelays,
|
||||||
|
hasTried,
|
||||||
)
|
)
|
||||||
indexRelays.addAll(acc.proxyRelayList.flow.value)
|
|
||||||
|
|
||||||
searchRelays.addAll(acc.nip65RelayList.allFlowNoDefaults.value)
|
return perRelayKeysBoth.mapNotNull {
|
||||||
searchRelays.addAll(acc.privateStorageRelayList.flow.value)
|
val sortedUsers = it.value.sorted()
|
||||||
searchRelays.addAll(acc.localRelayList.flow.value)
|
if (sortedUsers.isNotEmpty()) {
|
||||||
searchRelays.addAll(acc.searchRelayList.flow.value)
|
RelayBasedFilter(
|
||||||
searchRelays.addAll(acc.trustedRelayList.flow.value)
|
relay = it.key,
|
||||||
|
filter = Filter(kinds = relayListKinds, authors = sortedUsers),
|
||||||
allRelays.addAll(acc.followOutboxesOrProxy.flow.value)
|
)
|
||||||
|
} 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.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.client.EmptyNostrClient
|
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.client.auth.EmptyIAuthStatus
|
||||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||||
@@ -1678,6 +1679,7 @@ fun mockAccountViewModel(): AccountViewModel {
|
|||||||
val authenticator = EmptyIAuthStatus
|
val authenticator = EmptyIAuthStatus
|
||||||
|
|
||||||
val nwcFilters = NWCPaymentFilterAssembler(client)
|
val nwcFilters = NWCPaymentFilterAssembler(client)
|
||||||
|
val failureTracker = RelayOfflineTracker(client)
|
||||||
|
|
||||||
val account =
|
val account =
|
||||||
Account(
|
Account(
|
||||||
@@ -1696,7 +1698,7 @@ fun mockAccountViewModel(): AccountViewModel {
|
|||||||
settings = uiState,
|
settings = uiState,
|
||||||
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
||||||
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
||||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, scope),
|
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, failureTracker, scope),
|
||||||
).also {
|
).also {
|
||||||
mockedCache = it
|
mockedCache = it
|
||||||
}
|
}
|
||||||
@@ -1727,6 +1729,7 @@ fun mockVitorAccountViewModel(): AccountViewModel {
|
|||||||
val authenticator = EmptyIAuthStatus
|
val authenticator = EmptyIAuthStatus
|
||||||
|
|
||||||
val nwcFilters = NWCPaymentFilterAssembler(client)
|
val nwcFilters = NWCPaymentFilterAssembler(client)
|
||||||
|
val failureTracker = RelayOfflineTracker(client)
|
||||||
|
|
||||||
val account =
|
val account =
|
||||||
Account(
|
Account(
|
||||||
@@ -1745,7 +1748,7 @@ fun mockVitorAccountViewModel(): AccountViewModel {
|
|||||||
settings = uiState,
|
settings = uiState,
|
||||||
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
torSettings = TorSettingsFlow(torType = MutableStateFlow(TorType.OFF)),
|
||||||
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
httpClientBuilder = EmptyRoleBasedHttpClientBuilder(),
|
||||||
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, scope),
|
dataSources = RelaySubscriptionsCoordinator(LocalCache, client, authenticator, failureTracker, scope),
|
||||||
).also {
|
).also {
|
||||||
vitorCache = it
|
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