Files
codium_nostr_extension/src/nostr/nip94.ts
T

113 lines
3.8 KiB
TypeScript

import type { EventTemplate } from "nostr-tools/core";
import type { BlobDescriptor } from "../blossom/client";
import type { BlossomFrontMatter } from "../blossom/frontmatter";
/** NIP-94 file metadata event kind. */
export const FILE_METADATA_KIND = 1063;
/** Input for building a NIP-94 file-metadata (kind 1063) event. */
export interface FileMetadataInput {
/** Blob descriptor returned by the Blossom server (BUD-02/BUD-08). */
descriptor: BlobDescriptor;
/** Front-matter-derived metadata (title, summary, author, …). */
meta: BlossomFrontMatter;
/** First-publish timestamp (unix seconds). Reused on updates. */
publishedAt: number;
/**
* Override for the `m` tag (the file's semantic MIME type).
*
* Markdown blobs are uploaded as `text/plain` so browsers render them
* inline (no mainstream browser displays `text/markdown`, and
* `X-Content-Type-Options: nosniff` forbids guessing). The server then
* reports `type: text/plain` in the descriptor. This override keeps the
* Nostr `m` tag advertising the true semantic type (`text/markdown`) so
* clients that render markdown can still detect it.
*/
mimeType?: string;
}
/**
* Build an unsigned NIP-94 file-metadata event (kind 1063) from a Blossom blob
* descriptor and front-matter metadata.
*
* Tag precedence:
* - The server-returned `nip94` tags (BUD-08) provide the canonical `url`,
* `m`, `x`, and `size` tags. If absent, they are derived from the descriptor.
* - Descriptive tags (`title`, `summary`, `image`, `author`, `language`,
* `year`, `published_at`, `t`, `alt`) are added from the front-matter.
*
* The `content` field carries the front-matter `summary` (empty string if
* absent) so clients that only render `content` still show a description.
*/
export function buildFileMetadataEvent(input: FileMetadataInput): EventTemplate {
const { descriptor, meta, publishedAt } = input;
// Start from server-returned nip94 tags if present; otherwise synthesize the
// core tags from the descriptor.
let tags: string[][];
if (Array.isArray(descriptor.nip94) && descriptor.nip94.length > 0) {
tags = descriptor.nip94.map((t) => [...t]);
} else {
tags = [
["url", descriptor.url],
["m", descriptor.type],
["x", descriptor.sha256],
["size", String(descriptor.size)],
];
}
// Ensure the core tags exist even if nip94 omitted them.
ensureTag(tags, "url", descriptor.url);
ensureTag(tags, "m", input.mimeType ?? descriptor.type);
ensureTag(tags, "x", descriptor.sha256);
ensureTag(tags, "size", String(descriptor.size));
// When an explicit mimeType override is given, replace any server-provided
// `m` tag (BUD-08 nip94 tags carry the upload Content-Type, which for
// markdown is deliberately text/plain — see FileMetadataInput.mimeType).
if (input.mimeType) {
for (const t of tags) {
if (t[0] === "m") {
t[1] = input.mimeType;
}
}
}
// Descriptive metadata from front-matter.
tags.push(["title", meta.title]);
if (meta.summary) {
tags.push(["summary", meta.summary]);
}
if (meta.cover) {
tags.push(["image", meta.cover]);
}
if (meta.author) {
tags.push(["author", meta.author]);
}
if (meta.language) {
tags.push(["language", meta.language]);
}
if (meta.year) {
tags.push(["year", meta.year]);
}
tags.push(["published_at", String(publishedAt)]);
for (const t of meta.tags) {
tags.push(["t", t]);
}
tags.push(["alt", `File: ${meta.title}`]);
return {
kind: FILE_METADATA_KIND,
created_at: Math.floor(Date.now() / 1000),
tags,
content: meta.summary ?? "",
};
}
/** Add a tag only if no tag with that name already exists. */
function ensureTag(tags: string[][], name: string, value: string): void {
if (!tags.some((t) => t[0] === name)) {
tags.push([name, value]);
}
}