diff --git a/src/components/BlossomViewer.tsx b/src/components/BlossomViewer.tsx index a27de4f..1f17c40 100644 --- a/src/components/BlossomViewer.tsx +++ b/src/components/BlossomViewer.tsx @@ -67,6 +67,8 @@ export function BlossomViewer({ switch (subcommand) { case "servers": return ; + case "server": + return ; case "upload": return ; case "list": @@ -306,6 +308,134 @@ function ServerRow({ ); } +/** + * ServerView - View info about a specific Blossom server + */ +function ServerView({ serverUrl }: { serverUrl: string }) { + const { copy, copied } = useCopy(); + const [status, setStatus] = useState(null); + const [loading, setLoading] = useState(true); + + // Check server status on mount + useEffect(() => { + let cancelled = false; + + const check = async () => { + setLoading(true); + const result = await checkServer(serverUrl); + if (!cancelled) { + setStatus(result); + setLoading(false); + } + }; + + check(); + + return () => { + cancelled = true; + }; + }, [serverUrl]); + + const hostname = (() => { + try { + return new URL(serverUrl).hostname; + } catch { + return serverUrl; + } + })(); + + return ( + + {/* Header */} + + + Blossom Server + + + + {/* Server Info */} + + + + URL + + + {serverUrl} + copy(serverUrl)} + > + {copied ? ( + + ) : ( + + )} + + window.open(serverUrl, "_blank")} + > + + + + + + + + Hostname + + {hostname} + + + + + Status + + {loading ? ( + + + + Checking... + + + ) : status ? ( + + {status.online ? ( + <> + + + Online ({status.responseTime}ms) + + > + ) : ( + <> + + {status.error} + > + )} + + ) : null} + + + + {/* Actions */} + + window.open(serverUrl, "_blank")} + > + + Open in Browser + + + + + ); +} + /** * UploadView - File upload interface with server selection */ diff --git a/src/components/nostr/kinds/BlossomServerListRenderer.tsx b/src/components/nostr/kinds/BlossomServerListRenderer.tsx index f60eb6b..ddfb84e 100644 --- a/src/components/nostr/kinds/BlossomServerListRenderer.tsx +++ b/src/components/nostr/kinds/BlossomServerListRenderer.tsx @@ -14,11 +14,11 @@ export function BlossomServerListRenderer({ event }: BaseEventProps) { const servers = getServersFromEvent(event); const handleServerClick = (serverUrl: string) => { - // Open the blossom viewer with server info + // Open the blossom viewer with specific server info addWindow( "blossom", - { subcommand: "servers", serverUrl }, - `blossom servers`, + { subcommand: "server", serverUrl }, + `blossom server ${serverUrl}`, undefined, ); }; @@ -79,8 +79,8 @@ export function BlossomServerListDetailRenderer({ const handleServerClick = (serverUrl: string) => { addWindow( "blossom", - { subcommand: "servers", serverUrl }, - `blossom servers`, + { subcommand: "server", serverUrl }, + `blossom server ${serverUrl}`, undefined, ); }; diff --git a/src/constants/kinds.ts b/src/constants/kinds.ts index fd62988..462cc1a 100644 --- a/src/constants/kinds.ts +++ b/src/constants/kinds.ts @@ -27,6 +27,7 @@ import { GitMerge, GitPullRequest, BookHeart, + HardDrive, Hash, Heart, Highlighter, @@ -828,13 +829,13 @@ export const EVENT_KINDS: Record = { // nip: "Marmot", // icon: Key, // }, - // 10063: { - // kind: 10063, - // name: "User Server List", - // description: "User server list", - // nip: "Blossom", - // icon: Server, - // }, + 10063: { + kind: 10063, + name: "Blossom Server List", + description: "User's Blossom blob storage servers", + nip: "BUD-03", + icon: HardDrive, + }, 10096: { kind: 10096, name: "File Storage", diff --git a/src/lib/blossom-parser.ts b/src/lib/blossom-parser.ts index 7a20c58..515fbd4 100644 --- a/src/lib/blossom-parser.ts +++ b/src/lib/blossom-parser.ts @@ -3,6 +3,7 @@ * * Parses arguments for the blossom command with subcommands: * - servers: Show/manage user's Blossom server list + * - server : View info about a specific Blossom server * - upload: Upload a file (handled by UI file picker) * - list [pubkey]: List blobs for a user * - blob [server]: View a specific blob @@ -11,9 +12,12 @@ */ import { nip19 } from "nostr-tools"; +import { isNip05, resolveNip05 } from "./nip05"; +import { isValidHexPubkey, normalizeHex } from "./nostr-validation"; export type BlossomSubcommand = | "servers" + | "server" | "upload" | "list" | "blob" @@ -43,20 +47,20 @@ function normalizeServerUrl(url: string): string { } /** - * Resolve a pubkey from various formats (npub, nprofile, hex, $me) + * Resolve a pubkey from various formats (npub, nprofile, hex, NIP-05, $me) */ -function resolvePubkey( +async function resolvePubkey( input: string, activeAccountPubkey?: string, -): string | undefined { +): Promise { // Handle $me alias if (input === "$me") { return activeAccountPubkey; } // Handle hex pubkey - if (/^[0-9a-f]{64}$/i.test(input)) { - return input.toLowerCase(); + if (isValidHexPubkey(input)) { + return normalizeHex(input); } // Handle npub @@ -83,6 +87,14 @@ function resolvePubkey( } } + // Handle NIP-05 identifier (user@domain.com or domain.com) + if (isNip05(input)) { + const pubkey = await resolveNip05(input); + if (pubkey) { + return pubkey; + } + } + return undefined; } @@ -91,16 +103,17 @@ function resolvePubkey( * * Usage: * blossom servers - Show your Blossom servers + * blossom server - View info about a specific server * blossom upload - Open upload dialog * blossom list [pubkey] - List blobs (defaults to $me) * blossom blob [server] - View blob details * blossom mirror - Mirror blob to server * blossom delete - Delete blob from server */ -export function parseBlossomCommand( +export async function parseBlossomCommand( args: string[], activeAccountPubkey?: string, -): BlossomCommandResult { +): Promise { // Default to 'servers' if no subcommand if (args.length === 0) { return { subcommand: "servers" }; @@ -110,9 +123,19 @@ export function parseBlossomCommand( switch (subcommand) { case "servers": - case "server": return { subcommand: "servers" }; + case "server": { + // View info about a specific Blossom server + if (args.length < 2) { + throw new Error("Server URL required. Usage: blossom server "); + } + return { + subcommand: "server", + serverUrl: normalizeServerUrl(args[1]), + }; + } + case "upload": return { subcommand: "upload" }; @@ -123,10 +146,10 @@ export function parseBlossomCommand( let pubkey: string | undefined; if (pubkeyArg) { - pubkey = resolvePubkey(pubkeyArg, activeAccountPubkey); + pubkey = await resolvePubkey(pubkeyArg, activeAccountPubkey); if (!pubkey) { throw new Error( - `Invalid pubkey format: ${pubkeyArg}. Use npub, nprofile, hex, or $me`, + `Invalid pubkey format: ${pubkeyArg}. Use npub, nprofile, hex, user@domain.com, or $me`, ); } } else { @@ -194,6 +217,7 @@ export function parseBlossomCommand( Available subcommands: servers Show your configured Blossom servers + server View info about a specific server upload Open file upload dialog list [pubkey] List blobs (defaults to your account) blob [server] View blob details diff --git a/src/types/man.ts b/src/types/man.ts index 1f5fbec..d12be49 100644 --- a/src/types/man.ts +++ b/src/types/man.ts @@ -528,6 +528,10 @@ export const manPages: Record = { description: "Show your configured Blossom servers from kind 10063 event", }, + { + flag: "server ", + description: "View info about a specific Blossom server", + }, { flag: "upload", description: @@ -536,7 +540,7 @@ export const manPages: Record = { { flag: "list [pubkey]", description: - "List blobs uploaded by a user (defaults to your account). Supports npub, hex, or $me", + "List blobs uploaded by a user. Supports npub, hex, NIP-05 (user@domain.com), or $me", }, { flag: "blob [server]", @@ -555,9 +559,10 @@ export const manPages: Record = { examples: [ "blossom Show your Blossom servers", "blossom servers Show your Blossom servers", + "blossom server blossom.primal.net View specific server info", "blossom upload Open file upload dialog", "blossom list List your uploaded blobs", - "blossom list $me List your uploaded blobs", + "blossom list fiatjaf.com List blobs for a NIP-05 user", "blossom list npub1... List blobs for another user", "blossom blob abc123... View blob details", "blossom mirror https://... cdn.example.com Mirror blob to server", @@ -565,8 +570,8 @@ export const manPages: Record = { seeAlso: ["profile"], appId: "blossom", category: "Nostr", - argParser: (args: string[], activeAccountPubkey?: string) => { - return parseBlossomCommand(args, activeAccountPubkey); + argParser: async (args: string[], activeAccountPubkey?: string) => { + return await parseBlossomCommand(args, activeAccountPubkey); }, defaultProps: { subcommand: "servers" }, },
{serverUrl}