mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-10 11:13:08 +00:00
208 lines
6.9 KiB
Python
208 lines
6.9 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import random
|
|
import re
|
|
import string
|
|
|
|
import httpx
|
|
import websockets
|
|
from fastapi import APIRouter
|
|
|
|
providers_router = APIRouter(prefix="/v1/providers")
|
|
|
|
|
|
def generate_subscription_id() -> str:
|
|
"""Generate a random subscription ID."""
|
|
return "".join(random.choices(string.ascii_lowercase + string.digits, k=10))
|
|
|
|
|
|
def extract_onion_urls(content: str) -> list[str]:
|
|
"""Extract onion URLs from content."""
|
|
pattern = r"http?://[a-zA-Z0-9\-._~]+\.onion"
|
|
return re.findall(pattern, content)
|
|
|
|
|
|
async def query_nostr_relay_with_search(
|
|
search_term: str,
|
|
relay_url: str,
|
|
kinds: list[int] | None = None,
|
|
limit: int = 1000,
|
|
timeout: int = 30,
|
|
) -> list[dict]:
|
|
"""
|
|
Query a Nostr relay and filter for events containing a search term.
|
|
"""
|
|
if kinds is None:
|
|
kinds = [1]
|
|
|
|
events = []
|
|
|
|
# If searching for an npub mention, try tag-based search first
|
|
if search_term.startswith("nostr:npub"):
|
|
# Extract the npub and convert to hex
|
|
npub = search_term.replace("nostr:", "")
|
|
try:
|
|
# Convert npub to hex (you might need to implement or import this)
|
|
# For now, try tag-based search with the npub
|
|
filter_obj = {
|
|
"kinds": kinds,
|
|
"limit": limit,
|
|
"#p": [npub], # Posts that tag this pubkey
|
|
}
|
|
except Exception:
|
|
# If conversion fails, try regular search
|
|
filter_obj = {
|
|
"kinds": kinds,
|
|
"limit": limit,
|
|
}
|
|
else:
|
|
# Try relay's search functionality (NIP-50)
|
|
filter_obj = {
|
|
"kinds": kinds,
|
|
"search": search_term,
|
|
"limit": limit,
|
|
}
|
|
|
|
sub_id = generate_subscription_id()
|
|
req_message = json.dumps(["REQ", sub_id, filter_obj])
|
|
|
|
try:
|
|
async with websockets.connect(relay_url, timeout=timeout) as websocket:
|
|
print(f"Connected to relay, sending request with filter: {filter_obj}")
|
|
await websocket.send(req_message)
|
|
|
|
while True:
|
|
try:
|
|
message = await asyncio.wait_for(websocket.recv(), timeout=5)
|
|
data = json.loads(message)
|
|
|
|
if data[0] == "EVENT" and data[1] == sub_id:
|
|
# For tag-based search, also check content
|
|
if search_term.startswith("nostr:npub"):
|
|
if search_term.lower() in data[2]["content"].lower():
|
|
print(f"Found matching event: {data[2]['id']}")
|
|
events.append(data[2])
|
|
else:
|
|
print(f"Found matching event: {data[2]['id']}")
|
|
events.append(data[2])
|
|
elif data[0] == "EOSE" and data[1] == sub_id:
|
|
print("Received EOSE message")
|
|
break
|
|
elif data[0] == "NOTICE":
|
|
print(f"Relay notice: {data[1]}")
|
|
# If search not supported, could break and try different approach
|
|
if "unrecognised filter item" in data[1] and "search" in str(
|
|
filter_obj
|
|
):
|
|
print("Search not supported on this relay")
|
|
break
|
|
|
|
except asyncio.TimeoutError:
|
|
print("Timeout waiting for message")
|
|
break
|
|
except json.JSONDecodeError:
|
|
print("Failed to decode message as JSON")
|
|
continue
|
|
|
|
await websocket.send(json.dumps(["CLOSE", sub_id]))
|
|
|
|
except Exception as e:
|
|
print(f"Query failed: {e}")
|
|
|
|
print(f"Query complete. Found {len(events)} matching events")
|
|
return events
|
|
|
|
|
|
async def get_cache() -> list[dict]:
|
|
return [] # TODO: Implement cache
|
|
|
|
|
|
async def fetch_onion(provider: str) -> dict:
|
|
"""Check if an onion service is healthy by making a GET request to its root."""
|
|
try:
|
|
# Get Tor proxy URL from environment variable, default to local Tor SOCKS5 proxy
|
|
tor_proxy = os.getenv("TOR_PROXY_URL", "socks5://127.0.0.1:9050")
|
|
|
|
# Configure httpx to use Tor SOCKS5 proxy
|
|
async with httpx.AsyncClient(
|
|
proxies={"http://": tor_proxy, "https://": tor_proxy}, # type: ignore
|
|
timeout=httpx.Timeout(30.0),
|
|
follow_redirects=True,
|
|
) as client:
|
|
response = await client.get(provider)
|
|
# Consider 2xx and 3xx status codes as healthy
|
|
return {"status_code": response.status_code, "json": response.json()}
|
|
except Exception:
|
|
# Any exception means the service is not healthy
|
|
return {"status_code": 500, "json": {"error": "Failed to fetch onion"}}
|
|
|
|
|
|
@providers_router.get("/")
|
|
async def get_providers(include_json: bool = False) -> dict[str, list[dict | str]]:
|
|
npub = "npub130mznv74rxs032peqym6g3wqavh472623mt3z5w73xq9r6qqdufs7ql29s"
|
|
|
|
# Relays that support NIP-50 text search
|
|
search_relays = [
|
|
"wss://relay.nostr.band", # Known to support search
|
|
"wss://nostr.wine", # Known to support search
|
|
"wss://relay.damus.io",
|
|
"wss://nos.lol",
|
|
]
|
|
|
|
# Search for the mention format that appears in posts
|
|
search_term = f"nostr:{npub}"
|
|
|
|
all_events = []
|
|
event_ids = set() # To avoid duplicates
|
|
|
|
# Try multiple relays
|
|
for relay_url in search_relays:
|
|
print(f"\nTrying relay: {relay_url}")
|
|
try:
|
|
events = await query_nostr_relay_with_search(
|
|
search_term=search_term,
|
|
relay_url=relay_url,
|
|
kinds=[1], # Text notes
|
|
limit=500,
|
|
)
|
|
|
|
# Add unique events
|
|
for event in events:
|
|
if event["id"] not in event_ids:
|
|
event_ids.add(event["id"])
|
|
all_events.append(event)
|
|
|
|
print(f"Got {len(events)} events from {relay_url}")
|
|
|
|
# If we have enough events, we can stop
|
|
if len(all_events) >= 100:
|
|
break
|
|
|
|
except Exception as e:
|
|
print(f"Failed to query {relay_url}: {e}")
|
|
continue
|
|
|
|
print(f"Found {len(all_events)} total unique events mentioning routstr")
|
|
|
|
providers = []
|
|
for event in all_events:
|
|
onion_urls = extract_onion_urls(event["content"])
|
|
providers.extend(onion_urls)
|
|
|
|
unique_providers = list(set(providers))
|
|
|
|
print(f"Found {len(unique_providers)} unique onion URLs")
|
|
print(unique_providers)
|
|
|
|
healthy_providers: list[dict | str] = []
|
|
for provider in unique_providers:
|
|
response = await fetch_onion(provider)
|
|
|
|
if include_json:
|
|
healthy_providers.append({provider: response["json"]})
|
|
else:
|
|
healthy_providers.append(provider)
|
|
|
|
return {"providers": healthy_providers}
|