Refactors the old NIP-05 code on Quartz
New Caching system for User metadata New Caching system for NIP-05 verifications
This commit is contained in:
+11
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.metadata
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
@@ -37,6 +38,7 @@ import com.vitorpamplona.quartz.nip01Core.metadata.tags.PronounsTag
|
||||
import com.vitorpamplona.quartz.nip01Core.metadata.tags.WebsiteTag
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
|
||||
import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag
|
||||
@@ -62,6 +64,8 @@ class MetadataEvent(
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
fun contactMetadataJson() =
|
||||
try {
|
||||
Json.parseToJsonElement(content) as JsonObject
|
||||
@@ -82,6 +86,13 @@ class MetadataEvent(
|
||||
|
||||
companion object {
|
||||
const val KIND = 0
|
||||
const val FIXED_D_TAG = ""
|
||||
|
||||
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
|
||||
|
||||
fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG)
|
||||
|
||||
fun newUser(
|
||||
name: String?,
|
||||
|
||||
+16
-9
@@ -21,7 +21,6 @@
|
||||
package com.vitorpamplona.quartz.nip01Core.metadata
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -38,21 +37,13 @@ class UserMetadata {
|
||||
var about: String? = null
|
||||
var bot: Boolean? = null
|
||||
var pronouns: String? = null
|
||||
|
||||
var nip05: String? = null
|
||||
var nip05Verified: Boolean = false
|
||||
var nip05LastVerificationTime: Long? = 0
|
||||
|
||||
var domain: String? = null
|
||||
var lud06: String? = null
|
||||
var lud16: String? = null
|
||||
|
||||
var twitter: String? = null
|
||||
|
||||
@kotlinx.serialization.Transient
|
||||
@kotlin.jvm.Transient
|
||||
var tags: ImmutableListOfLists<String>? = null
|
||||
|
||||
fun anyName(): String? = displayName ?: name
|
||||
|
||||
fun anyNameStartsWith(prefix: String): Boolean =
|
||||
@@ -64,6 +55,22 @@ class UserMetadata {
|
||||
|
||||
fun bestName(): String? = displayName ?: name
|
||||
|
||||
fun firstName(): String? {
|
||||
val fullName = bestName() ?: return null
|
||||
|
||||
val names = fullName.split(' ')
|
||||
|
||||
val firstName =
|
||||
if (names[0].length <= 3) {
|
||||
// too short. Remove Dr.
|
||||
"${names[0]} ${names.getOrNull(1) ?: ""}"
|
||||
} else {
|
||||
names[0]
|
||||
}
|
||||
|
||||
return firstName
|
||||
}
|
||||
|
||||
fun nip05(): String? = nip05
|
||||
|
||||
fun profilePicture(): String? = picture
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
class Nip05 {
|
||||
fun assembleUrl(nip05address: String): String? {
|
||||
val parts = nip05address.trim().split("@")
|
||||
|
||||
if (parts.size == 2) {
|
||||
return "https://${parts[1]}/.well-known/nostr.json?name=${parts[0]}"
|
||||
}
|
||||
if (parts.size == 1) {
|
||||
return "https://${parts[0]}/.well-known/nostr.json?name=_"
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun parseHexKeyFor(
|
||||
nip05: String,
|
||||
returnBody: String,
|
||||
): Result<String?> {
|
||||
val parts = nip05.split("@")
|
||||
val user =
|
||||
if (parts.size == 2) {
|
||||
parts[0].lowercase()
|
||||
} else {
|
||||
"_"
|
||||
}
|
||||
|
||||
// NIP05 usernames are case insensitive, but JSON properties are not
|
||||
// converts the json to lowercase and then tries to access the username via a
|
||||
// lowercase version of the username.
|
||||
return try {
|
||||
val rootElement = Json.parseToJsonElement(returnBody.lowercase())
|
||||
val hexKey =
|
||||
rootElement.jsonObject["names"]
|
||||
?.jsonObject[user]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
Result.success(hexKey)
|
||||
} catch (e: Throwable) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w("Nip05", "Unable to Parse NIP-05 for $nip05 with $returnBody", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
data class Nip05KeyInfo(
|
||||
val pubkey: HexKey,
|
||||
val relays: List<String>,
|
||||
)
|
||||
|
||||
class Nip05Client(
|
||||
val fetcher: Nip05Fetcher,
|
||||
) {
|
||||
val parser = Nip05Parser()
|
||||
|
||||
suspend fun verify(
|
||||
nip05: Nip05Id,
|
||||
hexKey: HexKey,
|
||||
): Boolean {
|
||||
val json = fetchNip05Data(nip05)
|
||||
|
||||
val key =
|
||||
try {
|
||||
parser.parseHexKey(nip05, json)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
throw IllegalStateException("Error Parsing JSON from NIP-05 address $nip05", e)
|
||||
}
|
||||
|
||||
return if (key == null) {
|
||||
false
|
||||
} else {
|
||||
key == hexKey
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun get(nip05: Nip05Id) = parser.parseHexKeyAndRelays(nip05, fetchNip05Data(nip05))
|
||||
|
||||
suspend fun load(nip05: Nip05Id) = parser.parse(fetchNip05Data(nip05))
|
||||
|
||||
suspend fun list(domain: String) = parser.parse(fetchNip05Data(domain))
|
||||
|
||||
suspend fun fetchNip05Data(nip05: Nip05Id): String {
|
||||
val url = nip05.toUserUrl()
|
||||
|
||||
return try {
|
||||
fetcher.fetch(url)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
throw IllegalStateException("Error Fetching JSON from NIP-05 address $nip05 at $url", e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchNip05Data(domain: String): String {
|
||||
val url = Nip05Id.domainUrl(domain)
|
||||
|
||||
return try {
|
||||
fetcher.fetch(url)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
throw IllegalStateException("Error Fetching JSON from the entire NIP-05 domain $domain at $url", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-11
@@ -18,15 +18,8 @@
|
||||
* 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.core
|
||||
package com.vitorpamplona.quartz.nip05DnsIdentifiers
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
class ImmutableListOfLists<T>(
|
||||
val lists: Array<Array<T>>,
|
||||
)
|
||||
|
||||
val EmptyTagList = ImmutableListOfLists<String>(emptyArray())
|
||||
|
||||
fun Array<Array<String>>.toImmutableListOfLists(): ImmutableListOfLists<String> = ImmutableListOfLists(this)
|
||||
interface Nip05Fetcher {
|
||||
suspend fun fetch(url: String): String
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
data class Nip05Id(
|
||||
val name: String,
|
||||
val domain: String,
|
||||
) {
|
||||
fun toValue(): String = assemble(name, domain)
|
||||
|
||||
fun toUserUrl(): String = userUrl(name, domain)
|
||||
|
||||
fun toDomainUrl(): String = domainUrl(domain)
|
||||
|
||||
companion object {
|
||||
fun parse(nip05address: String): Nip05Id? {
|
||||
val parts = nip05address.trim().lowercase().split("@")
|
||||
|
||||
return when (parts.size) {
|
||||
2 -> Nip05Id(parts[0], parts[1])
|
||||
1 -> Nip05Id(parts[0], "_")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
name: String,
|
||||
domain: String,
|
||||
) = "$name@$domain"
|
||||
|
||||
fun userUrl(
|
||||
name: String,
|
||||
domain: String,
|
||||
) = "https://$domain/.well-known/nostr.json?name=$name"
|
||||
|
||||
fun domainUrl(domain: String) = "https://$domain/.well-known/nostr.json"
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.utils.text
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
@Serializable
|
||||
data class KeyInfoSet(
|
||||
val names: Map<String, String>,
|
||||
val relays: Map<String, List<String>>,
|
||||
)
|
||||
|
||||
class Nip05Parser {
|
||||
fun toJson(keyInfo: KeyInfoSet) = Json.encodeToString(keyInfo)
|
||||
|
||||
fun parse(json: String) = Json.decodeFromString<KeyInfoSet>(json)
|
||||
|
||||
fun parseHexKey(
|
||||
nip05: Nip05Id,
|
||||
json: String,
|
||||
): HexKey? =
|
||||
Json
|
||||
.parseToJsonElement(json)
|
||||
.jsonObject["names"]
|
||||
?.jsonObject[nip05.name]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
|
||||
fun parseHexKeyAndRelays(
|
||||
nip05: Nip05Id,
|
||||
json: String,
|
||||
): Nip05KeyInfo? {
|
||||
val rootElement = Json.parseToJsonElement(json)
|
||||
|
||||
val hexKey =
|
||||
rootElement.jsonObject["names"]
|
||||
?.jsonObject[nip05.name]
|
||||
?.jsonPrimitive
|
||||
?.content
|
||||
|
||||
return if (!hexKey.isNullOrEmpty()) {
|
||||
val relays =
|
||||
rootElement.jsonObject["relays"]
|
||||
?.jsonObject[hexKey]
|
||||
?.jsonArray
|
||||
?.map {
|
||||
it.jsonPrimitive.text
|
||||
} ?: emptyList()
|
||||
|
||||
Nip05KeyInfo(hexKey, relays)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,8 @@ object TimeUtils {
|
||||
|
||||
fun fiveMinutesAgo() = now() - FIVE_MINUTES
|
||||
|
||||
fun fiveMinutesAhead() = now() + FIVE_MINUTES
|
||||
|
||||
fun fifteenMinutesAgo() = now() - FIFTEEN_MINUTES
|
||||
|
||||
fun oneHourAgo() = now() - ONE_HOUR
|
||||
|
||||
+81
-23
@@ -23,45 +23,66 @@ package com.vitorpamplona.quartz.nip05DnsIdentifiers
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.fail
|
||||
|
||||
class Nip05Test {
|
||||
companion object {
|
||||
private val ALL_UPPER_CASE_USER_NAME = "ONETWO"
|
||||
private val ALL_LOWER_CASE_USER_NAME = "onetwo"
|
||||
private const val ALL_UPPER_CASE_USER_NAME = "ONETWO"
|
||||
private const val ALL_LOWER_CASE_USER_NAME = "onetwo"
|
||||
}
|
||||
|
||||
var nip05Verifier = Nip05()
|
||||
val parser = Nip05Parser()
|
||||
val baseTestJson =
|
||||
"""
|
||||
{
|
||||
"names": {
|
||||
"bob": "b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9"
|
||||
},
|
||||
"relays": {
|
||||
"b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9": [ "wss://relay.example.com", "wss://relay2.example.com" ]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
@Test
|
||||
fun `test with matching case on user name`() =
|
||||
runTest {
|
||||
// Set-up
|
||||
val userNameToTest = ALL_UPPER_CASE_USER_NAME
|
||||
val userNameToTest = ALL_LOWER_CASE_USER_NAME
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
val nostrJson = "{\n \"names\": {\n \"$userNameToTest\": \"$expectedPubKey\" \n }\n}"
|
||||
val nip05 = "$userNameToTest@domain.com"
|
||||
|
||||
nip05Verifier.parseHexKeyFor(nip05, nostrJson).fold(
|
||||
onSuccess = { assertEquals(expectedPubKey, it) },
|
||||
onFailure = { fail("Test failure") },
|
||||
)
|
||||
val parsedNip05 = Nip05Id.parse(nip05)
|
||||
assertNotNull(parsedNip05)
|
||||
assertEquals(expectedPubKey, parser.parseHexKey(parsedNip05, nostrJson))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test with NOT matching case on user name`() =
|
||||
fun `test failure of lowercase name with uppercase in the json`() =
|
||||
runTest {
|
||||
// Set-up
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
val nostrJson = "{ \"names\": { \"$ALL_UPPER_CASE_USER_NAME\": \"$expectedPubKey\" }}"
|
||||
|
||||
val nip05 = "$ALL_LOWER_CASE_USER_NAME@domain.com"
|
||||
val parsedNip05 = Nip05Id.parse(nip05)
|
||||
assertNotNull(parsedNip05)
|
||||
assertEquals(null, parser.parseHexKey(parsedNip05, nostrJson))
|
||||
}
|
||||
|
||||
nip05Verifier.parseHexKeyFor(nip05, nostrJson).fold(
|
||||
onSuccess = { assertEquals(expectedPubKey, it) },
|
||||
onFailure = { fail("Test failure") },
|
||||
)
|
||||
@Test
|
||||
fun `test uppercase name with lowercase name in the json`() =
|
||||
runTest {
|
||||
// Set-up
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
val nostrJson = "{ \"names\": { \"$ALL_LOWER_CASE_USER_NAME\": \"$expectedPubKey\" }}"
|
||||
|
||||
val nip05 = "$ALL_UPPER_CASE_USER_NAME@domain.com"
|
||||
val parsedNip05 = Nip05Id.parse(nip05)
|
||||
assertNotNull(parsedNip05)
|
||||
assertEquals(expectedPubKey, parser.parseHexKey(parsedNip05, nostrJson))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -69,11 +90,8 @@ class Nip05Test {
|
||||
// given
|
||||
val nip05address = "this@that@that.com"
|
||||
|
||||
// when
|
||||
val actualValue = nip05Verifier.assembleUrl(nip05address)
|
||||
|
||||
// then
|
||||
assertNull(actualValue)
|
||||
val parsedNip05 = Nip05Id.parse(nip05address)
|
||||
assertNull(parsedNip05)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,12 +100,52 @@ class Nip05Test {
|
||||
val userName = "TheUser"
|
||||
val domain = "domain.com"
|
||||
val nip05address = "$userName@$domain"
|
||||
val expectedValue = "https://$domain/.well-known/nostr.json?name=$userName"
|
||||
|
||||
// when
|
||||
val actualValue = nip05Verifier.assembleUrl(nip05address)
|
||||
val parsedNip05 = Nip05Id.parse(nip05address)
|
||||
assertNotNull(parsedNip05)
|
||||
|
||||
// then
|
||||
assertEquals(expectedValue, actualValue)
|
||||
assertEquals("https://$domain/.well-known/nostr.json?name=${userName.lowercase()}", parsedNip05.toUserUrl())
|
||||
assertEquals("https://$domain/.well-known/nostr.json", parsedNip05.toDomainUrl())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test json parsing with relays`() {
|
||||
val parsedNip05 = Nip05Id.parse("bob@test.com")
|
||||
assertNotNull(parsedNip05)
|
||||
|
||||
val result = parser.parseHexKeyAndRelays(parsedNip05, baseTestJson)
|
||||
assertNotNull(result)
|
||||
assertEquals("b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", result.pubkey)
|
||||
assertEquals(
|
||||
listOf("wss://relay.example.com", "wss://relay2.example.com"),
|
||||
result.relays,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test full json parsing with relays`() {
|
||||
val result = parser.parse(baseTestJson)
|
||||
|
||||
assertNotNull(result)
|
||||
|
||||
assertEquals(1, result.names.size)
|
||||
assertEquals(1, result.relays.size)
|
||||
|
||||
assertEquals("bob", result.names.keys.first())
|
||||
assertEquals("b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", result.names["bob"])
|
||||
assertEquals("b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9", result.relays.keys.first())
|
||||
assertEquals(
|
||||
listOf("wss://relay.example.com", "wss://relay2.example.com"),
|
||||
result.relays["b0635d6a9851d3aed0cd6c495b282167acf761729078d975fc341b22650b07b9"],
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test full json generation with relays`() {
|
||||
val result = parser.parse(baseTestJson)
|
||||
val newResult = parser.parse(parser.toJson(result))
|
||||
|
||||
assertEquals(result, newResult)
|
||||
}
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.coroutines.executeAsync
|
||||
|
||||
class OkHttpNip05Fetcher(
|
||||
val okHttpClient: (String) -> OkHttpClient,
|
||||
) : Nip05Fetcher {
|
||||
override suspend fun fetch(url: String): String =
|
||||
withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder().url(url).build()
|
||||
|
||||
// Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint.
|
||||
val client = okHttpClient(url).newBuilder().followRedirects(false).build()
|
||||
|
||||
client.newCall(request).executeAsync().use { response ->
|
||||
withContext(Dispatchers.IO) {
|
||||
if (response.isSuccessful) {
|
||||
response.body.string()
|
||||
} else {
|
||||
throw IllegalStateException("Error: ${response.code}, ${response.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user