mirror of
https://github.com/permissionlesstech/georelays.git
synced 2026-07-24 21:45:19 +00:00
add scripts
This commit is contained in:
Executable
+48
@@ -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 "$@"' _ {}
|
||||
@@ -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()))
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Executable
+53
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user