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

@@ -268,7 +268,7 @@ val readOnly = KeyPair(pubKey = "npub1...".bechToBytes())
```
Create signers that can be Internal, when you have the private key or a read-only public key,
or External, when it is controlled by Amber in NIP-55.
or External, when it is controlled by Amber in NIP-55.
Use either the `NostrSignerInternal` or `NostrSignerExternal` class:
@@ -277,7 +277,7 @@ val signer = NostrSignerInternal(keyPair)
val amberSigner = NostrSignerExternal(
pubKey = keyPair.pubKey.toHexKey(),
packageName = signerPackageName, // Amber package name
contentResolver = appContext.contentResolver,
contentResolver = appContext.contentResolver,
)
```
@@ -305,39 +305,54 @@ val authCoordinator = RelayAuthenticator(client, appScope) { challenge, relay ->
}
```
To manage subscriptions, the simplest approach is to build mutable subscriptions in
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.
To make a request subscription simply do:
```kt
val metadataSub = NostrClientSubscription(
client = client,
filter = {
val metadataSub = client.req(
relays = listOf("wss://nos.lol", "wss://nostr.mom"),
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(
Filter(
kinds = listOf(MetadataEvent.KIND),
authors = listOf(signer.pubkey)
authors = users
)
)
val signerOutboxRelays = listOfNotNull(
RelayUrlNormalizer.normalizeOrNull("wss://relay1.com"),
RelayUrlNormalizer.normalizeOrNull("wss://relay2.com")
)
signerOutboxRelays.associateWith { filters }
outboxRelays.associateWith { filters }
}
) { event ->
/* consume event */
}
```
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
In that way, you can simply call `metadataSub.updateFilter()` when you need to update
subscriptions to all relays. Or call `metadataSub.close()` to stop the sub
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`
methods to stop all communication to relays. Add the `connect` to your `onResume` and `disconnect`
to `onPause` methods.

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
* 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.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.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.utils.RandomInstance
class NostrClientSubscription(
class NostrClientDynamicFilterRequest(
val client: INostrClient,
val filter: () -> Map<NormalizedRelayUrl, List<Filter>> = { emptyMap() },
val filter: () -> Map<NormalizedRelayUrl, List<Filter>>,
val onEvent: (event: Event) -> Unit = {},
) : IRequestListener {
) : IRequestListener,
IOpenNostrRequest {
private val subId = RandomInstance.randomChars(10)
override fun onEvent(
@@ -46,15 +47,16 @@ class NostrClientSubscription(
* Creates or Updates the filter with relays. This method should be called
* everytime the filter changes.
*/
fun updateFilter() = client.openReqSubscription(subId, filter(), this)
override fun updateFilter() = client.openReqSubscription(subId, filter(), this)
fun closeSubscription() = client.close(subId)
fun destroy() {
closeSubscription()
}
override fun close() = client.close(subId)
init {
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.metadata.MetadataEvent
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.normalizer.RelayUrlNormalizer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -48,19 +47,13 @@ class NostrClientSubscriptionTest : BaseNostrClientTest() {
val events = mutableSetOf<Event>()
val sub =
NostrClientSubscription(
client = client,
filter = {
mapOf(
RelayUrlNormalizer.normalize("wss://relay.damus.io") to
listOf(
Filter(
kinds = listOf(MetadataEvent.KIND),
limit = 100,
),
),
)
},
client.req(
relay = "wss://relay.damus.io",
filter =
Filter(
kinds = listOf(MetadataEvent.KIND),
limit = 100,
),
) { event ->
assertEquals(MetadataEvent.KIND, event.kind)
resultChannel.trySend(event)
@@ -75,7 +68,7 @@ class NostrClientSubscriptionTest : BaseNostrClientTest() {
resultChannel.close()
sub.closeSubscription()
sub.close()
client.disconnect()
appScope.cancel()