From e9262726869e95ccf09d88ac0398a90f90fa47ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Thu, 11 Dec 2025 00:09:26 +0100 Subject: [PATCH] feat: relay command --- .gitignore | 1 + TODO.md | 25 ++ src/components/Home.tsx | 3 + src/components/JsonViewer.tsx | 3 +- src/components/NIPBadge.tsx | 46 +++ src/components/RelayViewer.tsx | 88 +++++ src/hooks/useRelayInfo.ts | 51 +++ src/lib/nip-icons.ts | 577 +++++++++++++++++++++++++++++++++ src/lib/nip11.ts | 100 ++++++ src/lib/nostr-validation.ts | 24 ++ src/lib/relay-parser.ts | 36 ++ src/services/db.ts | 11 +- src/types/app.ts | 3 +- src/types/man.ts | 1 + src/types/nip11.ts | 99 ++++++ tsconfig.app.tsbuildinfo | 1 - 16 files changed, 1064 insertions(+), 5 deletions(-) create mode 100644 TODO.md create mode 100644 src/components/NIPBadge.tsx create mode 100644 src/components/RelayViewer.tsx create mode 100644 src/hooks/useRelayInfo.ts create mode 100644 src/lib/nip-icons.ts create mode 100644 src/lib/nip11.ts create mode 100644 src/lib/nostr-validation.ts create mode 100644 src/lib/relay-parser.ts create mode 100644 src/types/nip11.ts delete mode 100644 tsconfig.app.tsbuildinfo diff --git a/.gitignore b/.gitignore index a547bf3..ce060ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Logs logs *.log +*.tsbuildinfo npm-debug.log* yarn-debug.log* yarn-error.log* diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..d7a8624 --- /dev/null +++ b/TODO.md @@ -0,0 +1,25 @@ +# TODO + +## Known Issues + +### RTL Support in Rich Text +**Priority**: Medium +**File**: `src/components/nostr/RichText/Text.tsx` + +Current RTL implementation is partial and has limitations: +- RTL text direction works (`dir` attribute on spans) +- RTL text alignment (right-align) doesn't work properly with inline elements +- Mixed LTR/RTL content with inline elements (hashtags, mentions) creates layout conflicts + +**The core problem**: +- Inline elements (hashtags, mentions) need inline flow to stay on same line +- RTL alignment requires block-level containers +- These two requirements conflict + +**Potential solutions to explore**: +1. Line-aware rendering at RichText component level (parse and group by lines) +2. CSS-based approach with unicode-bidi and direction properties +3. Separate rendering paths for pure RTL content vs mixed content +4. Accept partial RTL support and document limitations + +**Test case**: Arabic text with hashtags on same line should display properly with right-alignment. diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 3acaf07..8e342b8 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -103,6 +103,9 @@ export default function Home() { case "decode": content = ; break; + case "relay": + content = ; + break; default: content = (
diff --git a/src/components/JsonViewer.tsx b/src/components/JsonViewer.tsx index 1b05622..2697087 100644 --- a/src/components/JsonViewer.tsx +++ b/src/components/JsonViewer.tsx @@ -1,5 +1,4 @@ -import { useState } from "react"; -import { Copy, Check } from "lucide-react"; +import { Check, Copy } from "lucide-react"; import { Dialog, DialogContent, diff --git a/src/components/NIPBadge.tsx b/src/components/NIPBadge.tsx new file mode 100644 index 0000000..a32cc39 --- /dev/null +++ b/src/components/NIPBadge.tsx @@ -0,0 +1,46 @@ +import { getNIPInfo } from "../lib/nip-icons"; +import { useGrimoire } from "@/core/state"; + +export interface NIPBadgeProps { + nipNumber: number; + className?: string; + showName?: boolean; +} + +/** + * NIPBadge - Reusable component for displaying NIP badges + * Shows icon, number, optional name, and links to NIP page + */ +export function NIPBadge({ + nipNumber, + className = "", + showName = true, +}: NIPBadgeProps) { + const { addWindow } = useGrimoire(); + const nipInfo = getNIPInfo(nipNumber); + const name = nipInfo?.name || `NIP-${nipNumber}`; + const description = + nipInfo?.description || `Nostr Implementation Possibility ${nipNumber}`; + + const openNIP = () => { + const paddedNum = nipNumber.toString().padStart(2, "0"); + addWindow( + "nip", + { number: paddedNum }, + nipInfo ? `NIP ${paddedNum} - ${nipInfo?.name}` : `NIP ${paddedNum}`, + ); + }; + + return ( + + ); +} diff --git a/src/components/RelayViewer.tsx b/src/components/RelayViewer.tsx new file mode 100644 index 0000000..d56fee8 --- /dev/null +++ b/src/components/RelayViewer.tsx @@ -0,0 +1,88 @@ +import { Copy, Check, Server } from "lucide-react"; +import { useRelayInfo } from "../hooks/useRelayInfo"; +import { useCopy } from "../hooks/useCopy"; +import { Button } from "./ui/button"; +import { UserName } from "./nostr/UserName"; +import { NIPBadge } from "./NIPBadge"; + +export interface RelayViewerProps { + url: string; +} + +export function RelayViewer({ url }: RelayViewerProps) { + const info = useRelayInfo(url); + const { copy, copied } = useCopy(); + + return ( +
+ {/* Header */} +
+ {info?.icon ? ( + {info.name + ) : ( + + )} +
+

+ {info?.name || "Unknown Relay"} +

+
+ {url} + +
+ {info?.description && ( +

{info.description}

+ )} +
+
+ + {/* Operator */} + {(info?.contact || info?.pubkey) && ( +
+

Operator

+
+ {info.contact && info.contact.length == 64 && ( + + )} + {info.pubkey && info.pubkey.length === 64 && ( + + )} +
+
+ )} + + {/* Software */} + {(info?.software || info?.version) && ( +
+

Software

+ + {info.software || info.version} + +
+ )} + + {/* Supported NIPs */} + {info?.supported_nips && info.supported_nips.length > 0 && ( +
+

Supported NIPs

+
+ {info.supported_nips.map((num: number) => ( + + ))} +
+
+ )} +
+ ); +} diff --git a/src/hooks/useRelayInfo.ts b/src/hooks/useRelayInfo.ts new file mode 100644 index 0000000..f760a4b --- /dev/null +++ b/src/hooks/useRelayInfo.ts @@ -0,0 +1,51 @@ +import { useLiveQuery } from "dexie-react-hooks"; +import { useEffect } from "react"; +import { RelayInformation } from "../types/nip11"; +import { fetchRelayInfo } from "../lib/nip11"; +import db from "../services/db"; + +/** + * React hook to fetch and cache relay information (NIP-11) + * @param wsUrl - WebSocket URL of the relay (ws:// or wss://) + * @returns Relay information or undefined if not yet loaded + */ +export function useRelayInfo(wsUrl: string | undefined): RelayInformation | undefined { + const cached = useLiveQuery( + () => (wsUrl ? db.relayInfo.get(wsUrl) : undefined), + [wsUrl], + ); + + useEffect(() => { + if (!wsUrl) return; + if (cached) return; + + // Fetch relay info if not in cache + fetchRelayInfo(wsUrl).then((info) => { + if (info) { + db.relayInfo.put({ + url: wsUrl, + info, + fetchedAt: Date.now(), + }); + } + }); + }, [cached, wsUrl]); + + return cached?.info; +} + +/** + * React hook to check if a relay supports a specific NIP + * @param wsUrl - WebSocket URL of the relay + * @param nipNumber - NIP number to check (e.g., 42 for NIP-42) + * @returns true if supported, false if not, undefined if not yet loaded + */ +export function useRelaySupportsNip( + wsUrl: string | undefined, + nipNumber: number, +): boolean | undefined { + const info = useRelayInfo(wsUrl); + + if (!info) return undefined; + return info.supported_nips?.includes(nipNumber) ?? false; +} diff --git a/src/lib/nip-icons.ts b/src/lib/nip-icons.ts new file mode 100644 index 0000000..eec6567 --- /dev/null +++ b/src/lib/nip-icons.ts @@ -0,0 +1,577 @@ +/** + * NIP Icon Mapping + * Maps NIP numbers to Lucide icons for visual representation + */ + +import { + FileText, + Lock, + Hash, + MessageSquare, + Tag, + Image, + Link, + Zap, + Key, + Shield, + Search, + Calendar, + Users, + Mail, + Gift, + Flag, + AlertCircle, + Globe, + Server, + Database, + Eye, + Heart, + Star, + Bookmark, + Share2, + Filter, + Coins, + Video, + Music, + Code, + ShoppingCart, + GitBranch, + Package, + Wallet, + Radio, + Compass, + Gamepad2, + type LucideIcon, +} from "lucide-react"; + +export interface NIPInfo { + number: number; + name: string; + description: string; + icon: LucideIcon; + deprecated?: boolean; +} + +export const NIP_METADATA: Record = { + // Core Protocol + 1: { + number: 1, + name: "Basic Protocol", + description: "Basic protocol flow description", + icon: FileText, + }, + 2: { + number: 2, + name: "Follow List", + description: "Contact list and petnames", + icon: Users, + }, + 4: { + number: 4, + name: "Encrypted DMs", + description: "Encrypted direct messages", + icon: Mail, + deprecated: true, + }, + 5: { + number: 5, + name: "Mapping Nostr keys to DNS", + description: "Mapping Nostr keys to DNS-based internet identifiers", + icon: Globe, + }, + 6: { + number: 6, + name: "Key Derivation", + description: "Basic key derivation from mnemonic seed phrase", + icon: Key, + }, + 7: { + number: 7, + name: "window.nostr", + description: "window.nostr capability for web browsers", + icon: Globe, + }, + 8: { + number: 8, + name: "Mentions", + description: "Handling mentions", + icon: Tag, + deprecated: true, + }, + 9: { + number: 9, + name: "Event Deletion", + description: "Event deletion", + icon: AlertCircle, + }, + 10: { + number: 10, + name: "Conventions", + description: "Conventions for clients' use of e and p tags", + icon: Tag, + }, + 11: { + number: 11, + name: "Relay Info", + description: "Relay information document", + icon: Server, + }, + 13: { + number: 13, + name: "Proof of Work", + description: "Proof of work", + icon: Zap, + }, + 14: { + number: 14, + name: "Subject Tag", + description: "Subject tag in text events", + icon: Tag, + }, + 15: { + number: 15, + name: "Marketplace", + description: "Marketplace (for resilient marketplaces)", + icon: ShoppingCart, + }, + 17: { + number: 17, + name: "Private DMs", + description: "Private Direct Messages", + icon: Lock, + }, + 18: { number: 18, name: "Reposts", description: "Reposts", icon: Share2 }, + 19: { + number: 19, + name: "bech32 Entities", + description: "bech32-encoded entities", + icon: Hash, + }, + 21: { + number: 21, + name: "nostr: URI", + description: "nostr: URI scheme", + icon: Link, + }, + 22: { + number: 22, + name: "Comment", + description: "Comment", + icon: MessageSquare, + }, + 23: { + number: 23, + name: "Long-form", + description: "Long-form content", + icon: FileText, + }, + 24: { + number: 24, + name: "Extra Metadata", + description: "Extra metadata fields and tags", + icon: Tag, + }, + 25: { number: 25, name: "Reactions", description: "Reactions", icon: Heart }, + 26: { + number: 26, + name: "Delegated Signing", + description: "Delegated event signing", + icon: Key, + deprecated: true, + }, + 27: { + number: 27, + name: "Text Note References", + description: "Text note references", + icon: Link, + }, + 28: { + number: 28, + name: "Public Chat", + description: "Public chat", + icon: MessageSquare, + }, + 29: { + number: 29, + name: "Relay Groups", + description: "Relay-based groups", + icon: Users, + }, + 30: { + number: 30, + name: "Custom Emoji", + description: "Custom emoji", + icon: Gift, + }, + 31: { + number: 31, + name: "Unknown Events", + description: "Dealing with unknown event kinds", + icon: AlertCircle, + }, + 32: { number: 32, name: "Labeling", description: "Labeling", icon: Tag }, + 34: { number: 34, name: "Git", description: "git stuff", icon: GitBranch }, + 35: { number: 35, name: "Torrents", description: "Torrents", icon: Share2 }, + 36: { + number: 36, + name: "Sensitive Content", + description: "Sensitive content warnings", + icon: Eye, + }, + 37: { + number: 37, + name: "Draft Events", + description: "Draft Events", + icon: FileText, + }, + 38: { + number: 38, + name: "User Status", + description: "User statuses", + icon: Flag, + }, + 39: { + number: 39, + name: "External Identity", + description: "External identities in profiles", + icon: Globe, + }, + 40: { + number: 40, + name: "Expiration", + description: "Expiration timestamp", + icon: Calendar, + }, + 42: { + number: 42, + name: "Authentication", + description: "Authentication of clients to relays", + icon: Shield, + }, + 43: { + number: 43, + name: "Relay Access", + description: "Fast Authentication and Relay Access", + icon: Server, + }, + 44: { + number: 44, + name: "Encrypted Payloads", + description: "Encrypted Payloads (Versioned)", + icon: Lock, + }, + 45: { + number: 45, + name: "Event Counts", + description: "Counting results", + icon: Hash, + }, + 46: { + number: 46, + name: "Remote Signing", + description: "Nostr connect protocol", + icon: Key, + }, + 47: { + number: 47, + name: "Wallet Connect", + description: "Wallet connect", + icon: Wallet, + }, + 48: { number: 48, name: "Proxy Tags", description: "Proxy tags", icon: Tag }, + 49: { + number: 49, + name: "Private Key Encryption", + description: "Private key encryption", + icon: Lock, + }, + 50: { + number: 50, + name: "Search", + description: "Search capability", + icon: Search, + }, + 51: { number: 51, name: "Lists", description: "Lists", icon: Filter }, + 52: { + number: 52, + name: "Calendar Events", + description: "Calendar Events", + icon: Calendar, + }, + 53: { + number: 53, + name: "Live Activities", + description: "Live Activities", + icon: Radio, + }, + 54: { number: 54, name: "Wiki", description: "Wiki", icon: FileText }, + 55: { + number: 55, + name: "Android Signer", + description: "Android Signer Application", + icon: Key, + }, + 56: { number: 56, name: "Reporting", description: "Reporting", icon: Flag }, + 57: { + number: 57, + name: "Lightning Zaps", + description: "Lightning zaps", + icon: Zap, + }, + 58: { number: 58, name: "Badges", description: "Badges", icon: Star }, + 59: { number: 59, name: "Gift Wrap", description: "Gift Wrap", icon: Gift }, + 60: { + number: 60, + name: "Cashu Wallet", + description: "Cashu Wallet", + icon: Wallet, + }, + 61: { number: 61, name: "Nutzaps", description: "Nutzaps", icon: Zap }, + 62: { + number: 62, + name: "Request to Vanish", + description: "Request to Vanish", + icon: Eye, + }, + 64: { number: 64, name: "Chess", description: "Chess (PGN)", icon: Gamepad2 }, + 65: { + number: 65, + name: "Relay List", + description: "Relay list metadata", + icon: Server, + }, + 66: { + number: 66, + name: "Relay Discovery", + description: "Relay Discovery", + icon: Compass, + }, + 68: { + number: 68, + name: "Picture-first", + description: "Picture-first feeds", + icon: Image, + }, + 69: { + number: 69, + name: "P2P Order", + description: "Peer-to-peer Order events", + icon: ShoppingCart, + }, + 70: { + number: 70, + name: "Protected Events", + description: "Protected Events", + icon: Shield, + }, + 71: { + number: 71, + name: "Video Events", + description: "Video Events", + icon: Video, + }, + 72: { + number: 72, + name: "Moderation", + description: "Moderated communities", + icon: Shield, + }, + 73: { + number: 73, + name: "External Content IDs", + description: "External Content IDs", + icon: Link, + }, + 75: { number: 75, name: "Zap Goals", description: "Zap Goals", icon: Zap }, + 77: { + number: 77, + name: "Negentropy", + description: "Negentropy Protocol Sync", + icon: Server, + }, + 78: { + number: 78, + name: "App Data", + description: "Application-specific data", + icon: Database, + }, + 84: { + number: 84, + name: "Highlights", + description: "Highlights", + icon: Bookmark, + }, + 86: { + number: 86, + name: "Relay Management", + description: "Relay Management API", + icon: Server, + }, + 87: { + number: 87, + name: "Ecash Mints", + description: "Ecash Mint Discoverability", + icon: Coins, + }, + 88: { number: 88, name: "Polls", description: "Polls", icon: Filter }, + 89: { + number: 89, + name: "App Handlers", + description: "Recommended application handlers", + icon: Package, + }, + 90: { + number: 90, + name: "Data Vending", + description: "Data Vending Machines", + icon: Database, + }, + 92: { + number: 92, + name: "Media Attachments", + description: "Media Attachments", + icon: Image, + }, + 94: { + number: 94, + name: "File Metadata", + description: "File metadata", + icon: Image, + }, + 96: { + number: 96, + name: "HTTP File Storage", + description: "HTTP File Storage Integration", + icon: Server, + deprecated: true, + }, + 98: { + number: 98, + name: "HTTP Auth", + description: "HTTP authentication", + icon: Lock, + }, + 99: { + number: 99, + name: "Classified Listings", + description: "Classified listings", + icon: Tag, + }, + + // Hex NIPs (A0-EE) + A0: { + number: 0xa0, + name: "Voice Messages", + description: "Voice Messages", + icon: Music, + }, + B0: { + number: 0xb0, + name: "Web Bookmarks", + description: "Web Bookmarks", + icon: Bookmark, + }, + B7: { number: 0xb7, name: "Blossom", description: "Blossom", icon: Package }, + BE: { + number: 0xbe, + name: "BLE", + description: "BLE Communications", + icon: Radio, + }, + C0: { + number: 0xc0, + name: "Code Snippets", + description: "Code Snippets", + icon: Code, + }, + C7: { + number: 0xc7, + name: "Chats", + description: "Chats", + icon: MessageSquare, + }, + EE: { + number: 0xee, + name: "E2EE MLS", + description: "E2EE Messaging (MLS)", + icon: Lock, + }, + "7D": { + number: 0x7d, + name: "Threads", + description: "Threads", + icon: MessageSquare, + }, +}; + +/** + * Get NIP metadata by number (handles both decimal and hex) + */ +export function getNIPInfo(nipNumber: number | string): NIPInfo | undefined { + // Try direct lookup + if (NIP_METADATA[nipNumber]) { + return NIP_METADATA[nipNumber]; + } + + // Try hex conversion for numbers > 99 + if (typeof nipNumber === "number" && nipNumber > 99) { + const hexKey = nipNumber.toString(16).toUpperCase(); + return NIP_METADATA[hexKey]; + } + + return undefined; +} + +/** + * Get all supported NIPs with their metadata, excluding deprecated ones + */ +export function getSupportedNIPsInfo( + nipNumbers: number[], + includeDeprecated: boolean = false, +): (NIPInfo | { number: number; name: string; icon: LucideIcon })[] { + return nipNumbers + .map((num) => { + const info = getNIPInfo(num); + if (info) { + // Skip deprecated NIPs unless explicitly included + if (!includeDeprecated && info.deprecated) { + return null; + } + return info; + } + // Fallback for unknown NIPs + return { + number: num, + name: `NIP-${num}`, + icon: FileText, + }; + }) + .filter( + ( + nip, + ): nip is NIPInfo | { number: number; name: string; icon: LucideIcon } => + nip !== null, + ); +} + +/** + * Group NIPs by a single consolidated category + * For relay viewer, we show all NIPs in one list + */ +export function groupNIPsByCategory( + nipNumbers: number[], +): Record { + const grouped: Record = { + "Supported NIPs": [], + }; + + nipNumbers.forEach((num) => { + const info = getNIPInfo(num); + // Skip deprecated NIPs + if (info && !info.deprecated) { + grouped["Supported NIPs"].push(info); + } + }); + + return grouped; +} diff --git a/src/lib/nip11.ts b/src/lib/nip11.ts new file mode 100644 index 0000000..43d75fd --- /dev/null +++ b/src/lib/nip11.ts @@ -0,0 +1,100 @@ +import { RelayInformation } from "../types/nip11"; +import db from "../services/db"; + +/** + * NIP-11: Relay Information Document + * https://github.com/nostr-protocol/nips/blob/master/11.md + */ + +const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours + +/** + * Fetch relay information document + * NIP-11 specifies: GET request with Accept: application/nostr+json header + */ +export async function fetchRelayInfo( + wsUrl: string, +): Promise { + try { + // Convert ws:// or wss:// to https:// + const httpUrl = wsUrl.replace(/^ws(s)?:/, "https:"); + + const response = await fetch(httpUrl, { + headers: { Accept: "application/nostr+json" }, + }); + + if (!response.ok) return null; + + return (await response.json()) as RelayInformation; + } catch (error) { + console.warn(`NIP-11: Failed to fetch ${wsUrl}:`, error); + return null; + } +} + +/** + * Get relay information with caching (fetches if needed) + */ +export async function getRelayInfo( + wsUrl: string, +): Promise { + const cached = await db.relayInfo.get(wsUrl); + const isExpired = !cached || Date.now() - cached.fetchedAt > CACHE_DURATION; + + if (!isExpired) return cached.info; + + const info = await fetchRelayInfo(wsUrl); + if (info) { + await db.relayInfo.put({ url: wsUrl, info, fetchedAt: Date.now() }); + } + + return info; +} + +/** + * Get cached relay info only (no network request) + */ +export async function getCachedRelayInfo( + wsUrl: string, +): Promise { + const cached = await db.relayInfo.get(wsUrl); + return cached?.info ?? null; +} + +/** + * Fetch multiple relays in parallel + */ +export async function getRelayInfoBatch( + wsUrls: string[], +): Promise> { + const results = new Map(); + const infos = await Promise.all(wsUrls.map((url) => getRelayInfo(url))); + + infos.forEach((info, i) => { + if (info) results.set(wsUrls[i], info); + }); + + return results; +} + +/** + * Clear relay info cache + */ +export async function clearRelayInfoCache(wsUrl?: string): Promise { + if (wsUrl) { + await db.relayInfo.delete(wsUrl); + } else { + await db.relayInfo.clear(); + } +} + +/** + * Check if relay supports a specific NIP + */ +export async function relaySupportsNip( + wsUrl: string, + nipNumber: number, +): Promise { + const info = await getRelayInfo(wsUrl); + return info?.supported_nips?.includes(nipNumber) ?? false; +} diff --git a/src/lib/nostr-validation.ts b/src/lib/nostr-validation.ts new file mode 100644 index 0000000..7972ec0 --- /dev/null +++ b/src/lib/nostr-validation.ts @@ -0,0 +1,24 @@ +/** + * Nostr validation utilities for hex strings and identifiers + */ + +/** + * Check if a string is a valid 64-character hex pubkey + */ +export function isValidHexPubkey(value: string): boolean { + return /^[0-9a-f]{64}$/i.test(value); +} + +/** + * Check if a string is a valid 64-character hex event ID + */ +export function isValidHexEventId(value: string): boolean { + return /^[0-9a-f]{64}$/i.test(value); +} + +/** + * Normalize hex string to lowercase + */ +export function normalizeHex(value: string): string { + return value.toLowerCase(); +} diff --git a/src/lib/relay-parser.ts b/src/lib/relay-parser.ts new file mode 100644 index 0000000..4a90a6c --- /dev/null +++ b/src/lib/relay-parser.ts @@ -0,0 +1,36 @@ +export interface ParsedRelayCommand { + url: string; +} + +/** + * Parse RELAY command arguments + * + * Examples: + * relay wss://relay.damus.io + * relay relay.primal.net + * relay nos.lol + */ +export function parseRelayCommand(args: string[]): ParsedRelayCommand { + if (args.length < 1) { + throw new Error("Usage: RELAY "); + } + + let url = args[0]; + + // Auto-add wss:// protocol if not present + if (!url.startsWith("ws://") && !url.startsWith("wss://")) { + url = `wss://${url}`; + } + + // Validate URL format + try { + const parsedUrl = new URL(url); + if (!parsedUrl.protocol.startsWith("ws")) { + throw new Error("Relay must be a WebSocket URL (ws:// or wss://)"); + } + } catch { + throw new Error(`Invalid relay URL: ${url}`); + } + + return { url }; +} diff --git a/src/services/db.ts b/src/services/db.ts index 498e3b5..96d529a 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -1,5 +1,6 @@ import { ProfileContent } from "applesauce-core/helpers"; import { Dexie, Table } from "dexie"; +import { RelayInformation } from "../types/nip11"; export interface Profile extends ProfileContent { pubkey: string; @@ -17,17 +18,25 @@ export interface Nip { fetchedAt: number; } +export interface RelayInfo { + url: string; + info: RelayInformation; + fetchedAt: number; +} + class GrimoireDb extends Dexie { profiles!: Table; nip05!: Table; nips!: Table; + relayInfo!: Table; constructor(name: string) { super(name); - this.version(3).stores({ + this.version(4).stores({ profiles: "&pubkey", nip05: "&nip05", nips: "&id", + relayInfo: "&url", }); } } diff --git a/src/types/app.ts b/src/types/app.ts index 8b8d24d..c572d0f 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -10,7 +10,8 @@ export type AppId = | "open" | "profile" | "encode" - | "decode"; + | "decode" + | "relay"; export interface WindowInstance { id: string; diff --git a/src/types/man.ts b/src/types/man.ts index d9f4541..ab3f1ac 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -3,6 +3,7 @@ import type { AppId } from "./app"; import { parseOpenCommand } from "@/lib/open-parser"; import { parseProfileCommand } from "@/lib/profile-parser"; +import { parseRelayCommand } from "@/lib/relay-parser"; import { resolveNip05Batch } from "@/lib/nip05"; export interface ManPageEntry { diff --git a/src/types/nip11.ts b/src/types/nip11.ts new file mode 100644 index 0000000..2a84a21 --- /dev/null +++ b/src/types/nip11.ts @@ -0,0 +1,99 @@ +/** + * NIP-11: Relay Information Document + * https://github.com/nostr-protocol/nips/blob/master/11.md + */ + +export interface RelayInformation { + /** DNS name of the relay */ + name?: string; + + /** Description of the relay in plain text */ + description?: string; + + /** Public key of the relay administrator */ + pubkey?: string; + + /** Administrative contact for the relay */ + contact?: string; + + /** List of NIPs supported by this relay */ + supported_nips?: number[]; + + /** Software version running the relay */ + software?: string; + + /** Software version identifier */ + version?: string; + + /** Relay limitations and policies */ + limitation?: RelayLimitation; + + /** Payment information for paid relays */ + payments_url?: string; + + /** Relay usage fees */ + fees?: RelayFees; + + /** URL to the relay's icon */ + icon?: string; +} + +export interface RelayLimitation { + /** Maximum length of the content field */ + max_message_length?: number; + + /** Maximum number of subscriptions per WebSocket connection */ + max_subscriptions?: number; + + /** Maximum number of filters per subscription */ + max_filters?: number; + + /** Maximum length of subscription ID */ + max_subid_length?: number; + + /** Minimum prefix length for search filters */ + min_prefix?: number; + + /** Maximum number of elements in various arrays */ + max_limit?: number; + + /** Minimum POW difficulty for events */ + min_pow_difficulty?: number; + + /** Whether authentication is required */ + auth_required?: boolean; + + /** Whether payment is required */ + payment_required?: boolean; + + /** Restricted write access */ + restricted_writes?: boolean; + + /** Created at lower limit (oldest events accepted) */ + created_at_lower_limit?: number; + + /** Created at upper limit (newest events accepted) */ + created_at_upper_limit?: number; +} + +export interface RelayFees { + /** Admission fee structure */ + admission?: Array<{ amount: number; unit: string }>; + + /** Subscription fee structure */ + subscription?: Array<{ amount: number; unit: string; period?: number }>; + + /** Publication fee structure */ + publication?: Array<{ kinds?: number[]; amount: number; unit: string }>; +} + +export interface CachedRelayInfo { + /** Relay URL (websocket) */ + url: string; + + /** Relay information document */ + info: RelayInformation; + + /** Timestamp when the info was fetched */ + fetchedAt: number; +} diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo deleted file mode 100644 index c0e145c..0000000 --- a/tsconfig.app.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./src/main.tsx","./src/root.tsx","./src/vite-env.d.ts","./src/components/command.tsx","./src/components/commandlauncher.tsx","./src/components/decodeviewer.tsx","./src/components/encodeviewer.tsx","./src/components/eventdetailviewer.tsx","./src/components/grimoirewelcome.tsx","./src/components/home.tsx","./src/components/jsonviewer.tsx","./src/components/kindbadge.tsx","./src/components/kindrenderer.tsx","./src/components/manpage.tsx","./src/components/markdown.tsx","./src/components/nipbadge.tsx","./src/components/niprenderer.tsx","./src/components/profileviewer.tsx","./src/components/relayviewer.tsx","./src/components/reqviewer.tsx","./src/components/tabbar.tsx","./src/components/timestamp.tsx","./src/components/winviewer.tsx","./src/components/windowtoolbar.tsx","./src/components/nostr/embeddedevent.tsx","./src/components/nostr/feed.tsx","./src/components/nostr/mediadialog.tsx","./src/components/nostr/mediaembed.tsx","./src/components/nostr/quotedevent.tsx","./src/components/nostr/richtext.tsx","./src/components/nostr/username.tsx","./src/components/nostr/index.ts","./src/components/nostr/nip05.tsx","./src/components/nostr/npub.tsx","./src/components/nostr/relay-pool.tsx","./src/components/nostr/user-menu.tsx","./src/components/nostr/linkpreview/audiolink.tsx","./src/components/nostr/linkpreview/imagelink.tsx","./src/components/nostr/linkpreview/plainlink.tsx","./src/components/nostr/linkpreview/videolink.tsx","./src/components/nostr/linkpreview/index.ts","./src/components/nostr/richtext/emoji.tsx","./src/components/nostr/richtext/eventembed.tsx","./src/components/nostr/richtext/gallery.tsx","./src/components/nostr/richtext/hashtag.tsx","./src/components/nostr/richtext/link.tsx","./src/components/nostr/richtext/mention.tsx","./src/components/nostr/richtext/text.tsx","./src/components/nostr/richtext/index.ts","./src/components/nostr/kinds/baseeventrenderer.tsx","./src/components/nostr/kinds/kind0detailrenderer.tsx","./src/components/nostr/kinds/kind0renderer.tsx","./src/components/nostr/kinds/kind1063renderer.tsx","./src/components/nostr/kinds/kind1renderer.tsx","./src/components/nostr/kinds/kind20renderer.tsx","./src/components/nostr/kinds/kind21renderer.tsx","./src/components/nostr/kinds/kind22renderer.tsx","./src/components/nostr/kinds/kind30023detailrenderer.tsx","./src/components/nostr/kinds/kind30023renderer.tsx","./src/components/nostr/kinds/kind3renderer.tsx","./src/components/nostr/kinds/kind6renderer.tsx","./src/components/nostr/kinds/kind7renderer.tsx","./src/components/nostr/kinds/kind9735renderer.tsx","./src/components/nostr/kinds/kind9802detailrenderer.tsx","./src/components/nostr/kinds/kind9802renderer.tsx","./src/components/nostr/kinds/index.tsx","./src/components/ui/avatar.tsx","./src/components/ui/button.tsx","./src/components/ui/dialog.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input.tsx","./src/components/ui/scroll-area.tsx","./src/constants/kinds.ts","./src/constants/nips.ts","./src/core/logic.ts","./src/core/state.ts","./src/hooks/useaccountsync.ts","./src/hooks/usecopy.ts","./src/hooks/uselocale.ts","./src/hooks/usenip.ts","./src/hooks/usenip05.ts","./src/hooks/usenostrevent.ts","./src/hooks/useprofile.ts","./src/hooks/userelayconnection.ts","./src/hooks/userelayinfo.ts","./src/hooks/usereqtimeline.ts","./src/hooks/usetimeline.ts","./src/lib/decode-parser.ts","./src/lib/encode-parser.ts","./src/lib/imeta.ts","./src/lib/nip-icons.ts","./src/lib/nip-kinds.ts","./src/lib/nip05.ts","./src/lib/nip11.ts","./src/lib/nostr-utils.ts","./src/lib/nostr-validation.ts","./src/lib/open-parser.ts","./src/lib/profile-parser.ts","./src/lib/relay-parser.ts","./src/lib/req-parser.ts","./src/lib/utils.ts","./src/services/accounts.ts","./src/services/db.ts","./src/services/event-store.ts","./src/services/loaders.ts","./src/services/relay-pool.ts","./src/types/app.ts","./src/types/man.ts","./src/types/nip11.ts","./src/types/nostr.ts","./src/types/profile.ts"],"version":"5.6.3"} \ No newline at end of file