mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-11 00:07:03 +01:00
Adds a basic OkHttp builder to Quartz (new dependency :( )
Adds some instructions to the basic readme.md
This commit is contained in:
74
README.md
74
README.md
@@ -257,6 +257,80 @@ Add the dependency
|
|||||||
implementation('com.github.vitorpamplona.amethyst:quartz:v0.85.1')
|
implementation('com.github.vitorpamplona.amethyst:quartz:v0.85.1')
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Manage logged in users with the `KeyPair` class
|
||||||
|
|
||||||
|
```kt
|
||||||
|
val keys = KeyPair() // creates a random key
|
||||||
|
val keys = KeyPair("hex...".hexToByteArray())
|
||||||
|
val keys = KeyPair("nsec1...".bechToBytes())
|
||||||
|
val keys = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
|
||||||
|
val readOnly = KeyPair(pubKey = "hex...".hexToByteArray())
|
||||||
|
val readOnly = KeyPair(pubKey = "npub1...".bechToBytes())
|
||||||
|
```
|
||||||
|
|
||||||
|
Create signers that can be internal, when you have the private key or when it is a read-only user
|
||||||
|
or external, when it is controlled by Amber in NIP-55
|
||||||
|
|
||||||
|
the `NostrSignerInternal` and `NostrSignerExternal` classes.
|
||||||
|
|
||||||
|
```kt
|
||||||
|
val signer = NostrSignerInternal(keyPair)
|
||||||
|
val amberSigner = NostrSignerExternal(
|
||||||
|
pubKey = keyPair.pubKey.toHexKey(),
|
||||||
|
packageName = signerPackageName,
|
||||||
|
contentResolver = appContext.contentResolver,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a single NostrClient for the entire application and control which relays it will access by
|
||||||
|
registering subscriptions and sending events. The pool will automatically changed based on filters +
|
||||||
|
outbox events.
|
||||||
|
|
||||||
|
You will need a coroutine scope to process events and if you are using OKHttp, we offer a basic
|
||||||
|
wrapper to create the socket connections themselves.
|
||||||
|
|
||||||
|
```kt
|
||||||
|
val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||||
|
val rootClient = OkHttpClient.Builder().build()
|
||||||
|
val socketBuilder = BasicOkHttpWebSocket.Builder { url -> rootClient }
|
||||||
|
|
||||||
|
val client = NostrClient(socketBuilder, appScope)
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to auth, given a logged-in `signer`:
|
||||||
|
|
||||||
|
```kt
|
||||||
|
val authCoordinator = RelayAuthenticator(client, applicationIOScope) { challenge, relay ->
|
||||||
|
val authedEvent = RelayAuthEvent.create(relayUrl, challenge, signer)
|
||||||
|
client.sendIfExists(authedEvent, relay.url)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To manage subscriptions, the suggested approach is to use subscriptions in the Application class.
|
||||||
|
|
||||||
|
```kt
|
||||||
|
val metadataSub = RelayClientSubscription(
|
||||||
|
client = client,
|
||||||
|
filter = {
|
||||||
|
val filters = listOf(
|
||||||
|
Filter(
|
||||||
|
kinds = listOf(MetadataEvent.KIND),
|
||||||
|
authors = listOf(signer.pubkey)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val signerOutboxRelays = listOfNotNull(
|
||||||
|
RelayUrlNormalizer.normalizeOrNull("wss://relay1.com"),
|
||||||
|
RelayUrlNormalizer.normalizeOrNull("wss://relay2.com")
|
||||||
|
)
|
||||||
|
|
||||||
|
signerOutboxRelays.associateWith { filters }
|
||||||
|
}
|
||||||
|
) { event ->
|
||||||
|
/* consume event */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Issues can be logged on: [https://gitworkshop.dev/repo/amethyst](https://gitworkshop.dev/repo/amethyst)
|
Issues can be logged on: [https://gitworkshop.dev/repo/amethyst](https://gitworkshop.dev/repo/amethyst)
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ dependencies {
|
|||||||
// Normalizes URLs
|
// Normalizes URLs
|
||||||
api libs.rfc3986.normalizer
|
api libs.rfc3986.normalizer
|
||||||
|
|
||||||
|
// Websockets API
|
||||||
|
implementation libs.okhttp
|
||||||
|
|
||||||
testImplementation libs.junit
|
testImplementation libs.junit
|
||||||
testImplementation libs.secp256k1.kmp.jni.jvm
|
testImplementation libs.secp256k1.kmp.jni.jvm
|
||||||
androidTestImplementation platform(libs.androidx.compose.bom)
|
androidTestImplementation platform(libs.androidx.compose.bom)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* 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.single.IRelayClient
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||||
|
|
||||||
|
class NostrClientSubscription(
|
||||||
|
val client: NostrClient,
|
||||||
|
val filter: () -> Map<NormalizedRelayUrl, List<Filter>> = { emptyMap() },
|
||||||
|
val onEvent: (event: Event) -> Unit = {},
|
||||||
|
) : IRelayClientListener {
|
||||||
|
private val subId = RandomInstance.randomChars(10)
|
||||||
|
|
||||||
|
init {
|
||||||
|
client.subscribe(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEvent(
|
||||||
|
relay: IRelayClient,
|
||||||
|
subId: String,
|
||||||
|
event: Event,
|
||||||
|
arrivalTime: Long,
|
||||||
|
afterEOSE: Boolean,
|
||||||
|
) {
|
||||||
|
if (this.subId == subId) {
|
||||||
|
onEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or Updates the filter with relays. This method should be called
|
||||||
|
* everytime the filter changes.
|
||||||
|
*/
|
||||||
|
fun updateFilter() = client.openReqSubscription(subId, filter())
|
||||||
|
|
||||||
|
fun closeSubscription() = client.close(subId)
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* 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.sockets.okhttp
|
||||||
|
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
||||||
|
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class BasicOkHttpWebSocket(
|
||||||
|
val url: NormalizedRelayUrl,
|
||||||
|
val httpClient: OkHttpClient,
|
||||||
|
val out: WebSocketListener,
|
||||||
|
) : WebSocket {
|
||||||
|
private var socket: okhttp3.WebSocket? = null
|
||||||
|
|
||||||
|
override fun needsReconnect() = socket == null
|
||||||
|
|
||||||
|
override fun connect() {
|
||||||
|
val request = Request.Builder().url(url.url).build()
|
||||||
|
|
||||||
|
val listener =
|
||||||
|
object : okhttp3.WebSocketListener() {
|
||||||
|
override fun onOpen(
|
||||||
|
webSocket: okhttp3.WebSocket,
|
||||||
|
response: Response,
|
||||||
|
) = out.onOpen(
|
||||||
|
response.receivedResponseAtMillis - response.sentRequestAtMillis,
|
||||||
|
response.headers["Sec-WebSocket-Extensions"]?.contains("permessage-deflate") ?: false,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onMessage(
|
||||||
|
webSocket: okhttp3.WebSocket,
|
||||||
|
text: String,
|
||||||
|
) = out.onMessage(text)
|
||||||
|
|
||||||
|
override fun onClosing(
|
||||||
|
webSocket: okhttp3.WebSocket,
|
||||||
|
code: Int,
|
||||||
|
reason: String,
|
||||||
|
) = out.onClosing(code, reason)
|
||||||
|
|
||||||
|
override fun onClosed(
|
||||||
|
webSocket: okhttp3.WebSocket,
|
||||||
|
code: Int,
|
||||||
|
reason: String,
|
||||||
|
) = out.onClosed(code, reason)
|
||||||
|
|
||||||
|
override fun onFailure(
|
||||||
|
webSocket: okhttp3.WebSocket,
|
||||||
|
t: Throwable,
|
||||||
|
r: Response?,
|
||||||
|
) = out.onFailure(t, r?.code, r?.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = httpClient.newWebSocket(request, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disconnect() {
|
||||||
|
socket?.cancel()
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun send(msg: String): Boolean = socket?.send(msg) ?: false
|
||||||
|
|
||||||
|
class Builder(
|
||||||
|
val httpClient: OkHttpClient,
|
||||||
|
) : WebsocketBuilder {
|
||||||
|
// Called when connecting.
|
||||||
|
override fun build(
|
||||||
|
url: NormalizedRelayUrl,
|
||||||
|
out: WebSocketListener,
|
||||||
|
) = BasicOkHttpWebSocket(url, httpClient, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user