import { useCallback, useEffect, useState } from 'react' import relayAdmin from '@/services/relay-admin.service' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { toast } from 'sonner' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface PubkeyEntry { pubkey: string reason?: string } interface EventEntry { id: string reason?: string } interface IPEntry { ip: string reason?: string } interface RelayConfig { relay_name: string relay_description: string relay_icon: string } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- async function nip86(method: string, params: unknown[] = []): Promise { const res = await relayAdmin.nip86Request(method, params) if ((res as { error?: string }).error) { throw new Error((res as { error: string }).error) } return (res as { result?: unknown }).result } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- function ListShell({ children, empty }: { children: React.ReactNode empty: string }) { const hasChildren = Array.isArray(children) ? children.length > 0 : !!children return (
{hasChildren ? children : (
{empty}
)}
) } function ListRow({ children }: { children: React.ReactNode }) { return (
{children}
) } // --------------------------------------------------------------------------- // Banned Pubkeys // --------------------------------------------------------------------------- function BannedPubkeysSection() { const [items, setItems] = useState([]) const [pubkey, setPubkey] = useState('') const [reason, setReason] = useState('') const [busy, setBusy] = useState(false) const load = useCallback(async () => { try { const r = await nip86('listbannedpubkeys') setItems(Array.isArray(r) ? r : []) } catch (e) { toast.error(`Load banned pubkeys: ${e instanceof Error ? e.message : String(e)}`) } }, []) useEffect(() => { load() }, [load]) const ban = async () => { if (!pubkey.trim()) return setBusy(true) try { await nip86('banpubkey', [pubkey.trim(), reason.trim()]) toast.success('Pubkey banned') setPubkey('') setReason('') await load() } catch (e) { toast.error(`Ban failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } return (

Banned Pubkeys

setPubkey(e.target.value)} /> setReason(e.target.value)} />
{items.map((it, i) => ( {it.pubkey} {it.reason && {it.reason}} ))}
) } // --------------------------------------------------------------------------- // Allowed Pubkeys // --------------------------------------------------------------------------- function AllowedPubkeysSection() { const [items, setItems] = useState([]) const [pubkey, setPubkey] = useState('') const [reason, setReason] = useState('') const [busy, setBusy] = useState(false) const load = useCallback(async () => { try { const r = await nip86('listallowedpubkeys') setItems(Array.isArray(r) ? r : []) } catch (e) { toast.error(`Load allowed pubkeys: ${e instanceof Error ? e.message : String(e)}`) } }, []) useEffect(() => { load() }, [load]) const allow = async () => { if (!pubkey.trim()) return setBusy(true) try { await nip86('allowpubkey', [pubkey.trim(), reason.trim()]) toast.success('Pubkey allowed') setPubkey('') setReason('') await load() } catch (e) { toast.error(`Allow failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } return (

Allowed Pubkeys

setPubkey(e.target.value)} /> setReason(e.target.value)} />
{items.map((it, i) => ( {it.pubkey} {it.reason && {it.reason}} ))}
) } // --------------------------------------------------------------------------- // Banned Events // --------------------------------------------------------------------------- function BannedEventsSection() { const [items, setItems] = useState([]) const [eventId, setEventId] = useState('') const [reason, setReason] = useState('') const [busy, setBusy] = useState(false) const load = useCallback(async () => { try { const r = await nip86('listbannedevents') setItems(Array.isArray(r) ? r : []) } catch (e) { toast.error(`Load banned events: ${e instanceof Error ? e.message : String(e)}`) } }, []) useEffect(() => { load() }, [load]) const ban = async () => { if (!eventId.trim()) return setBusy(true) try { await nip86('banevent', [eventId.trim(), reason.trim()]) toast.success('Event banned') setEventId('') setReason('') await load() } catch (e) { toast.error(`Ban failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } return (

Banned Events

setEventId(e.target.value)} /> setReason(e.target.value)} />
{items.map((it, i) => ( {it.id} {it.reason && {it.reason}} ))}
) } // --------------------------------------------------------------------------- // Allowed Events (allow only, no list endpoint) // --------------------------------------------------------------------------- function AllowedEventsSection() { const [eventId, setEventId] = useState('') const [reason, setReason] = useState('') const [busy, setBusy] = useState(false) const allow = async () => { if (!eventId.trim()) return setBusy(true) try { await nip86('allowevent', [eventId.trim(), reason.trim()]) toast.success('Event allowed') setEventId('') setReason('') } catch (e) { toast.error(`Allow failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } return (

Allow Event

