add scripts

This commit is contained in:
lollerfirst
2025-08-24 15:02:40 +02:00
parent bfac2a8e32
commit 73d0a526c0
4 changed files with 921 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
#!/bin/bash
# How many relays to query in parallel
CONCURRENCY=${CONCURRENCY:-10}
# Read relays from stdin or file into array
mapfile -t relays
# Function to test a single relay for kind 20000 support (both read and write)
test_relay() {
local relay="$1"
local count
local post_result
# Test 1: Try to request kind 20000 events and count them
# Use timeout to avoid hanging on unresponsive relays
count=$(timeout 10s nak req -k 20000 "$relay" 2>/dev/null | jq -s 'length' 2>/dev/null)
# Check if we got a valid response (number >= 0) for reading
if [[ ! "$count" =~ ^[0-9]+$ ]]; then
return 1 # Failed to read kind 20000 events
fi
# Test 2: Try to post a kind 20000 event
# Generate a unique test content to avoid duplicate events
local test_content="test_$(date +%s)_$$"
# Try to post a test kind 20000 event
post_result=$(timeout 10s nak event -k 20000 --tag n=test --tag g=test --content "$test_content" "$relay" 2>&1)
# Check if the post was successful
if echo "$post_result" | grep -q "success"; then
echo "$relay"
else
return 1
fi
}
export -f test_relay
total_relays=${#relays[@]}
batch_size=$((total_relays / CONCURRENCY))
if [ $batch_size -eq 0 ]; then
batch_size=1
fi
# Process relays in parallel batches
printf '%s\n' "${relays[@]}" | xargs -P "$CONCURRENCY" -I {} bash -c 'test_relay "$@"' _ {}
+562
View File
@@ -0,0 +1,562 @@
#!/usr/bin/env python3
"""
Nostr Relay Discovery Tool
This tool discovers Nostr relays by performing breadth-first search through
follow lists (kind 3 events) and analyzing relay tags to build a network map.
"""
import asyncio
import json
import logging
import re
import time
import argparse
import secrets
import base64
from collections import deque
from dataclasses import dataclass, field
from typing import Set, List, Dict, Optional, Tuple
from urllib.parse import urlparse
import websockets
import websockets.exceptions
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Save progress every N relays processed
SAVE_POINT = 10
@dataclass
class RelayDiscoveryStats:
"""Statistics for the relay discovery process"""
total_relays_found: int = 0
functioning_relays: int = 0
events_processed: int = 0
existing_relays_verified: int = 0
existing_relays_failed: int = 0
start_time: float = field(default_factory=time.time)
def print_stats(self):
"""Print current statistics"""
elapsed = time.time() - self.start_time
print(f"\n=== Discovery Statistics ===")
print(f"Elapsed time: {elapsed:.2f} seconds")
print(f"Total relays found: {self.total_relays_found}")
print(f"Functioning relays: {self.functioning_relays}")
print(f"Events processed: {self.events_processed}")
if self.existing_relays_verified > 0 or self.existing_relays_failed > 0:
print(f"Existing relays verified: {self.existing_relays_verified}")
print(f"Existing relays failed verification: {self.existing_relays_failed}")
print(f"Success rate: {(self.functioning_relays/max(1, self.total_relays_found)*100):.1f}%")
class NostrRelayDiscovery:
"""Nostr relay discovery tool using breadth-first search through follow lists"""
def __init__(self, initial_relay: str, max_depth: int = 3, connection_timeout: int = 10, output_file: str = "relay_discovery_results.json", save_point: int = SAVE_POINT):
self.initial_relay = initial_relay
self.max_depth = max_depth
self.connection_timeout = connection_timeout
self.output_file = output_file
self.save_point = save_point
# Discovery state
self.to_visit: deque = deque() # (relay_url, depth) - will be populated by load_existing_results
self.to_visit_set: Set[str] = set()
self.visited_relays: Set[str] = set()
self.functioning_relays: Set[str] = set()
# Statistics
self.stats = RelayDiscoveryStats()
# Regex for validating relay URLs
self.relay_url_pattern = re.compile(
r'^wss?://[a-zA-Z0-9.-]+(?:\:[0-9]+)?(?:/[a-zA-Z0-9._~:/?#[\]@!$&\'()*+,;=-]*)?$'
)
def is_valid_relay_url(self, url: str) -> bool:
"""Validate if a URL looks like a valid relay URL"""
if not url or not isinstance(url, str):
return False
# Clean up the URL
url = url.strip()
# Check basic format
if not self.relay_url_pattern.match(url):
return False
# Parse and validate
try:
parsed = urlparse(url)
return (
parsed.scheme in ['ws', 'wss'] and
parsed.netloc and
len(parsed.netloc) > 0
)
except Exception:
return False
def normalize_relay_url(self, url: str) -> str:
"""Normalize relay URL (remove trailing slashes, etc.)"""
url = url.strip()
match = re.match(r'^(wss://[^/]+)/?$', url)
if match:
matched_url = match.group(1)
logger.debug(f"matched url: {matched_url}")
return matched_url
else:
return ''
def generate_subscription_id(self) -> str:
"""Generate a random subscription ID using 16 random bytes encoded as base64"""
random_bytes = secrets.token_bytes(16)
return base64.b64encode(random_bytes).decode()
async def test_relay_connection(self, relay_url: str) -> bool:
"""Test if a relay is functioning by attempting to connect and validate Nostr protocol responses"""
try:
logger.debug(f"Testing connection to {relay_url}")
async with websockets.connect(
relay_url,
timeout=self.connection_timeout,
max_size=2**20, # 1MB max message size
ping_interval=None # Disable ping
) as websocket:
# Send a simple REQ to test functionality
subscription_id = self.generate_subscription_id()
test_filter = {
"kinds": [1],
"limit": 1
}
req_msg = ["REQ", subscription_id, test_filter]
await websocket.send(json.dumps(req_msg))
logger.debug(f"Sent REQ with subscription ID: {subscription_id}")
# Wait for a response and validate it's a proper Nostr protocol message
try:
response = await asyncio.wait_for(websocket.recv(), timeout=5.0)
logger.debug(f"Received response: {response[:200]}...") # Log first 200 chars
try:
data = json.loads(response)
# Check if it's a valid Nostr protocol message
if not isinstance(data, list) or len(data) < 2:
logger.debug(f"Invalid Nostr message format from {relay_url}: not a list or too short")
return False
message_type = data[0]
message_subscription_id = data[1]
# Validate subscription ID matches
if message_subscription_id != subscription_id:
logger.debug(f"Subscription ID mismatch from {relay_url}: expected {subscription_id}, got {message_subscription_id}")
return False
# Check for valid Nostr protocol message types
if message_type == "EVENT":
logger.debug(f"✓ Received EVENT from {relay_url}")
# Send CLOSE to clean up
close_msg = ["CLOSE", subscription_id]
await websocket.send(json.dumps(close_msg))
return True
elif message_type == "EOSE":
logger.warning(f"Received EOSE immediately from {relay_url} - relay has no events (unusual)")
# Send CLOSE to clean up
close_msg = ["CLOSE", subscription_id]
await websocket.send(json.dumps(close_msg))
return True
elif message_type == "NOTICE":
notice_message = data[2] if len(data) > 2 else "unknown"
logger.debug(f"Received NOTICE from {relay_url}: {notice_message}")
return False
else:
logger.debug(f"Unexpected message type from {relay_url}: {message_type}")
return False
except json.JSONDecodeError as e:
logger.debug(f"Invalid JSON response from {relay_url}: {e}")
return False
except asyncio.TimeoutError:
logger.debug(f"Timeout waiting for response from {relay_url}")
return False
except Exception as e:
logger.debug(f"Failed to connect to {relay_url}: {e}")
return False
async def fetch_follow_lists(self, relay_url: str) -> List[Dict]:
"""Fetch kind 3 events (follow lists) from a relay"""
follow_events = []
try:
logger.info(f"Fetching follow lists from {relay_url}")
async with websockets.connect(
relay_url,
timeout=self.connection_timeout,
max_size=2**20
) as websocket:
# Request follow lists (kind 3 events)
filter_req = {
"kinds": [3],
"limit": 300,
}
subscription_id = self.generate_subscription_id()
req_msg = ["REQ", subscription_id, filter_req]
await websocket.send(json.dumps(req_msg))
logger.debug(f"Sent request: {req_msg}")
# Collect events until EOSE
start_time = time.time()
timeout_duration = 30.0 # 30 second timeout
while time.time() - start_time < timeout_duration:
try:
message = await asyncio.wait_for(websocket.recv(), timeout=5.0)
data = json.loads(message)
if data[0] == "EVENT" and data[1] == subscription_id:
event = data[2]
if event.get("kind") == 3:
follow_events.append(event)
logger.debug(f"Collected follow event from {event.get('pubkey', 'unknown')[:8]}...")
elif data[0] == "EOSE" and data[1] == subscription_id:
logger.debug(f"Received EOSE for {subscription_id}")
break
except asyncio.TimeoutError:
continue
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse message from {relay_url}: {e}")
continue
# Send CLOSE message
close_msg = ["CLOSE", subscription_id]
await websocket.send(json.dumps(close_msg))
except Exception as e:
logger.error(f"Error fetching follow lists from {relay_url}: {e}")
logger.info(f"Collected {len(follow_events)} follow events from {relay_url}")
return follow_events
def extract_relays_from_events(self, events: List[Dict]) -> Set[str]:
"""Extract relay URLs from follow list events"""
relay_urls = set()
for event in events:
self.stats.events_processed += 1
# Process tags to find relay information
tags = event.get('tags', [])
for tag in tags:
if not isinstance(tag, list) or len(tag) < 2:
continue
# Look for 'r' tags (relay tags)
if tag[0] == 'r' and len(tag) >= 2:
potential_relay = tag[1]
if self.is_valid_relay_url(potential_relay):
normalized_url = self.normalize_relay_url(potential_relay)
relay_urls.add(normalized_url)
# Also check 'p' tags for potential relay info in some implementations
elif tag[0] == 'p' and len(tag) >= 3:
# Some implementations put relay info in the 3rd element of p tags
if len(tag) > 2 and self.is_valid_relay_url(tag[2]):
normalized_url = self.normalize_relay_url(tag[2])
relay_urls.add(normalized_url)
return relay_urls
async def load_existing_results(self) -> bool:
"""Load existing results and verify that functioning relays still work"""
import os
if not os.path.exists(self.output_file):
logger.info(f"No existing results file found at {self.output_file}, starting fresh")
# Initialize with just the initial relay
self.to_visit.append((self.initial_relay, 0))
self.to_visit_set.add(self.initial_relay)
return False
try:
logger.info(f"Loading existing results from {self.output_file}")
with open(self.output_file, 'r') as f:
data = json.load(f)
existing_relays = data.get('functioning_relays', [])
logger.info(f"Found {len(existing_relays)} existing functioning relays to verify")
if not existing_relays:
logger.info("No existing functioning relays found, starting fresh")
self.to_visit.append((self.initial_relay, 0))
self.to_visit_set.add(self.initial_relay)
return False
# Verify existing relays still work
verified_relays = await self.verify_existing_relays(existing_relays)
if verified_relays:
logger.info(f"Successfully verified {len(verified_relays)} existing relays")
# Use verified relays as starting points for discovery
for relay in verified_relays:
self.to_visit.append((relay, 0))
self.to_visit_set.add(relay)
self.functioning_relays.add(relay)
self.stats.functioning_relays += 1
# Also preserve other statistics if available
if 'statistics' in data:
old_stats = data['statistics']
self.stats.total_relays_found = old_stats.get('total_relays_found', len(verified_relays))
self.stats.events_processed = old_stats.get('events_processed', 0)
return True
else:
logger.warning("No existing relays could be verified, starting fresh")
self.to_visit.append((self.initial_relay, 0))
self.to_visit_set.add(self.initial_relay)
return False
except Exception as e:
logger.error(f"Error loading existing results: {e}")
logger.info("Starting fresh due to error")
self.to_visit.append((self.initial_relay, 0))
self.to_visit_set.add(self.initial_relay)
return False
async def verify_existing_relays(self, existing_relays: List[str]) -> List[str]:
"""Verify that existing relays still work and filter out non-working ones"""
verified_relays = []
logger.info(f"Verifying {len(existing_relays)} existing relays...")
# Test relays in batches to avoid overwhelming connections
batch_size = 10
for i in range(0, len(existing_relays), batch_size):
batch = existing_relays[i:i + batch_size]
batch_tasks = []
for relay in batch:
if self.is_valid_relay_url(relay):
batch_tasks.append(self.test_relay_connection(relay))
else:
logger.warning(f"Invalid relay URL in existing results: {relay}")
self.stats.existing_relays_failed += 1
if batch_tasks:
# Run batch tests concurrently
batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
for j, result in enumerate(batch_results):
relay = batch[j]
if isinstance(result, bool) and result:
verified_relays.append(relay)
self.stats.existing_relays_verified += 1
logger.info(f"✓ Verified existing relay: {relay}")
else:
self.stats.existing_relays_failed += 1
logger.warning(f"✗ Existing relay failed verification: {relay}")
# Small delay between batches to be nice to the relays
if i + batch_size < len(existing_relays):
await asyncio.sleep(0.5)
logger.info(f"Verification complete: {len(verified_relays)}/{len(existing_relays)} relays still functioning")
return verified_relays
async def discover_relays(self) -> Set[str]:
"""Main discovery method using breadth-first search"""
# First, try to load existing results and verify them
logger.info("Checking for existing results to build upon...")
await self.load_existing_results()
logger.info(f"Starting relay discovery from existing verified relays or {self.initial_relay}")
logger.info(f"Maximum depth: {self.max_depth}")
while self.to_visit:
current_relay, depth = self.to_visit.popleft()
self.to_visit_set.remove(current_relay)
# Skip if already visited
if current_relay in self.visited_relays:
continue
# Skip if depth exceeds maximum
if depth > self.max_depth:
logger.info(f"Reached maximum depth {self.max_depth}, stopping exploration")
continue
logger.info(f"Processing relay {current_relay} at depth {depth}")
self.visited_relays.add(current_relay)
if await self.test_relay_connection(current_relay):
logger.info(f"✓ Relay {current_relay} is functioning")
self.functioning_relays.add(current_relay)
self.stats.functioning_relays += 1
# Only fetch follow lists if we haven't reached max depth
if depth < self.max_depth:
try:
# Fetch follow lists from this relay
follow_events = await self.fetch_follow_lists(current_relay)
if follow_events:
# Extract new relays from follow lists
new_relays = self.extract_relays_from_events(follow_events)
# Add new relays to visit queue for next depth level
for relay_url in new_relays:
if relay_url not in self.visited_relays and relay_url not in self.to_visit_set:
self.to_visit.append((relay_url, depth + 1))
self.to_visit_set.add(relay_url)
self.stats.total_relays_found += 1
logger.debug(f"Added {relay_url} to visit queue at depth {depth + 1}")
except Exception as e:
logger.error(f"Error processing relay {current_relay}: {e}")
else:
logger.warning(f"✗ Relay {current_relay} is not functioning")
# Save progress periodically every save_point relays processed
if len(self.visited_relays) % self.save_point == 0:
logger.info(f"Progress checkpoint: Saving results after processing {len(self.visited_relays)} relays")
self.save_results()
# Print periodic statistics
if len(self.visited_relays) % 10 == 0:
self.stats.print_stats()
logger.info("Discovery completed!")
return self.functioning_relays
def save_results(self, output_file: str = None):
"""Save discovery results to a JSON file"""
if output_file is None:
output_file = self.output_file
results = {
"discovery_settings": {
"initial_relay": self.initial_relay,
"max_depth": self.max_depth,
"connection_timeout": self.connection_timeout,
"save_point": self.save_point
},
"progress_info": {
"relays_processed": len(self.visited_relays),
"relays_remaining": len(self.to_visit),
"discovery_complete": len(self.to_visit) == 0,
"last_saved": time.time()
},
"statistics": {
"total_relays_found": self.stats.total_relays_found,
"functioning_relays_count": len(self.functioning_relays),
"events_processed": self.stats.events_processed,
"existing_relays_verified": self.stats.existing_relays_verified,
"existing_relays_failed": self.stats.existing_relays_failed,
"discovery_duration": time.time() - self.stats.start_time
},
"functioning_relays": list(self.functioning_relays)
}
with open(output_file, 'w') as f:
json.dump(results, f, indent=2)
logger.info(f"Results saved to {output_file}")
async def main():
"""Main function"""
parser = argparse.ArgumentParser(description="Discover Nostr relays using breadth-first search")
parser.add_argument(
"initial_relay",
help="Initial relay URL to start discovery from (e.g., wss://relay.damus.io)"
)
parser.add_argument(
"--max-depth",
type=int,
default=3,
help="Maximum depth for breadth-first search (default: 3)"
)
parser.add_argument(
"--timeout",
type=int,
default=10,
help="Connection timeout in seconds (default: 10)"
)
parser.add_argument(
"--output",
default="relay_discovery_results.json",
help="Output file for results (default: relay_discovery_results.json)"
)
parser.add_argument(
"--save-point",
type=int,
default=SAVE_POINT,
help=f"Save progress every N relays processed (default: {SAVE_POINT})"
)
parser.add_argument(
"--verbose",
action="store_true",
help="Enable verbose logging"
)
args = parser.parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
# Validate initial relay URL
discovery = NostrRelayDiscovery(args.initial_relay, args.max_depth, args.timeout, args.output, args.save_point)
if not discovery.is_valid_relay_url(args.initial_relay):
print(f"Error: Invalid relay URL: {args.initial_relay}")
print("Relay URL should start with ws:// or wss://")
return 1
try:
# Run discovery
functioning_relays = await discovery.discover_relays()
# Print final results
print("\n=== DISCOVERY COMPLETED ===")
discovery.stats.print_stats()
print(f"\n=== FUNCTIONING RELAYS ({len(functioning_relays)}) ===")
for relay in sorted(functioning_relays):
print(f" {relay}")
# Save results
discovery.save_results()
return 0
except KeyboardInterrupt:
print("\nDiscovery interrupted by user")
discovery.save_results()
return 1
except Exception as e:
logger.error(f"Discovery failed: {e}")
return 1
if __name__ == "__main__":
import sys
sys.exit(asyncio.run(main()))
+258
View File
@@ -0,0 +1,258 @@
{
"discovery_settings": {
"initial_relay": "wss://relay.damus.io",
"max_depth": 3,
"connection_timeout": 10,
"save_point": 10
},
"progress_info": {
"relays_processed": 723,
"relays_remaining": 0,
"discovery_complete": true,
"last_saved": 1756039994.605474
},
"statistics": {
"total_relays_found": 1200,
"functioning_relays_count": 234,
"events_processed": 69208,
"existing_relays_verified": 219,
"existing_relays_failed": 5,
"discovery_duration": 2709.6405580043793
},
"functioning_relays": [
"wss://relay.hodl.ar",
"wss://at.nostrworks.com",
"wss://relay.pleb.to",
"wss://nostr.reckless.dev",
"wss://no.str.cr",
"wss://relay.usefusion.ai",
"wss://noornode.nostr1.com",
"wss://purplerelay.com",
"wss://xmr.usenostr.org",
"wss://nostr-pub.wellorder.net",
"wss://nostr-dev.wellorder.net",
"wss://nostr.lopp.social",
"wss://bitcoinmaximalists.online",
"wss://adre.su",
"wss://yabu.me",
"wss://nostr.256k1.dev",
"wss://nostr.hashbang.nl",
"wss://relay.geyser.fund",
"wss://thebarn.nostr1.com",
"wss://n.ok0.org",
"wss://nostr.zbd.gg",
"wss://relay.olas.app",
"wss://nostrue.com",
"wss://nostr.1sat.org",
"wss://asia.azzamo.net",
"wss://nostrelites.org",
"wss://kitchen.zap.cooking",
"wss://noxir.kpherox.dev",
"wss://christpill.nostr1.com",
"wss://relay.nos.social",
"wss://lightningrelay.com",
"wss://wot.brightbolt.net",
"wss://relay.bitcoinpark.com",
"wss://relay.westernbtc.com",
"wss://nos.lol",
"wss://relay.235421.xyz",
"wss://relay.minibits.cash",
"wss://us.nostr.wine",
"wss://nostr.mom",
"wss://njump.me",
"wss://f7z.io",
"wss://chronicle.dtonon.com",
"wss://haven.calva.dev",
"wss://paid.nostrified.org",
"wss://frens.nostr1.com",
"wss://hbr.coracle.social",
"wss://nostr.wine",
"wss://nostr.sathoarder.com",
"wss://relay.minds.com",
"wss://relay.bullishbounty.com",
"wss://nfrelay.app",
"wss://relay.primal.net",
"wss://profiles.nostr1.com",
"wss://fiatjaf.nostr1.com",
"wss://zap.watch",
"wss://nostr.middling.mydns.jp",
"wss://nostr.azzamo.net",
"wss://nostr.kungfu-g.rip",
"wss://purplepag.es",
"wss://nostrelay.circum.space",
"wss://nostr.data.haus",
"wss://relay.nostr.wirednet.jp",
"wss://wot.nostr.net",
"wss://relay.benthecarman.com",
"wss://wot.sebastix.social",
"wss://relay.beta.fogtype.com",
"wss://cellar.nostr.wine",
"wss://relay.drss.io",
"wss://relay.lawallet.ar",
"wss://nostr-relay.bitcoin.ninja",
"wss://nostr.thank.eu",
"wss://nostr.xmr.rocks",
"wss://nostr.pareto.space",
"wss://nostr.4rs.nl",
"wss://relay1.nostrchat.io",
"wss://nostr.stakey.net",
"wss://relay.dergigi.com",
"wss://nostr.sovbit.host",
"wss://relay.dwadziesciajeden.pl",
"wss://premium.primal.net",
"wss://nostr21.com",
"wss://nostr.chaima.info",
"wss://nostr.cloud.vinney.xyz",
"wss://nostr.roundrockbitcoiners.com",
"wss://relay.livefreebtc.dev",
"wss://nostr.d11n.net",
"wss://frjosh.nostr1.com",
"wss://nostr.sebastix.dev",
"wss://relay.lexingtonbitcoin.org",
"wss://relay.satlantis.io",
"wss://nostr.bitpunk.fm",
"wss://pyramid.fiatjaf.com",
"wss://relay.nostromo.social",
"wss://nostr-verified.wellorder.net",
"wss://nostr-01.yakihonne.com",
"wss://nostr.bitcoinist.org",
"wss://zaplab.nostr1.com",
"wss://articles.layer3.news",
"wss://relay.g1sms.fr",
"wss://relay.patrickulrich.com",
"wss://nostr.slothy.win",
"wss://knostr.neutrine.com",
"wss://relay.bitdevs.tw",
"wss://relay.jthecodemonkey.xyz",
"wss://relay.damus.io",
"wss://inbox.azzamo.net",
"wss://nostr.spicyz.io",
"wss://thecitadel.nostr1.com",
"wss://chadf.nostr1.com",
"wss://wot.nostr.party",
"wss://relay.nosflare.com",
"wss://aegis.relayted.de",
"wss://relay.nostr.wf",
"wss://nostr-03.dorafactory.org",
"wss://relay.nostrr.de",
"wss://nostr.bilthon.dev",
"wss://slick.mjex.me",
"wss://nostr.yael.at",
"wss://yestr.me",
"wss://hivetalk.nostr1.com",
"wss://relay.gasteazi.net",
"wss://relay.notoshi.win",
"wss://bitcoiner.social",
"wss://relay.nostr.sc",
"wss://wot.dtonon.com",
"wss://nostr.cizmar.net",
"wss://relay.guggero.org",
"wss://nostr.jcloud.es",
"wss://inbox.relays.land",
"wss://nostr.malin.onl",
"wss://nostr-02.dorafactory.org",
"wss://nostr-02.yakihonne.com",
"wss://relay.mostro.network",
"wss://pareto.nostr1.com",
"wss://relay.angor.io",
"wss://nostr.ussenterprise.xyz",
"wss://relay.weloveit.info",
"wss://relay.copylaradio.com",
"wss://offchain.pub",
"wss://relay.nostrdam.com",
"wss://wons.calva.dev",
"wss://welcome.nostr.wine",
"wss://carlos-cdb.top",
"wss://a.nos.lol",
"wss://relay.nostr.vet",
"wss://relay.lumina.rocks",
"wss://nostr.easydns.ca",
"wss://search.nos.today",
"wss://nostr.bitcoiner.social",
"wss://relay.ziomc.com",
"wss://relay.vanderwarker.family",
"wss://orangesync.tech",
"wss://relay.wellorder.net",
"wss://relay.noswhere.com",
"wss://ithurtswhenip.ee",
"wss://relay.azzamo.net",
"wss://relay.nostr.net",
"wss://nostr.0x7e.xyz",
"wss://nrelay.c-stellar.net",
"wss://Relay.Damus.io",
"wss://relay.nostrplebs.com",
"wss://nostr.hekster.org",
"wss://nostr.massmux.com",
"wss://nostr.oxtr.dev",
"wss://paid.no.str.cr",
"wss://mleku.nostr1.com",
"wss://relay.jeffg.fyi",
"wss://140.f7z.io",
"wss://wot.sudocarlos.com",
"wss://srtrelay.c-stellar.net",
"wss://nostr.1f52b.xyz",
"wss://relay.ingwie.me",
"wss://wot.codingarena.top",
"wss://nostr.liberty.fans",
"wss://relay.nostr.band",
"wss://nostr.plantroon.com",
"wss://bots.utxo.one",
"wss://fanfares.nostr1.com",
"wss://nostr.pjv.me",
"wss://nostr.decentony.com",
"wss://wot.downisontheup.ca",
"wss://relay.barine.co",
"wss://theforest.nostr1.com",
"wss://wheat.happytavern.co",
"wss://wot.girino.org",
"wss://relay.orangepill.ovh",
"wss://nostr-relay.cbrx.io",
"wss://nostr-1.nbo.angani.co",
"wss://nostr-relay.derekross.me",
"wss://haven.accioly.social",
"wss://btc.klendazu.com",
"wss://relay.chakany.systems",
"wss://relay.coinos.io",
"wss://relay.exit.pub",
"wss://user.kindpag.es",
"wss://nostr.vulpem.com",
"wss://freelay.sovbit.host",
"wss://nostr.notribe.net",
"wss://wot.nostr.sats4.life",
"wss://nostrja-kari.heguro.com",
"wss://relay.mostr.pub",
"wss://nostr.pareto.town",
"wss://haven.slidestr.net",
"wss://support.nostr1.com",
"wss://vitor.nostr1.com",
"wss://relay.nostr.com.au",
"wss://relay.noderunners.network",
"wss://relay.artx.market",
"wss://relay.nostrhub.fr",
"wss://relay.magiccity.live",
"wss://cyberspace.nostr1.com",
"wss://vault.iris.to",
"wss://hist.nostr.land",
"wss://soloco.nl",
"wss://relay.wavlake.com",
"wss://relay.vertexlab.io",
"wss://relay.theuncounted.site",
"wss://primus.nostr1.com",
"wss://news.utxo.one",
"wss://wot.danieldaquino.me",
"wss://relay.nostrified.org",
"wss://nproxy.kristapsk.lv",
"wss://relay.utxo.one",
"wss://nostr.einundzwanzig.space",
"wss://relay.verified-nostr.com",
"wss://directory.yabu.me",
"wss://strfry.openhoofd.nl",
"wss://jellyfish.land",
"wss://xmr.ithurtswhenip.ee",
"wss://nostr.app.runonflux.io",
"wss://nostr.hifish.org",
"wss://satsage.xyz",
"wss://relay.reya.su",
"wss://nostr.huszonegy.world"
]
}
+53
View File
@@ -0,0 +1,53 @@
#!/bin/bash
# Check if the output file is provided
if [ "$#" -ne 1 ]; then
echo "Usage: $0 output.csv"
exit 1
fi
output_file="$1"
if [ ! -f "$output_file" ] || [ ! -s "$output_file" ]; then
echo "Relay URL,Latitude,Longitude" > "$output_file"
fi
while IFS= read -r url; do
a_records=$(dig +short "$url" A)
if [[ -n "$a_records" ]]; then
for ip in $a_records; do
echo "Attempting geo-location lookup for $url -> $ip"
location_data=""
retries=3
attempt=0
while [[ -z "$location_data" && $attempt -lt $retries ]]; do
location_data=$(curl -s "https://ipinfo.io/$ip/json")
#echo "location data: $(echo $location_data | jq)"
attempt=$((attempt + 1))
sleep 1
done
latitude=$(echo "$location_data" | jq -r '.loc' | cut -d',' -f1)
longitude=$(echo "$location_data" | jq -r '.loc' | cut -d',' -f2)
if [[ -n "$latitude" && -n "$longitude" && $latitude != "null" && $longitude != "null" ]]; then
echo "$url: latitude=$latitude, longitude=$longitude"
echo "$url,$latitude,$longitude" >> "$output_file"
break
else
echo "geolocation failed for $url"
continue
fi
done
else
echo "geolocation failed for $url"
fi
done
echo "Results written to $output_file"