Adds simplified methods to create Requests from NostrClient

This commit is contained in:
Vitor Pamplona
2025-10-29 16:51:49 -04:00
parent 40ce627de0
commit b8511b5ac3
5 changed files with 180 additions and 46 deletions

View File

@@ -305,28 +305,43 @@ val authCoordinator = RelayAuthenticator(client, appScope) { challenge, relay ->
} }
``` ```
To manage subscriptions, the simplest approach is to build mutable subscriptions in To make a request subscription simply do:
the Application class. To use the best of the outbox model, this class allows you to
build filters for as many relays as needed. The `NostrClient` will connect to the
complete set of relays for all subscriptions.
```kt ```kt
val metadataSub = NostrClientSubscription( val metadataSub = client.req(
client = client, relays = listOf("wss://nos.lol", "wss://nostr.mom"),
filter = { filters = Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(signer.pubkey)
)
) { event ->
/* consume event */
}
```
The client will add the relay to the pool, connect to it and start receiving events. The
`metadataSub` will be active until you call `metadataSub.close()`. If the client disconnects
and reconnects, the sub will be active again.
To manage subscriptions that change over time, the simplest approach is to build mutable
subscriptions in the Application class.
```kt
val metadataSub = client.req(
filters = {
// Let's say you have a list of users that need to be rendered
val users = pubkeysSeeingInTheScreen()
// And a cache repository with their outbox relays
val outboxRelays = outboxRelays(users)
val filters = listOf( val filters = listOf(
Filter( Filter(
kinds = listOf(MetadataEvent.KIND), kinds = listOf(MetadataEvent.KIND),
authors = listOf(signer.pubkey) authors = users
) )
) )
val signerOutboxRelays = listOfNotNull( outboxRelays.associateWith { filters }
RelayUrlNormalizer.normalizeOrNull("wss://relay1.com"),
RelayUrlNormalizer.normalizeOrNull("wss://relay2.com")
)
signerOutboxRelays.associateWith { filters }
} }
) { event -> ) { event ->
/* consume event */ /* consume event */
@@ -334,7 +349,7 @@ val metadataSub = NostrClientSubscription(
``` ```
In that way, you can simply call `metadataSub.updateFilter()` when you need to update In that way, you can simply call `metadataSub.updateFilter()` when you need to update
subscriptions to all relays. Or call `metadataSub.closeSubscription()` to stop the sub subscriptions to all relays. Or call `metadataSub.close()` to stop the sub
without deleting it. without deleting it.
When your app goes to the background, you can use NostrClient's `connect` and `disconnect` When your app goes to the background, you can use NostrClient's `connect` and `disconnect`

View File

@@ -0,0 +1,27 @@
/**
* 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.reqs
interface IOpenNostrRequest {
fun updateFilter()
fun close()
}

View File

@@ -18,19 +18,20 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.quartz.nip01Core.relay.client package com.vitorpamplona.quartz.nip01Core.relay.client.reqs
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
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.utils.RandomInstance import com.vitorpamplona.quartz.utils.RandomInstance
class NostrClientSubscription( class NostrClientDynamicFilterRequest(
val client: INostrClient, val client: INostrClient,
val filter: () -> Map<NormalizedRelayUrl, List<Filter>> = { emptyMap() }, val filter: () -> Map<NormalizedRelayUrl, List<Filter>>,
val onEvent: (event: Event) -> Unit = {}, val onEvent: (event: Event) -> Unit = {},
) : IRequestListener { ) : IRequestListener,
IOpenNostrRequest {
private val subId = RandomInstance.randomChars(10) private val subId = RandomInstance.randomChars(10)
override fun onEvent( override fun onEvent(
@@ -46,15 +47,16 @@ class NostrClientSubscription(
* Creates or Updates the filter with relays. This method should be called * Creates or Updates the filter with relays. This method should be called
* everytime the filter changes. * everytime the filter changes.
*/ */
fun updateFilter() = client.openReqSubscription(subId, filter(), this) override fun updateFilter() = client.openReqSubscription(subId, filter(), this)
fun closeSubscription() = client.close(subId) override fun close() = client.close(subId)
fun destroy() {
closeSubscription()
}
init { init {
updateFilter() updateFilter()
} }
} }
fun INostrClient.req(
filters: () -> Map<NormalizedRelayUrl, List<Filter>>,
onEvent: (event: Event) -> Unit = {},
): IOpenNostrRequest = NostrClientDynamicFilterRequest(this, filters, onEvent)