setEventId(e.target.value)} /> setReason(e.target.value)} />
) } // --------------------------------------------------------------------------- // Allowed Kinds // --------------------------------------------------------------------------- function AllowedKindsSection() { const [items, setItems] = useState([]) const [kindInput, setKindInput] = useState('') const [busy, setBusy] = useState(false) const load = useCallback(async () => { try { const r = await nip86('listallowedkinds') setItems(Array.isArray(r) ? r : []) } catch (e) { toast.error(`Load allowed kinds: ${e instanceof Error ? e.message : String(e)}`) } }, []) useEffect(() => { load() }, [load]) const addKind = async () => { const num = parseInt(kindInput, 10) if (isNaN(num)) { toast.error('Invalid kind number') return } setBusy(true) try { await nip86('allowkind', [num]) toast.success(`Kind ${num} allowed`) setKindInput('') await load() } catch (e) { toast.error(`Allow kind failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } const removeKind = async (kind: number) => { setBusy(true) try { await nip86('disallowkind', [kind]) toast.success(`Kind ${kind} disallowed`) await load() } catch (e) { toast.error(`Disallow kind failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } return (

Allowed Event Kinds

setKindInput(e.target.value)} />
{items.map((kind) => ( Kind {kind} ))}
) } // --------------------------------------------------------------------------- // Blocked IPs // --------------------------------------------------------------------------- function BlockedIPsSection() { const [items, setItems] = useState([]) const [ip, setIp] = useState('') const [reason, setReason] = useState('') const [busy, setBusy] = useState(false) const load = useCallback(async () => { try { const r = await nip86('listblockedips') setItems(Array.isArray(r) ? r : []) } catch (e) { toast.error(`Load blocked IPs: ${e instanceof Error ? e.message : String(e)}`) } }, []) useEffect(() => { load() }, [load]) const block = async () => { if (!ip.trim()) return setBusy(true) try { await nip86('blockip', [ip.trim(), reason.trim()]) toast.success('IP blocked') setIp('') setReason('') await load() } catch (e) { toast.error(`Block failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } const unblock = async (addr: string) => { setBusy(true) try { await nip86('unblockip', [addr]) toast.success('IP unblocked') await load() } catch (e) { toast.error(`Unblock failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } return (

Blocked IPs

setIp(e.target.value)} /> setReason(e.target.value)} />
{items.map((it, i) => ( {it.ip} {it.reason && {it.reason}} ))}
) } // --------------------------------------------------------------------------- // Events Needing Moderation // --------------------------------------------------------------------------- function ModerationSection() { const [items, setItems] = useState([]) const [busy, setBusy] = useState(false) const load = useCallback(async () => { setBusy(true) try { const r = await nip86('listeventsneedingmoderation') setItems(Array.isArray(r) ? r : []) } catch (e) { toast.error(`Load moderation queue: ${e instanceof Error ? e.message : String(e)}`) setItems([]) } finally { setBusy(false) } }, []) useEffect(() => { load() }, [load]) const approve = async (id: string) => { setBusy(true) try { await nip86('allowevent', [id, 'Approved from moderation queue']) toast.success('Event approved') await load() } catch (e) { toast.error(`Approve failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } const reject = async (id: string) => { setBusy(true) try { await nip86('banevent', [id, 'Rejected from moderation queue']) toast.success('Event rejected') await load() } catch (e) { toast.error(`Reject failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } return (

Events Needing Moderation

{items.map((it, i) => ( {it.id} {it.reason && {it.reason}}
))}
) } // --------------------------------------------------------------------------- // Relay Config // --------------------------------------------------------------------------- function RelayConfigSection() { const [config, setConfig] = useState({ relay_name: '', relay_description: '', relay_icon: '' }) const [busy, setBusy] = useState(false) const fetchInfo = useCallback(async () => { setBusy(true) try { const info = await relayAdmin.fetchRelayInfo() if (info) { setConfig({ relay_name: (info.name as string) || '', relay_description: (info.description as string) || '', relay_icon: (info.icon as string) || '' }) } } catch (e) { toast.error(`Fetch relay info: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } }, []) useEffect(() => { fetchInfo() }, [fetchInfo]) const save = async () => { setBusy(true) try { const updates: Promise[] = [] if (config.relay_name) updates.push(nip86('changerelayname', [config.relay_name])) if (config.relay_description) updates.push(nip86('changerelaydescription', [config.relay_description])) if (config.relay_icon) updates.push(nip86('changerelayicon', [config.relay_icon])) if (updates.length === 0) { toast.info('No changes to update') return } await Promise.all(updates) toast.success('Relay configuration updated') await fetchInfo() } catch (e) { toast.error(`Update failed: ${e instanceof Error ? e.message : String(e)}`) } finally { setBusy(false) } } return (

Relay Configuration

setConfig((c) => ({ ...c, relay_name: e.target.value }))} />