Creates an interface for NostrClient

This commit is contained in:
Vitor Pamplona
2025-09-06 10:23:33 -04:00
parent a03ba7b260
commit 69e3d2d0d8
9 changed files with 143 additions and 30 deletions
@@ -121,7 +121,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
@@ -220,7 +220,7 @@ class Account(
val signer: NostrSigner, val signer: NostrSigner,
geolocationFlow: StateFlow<LocationState.LocationResult>, geolocationFlow: StateFlow<LocationState.LocationResult>,
val cache: LocalCache, val cache: LocalCache,
val client: NostrClient, val client: INostrClient,
val scope: CoroutineScope, val scope: CoroutineScope,
) { ) {
private var userProfileCache: User? = null private var userProfileCache: User? = null
@@ -23,14 +23,14 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers
import android.util.Log import android.util.Log
import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.isDebug
import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.SubscriptionController import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.SubscriptionController
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
abstract class BaseEoseManager<T>( abstract class BaseEoseManager<T>(
val client: NostrClient, val client: INostrClient,
val allKeys: () -> Set<T>, val allKeys: () -> Set<T>,
) { ) {
protected val logTag: String = this.javaClass.simpleName protected val logTag: String = this.javaClass.simpleName
@@ -20,7 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.service.relayClient.eoseManagers package com.vitorpamplona.amethyst.service.relayClient.eoseManagers
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
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
@@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay
* filters * filters
*/ */
abstract class SingleSubNoEoseCacheEoseManager<T>( abstract class SingleSubNoEoseCacheEoseManager<T>(
client: NostrClient, client: INostrClient,
allKeys: () -> Set<T>, allKeys: () -> Set<T>,
val invalidateAfterEose: Boolean = false, val invalidateAfterEose: Boolean = false,
) : BaseEoseManager<T>(client, allKeys) { ) : BaseEoseManager<T>(client, allKeys) {
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc
import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
// 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
@@ -34,7 +34,7 @@ class NWCPaymentQueryState(
) )
class NWCPaymentFilterAssembler( class NWCPaymentFilterAssembler(
client: NostrClient, client: INostrClient,
) : ComposeSubscriptionManager<NWCPaymentQueryState>() { ) : ComposeSubscriptionManager<NWCPaymentQueryState>() {
val group = val group =
listOf( listOf(
@@ -21,11 +21,11 @@
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
class NWCPaymentWatcherSubAssembler( class NWCPaymentWatcherSubAssembler(
client: NostrClient, client: INostrClient,
allKeys: () -> Set<NWCPaymentQueryState>, allKeys: () -> Set<NWCPaymentQueryState>,
) : SingleSubNoEoseCacheEoseManager<NWCPaymentQueryState>(client, allKeys) { ) : SingleSubNoEoseCacheEoseManager<NWCPaymentQueryState>(client, allKeys) {
override fun updateFilter(keys: List<NWCPaymentQueryState>): List<RelayBasedFilter>? { override fun updateFilter(keys: List<NWCPaymentQueryState>): List<RelayBasedFilter>? {
@@ -0,0 +1,110 @@
/**
* 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
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
interface INostrClient {
fun relayStatusFlow(): StateFlow<RelayPool.RelayPoolStatus>
fun connect()
fun disconnect()
fun isActive(): Boolean
fun openReqSubscription(
subId: String = newSubId(),
filters: Map<NormalizedRelayUrl, List<Filter>>,
)
fun openCountSubscription(
subId: String = newSubId(),
filters: Map<NormalizedRelayUrl, List<Filter>>,
)
fun close(subscriptionId: String)
fun sendIfExists(
event: Event,
connectedRelay: NormalizedRelayUrl,
)
fun send(
event: Event,
relayList: Set<NormalizedRelayUrl>,
)
fun subscribe(listener: IRelayClientListener)
fun unsubscribe(listener: IRelayClientListener)
fun getReqFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>?
fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>?
}
object EmptyNostrClient : INostrClient {
override fun relayStatusFlow() = MutableStateFlow(RelayPool.RelayPoolStatus())
override fun connect() { }
override fun disconnect() { }
override fun isActive() = false
override fun openReqSubscription(
subId: String,
filters: Map<NormalizedRelayUrl, List<Filter>>,
) { }
override fun openCountSubscription(
subId: String,
filters: Map<NormalizedRelayUrl, List<Filter>>,
) { }
override fun close(subscriptionId: String) { }
override fun sendIfExists(
event: Event,
connectedRelay: NormalizedRelayUrl,
) { }
override fun send(
event: Event,
relayList: Set<NormalizedRelayUrl>,
) { }
override fun subscribe(listener: IRelayClientListener) {}
override fun unsubscribe(listener: IRelayClientListener) {}
override fun getReqFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = null
override fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = null
}
@@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.PoolSubscriptionRepo
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
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
@@ -73,7 +72,8 @@ import kotlinx.coroutines.flow.stateIn
class NostrClient( class NostrClient(
private val websocketBuilder: WebsocketBuilder, private val websocketBuilder: WebsocketBuilder,
private val scope: CoroutineScope, private val scope: CoroutineScope,
) : IRelayClientListener { ) : INostrClient,
IRelayClientListener {
private val relayPool: RelayPool = RelayPool(this, ::buildRelay) private val relayPool: RelayPool = RelayPool(this, ::buildRelay)
private val activeRequests: PoolSubscriptionRepository = PoolSubscriptionRepository() private val activeRequests: PoolSubscriptionRepository = PoolSubscriptionRepository()
private val activeCounts: PoolSubscriptionRepository = PoolSubscriptionRepository() private val activeCounts: PoolSubscriptionRepository = PoolSubscriptionRepository()
@@ -124,17 +124,17 @@ class NostrClient(
} }
// Reconnects all relays that may have disconnected // Reconnects all relays that may have disconnected
fun connect() { override fun connect() {
isActive = true isActive = true
relayPool.connect() relayPool.connect()
} }
fun disconnect() { override fun disconnect() {
isActive = false isActive = false
relayPool.disconnect() relayPool.disconnect()
} }
fun isActive() = isActive override fun isActive() = isActive
@Synchronized @Synchronized
fun reconnect(onlyIfChanged: Boolean = false) { fun reconnect(onlyIfChanged: Boolean = false) {
@@ -204,8 +204,8 @@ class NostrClient(
return false return false
} }
fun openReqSubscription( override fun openReqSubscription(
subId: String = newSubId(), subId: String,
filters: Map<NormalizedRelayUrl, List<Filter>>, filters: Map<NormalizedRelayUrl, List<Filter>>,
) { ) {
val oldFilters = activeRequests.getSubscriptionFiltersOrNull(subId) ?: emptyMap() val oldFilters = activeRequests.getSubscriptionFiltersOrNull(subId) ?: emptyMap()
@@ -240,8 +240,8 @@ class NostrClient(
} }
} }
fun openCountSubscription( override fun openCountSubscription(
subId: String = newSubId(), subId: String,
filters: Map<NormalizedRelayUrl, List<Filter>>, filters: Map<NormalizedRelayUrl, List<Filter>>,
) { ) {
val oldFilters = activeCounts.getSubscriptionFiltersOrNull(subId) ?: emptyMap() val oldFilters = activeCounts.getSubscriptionFiltersOrNull(subId) ?: emptyMap()
@@ -276,7 +276,7 @@ class NostrClient(
} }
} }
fun sendIfExists( override fun sendIfExists(
event: Event, event: Event,
connectedRelay: NormalizedRelayUrl, connectedRelay: NormalizedRelayUrl,
) { ) {
@@ -288,7 +288,7 @@ class NostrClient(
} }
} }
fun send( override fun send(
event: Event, event: Event,
relayList: Set<NormalizedRelayUrl>, relayList: Set<NormalizedRelayUrl>,
) { ) {
@@ -301,7 +301,7 @@ class NostrClient(
} }
} }
fun close(subscriptionId: String) { override fun close(subscriptionId: String) {
activeRequests.remove(subscriptionId) activeRequests.remove(subscriptionId)
activeCounts.remove(subscriptionId) activeCounts.remove(subscriptionId)
relayPool.close(subscriptionId) relayPool.close(subscriptionId)
@@ -386,13 +386,13 @@ class NostrClient(
listeners.forEach { it.onError(relay, subId, error) } listeners.forEach { it.onError(relay, subId, error) }
} }
fun subscribe(listener: IRelayClientListener) { override fun subscribe(listener: IRelayClientListener) {
listeners = listeners.plus(listener) listeners = listeners.plus(listener)
} }
fun isSubscribed(listener: IRelayClientListener): Boolean = listeners.contains(listener) fun isSubscribed(listener: IRelayClientListener): Boolean = listeners.contains(listener)
fun unsubscribe(listener: IRelayClientListener) { override fun unsubscribe(listener: IRelayClientListener) {
listeners = listeners.minus(listener) listeners = listeners.minus(listener)
} }
@@ -402,7 +402,9 @@ class NostrClient(
fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = eventOutbox.activeOutboxCacheFor(url) fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = eventOutbox.activeOutboxCacheFor(url)
fun getSubscriptionFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeRequests.getSubscriptionFiltersOrNull(subId) override fun getReqFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeRequests.getSubscriptionFiltersOrNull(subId)
fun relayStatusFlow() = relayPool.statusFlow override fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeCounts.getSubscriptionFiltersOrNull(subId)
override fun relayStatusFlow() = relayPool.statusFlow
} }
@@ -21,7 +21,7 @@
package com.vitorpamplona.quartz.nip01Core.relay.client.accessories package com.vitorpamplona.quartz.nip01Core.relay.client.accessories
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
@@ -32,7 +32,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
fun NostrClient.downloadFirstEvent( fun INostrClient.downloadFirstEvent(
subscriptionId: String = newSubId(), subscriptionId: String = newSubId(),
filters: Map<NormalizedRelayUrl, List<Filter>>, filters: Map<NormalizedRelayUrl, List<Filter>>,
onResponse: (Event) -> Unit, onResponse: (Event) -> Unit,
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions package com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions
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.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
@@ -47,7 +48,7 @@ import com.vitorpamplona.quartz.utils.LargeCache
* - Dismiss subscriptions with [dismissSubscription]. * - Dismiss subscriptions with [dismissSubscription].
*/ */
class SubscriptionController( class SubscriptionController(
val client: NostrClient, val client: INostrClient,
) { ) {
private val subscriptions = LargeCache<String, Subscription>() private val subscriptions = LargeCache<String, Subscription>()
private val stats = SubscriptionStats() private val stats = SubscriptionStats()
@@ -108,7 +109,7 @@ class SubscriptionController(
fun updateRelays() { fun updateRelays() {
val currentFilters = val currentFilters =
subscriptions.associateWith { id, sub -> subscriptions.associateWith { id, sub ->
client.getSubscriptionFiltersOrNull(id) client.getReqFiltersOrNull(id)
} }
subscriptions.forEach { id, sub -> subscriptions.forEach { id, sub ->