mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 07:27:23 +02:00
Add individual server view and NIP-05 support for blossom commands
- Add 'server' subcommand to view info about a specific Blossom server - Update BlossomServerListRenderer to open server view on click - Make blossom parser async to support NIP-05 resolution in 'list' command - Add kind 10063 (Blossom Server List) to EVENT_KINDS constants with BUD-03 reference - Update command examples with NIP-05 identifier support
This commit is contained in:
@@ -67,6 +67,8 @@ export function BlossomViewer({
|
||||
switch (subcommand) {
|
||||
case "servers":
|
||||
return <ServersView />;
|
||||
case "server":
|
||||
return <ServerView serverUrl={serverUrl!} />;
|
||||
case "upload":
|
||||
return <UploadView />;
|
||||
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<ServerCheckResult | null>(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 (
|
||||
<div className="h-full flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="border-b px-4 py-2 flex items-center gap-2">
|
||||
<HardDrive className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Blossom Server</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{/* Server Info */}
|
||||
<div className="border rounded-lg divide-y">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-xs text-muted-foreground uppercase mb-1">
|
||||
URL
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm break-all flex-1">{serverUrl}</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => copy(serverUrl)}
|
||||
>
|
||||
{copied ? (
|
||||
<CopyCheck className="size-4" />
|
||||
) : (
|
||||
<Copy className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => window.open(serverUrl, "_blank")}
|
||||
>
|
||||
<ExternalLink className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-xs text-muted-foreground uppercase mb-1">
|
||||
Hostname
|
||||
</div>
|
||||
<div className="text-sm">{hostname}</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-xs text-muted-foreground uppercase mb-1">
|
||||
Status
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="size-4 animate-spin text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Checking...
|
||||
</span>
|
||||
</div>
|
||||
) : status ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{status.online ? (
|
||||
<>
|
||||
<CheckCircle className="size-4 text-green-500" />
|
||||
<span className="text-sm text-green-600">
|
||||
Online ({status.responseTime}ms)
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<XCircle className="size-4 text-red-500" />
|
||||
<span className="text-sm text-red-600">{status.error}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => window.open(serverUrl, "_blank")}
|
||||
>
|
||||
<ExternalLink className="size-4 mr-2" />
|
||||
Open in Browser
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* UploadView - File upload interface with server selection
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
GitMerge,
|
||||
GitPullRequest,
|
||||
BookHeart,
|
||||
HardDrive,
|
||||
Hash,
|
||||
Heart,
|
||||
Highlighter,
|
||||
@@ -828,13 +829,13 @@ export const EVENT_KINDS: Record<number | string, EventKind> = {
|
||||
// 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",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Parses arguments for the blossom command with subcommands:
|
||||
* - servers: Show/manage user's Blossom server list
|
||||
* - server <url>: View info about a specific Blossom server
|
||||
* - upload: Upload a file (handled by UI file picker)
|
||||
* - list [pubkey]: List blobs for a user
|
||||
* - blob <sha256> [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<string | undefined> {
|
||||
// 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 <url> - View info about a specific server
|
||||
* blossom upload - Open upload dialog
|
||||
* blossom list [pubkey] - List blobs (defaults to $me)
|
||||
* blossom blob <sha256> [server] - View blob details
|
||||
* blossom mirror <url> <server> - Mirror blob to server
|
||||
* blossom delete <sha256> <server> - Delete blob from server
|
||||
*/
|
||||
export function parseBlossomCommand(
|
||||
export async function parseBlossomCommand(
|
||||
args: string[],
|
||||
activeAccountPubkey?: string,
|
||||
): BlossomCommandResult {
|
||||
): Promise<BlossomCommandResult> {
|
||||
// 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 <url>");
|
||||
}
|
||||
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 <url> View info about a specific server
|
||||
upload Open file upload dialog
|
||||
list [pubkey] List blobs (defaults to your account)
|
||||
blob <sha256> [server] View blob details
|
||||
|
||||
@@ -528,6 +528,10 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
description:
|
||||
"Show your configured Blossom servers from kind 10063 event",
|
||||
},
|
||||
{
|
||||
flag: "server <url>",
|
||||
description: "View info about a specific Blossom server",
|
||||
},
|
||||
{
|
||||
flag: "upload",
|
||||
description:
|
||||
@@ -536,7 +540,7 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
{
|
||||
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 <sha256> [server]",
|
||||
@@ -555,9 +559,10 @@ export const manPages: Record<string, ManPageEntry> = {
|
||||
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<string, ManPageEntry> = {
|
||||
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" },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user