You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
417 lines
12 KiB
417 lines
12 KiB
<script lang="ts"> |
|
import { nostrClient } from '../../services/nostr/nostr-client.js'; |
|
import { Relay } from 'nostr-tools'; |
|
import { onMount } from 'svelte'; |
|
|
|
interface Props { |
|
relayUrl: string; |
|
} |
|
|
|
let { relayUrl }: Props = $props(); |
|
|
|
interface RelayMetadata { |
|
name?: string; |
|
description?: string; |
|
pubkey?: string; |
|
contact?: string; |
|
supported_nips?: number[]; |
|
software?: string; |
|
version?: string; |
|
limitation?: { |
|
max_message_length?: number; |
|
max_subscriptions?: number; |
|
max_filters?: number; |
|
max_limit?: number; |
|
max_subid_length?: number; |
|
min_prefix?: number; |
|
max_event_tags?: number; |
|
max_content_length?: number; |
|
min_pow_difficulty?: number; |
|
auth_required?: boolean; |
|
payment_required?: boolean; |
|
restricted_writes?: boolean; |
|
}; |
|
} |
|
|
|
let metadata = $state<RelayMetadata | null>(null); |
|
let loading = $state(true); |
|
let error = $state<string | null>(null); |
|
let connectionStatus = $state<'connecting' | 'connected' | 'disconnected'>('connecting'); |
|
let eventCount = $state<number | null>(null); |
|
|
|
async function fetchRelayMetadata() { |
|
loading = true; |
|
error = null; |
|
|
|
try { |
|
// Get relay connection status from nostr-client |
|
const relay = await nostrClient.getRelay(relayUrl); |
|
if (relay) { |
|
// Check connection status |
|
const status = (relay as any).status; |
|
if (status === 1) { |
|
connectionStatus = 'connected'; |
|
} else if (status === 0) { |
|
connectionStatus = 'connecting'; |
|
} else { |
|
connectionStatus = 'disconnected'; |
|
} |
|
|
|
// Fetch NIP-11 metadata via HTTP GET |
|
try { |
|
// Convert ws:// or wss:// to http:// or https:// |
|
const httpUrl = relayUrl.replace(/^wss?:\/\//, (match) => { |
|
return match === 'wss://' ? 'https://' : 'http://'; |
|
}); |
|
const nip11Url = `${httpUrl}/.well-known/nostr.json`; |
|
|
|
const response = await fetch(nip11Url, { |
|
method: 'GET', |
|
headers: { |
|
'Accept': 'application/nostr+json' |
|
} |
|
}); |
|
|
|
if (response.ok) { |
|
const info = await response.json(); |
|
if (info) { |
|
metadata = info as RelayMetadata; |
|
} |
|
} |
|
} catch (err) { |
|
console.debug(`[RelayInfo] Could not fetch NIP-11 metadata for ${relayUrl}:`, err); |
|
// Not all relays support NIP-11, so this is not a critical error |
|
} |
|
} else { |
|
connectionStatus = 'disconnected'; |
|
error = 'Failed to connect to relay'; |
|
} |
|
} catch (err) { |
|
console.error(`[RelayInfo] Error fetching relay info for ${relayUrl}:`, err); |
|
error = err instanceof Error ? err.message : 'Unknown error'; |
|
connectionStatus = 'disconnected'; |
|
} finally { |
|
loading = false; |
|
} |
|
} |
|
|
|
async function getEventCount() { |
|
try { |
|
// Fetch a small sample to estimate activity |
|
const events = await nostrClient.fetchEvents( |
|
[{ kinds: [1], limit: 1 }], |
|
[relayUrl], |
|
{ useCache: false, cacheResults: false, timeout: 5000 } |
|
); |
|
// This is just a connectivity check, not a real count |
|
// Real event count would require a COUNT query which not all relays support |
|
eventCount = events.length > 0 ? -1 : 0; // -1 means "has events" but we don't know the count |
|
} catch (err) { |
|
// Silently fail - this is just informational |
|
eventCount = null; |
|
} |
|
} |
|
|
|
onMount(async () => { |
|
await fetchRelayMetadata(); |
|
await getEventCount(); |
|
}); |
|
|
|
function getStatusColor(status: string): string { |
|
switch (status) { |
|
case 'connected': |
|
return 'text-green-600 dark:text-green-400'; |
|
case 'connecting': |
|
return 'text-yellow-600 dark:text-yellow-400'; |
|
case 'disconnected': |
|
return 'text-red-600 dark:text-red-400'; |
|
default: |
|
return 'text-gray-600 dark:text-gray-400'; |
|
} |
|
} |
|
|
|
function getStatusIcon(status: string): string { |
|
switch (status) { |
|
case 'connected': |
|
return '●'; |
|
case 'connecting': |
|
return '○'; |
|
case 'disconnected': |
|
return '○'; |
|
default: |
|
return '○'; |
|
} |
|
} |
|
</script> |
|
|
|
<div class="relay-info-card"> |
|
{#if loading} |
|
<div class="relay-info-loading"> |
|
<p class="text-fog-text-light dark:text-fog-dark-text-light">Loading relay information...</p> |
|
</div> |
|
{:else if error} |
|
<div class="relay-info-error"> |
|
<p class="text-fog-text dark:text-fog-dark-text">Error: {error}</p> |
|
</div> |
|
{:else} |
|
<div class="relay-info-header"> |
|
<div class="relay-info-title-row"> |
|
<h3 class="relay-info-title">Relay Information</h3> |
|
<div class="relay-status"> |
|
<span class="relay-status-icon {getStatusColor(connectionStatus)}">{getStatusIcon(connectionStatus)}</span> |
|
<span class="relay-status-text {getStatusColor(connectionStatus)}"> |
|
{connectionStatus.charAt(0).toUpperCase() + connectionStatus.slice(1)} |
|
</span> |
|
</div> |
|
</div> |
|
<code class="relay-url">{relayUrl}</code> |
|
</div> |
|
|
|
{#if metadata} |
|
<div class="relay-info-content"> |
|
{#if metadata.name} |
|
<div class="relay-info-item"> |
|
<span class="relay-info-label">Name:</span> |
|
<span class="relay-info-value">{metadata.name}</span> |
|
</div> |
|
{/if} |
|
|
|
{#if metadata.description} |
|
<div class="relay-info-item"> |
|
<span class="relay-info-label">Description:</span> |
|
<span class="relay-info-value">{metadata.description}</span> |
|
</div> |
|
{/if} |
|
|
|
{#if metadata.software} |
|
<div class="relay-info-item"> |
|
<span class="relay-info-label">Software:</span> |
|
<span class="relay-info-value">{metadata.software}{metadata.version ? ` ${metadata.version}` : ''}</span> |
|
</div> |
|
{/if} |
|
|
|
{#if metadata.contact} |
|
<div class="relay-info-item"> |
|
<span class="relay-info-label">Contact:</span> |
|
<span class="relay-info-value"> |
|
{#if metadata.contact.startsWith('mailto:')} |
|
<a href="{metadata.contact}" class="relay-info-link">{metadata.contact.replace('mailto:', '')}</a> |
|
{:else if metadata.contact.startsWith('http')} |
|
<a href="{metadata.contact}" target="_blank" rel="noopener noreferrer" class="relay-info-link">{metadata.contact}</a> |
|
{:else} |
|
{metadata.contact} |
|
{/if} |
|
</span> |
|
</div> |
|
{/if} |
|
|
|
{#if metadata.pubkey} |
|
<div class="relay-info-item"> |
|
<span class="relay-info-label">Pubkey:</span> |
|
<code class="relay-info-value relay-pubkey">{metadata.pubkey.substring(0, 16)}...</code> |
|
</div> |
|
{/if} |
|
|
|
{#if metadata.supported_nips && metadata.supported_nips.length > 0} |
|
<div class="relay-info-item"> |
|
<span class="relay-info-label">Supported NIPs:</span> |
|
<span class="relay-info-value"> |
|
{metadata.supported_nips.slice(0, 20).join(', ')}{metadata.supported_nips.length > 20 ? '...' : ''} |
|
</span> |
|
</div> |
|
{/if} |
|
|
|
{#if metadata.limitation} |
|
<div class="relay-info-limitations"> |
|
<span class="relay-info-label">Limitations:</span> |
|
<div class="relay-limitations-list"> |
|
{#if metadata.limitation.max_message_length} |
|
<span class="relay-limitation-item">Max message: {metadata.limitation.max_message_length} bytes</span> |
|
{/if} |
|
{#if metadata.limitation.max_subscriptions} |
|
<span class="relay-limitation-item">Max subscriptions: {metadata.limitation.max_subscriptions}</span> |
|
{/if} |
|
{#if metadata.limitation.max_filters} |
|
<span class="relay-limitation-item">Max filters: {metadata.limitation.max_filters}</span> |
|
{/if} |
|
{#if metadata.limitation.max_limit} |
|
<span class="relay-limitation-item">Max limit: {metadata.limitation.max_limit}</span> |
|
{/if} |
|
{#if metadata.limitation.auth_required} |
|
<span class="relay-limitation-item">Auth required</span> |
|
{/if} |
|
{#if metadata.limitation.payment_required} |
|
<span class="relay-limitation-item">Payment required</span> |
|
{/if} |
|
{#if metadata.limitation.restricted_writes} |
|
<span class="relay-limitation-item">Restricted writes</span> |
|
{/if} |
|
</div> |
|
</div> |
|
{/if} |
|
</div> |
|
{:else} |
|
<div class="relay-info-no-metadata"> |
|
<p class="text-fog-text-light dark:text-fog-dark-text-light">No NIP-11 metadata available</p> |
|
</div> |
|
{/if} |
|
{/if} |
|
</div> |
|
|
|
<style> |
|
.relay-info-card { |
|
background: var(--fog-post, #ffffff); |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.5rem; |
|
padding: 1.5rem; |
|
margin-bottom: 1.5rem; |
|
} |
|
|
|
:global(.dark) .relay-info-card { |
|
background: var(--fog-dark-post, #1f2937); |
|
border-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.relay-info-header { |
|
margin-bottom: 1rem; |
|
padding-bottom: 1rem; |
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
} |
|
|
|
:global(.dark) .relay-info-header { |
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
} |
|
|
|
.relay-info-title-row { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.relay-info-title { |
|
margin: 0; |
|
font-size: 1.25rem; |
|
font-weight: 600; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .relay-info-title { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.relay-status { |
|
display: flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
} |
|
|
|
.relay-status-icon { |
|
font-size: 0.75rem; |
|
} |
|
|
|
.relay-status-text { |
|
font-size: 0.875rem; |
|
font-weight: 500; |
|
} |
|
|
|
.relay-url { |
|
display: block; |
|
font-family: monospace; |
|
font-size: 0.875rem; |
|
color: var(--fog-text-light, #9ca3af); |
|
word-break: break-all; |
|
} |
|
|
|
:global(.dark) .relay-url { |
|
color: var(--fog-dark-text-light, #6b7280); |
|
} |
|
|
|
.relay-info-content { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.75rem; |
|
} |
|
|
|
.relay-info-item { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.25rem; |
|
} |
|
|
|
.relay-info-label { |
|
font-size: 0.875rem; |
|
font-weight: 600; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .relay-info-label { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.relay-info-value { |
|
font-size: 0.875rem; |
|
color: var(--fog-text-light, #6b7280); |
|
word-break: break-word; |
|
} |
|
|
|
:global(.dark) .relay-info-value { |
|
color: var(--fog-dark-text-light, #9ca3af); |
|
} |
|
|
|
.relay-pubkey { |
|
font-family: monospace; |
|
} |
|
|
|
.relay-info-link { |
|
color: var(--fog-accent, #64748b); |
|
text-decoration: underline; |
|
} |
|
|
|
.relay-info-link:hover { |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .relay-info-link { |
|
color: var(--fog-dark-accent, #94a3b8); |
|
} |
|
|
|
:global(.dark) .relay-info-link:hover { |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.relay-info-limitations { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 0.5rem; |
|
margin-top: 0.5rem; |
|
} |
|
|
|
.relay-limitations-list { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 0.5rem; |
|
} |
|
|
|
.relay-limitation-item { |
|
font-size: 0.75rem; |
|
padding: 0.25rem 0.5rem; |
|
background: var(--fog-highlight, #f3f4f6); |
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
border-radius: 0.25rem; |
|
color: var(--fog-text, #1f2937); |
|
} |
|
|
|
:global(.dark) .relay-limitation-item { |
|
background: var(--fog-dark-highlight, #374151); |
|
border-color: var(--fog-dark-border, #475569); |
|
color: var(--fog-dark-text, #f9fafb); |
|
} |
|
|
|
.relay-info-loading, |
|
.relay-info-error, |
|
.relay-info-no-metadata { |
|
padding: 1rem; |
|
text-align: center; |
|
} |
|
</style>
|
|
|