mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 07:48:27 +00:00
Compare commits
1 Commits
c79b795477
...
claude/qua
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f22dcbd5bb |
@@ -30,6 +30,7 @@ jtorctl = "0.4.5.7"
|
||||
junit = "4.13.2"
|
||||
kchesslib = "1.0.5"
|
||||
kotlin = "2.3.20"
|
||||
ktor = "3.1.2"
|
||||
kotlinxCollectionsImmutable = "0.4.0"
|
||||
kotlinxCoroutinesCore = "1.10.2"
|
||||
kotlinxSerialization = "1.10.0"
|
||||
@@ -161,6 +162,11 @@ markdown-ui = { group = "com.github.vitorpamplona.compose-richtext", name = "ric
|
||||
markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-ui-material3", version.ref = "markdown" }
|
||||
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
|
||||
mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
|
||||
ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
|
||||
ktor-client-websockets = { group = "io.ktor", name = "ktor-client-websockets", version.ref = "ktor" }
|
||||
ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
|
||||
ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
|
||||
ktor-client-darwin = { group = "io.ktor", name = "ktor-client-darwin", version.ref = "ktor" }
|
||||
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"}
|
||||
net-thauvin-erik-urlencoder-lib = { module = "net.thauvin.erik.urlencoder:urlencoder-lib", version.ref = "netUrlencoderLibVersion" }
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
|
||||
@@ -198,6 +198,10 @@ kotlin {
|
||||
// immutable collections to avoid recomposition
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
// Ktor KMP HTTP & WebSocket client
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.websockets)
|
||||
|
||||
// SQLite KMP driver for event store
|
||||
api(libs.androidx.sqlite)
|
||||
implementation(libs.androidx.sqlite.bundled)
|
||||
@@ -231,6 +235,9 @@ kotlin {
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttpCoroutines)
|
||||
|
||||
// Ktor engine for JVM/Android (uses OkHttp under the hood)
|
||||
implementation(libs.ktor.client.okhttp)
|
||||
|
||||
// Chess engine for move validation and legal move generation
|
||||
// NOTE: 1.0.4+ uses Java 21's removeLast() which crashes on Android API < 34
|
||||
// TODO: Test if 1.0.0 works, or fork library to fix
|
||||
@@ -324,6 +331,9 @@ kotlin {
|
||||
iosMain {
|
||||
dependsOn(commonMain.get())
|
||||
dependencies {
|
||||
// Ktor engine for iOS (uses URLSession under the hood)
|
||||
implementation(libs.ktor.client.darwin)
|
||||
|
||||
implementation(libs.charlietap.cachemap)
|
||||
implementation(libs.net.thauvin.erik.urlencoder.lib)
|
||||
implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal)
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.ktor
|
||||
|
||||
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 com.vitorpamplona.quartz.utils.Log
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.websocket.CloseReason
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class KtorWebSocket(
|
||||
val url: NormalizedRelayUrl,
|
||||
val httpClient: HttpClient,
|
||||
val out: WebSocketListener,
|
||||
) : WebSocket {
|
||||
companion object {
|
||||
val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e("KtorWebSocket", "WebsocketListener Caught exception: ${throwable.message}", throwable)
|
||||
}
|
||||
}
|
||||
|
||||
private var session: WebSocketSession? = null
|
||||
private var connectionJob: Job? = null
|
||||
private val scope = CoroutineScope(Dispatchers.IO + exceptionHandler)
|
||||
|
||||
override fun needsReconnect() = session == null
|
||||
|
||||
override fun connect() {
|
||||
connectionJob =
|
||||
scope.launch {
|
||||
try {
|
||||
val wsSession = httpClient.webSocketSession(url.url)
|
||||
session = wsSession
|
||||
|
||||
// Notify open — Ktor doesn't expose ping timing or compression headers
|
||||
// the same way OkHttp does, so we use defaults.
|
||||
out.onOpen(
|
||||
pingMillis = 0,
|
||||
compression = false,
|
||||
)
|
||||
|
||||
// Process incoming frames sequentially to maintain message ordering
|
||||
for (frame in wsSession.incoming) {
|
||||
when (frame) {
|
||||
is Frame.Text -> {
|
||||
out.onMessage(frame.readText())
|
||||
}
|
||||
|
||||
else -> { /* ignore binary, ping, pong frames */ }
|
||||
}
|
||||
}
|
||||
|
||||
// If the loop ends normally, the connection was closed
|
||||
val closeReason = wsSession.closeReason.await()
|
||||
session = null
|
||||
out.onClosed(
|
||||
closeReason?.code?.toInt() ?: 1000,
|
||||
closeReason?.message ?: "",
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
session = null
|
||||
out.onFailure(e, null, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun disconnect() {
|
||||
val currentSession = session
|
||||
session = null
|
||||
scope.launch {
|
||||
try {
|
||||
currentSession?.close(CloseReason(CloseReason.Codes.NORMAL, ""))
|
||||
} catch (e: Exception) {
|
||||
Log.e("KtorWebSocket", "Error closing WebSocket: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
connectionJob?.cancel()
|
||||
connectionJob = null
|
||||
}
|
||||
|
||||
override fun send(msg: String): Boolean {
|
||||
val currentSession = session ?: return false
|
||||
if (!currentSession.isActive) return false
|
||||
scope.launch {
|
||||
try {
|
||||
currentSession.send(Frame.Text(msg))
|
||||
} catch (e: Exception) {
|
||||
Log.e("KtorWebSocket", "Error sending message: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class Builder(
|
||||
val httpClient: HttpClient,
|
||||
) : WebsocketBuilder {
|
||||
override fun build(
|
||||
url: NormalizedRelayUrl,
|
||||
out: WebSocketListener,
|
||||
) = KtorWebSocket(url, httpClient, out)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.nip03Timestamp.ktor
|
||||
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.BitcoinExplorer
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.isSuccess
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
class KtorBitcoinExplorer(
|
||||
val baseUrl: () -> String,
|
||||
val client: HttpClient,
|
||||
val cache: OtsBlockHeightCache,
|
||||
) : BitcoinExplorer {
|
||||
override suspend fun block(hash: String): BlockHeader {
|
||||
cache.getHeader(hash)?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val baseAPI = baseUrl()
|
||||
val url = "$baseAPI/block/$hash"
|
||||
|
||||
val response =
|
||||
client.get(url) {
|
||||
header("Accept", "application/json")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
Log.d("KtorBitcoinExplorer", "$baseAPI/block/$hash")
|
||||
|
||||
val jsonObject = Json.parseToJsonElement(response.bodyAsText()).jsonObject
|
||||
|
||||
val merkleRoot = jsonObject["merkle_root"]!!.jsonPrimitive.content
|
||||
val time = jsonObject["timestamp"]!!.jsonPrimitive.content
|
||||
val blockHeader = BlockHeader(merkleRoot, hash, time)
|
||||
cache.putHeader(hash, blockHeader)
|
||||
return blockHeader
|
||||
} else {
|
||||
throw UrlException("Couldn't open $url: ${response.status.description} ${response.status.value}")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override suspend fun blockHash(height: Int): String {
|
||||
cache.getHeight(height)?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val baseAPI = baseUrl()
|
||||
val url = "$baseAPI/block-height/$height"
|
||||
|
||||
val response = client.get(url)
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val blockHash = response.bodyAsText()
|
||||
|
||||
Log.d("KtorBitcoinExplorer", "$url $blockHash")
|
||||
|
||||
cache.putHeight(height, blockHash)
|
||||
return blockHash
|
||||
} else {
|
||||
throw UrlException("Couldn't open $url: ${response.status.description} ${response.status.value}")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// doesn't accept Tor
|
||||
const val BLOCKSTREAM_API_URL = "https://blockstream.info/api"
|
||||
|
||||
// accepts Tor
|
||||
const val MEMPOOL_API_URL = "https://mempool.space/api/"
|
||||
}
|
||||
}
|
||||
@@ -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.nip03Timestamp.ktor
|
||||
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.RemoteCalendar
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.CommitmentNotFoundException
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.DeserializationException
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.ExceededSizeException
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.readRawBytes
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.http.isSuccess
|
||||
|
||||
/**
|
||||
* Class representing remote calendar server interface using Ktor.
|
||||
*/
|
||||
class KtorCalendar(
|
||||
val httpClient: HttpClient,
|
||||
) : RemoteCalendar {
|
||||
override suspend fun submit(
|
||||
url: String,
|
||||
digest: ByteArray,
|
||||
): Timestamp {
|
||||
val requestUrl = "$url/digest"
|
||||
|
||||
val response =
|
||||
httpClient.post(requestUrl) {
|
||||
header("Accept", "application/vnd.opentimestamps.v1")
|
||||
contentType(ContentType.Application.FormUrlEncoded)
|
||||
setBody(digest)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val ctx = StreamDeserializationContext(response.readRawBytes())
|
||||
return Timestamp.deserialize(ctx, digest)
|
||||
} else {
|
||||
throw UrlException("Failed to open $requestUrl")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTimestamp(
|
||||
url: String,
|
||||
commitment: ByteArray,
|
||||
): Timestamp =
|
||||
try {
|
||||
val requestUrl = url + "/timestamp/" + Hex.encode(commitment)
|
||||
|
||||
val response =
|
||||
httpClient.get(requestUrl) {
|
||||
header("Accept", "application/vnd.opentimestamps.v1")
|
||||
contentType(ContentType.Application.FormUrlEncoded)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val ctx = StreamDeserializationContext(response.readRawBytes())
|
||||
Timestamp.deserialize(ctx, commitment)
|
||||
} else {
|
||||
throw CommitmentNotFoundException("Calendar response a status code != 200: " + response.status.value)
|
||||
}
|
||||
} catch (e: DeserializationException) {
|
||||
throw e
|
||||
} catch (e: ExceededSizeException) {
|
||||
throw e
|
||||
} catch (e: CommitmentNotFoundException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw UrlException(e.message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.nip03Timestamp.ktor
|
||||
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
class KtorOtsResolverBuilder(
|
||||
val cache: OtsBlockHeightCache = OtsBlockHeightCache(),
|
||||
val httpClient: HttpClient,
|
||||
) : OtsResolverBuilder {
|
||||
override fun build(): OtsResolver =
|
||||
OtsResolver(
|
||||
explorer =
|
||||
KtorBitcoinExplorer(
|
||||
baseUrl = {
|
||||
KtorBitcoinExplorer.BLOCKSTREAM_API_URL
|
||||
},
|
||||
client = httpClient,
|
||||
cache = cache,
|
||||
),
|
||||
calendar = KtorCalendar(httpClient),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.nip05DnsIdentifiers.ktor
|
||||
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05Fetcher
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.HttpRedirect
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.isSuccess
|
||||
|
||||
class KtorNip05Fetcher(
|
||||
val httpClient: HttpClient,
|
||||
) : Nip05Fetcher {
|
||||
// NIP-05 requires ignoring HTTP redirects, so we create a dedicated client with redirects disabled.
|
||||
private val noRedirectClient =
|
||||
HttpClient(httpClient.engine) {
|
||||
install(HttpRedirect) {
|
||||
checkHttpMethod = false
|
||||
allowHttpsDowngrade = false
|
||||
}
|
||||
followRedirects = false
|
||||
}
|
||||
|
||||
override suspend fun fetch(url: String): String {
|
||||
val response = noRedirectClient.get(url)
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
return response.bodyAsText()
|
||||
} else {
|
||||
throw IllegalStateException("Error: ${response.status.value}, ${response.status.description}")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user