View File

@@ -0,0 +1,97 @@
/**
* 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.reqs
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.utils.RandomInstance
class NostrClientStaticFilterRequest(
val client: INostrClient,
val filter: Map<NormalizedRelayUrl, List<Filter>>,
val onEvent: (event: Event) -> Unit = {},
) : IRequestListener,
IOpenNostrRequest {
private val subId = RandomInstance.randomChars(10)
override fun onEvent(
event: Event,
isLive: Boolean,
relay: NormalizedRelayUrl,
forFilters: List<Filter>?,
) {
onEvent(event)
}
/**
* Creates or Updates the filter with relays. This method should be called
* everytime the filter changes.
*/
override fun updateFilter() = client.openReqSubscription(subId, filter, this)
override fun close() = client.close(subId)
init {
updateFilter()
}
}
fun INostrClient.req(
relay: NormalizedRelayUrl,
filters: List<Filter>,
onEvent: (event: Event) -> Unit = {},
): IOpenNostrRequest = NostrClientStaticFilterRequest(this, mapOf(relay to filters), onEvent)
fun INostrClient.req(
relay: NormalizedRelayUrl,
filter: Filter,
onEvent: (event: Event) -> Unit = {},
): IOpenNostrRequest = NostrClientStaticFilterRequest(this, mapOf(relay to listOf(filter)), onEvent)
fun INostrClient.req(
relays: List<NormalizedRelayUrl>,
filters: List<Filter>,
onEvent: (event: Event) -> Unit = {},
): IOpenNostrRequest = NostrClientStaticFilterRequest(this, relays.associateWith { filters }, onEvent)
fun INostrClient.req(
relays: List<NormalizedRelayUrl>,
filter: Filter,
onEvent: (event: Event) -> Unit = {},
): IOpenNostrRequest = NostrClientStaticFilterRequest(this, relays.associateWith { listOf(filter) }, onEvent)
// -----------------------------------
// Helper methods with relay as string
// -----------------------------------
fun INostrClient.req(
relay: String,
filters: List<Filter>,
onEvent: (event: Event) -> Unit = {},
): IOpenNostrRequest = NostrClientStaticFilterRequest(this, mapOf(RelayUrlNormalizer.normalize(relay) to filters), onEvent)
fun INostrClient.req(
relay: String,
filter: Filter,
onEvent: (event: Event) -> Unit = {},
): IOpenNostrRequest = NostrClientStaticFilterRequest(this, mapOf(RelayUrlNormalizer.normalize(relay) to listOf(filter)), onEvent)

View File

@@ -23,9 +23,8 @@ package com.vitorpamplona.quartz.nip01Core.relay
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
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.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClientSubscription import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.req
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -48,19 +47,13 @@ class NostrClientSubscriptionTest : BaseNostrClientTest() {
val events = mutableSetOf<Event>() val events = mutableSetOf<Event>()
val sub = val sub =
NostrClientSubscription( client.req(
client = client, relay = "wss://relay.damus.io",
filter = { filter =
mapOf( Filter(
RelayUrlNormalizer.normalize("wss://relay.damus.io") to kinds = listOf(MetadataEvent.KIND),
listOf( limit = 100,
Filter( ),
kinds = listOf(MetadataEvent.KIND),
limit = 100,
),
),
)
},
) { event -> ) { event ->
assertEquals(MetadataEvent.KIND, event.kind) assertEquals(MetadataEvent.KIND, event.kind)
resultChannel.trySend(event) resultChannel.trySend(event)
@@ -75,7 +68,7 @@ class NostrClientSubscriptionTest : BaseNostrClientTest() {
resultChannel.close() resultChannel.close()
sub.closeSubscription() sub.close()
client.disconnect() client.disconnect()
appScope.cancel() appScope.cancel()