Files
grimoire/src/lib/blossom-parser.ts
Alejandro 9ef1fefd3d feat: BLOSSOM (#75)
* Add Blossom blob storage integration

- Add blossom-client-sdk dependency for blob storage operations
- Create blossom.ts service with upload, list, check, mirror, delete primitives
- Add kind 10063 server list fetching and parsing
- Create blossom-parser.ts for command argument parsing with subcommands
- Add BLOSSOM command to man.ts with subcommands:
  - servers: Show configured Blossom servers
  - check: Check server health
  - upload: Upload files to user's servers
  - list: List blobs for a user
  - mirror: Mirror blobs between servers
  - delete: Delete blobs from servers
- Create BlossomViewer component with views for each subcommand
- Wire up BlossomViewer in WindowRenderer
- Add Blossom servers dropdown to ProfileViewer header
- Upload primitives can be called programmatically for use in other components

* Enhance Blossom viewer with server selection and blob details

- Add server selection checkboxes to upload view for choosing target servers
- Add BlobDetailView with media preview (image/video/audio) and metadata display
- Add 'blob' subcommand to view individual blob details
- Remove unused 'check' subcommand

* Add Blossom upload dialog with chat integration

- Create BlossomUploadDialog component with file picker, server selection, and preview
- Create useBlossomUpload hook for easy integration in any component
- Add insertText method to MentionEditor for programmatic text insertion
- Integrate upload button (paperclip icon) in chat composer
- Supports image, video, and audio uploads with drag-and-drop

* Add rich blob attachments with imeta tags for chat

- Add BlobAttachment TipTap extension with inline preview (thumbnail for images, icons for video/audio)
- Store full blob metadata (sha256, url, mimeType, size, server) in editor nodes
- Convert blob nodes to URLs in content with NIP-92 imeta tags when sending
- Add insertBlob method to MentionEditor for programmatic blob insertion
- Update NIP-29 and NIP-53 adapters to include imeta tags with blob metadata
- Pass blob attachments through entire send flow (editor -> ChatViewer -> adapter)

* Add fallback public Blossom servers for users without server list

- Add well-known public servers as fallbacks (blossom.primal.net, nostr.download, files.v0l.io)
- Use fallbacks when user has no kind 10063 server list configured
- Show "Public Servers" label with Globe icon when using fallbacks
- Inform user that no server list was found
- Select first fallback server by default (vs all user servers)

* Fix: Don't show fallback servers when not logged in

Blossom uploads require signed auth events, so users must be logged in.
The 'Account required' message is already shown in this case.

* Remove files.v0l.io from fallback servers

* Add rich renderer for kind 10063 Blossom server list

- Create BlossomServerListRenderer.tsx with feed and detail views
- Show user's configured Blossom servers with clickable links
- Clicking a server opens the Blossom window with server info
- Register renderers for kind 10063 (BUD-03)
- Fix lint error by renaming useFallbackServers to applyFallbackServers

* 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

* Add comprehensive tests for blossom-parser

- 34 test cases covering all subcommands (servers, server, upload, list, blob, mirror, delete)
- Tests for NIP-05 resolution, npub/nprofile decoding, $me alias
- Tests for error handling and input validation
- Tests for case insensitivity and command aliases (ls, view, rm)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 17:16:31 +01:00

229 lines
5.8 KiB
TypeScript

/**
* Blossom Command Parser
*
* 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
* - mirror <url> <server>: Mirror a blob to another server
* - delete <sha256> <server>: Delete a blob from a server
*/
import { nip19 } from "nostr-tools";
import { isNip05, resolveNip05 } from "./nip05";
import { isValidHexPubkey, normalizeHex } from "./nostr-validation";
export type BlossomSubcommand =
| "servers"
| "server"
| "upload"
| "list"
| "blob"
| "mirror"
| "delete";
export interface BlossomCommandResult {
subcommand: BlossomSubcommand;
// For 'blob' and 'delete' subcommands
sha256?: string;
serverUrl?: string;
// For 'list' subcommand
pubkey?: string;
// For 'mirror' subcommand
sourceUrl?: string;
targetServer?: string;
}
/**
* Normalize a server URL (add https:// if missing)
*/
function normalizeServerUrl(url: string): string {
if (url.startsWith("http://") || url.startsWith("https://")) {
return url;
}
return `https://${url}`;
}
/**
* Resolve a pubkey from various formats (npub, nprofile, hex, NIP-05, $me)
*/
async function resolvePubkey(
input: string,
activeAccountPubkey?: string,
): Promise<string | undefined> {
// Handle $me alias
if (input === "$me") {
return activeAccountPubkey;
}
// Handle hex pubkey
if (isValidHexPubkey(input)) {
return normalizeHex(input);
}
// Handle npub
if (input.startsWith("npub1")) {
try {
const decoded = nip19.decode(input);
if (decoded.type === "npub") {
return decoded.data;
}
} catch {
// Invalid npub
}
}
// Handle nprofile
if (input.startsWith("nprofile1")) {
try {
const decoded = nip19.decode(input);
if (decoded.type === "nprofile") {
return decoded.data.pubkey;
}
} catch {
// Invalid nprofile
}
}
// Handle NIP-05 identifier (user@domain.com or domain.com)
if (isNip05(input)) {
const pubkey = await resolveNip05(input);
if (pubkey) {
return pubkey;
}
}
return undefined;
}
/**
* Parse blossom command arguments
*
* 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 async function parseBlossomCommand(
args: string[],
activeAccountPubkey?: string,
): Promise<BlossomCommandResult> {
// Default to 'servers' if no subcommand
if (args.length === 0) {
return { subcommand: "servers" };
}
const subcommand = args[0].toLowerCase();
switch (subcommand) {
case "servers":
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" };
case "list":
case "ls": {
// Default to active account if no pubkey specified
const pubkeyArg = args[1];
let pubkey: string | undefined;
if (pubkeyArg) {
pubkey = await resolvePubkey(pubkeyArg, activeAccountPubkey);
if (!pubkey) {
throw new Error(
`Invalid pubkey format: ${pubkeyArg}. Use npub, nprofile, hex, user@domain.com, or $me`,
);
}
} else {
pubkey = activeAccountPubkey;
}
return {
subcommand: "list",
pubkey,
};
}
case "blob":
case "view": {
if (args.length < 2) {
throw new Error(
"SHA256 hash required. Usage: blossom blob <sha256> [server]",
);
}
const sha256 = args[1].toLowerCase();
if (!/^[0-9a-f]{64}$/.test(sha256)) {
throw new Error("Invalid SHA256 hash. Must be 64 hex characters.");
}
return {
subcommand: "blob",
sha256,
serverUrl: args[2] ? normalizeServerUrl(args[2]) : undefined,
};
}
case "mirror": {
if (args.length < 3) {
throw new Error(
"Source URL and target server required. Usage: blossom mirror <url> <server>",
);
}
return {
subcommand: "mirror",
sourceUrl: args[1],
targetServer: normalizeServerUrl(args[2]),
};
}
case "delete":
case "rm": {
if (args.length < 3) {
throw new Error(
"SHA256 hash and server required. Usage: blossom delete <sha256> <server>",
);
}
const sha256 = args[1].toLowerCase();
if (!/^[0-9a-f]{64}$/.test(sha256)) {
throw new Error("Invalid SHA256 hash. Must be 64 hex characters.");
}
return {
subcommand: "delete",
sha256,
serverUrl: normalizeServerUrl(args[2]),
};
}
default:
throw new Error(
`Unknown subcommand: ${subcommand}
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
mirror <url> <server> Mirror a blob to another server
delete <sha256> <server> Delete a blob from a server`,
);
}
}