From ba7cc72dd28bdffee33fba3cc459e1e6be8b0381 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 13:22:55 +0000 Subject: [PATCH] feat(cli): amy relay probe --file to census external relay-url candidates Feeds a file of raw candidate urls (one per line) through the same RelayUrlNormalizer the app uses, then probes the surviving clearnet set alongside the store's known universe. Rejected and onion counts are reported (file_urls/file_normalized/file_rejected in the JSON output), and results land in the NIP-66 kind:30166 reachability cache keyed by the normalized url as d-tag, as usual. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01A9sSyh1QLJD3PZ18tVPPVK --- .../amethyst/cli/commands/RelayCommands.kt | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt index 64084a9356..8734ba1b02 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation @@ -50,6 +51,7 @@ import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType import com.vitorpamplona.quartz.nip66RelayMonitor.reachability.RelayProber import okhttp3.OkHttpClient import okhttp3.Request +import java.io.File /** * `amy relay …` — manage every relay list this account maintains, mirroring @@ -112,10 +114,12 @@ object RelayCommands { | relay info URL fetch + print a relay's NIP-11 info document (stateless) | relay probe [--timeout SECS] relay census: mass-connect every relay the store | [--concurrency N] knows and record live/dead + measured rtt-open - | into the reachability cache (NIP-66 kind:30166), + | [--file PATH] into the reachability cache (NIP-66 kind:30166), | so reachability-aware commands (graperank crawl/ | refresh) skip dead relays and wait once - | (--timeout: per wave, default 15s) + | (--timeout: per wave, default 15s; --file: also + | probe candidate urls, one per line, each run + | through the relay url normalizer first) """.trimMargin() // ------------------------------------------------------------------ @@ -337,12 +341,36 @@ object RelayCommands { // Relays dialed at once; --relay-concurrency accepted as the alias the // graperank verbs spell it with. val waveSize = args.intFlag("concurrency", args.intFlag("relay-concurrency", Context.defaultPreconnectCap)) + // Optional external candidate list: one raw url per line, run through the + // same RelayUrlNormalizer the app uses, so a probe doubles as a census of + // how a corpus of relay hints normalizes (rejects are counted, not dialed). + val fromFile = args.flag("file") args.rejectUnknown() + var fileRaw = 0 + var fileRejected = 0 + val fileRelays = HashSet() + if (fromFile != null) { + File(fromFile).forEachLine { line -> + if (line.isBlank()) return@forEachLine + fileRaw++ + val normalized = line.normalizeRelayUrlOrNull() + if (normalized == null) { + fileRejected++ + } else if (!RelayUrlNormalizer.isOnion(normalized.url)) { + fileRelays.add(normalized) + } + } + System.err.println( + "[relay-probe] $fromFile: $fileRaw urls → ${fileRelays.size} unique clearnet relays " + + "($fileRejected rejected by the normalizer)", + ) + } + Context.openOrAnonymous(dataDir).use { ctx -> ctx.prepare() val cached = ctx.reachability.snapshot() - val universe = RelayProber.knownRelayUniverse(ctx.store) + cached.live + cached.dead + val universe = RelayProber.knownRelayUniverse(ctx.store) + cached.live + cached.dead + fileRelays if (universe.isEmpty()) { Output.emit( linkedMapOf( @@ -381,6 +409,9 @@ object RelayCommands { Output.emit( linkedMapOf( "probed" to result.verdicts.size, + "file_urls" to (if (fromFile != null) fileRaw else null), + "file_normalized" to (if (fromFile != null) fileRelays.size else null), + "file_rejected" to (if (fromFile != null) fileRejected else null), "reachable" to result.reachable.size, "dead" to result.dead.size, "closed_by_policy" to authWalled,