From 49e2f2c7c99264ff89d3d586e729b59ea6eeb96e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 21:20:43 +0000 Subject: [PATCH] feat: add NIP-66 relay monitor and discovery support to quartz Implements both event kinds from NIP-66 (Relay Monitor Protocol): - Kind 10166 (RelayMonitorEvent) - replaceable event published by monitoring services, declaring check frequency, timeout values, and supported check types (open, read, write, auth, nip11, dns, geo) - Kind 30166 (RelayDiscoveryEvent) - addressable event (keyed by relay URL in d-tag) published per monitored relay, carrying RTT metrics, network type, relay type, supported NIPs, accepted kinds, access requirements, topics, and geohash Package: nip66RelayMonitor/{monitor,discovery} following existing NIP-88 patterns with tag classes, TagArrayBuilderExt, and TagArrayExt. Both events registered in EventFactory. https://claude.ai/code/session_01Wtg56Vudthgfyz8ASTb4p7 --- .../discovery/RelayDiscoveryEvent.kt | 115 ++++++++++++++++++ .../discovery/TagArrayBuilderExt.kt | 62 ++++++++++ .../discovery/TagArrayExt.kt | 47 +++++++ .../discovery/tags/AcceptedKindTag.kt | 67 ++++++++++ .../discovery/tags/NetworkTypeTag.kt | 54 ++++++++ .../discovery/tags/RelayTypeTag.kt | 47 +++++++ .../discovery/tags/RequirementTag.kt | 65 ++++++++++ .../discovery/tags/RttTag.kt | 66 ++++++++++ .../discovery/tags/SupportedNipTag.kt | 44 +++++++ .../monitor/RelayMonitorEvent.kt | 85 +++++++++++++ .../monitor/TagArrayBuilderExt.kt | 33 +++++ .../nip66RelayMonitor/monitor/TagArrayExt.kt | 32 +++++ .../monitor/tags/CheckTag.kt | 57 +++++++++ .../monitor/tags/FrequencyTag.kt | 42 +++++++ .../monitor/tags/TimeoutTag.kt | 73 +++++++++++ .../quartz/utils/EventFactory.kt | 4 + 16 files changed, 893 insertions(+) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/RelayDiscoveryEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/AcceptedKindTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/NetworkTypeTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RelayTypeTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RequirementTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RttTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/SupportedNipTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/RelayMonitorEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/CheckTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/FrequencyTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/TimeoutTag.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/RelayDiscoveryEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/RelayDiscoveryEvent.kt new file mode 100644 index 000000000..45809b6b3 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/RelayDiscoveryEvent.kt @@ -0,0 +1,115 @@ +/* + * 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.nip66RelayMonitor.discovery + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.AcceptedKindTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.NetworkType +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RequirementTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttType +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * NIP-66 Kind 30166: Relay Discovery Event + * + * Published by relay monitors to document the state of a specific relay. + * The `d` tag holds the normalized relay URL, making each relay URL + * addressable per monitor pubkey. + * + * Carries metrics and metadata observed during monitoring: + * - Round-trip times (rtt-open, rtt-read, rtt-write) + * - Network type (n), relay type (T) + * - Supported NIPs (N), accepted kinds (k) + * - Requirements (R: auth, payment, pow, writes) + * - Topics (t) and geohash (g) + */ +@Immutable +class RelayDiscoveryEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun relay(): NormalizedRelayUrl? = RelayUrlNormalizer.normalizeOrNull(dTag()) + + fun rtt() = tags.rtts() + + fun rttOpen() = tags.rtt(RttType.OPEN) + + fun rttRead() = tags.rtt(RttType.READ) + + fun rttWrite() = tags.rtt(RttType.WRITE) + + fun networkTypes() = tags.networkTypes() + + fun relayTypes() = tags.relayTypes() + + fun supportedNips() = tags.supportedNips() + + fun requirements() = tags.requirements() + + fun acceptedKinds() = tags.acceptedKinds() + + fun topics() = tags.topics() + + fun geohashes() = tags.geohashes() + + companion object { + const val KIND = 30166 + const val ALT_DESCRIPTION = "Relay discovery" + + fun build( + relayUrl: NormalizedRelayUrl, + content: String = "", + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, content, createdAt) { + alt(ALT_DESCRIPTION) + dTag(relayUrl.url) + initializer() + } + + fun build( + relayUrl: String, + content: String = "", + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build( + relayUrl = RelayUrlNormalizer.normalize(relayUrl), + content = content, + createdAt = createdAt, + initializer = initializer, + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayBuilderExt.kt new file mode 100644 index 000000000..73234032d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayBuilderExt.kt @@ -0,0 +1,62 @@ +/* + * 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.nip66RelayMonitor.discovery + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.AcceptedKindTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.NetworkType +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.NetworkTypeTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RelayTypeTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RequirementTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttType +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.SupportedNipTag + +fun TagArrayBuilder.rtt( + type: RttType, + milliseconds: Long, +) = addUnique(RttTag.assemble(type, milliseconds)) + +fun TagArrayBuilder.networkType(network: NetworkType) = addUnique(NetworkTypeTag.assemble(network)) + +fun TagArrayBuilder.networkTypes(networks: List) = addAll(NetworkTypeTag.assemble(networks)) + +fun TagArrayBuilder.relayType(type: String) = addUnique(RelayTypeTag.assemble(type)) + +fun TagArrayBuilder.relayTypes(types: List) = addAll(RelayTypeTag.assemble(types)) + +fun TagArrayBuilder.supportedNip(nip: Int) = add(SupportedNipTag.assemble(nip)) + +fun TagArrayBuilder.supportedNips(nips: List) = addAll(SupportedNipTag.assemble(nips)) + +fun TagArrayBuilder.requirement( + value: String, + negated: Boolean = false, +) = add(RequirementTag.assemble(value, negated)) + +fun TagArrayBuilder.requirements(reqs: List) = addAll(RequirementTag.assemble(reqs)) + +fun TagArrayBuilder.acceptedKind( + kind: Int, + negated: Boolean = false, +) = add(AcceptedKindTag.assemble(kind, negated)) + +fun TagArrayBuilder.acceptedKinds(kinds: List) = addAll(AcceptedKindTag.assemble(kinds)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayExt.kt new file mode 100644 index 000000000..a2944d40b --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/TagArrayExt.kt @@ -0,0 +1,47 @@ +/* + * 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.nip66RelayMonitor.discovery + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.AcceptedKindTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.NetworkTypeTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RelayTypeTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RequirementTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttTag +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.RttType +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.tags.SupportedNipTag + +fun TagArray.rtts() = mapNotNull(RttTag::parse) + +fun TagArray.rtt(type: RttType) = mapNotNull(RttTag::parse).firstOrNull { it.type == type }?.milliseconds + +fun TagArray.networkTypes() = mapNotNull(NetworkTypeTag::parse) + +fun TagArray.relayTypes() = mapNotNull(RelayTypeTag::parse) + +fun TagArray.supportedNips() = mapNotNull(SupportedNipTag::parse) + +fun TagArray.requirements() = mapNotNull(RequirementTag::parse) + +fun TagArray.acceptedKinds() = mapNotNull(AcceptedKindTag::parse) + +fun TagArray.topics() = hashtags() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/AcceptedKindTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/AcceptedKindTag.kt new file mode 100644 index 000000000..a19e1d93e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/AcceptedKindTag.kt @@ -0,0 +1,67 @@ +/* + * 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.nip66RelayMonitor.discovery.tags + +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * ["k", ""] - an event kind accepted by this relay. + * + * A leading "!" negates, meaning the relay does NOT accept that kind. + * Examples: "1" (accepts kind 1), "!1" (rejects kind 1) + */ +@Stable +class AcceptedKindTag( + val kind: Int, + val negated: Boolean, +) { + companion object { + const val TAG_NAME = "k" + private const val NEGATION_PREFIX = "!" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME + + fun parse(tag: Array): AcceptedKindTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + val raw = tag[1] + return if (raw.startsWith(NEGATION_PREFIX)) { + val kind = raw.removePrefix(NEGATION_PREFIX).toIntOrNull() ?: return null + AcceptedKindTag(kind = kind, negated = true) + } else { + val kind = raw.toIntOrNull() ?: return null + AcceptedKindTag(kind = kind, negated = false) + } + } + + fun assemble( + kind: Int, + negated: Boolean = false, + ) = arrayOf(TAG_NAME, if (negated) "$NEGATION_PREFIX$kind" else kind.toString()) + + fun assemble(entry: AcceptedKindTag) = assemble(entry.kind, entry.negated) + + fun assemble(kinds: List) = kinds.map { assemble(it) } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/NetworkTypeTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/NetworkTypeTag.kt new file mode 100644 index 000000000..91d76e68e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/NetworkTypeTag.kt @@ -0,0 +1,54 @@ +/* + * 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.nip66RelayMonitor.discovery.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** Network accessibility types for relay connections */ +enum class NetworkType( + val code: String, +) { + CLEARNET("clearnet"), + TOR("tor"), + I2P("i2p"), + LOKI("loki"), +} + +/** ["n", ""] - the network type the relay is accessible on */ +class NetworkTypeTag { + companion object { + const val TAG_NAME = "n" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME + + fun parse(tag: Array): NetworkType? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return NetworkType.entries.find { it.code == tag[1] } + } + + fun assemble(network: NetworkType) = arrayOf(TAG_NAME, network.code) + + fun assemble(networks: List) = networks.map { assemble(it) } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RelayTypeTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RelayTypeTag.kt new file mode 100644 index 000000000..2e6f9b7ff --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RelayTypeTag.kt @@ -0,0 +1,47 @@ +/* + * 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.nip66RelayMonitor.discovery.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * ["T", ""] - the type of relay in PascalCase (e.g. PrivateInbox, PublicOutbox). + * Values are arbitrary strings defined by relay operators; not an enum. + */ +class RelayTypeTag { + companion object { + const val TAG_NAME = "T" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME + + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(relayType: String) = arrayOf(TAG_NAME, relayType) + + fun assemble(relayTypes: List) = relayTypes.map { assemble(it) } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RequirementTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RequirementTag.kt new file mode 100644 index 000000000..4815bccbd --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RequirementTag.kt @@ -0,0 +1,65 @@ +/* + * 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.nip66RelayMonitor.discovery.tags + +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * ["R", ""] - access requirement enforced by the relay. + * + * A leading "!" negates the requirement, meaning the relay does NOT enforce it. + * Examples: "auth", "payment", "!payment" (no payment required), "pow", "writes" + */ +@Stable +class RequirementTag( + val value: String, + val negated: Boolean, +) { + companion object { + const val TAG_NAME = "R" + private const val NEGATION_PREFIX = "!" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME + + fun parse(tag: Array): RequirementTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + val raw = tag[1] + return if (raw.startsWith(NEGATION_PREFIX)) { + RequirementTag(value = raw.removePrefix(NEGATION_PREFIX), negated = true) + } else { + RequirementTag(value = raw, negated = false) + } + } + + fun assemble( + value: String, + negated: Boolean = false, + ) = arrayOf(TAG_NAME, if (negated) "$NEGATION_PREFIX$value" else value) + + fun assemble(req: RequirementTag) = assemble(req.value, req.negated) + + fun assemble(requirements: List) = requirements.map { assemble(it) } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RttTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RttTag.kt new file mode 100644 index 000000000..1a123df93 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/RttTag.kt @@ -0,0 +1,66 @@ +/* + * 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.nip66RelayMonitor.discovery.tags + +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** Round-trip time measurement types */ +enum class RttType( + val tagName: String, +) { + OPEN("rtt-open"), + READ("rtt-read"), + WRITE("rtt-write"), +} + +/** + * Round-trip time tags measured in milliseconds. + * + * ["rtt-open", ""] + * ["rtt-read", ""] + * ["rtt-write", ""] + */ +@Stable +class RttTag( + val type: RttType, + val milliseconds: Long, +) { + companion object { + fun isTag(tag: Array) = + tag.has(1) && RttType.entries.any { it.tagName == tag[0] } + + fun parse(tag: Array): RttTag? { + ensure(tag.has(1)) { return null } + val type = RttType.entries.find { it.tagName == tag[0] } ?: return null + val ms = tag[1].toLongOrNull() ?: return null + return RttTag(type, ms) + } + + fun assemble( + type: RttType, + milliseconds: Long, + ) = arrayOf(type.tagName, milliseconds.toString()) + + fun assemble(rtt: RttTag) = assemble(rtt.type, rtt.milliseconds) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/SupportedNipTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/SupportedNipTag.kt new file mode 100644 index 000000000..6cfe3ba87 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/discovery/tags/SupportedNipTag.kt @@ -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.nip66RelayMonitor.discovery.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** ["N", ""] - a NIP number supported by this relay (repeated per supported NIP) */ +class SupportedNipTag { + companion object { + const val TAG_NAME = "N" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME + + fun parse(tag: Array): Int? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1].toIntOrNull() + } + + fun assemble(nip: Int) = arrayOf(TAG_NAME, nip.toString()) + + fun assemble(nips: List) = nips.map { assemble(it) } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/RelayMonitorEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/RelayMonitorEvent.kt new file mode 100644 index 000000000..cb5d9541e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/RelayMonitorEvent.kt @@ -0,0 +1,85 @@ +/* + * 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.nip66RelayMonitor.monitor + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.CheckType +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.TimeoutTag +import com.vitorpamplona.quartz.utils.TimeUtils + +/** + * NIP-66 Kind 10166: Relay Monitor Announcement + * + * Published by monitoring services to advertise themselves. Declares: + * - How frequently the monitor publishes results (frequency tag, required) + * - Which types of checks it performs (c tags) + * - Timeout values used for each check type (timeout tags) + * - Geographical location of the monitor (g tags) + * + * Monitors should also publish complementary kind 0 (metadata) and + * kind 10002 (relay list) events for full discoverability. + */ +@Immutable +class RelayMonitorEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun frequency() = tags.frequency() + + fun checks() = tags.checks() + + fun timeouts() = tags.timeouts() + + fun geohashes() = tags.geohashes() + + + companion object { + const val KIND = 10166 + const val ALT_DESCRIPTION = "Relay monitor announcement" + + fun build( + frequencySeconds: Long, + checks: List = emptyList(), + timeouts: List = emptyList(), + geohash: String? = null, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) + frequency(frequencySeconds) + checks(checks) + timeouts(timeouts) + geohash?.let { geohash(it) } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayBuilderExt.kt new file mode 100644 index 000000000..ba2d64568 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayBuilderExt.kt @@ -0,0 +1,33 @@ +/* + * 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.nip66RelayMonitor.monitor + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.CheckTag +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.CheckType +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.FrequencyTag +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.TimeoutTag + +fun TagArrayBuilder.frequency(seconds: Long) = addUnique(FrequencyTag.assemble(seconds)) + +fun TagArrayBuilder.checks(checks: List) = addAll(CheckTag.assemble(checks)) + +fun TagArrayBuilder.timeouts(timeouts: List) = addAll(TimeoutTag.assemble(timeouts)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayExt.kt new file mode 100644 index 000000000..ee507b330 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/TagArrayExt.kt @@ -0,0 +1,32 @@ +/* + * 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.nip66RelayMonitor.monitor + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.CheckTag +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.FrequencyTag +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.tags.TimeoutTag + +fun TagArray.frequency() = firstNotNullOfOrNull(FrequencyTag::parse) + +fun TagArray.checks() = mapNotNull(CheckTag::parse) + +fun TagArray.timeouts() = mapNotNull(TimeoutTag::parse) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/CheckTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/CheckTag.kt new file mode 100644 index 000000000..2db69a314 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/CheckTag.kt @@ -0,0 +1,57 @@ +/* + * 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.nip66RelayMonitor.monitor.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** Types of checks a relay monitor can perform */ +enum class CheckType( + val code: String, +) { + OPEN("open"), + READ("read"), + WRITE("write"), + AUTH("auth"), + NIP11("nip11"), + DNS("dns"), + GEO("geo"), +} + +/** ["c", ""] - a capability check conducted by this monitor */ +class CheckTag { + companion object { + const val TAG_NAME = "c" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME + + fun parse(tag: Array): CheckType? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return CheckType.entries.find { it.code == tag[1] } + } + + fun assemble(check: CheckType) = arrayOf(TAG_NAME, check.code) + + fun assemble(checks: List) = checks.map { assemble(it) } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/FrequencyTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/FrequencyTag.kt new file mode 100644 index 000000000..95c6d7d1a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/FrequencyTag.kt @@ -0,0 +1,42 @@ +/* + * 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.nip66RelayMonitor.monitor.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** ["frequency", ""] - how often this monitor publishes check results */ +class FrequencyTag { + companion object { + const val TAG_NAME = "frequency" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME + + fun parse(tag: Array): Long? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1].toLongOrNull() + } + + fun assemble(seconds: Long) = arrayOf(TAG_NAME, seconds.toString()) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/TimeoutTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/TimeoutTag.kt new file mode 100644 index 000000000..da9a752a3 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip66RelayMonitor/monitor/tags/TimeoutTag.kt @@ -0,0 +1,73 @@ +/* + * 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.nip66RelayMonitor.monitor.tags + +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +/** + * Timeout tag for relay monitors. + * + * General form: ["timeout", ""] + * Per-check form: ["timeout", "", ""] + * + * Where check-type is one of: open, read, write, nip11 + */ +@Stable +class TimeoutTag( + val checkType: String?, + val milliseconds: Long, +) { + companion object { + const val TAG_NAME = "timeout" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME + + fun parse(tag: Array): TimeoutTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + return if (tag.has(2)) { + val ms = tag[2].toLongOrNull() ?: return null + TimeoutTag(checkType = tag[1], milliseconds = ms) + } else { + val ms = tag[1].toLongOrNull() ?: return null + TimeoutTag(checkType = null, milliseconds = ms) + } + } + + fun assemble(milliseconds: Long) = arrayOf(TAG_NAME, milliseconds.toString()) + + fun assemble( + checkType: String, + milliseconds: Long, + ) = arrayOf(TAG_NAME, checkType, milliseconds.toString()) + + fun assemble(timeout: TimeoutTag) = + if (timeout.checkType != null) { + assemble(timeout.checkType, timeout.milliseconds) + } else { + assemble(timeout.milliseconds) + } + + fun assemble(timeouts: List) = timeouts.map { assemble(it) } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index c570731f9..2870e06fa 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -121,6 +121,8 @@ import com.vitorpamplona.quartz.nip64Chess.LiveChessGameChallengeEvent import com.vitorpamplona.quartz.nip64Chess.LiveChessGameEndEvent import com.vitorpamplona.quartz.nip64Chess.LiveChessMoveEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nip66RelayMonitor.discovery.RelayDiscoveryEvent +import com.vitorpamplona.quartz.nip66RelayMonitor.monitor.RelayMonitorEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoNormalEvent @@ -281,6 +283,8 @@ class EventFactory { ReactionEvent.KIND -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) ContactCardEvent.KIND -> ContactCardEvent(id, pubKey, createdAt, tags, content, sig) RelayAuthEvent.KIND -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig) + RelayDiscoveryEvent.KIND -> RelayDiscoveryEvent(id, pubKey, createdAt, tags, content, sig) + RelayMonitorEvent.KIND -> RelayMonitorEvent(id, pubKey, createdAt, tags, content, sig) RelaySetEvent.KIND -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig) ReportEvent.KIND -> ReportEvent(id, pubKey, createdAt, tags, content, sig) RepostEvent.KIND -> RepostEvent(id, pubKey, createdAt, tags, content, sig)