Improvements on NIP-11

- Support for self, privacy_policy, terms_of_service, grasps
- New UI for the Relay Information Screen
- Improvements to the Debug Message
- Compose-stable objects
- Clickable elements for NIPs, external links, grasps, etc
This commit is contained in:
Vitor Pamplona
2026-01-06 17:38:47 -05:00
parent 36b50576d9
commit e26fb94933
10 changed files with 1008 additions and 365 deletions
@@ -21,8 +21,10 @@
package com.vitorpamplona.quartz.nip01Core.relay.client.stats
import androidx.collection.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.utils.TimeUtils
@Stable
class RelayStat(
var receivedBytes: Int = 0,
var sentBytes: Int = 0,
@@ -31,12 +33,11 @@ class RelayStat(
var pingInMs: Int = 0,
var compression: Boolean = false,
) {
val messages = LruCache<RelayDebugMessage, RelayDebugMessage>(100)
val messages = LruCache<IRelayDebugMessage, IRelayDebugMessage>(100)
fun newNotice(notice: String?) {
val debugMessage =
RelayDebugMessage(
type = RelayDebugMessageType.NOTICE,
NoticeDebugMessage(
message = notice ?: "No error message provided",
)
@@ -47,8 +48,7 @@ class RelayStat(
errorCounter++
val debugMessage =
RelayDebugMessage(
type = RelayDebugMessageType.ERROR,
ErrorDebugMessage(
message = error ?: "No error message provided",
)
@@ -63,27 +63,35 @@ class RelayStat(
sentBytes += bytesUsedInMemory
}
fun newSpam(spamDescriptor: String) {
fun newSpam(
link1: String,
link2: String,
) {
spamCounter++
val debugMessage =
RelayDebugMessage(
type = RelayDebugMessageType.SPAM,
message = spamDescriptor,
)
val debugMessage = SpamDebugMessage(link1, link2)
messages.put(debugMessage, debugMessage)
}
}
class RelayDebugMessage(
val type: RelayDebugMessageType,
val message: String,
val time: Long = TimeUtils.now(),
)
enum class RelayDebugMessageType {
SPAM,
NOTICE,
ERROR,
@Stable
sealed interface IRelayDebugMessage {
val time: Long
}
class SpamDebugMessage(
val link1: String,
val link2: String,
override val time: Long = TimeUtils.now(),
) : IRelayDebugMessage
class NoticeDebugMessage(
val message: String,
override val time: Long = TimeUtils.now(),
) : IRelayDebugMessage
class ErrorDebugMessage(
val message: String,
override val time: Long = TimeUtils.now(),
) : IRelayDebugMessage
@@ -20,6 +20,9 @@
*/
package com.vitorpamplona.quartz.nip01Core.relay.normalizer
import androidx.compose.runtime.Stable
@Stable
data class NormalizedRelayUrl(
val url: String,
) : Comparable<NormalizedRelayUrl> {
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip11RelayInfo
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.text
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
@@ -32,15 +33,14 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonPrimitive
object FlexibleIntListSerializer : KSerializer<List<Int>?> {
private val listSerializer = ListSerializer(Int.serializer())
object FlexibleIntListSerializer : KSerializer<List<String>?> {
private val listSerializer = ListSerializer(String.serializer())
override val descriptor: SerialDescriptor = listSerializer.descriptor
override fun deserialize(decoder: Decoder): List<Int>? {
override fun deserialize(decoder: Decoder): List<String>? {
require(decoder is JsonDecoder) { "This serializer can only be used with Json format" }
return when (val element = decoder.decodeJsonElement()) {
@@ -51,7 +51,7 @@ object FlexibleIntListSerializer : KSerializer<List<Int>?> {
is JsonArray -> {
element.mapNotNull { arrayElement ->
try {
arrayElement.jsonPrimitive.int
arrayElement.jsonPrimitive.text
} catch (e: Exception) {
// Skip elements that aren't valid integers (strings, booleans, floats, etc.)
Log.w("FlexibleIntListSerializer", "Invalid element in array: $arrayElement", e)
@@ -63,7 +63,7 @@ object FlexibleIntListSerializer : KSerializer<List<Int>?> {
// Handle single integer format (malformed but found in the wild): 1
is JsonPrimitive if !element.isString -> {
try {
listOf(element.int)
listOf(element.text)
} catch (e: Exception) {
// Can't parse as integer (e.g., float, boolean), treat as missing data
Log.w("FlexibleIntListSerializer", "Invalid primitive: $element", e)
@@ -79,7 +79,7 @@ object FlexibleIntListSerializer : KSerializer<List<Int>?> {
@OptIn(ExperimentalSerializationApi::class)
override fun serialize(
encoder: Encoder,
value: List<Int>?,
value: List<String>?,
) {
if (value == null) {
encoder.encodeNull()
@@ -21,19 +21,22 @@
package com.vitorpamplona.quartz.nip11RelayInfo
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
import kotlinx.serialization.Serializable
@Stable
@Serializable
class Nip11RelayInformation(
val id: String? = null,
val name: String? = null,
val description: String? = null,
val icon: String? = null,
val pubkey: String? = null,
val pubkey: HexKey? = null,
val self: HexKey? = null,
val contact: String? = null,
@Serializable(with = FlexibleIntListSerializer::class)
val supported_nips: List<Int>? = null,
val supported_nips: List<String>? = null,
val supported_nip_extensions: List<String>? = null,
val software: String? = null,
val version: String? = null,
@@ -42,53 +45,60 @@ class Nip11RelayInformation(
val language_tags: List<String>? = null,
val tags: List<String>? = null,
val posting_policy: String? = null,
val privacy_policy: String? = null,
val terms_of_service: String? = null,
val payments_url: String? = null,
val retention: List<RelayInformationRetentionData>? = null,
val fees: RelayInformationFees? = null,
val nip50: List<String>? = null,
val supported_grasps: List<String>? = null,
) {
companion object {
fun fromJson(json: String): Nip11RelayInformation = JsonMapper.fromJson<Nip11RelayInformation>(json)
}
@Stable
@Serializable
class RelayInformationFee(
val amount: Int? = null,
val unit: String? = null,
val period: Int? = null,
val kinds: List<Int>? = null,
)
@Stable
@Serializable
class RelayInformationFees(
val admission: List<RelayInformationFee>? = null,
val subscription: List<RelayInformationFee>? = null,
val publication: List<RelayInformationFee>? = null,
)
@Stable
@Serializable
class RelayInformationLimitation(
val max_message_length: Int? = null,
val max_subscriptions: Int? = null,
val max_filters: Int? = null,
val max_limit: Int? = null,
val default_limit: Int? = null,
val max_subid_length: Int? = null,
val min_prefix: Int? = null,
val max_event_tags: Int? = null,
val max_content_length: Int? = null,
val min_pow_difficulty: Int? = null,
val auth_required: Boolean? = null,
val payment_required: Boolean? = null,
val restricted_writes: Boolean? = null,
val created_at_lower_limit: Int? = null,
val created_at_upper_limit: Int? = null,
)
@Stable
@Serializable
class RelayInformationRetentionData(
val kinds: ArrayList<Int>? = null,
val time: Int? = null,
val count: Int? = null,
)
}
@Stable
@Serializable
class RelayInformationFee(
val amount: Int? = null,
val unit: String? = null,
val period: Int? = null,
val kinds: List<Int>? = null,
)
@Serializable
class RelayInformationFees(
val admission: List<RelayInformationFee>? = null,
val subscription: List<RelayInformationFee>? = null,
val publication: List<RelayInformationFee>? = null,
)
@Serializable
class RelayInformationLimitation(
val max_message_length: Int? = null,
val max_subscriptions: Int? = null,
val max_filters: Int? = null,
val max_limit: Int? = null,
val max_subid_length: Int? = null,
val min_prefix: Int? = null,
val max_event_tags: Int? = null,
val max_content_length: Int? = null,
val min_pow_difficulty: Int? = null,
val auth_required: Boolean? = null,
val payment_required: Boolean? = null,
val restricted_writes: Boolean? = null,
val created_at_lower_limit: Int? = null,
val created_at_upper_limit: Int? = null,
)
@Serializable
class RelayInformationRetentionData(
val kinds: ArrayList<Int>? = null,
val time: Int? = null,
val count: Int? = null,
)