mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 12:52:26 +00:00
Add video comment schema and builder
This commit is contained in:
1 parent
34dd506f05
commit
bedc0fd5c6
3 files changed
+206
No files matched your search
@@ -138,6 +138,7 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
|
||||
| Video post (`NOTE_TYPES.VIDEO_POST`) | `30078` | `['t','video']`, `['d', <stable video identifier>]` plus optional schema append tags | JSON payload using Content Schema v3 (`version`, `title`, optional `url`, `magnet`, `thumbnail`, `description`, `mode`, `videoRootId`, `deleted`, `isPrivate`, `isNsfw`, `isForKids`, `enableComments`, `ws`, `xs`) |
|
||||
| NIP-94 mirror (`NOTE_TYPES.VIDEO_MIRROR`) | `1063` | Tags forwarded from `publishVideo` (URL, mime type, thumbnail, alt text, magnet) | Plain text alt description |
|
||||
| Repost (`NOTE_TYPES.REPOST`) | `6` | `['e', <event id>, <relay?>]` with optional address pointer `['a', <kind:pubkey:identifier>, <relay?>]`, and `['p', <pubkey>]` when the origin author is known; inherits schema append tags | Empty content |
|
||||
| Video comment (`NOTE_TYPES.VIDEO_COMMENT`) | `1` | Mandatory `['e', <video event id>, <relay?>?]` and `['a', <kind:pubkey:identifier>, <relay?>?]` tags, optional parent pointers `['e', <parent comment id>, <relay?>?]`, and optional participant markers `['p', <pubkey>, <relay?>?]`; inherits schema append tags | Plain text body sanitized to valid UTF-8 |
|
||||
| NIP-71 video (`NOTE_TYPES.NIP71_VIDEO`) | `21` | `['title', <title>]`, optional `['published_at', <unix seconds>]`, optional `['alt', <text>]`, repeated `['imeta', ...]` entries describing NIP-92 media variants, optional `['duration', <seconds>]`, repeated `['text-track', <url>, <kind>, <language>]`, optional `['content-warning', <reason>]`, repeated `['segment', <start>, <end>, <title>, <thumbnail>]`, repeated hashtags `['t', <tag>]`, repeated participants `['p', <pubkey>, <relay?>]`, repeated references `['r', <url>]` | Plain text summary carried in the content field. Publishing is gated by the `FEATURE_PUBLISH_NIP71` runtime flag while the rollout stabilizes. |
|
||||
| NIP-71 short video (`NOTE_TYPES.NIP71_SHORT_VIDEO`) | `22` | Same as `NOTE_TYPES.NIP71_VIDEO`; the kind differentiates short-form presentations. | Plain text summary; gated by `FEATURE_PUBLISH_NIP71`. |
|
||||
| Relay list (`NOTE_TYPES.RELAY_LIST`) | `10002` | Repeating `['r', <relay url>]` tags, optionally with a marker of `'read'` or `'write'` to scope the relay; marker omitted for read/write relays | Empty content |
|
||||
|
||||
@@ -17,6 +17,7 @@ export const NOTE_TYPES = Object.freeze({
|
||||
RELAY_LIST: "relayList",
|
||||
VIEW_EVENT: "viewEvent",
|
||||
VIDEO_REACTION: "videoReaction",
|
||||
VIDEO_COMMENT: "videoComment",
|
||||
WATCH_HISTORY_INDEX: "watchHistoryIndex",
|
||||
WATCH_HISTORY_CHUNK: "watchHistoryChunk",
|
||||
SUBSCRIPTION_LIST: "subscriptionList",
|
||||
@@ -64,6 +65,33 @@ function ensureValidUtf8Content(value) {
|
||||
normalized = String(value);
|
||||
}
|
||||
|
||||
if (normalized) {
|
||||
const builder = [];
|
||||
let mutated = false;
|
||||
for (let index = 0; index < normalized.length; index += 1) {
|
||||
const code = normalized.charCodeAt(index);
|
||||
if (code >= 0xd800 && code <= 0xdbff) {
|
||||
const nextCode = normalized.charCodeAt(index + 1);
|
||||
if (nextCode >= 0xdc00 && nextCode <= 0xdfff) {
|
||||
builder.push(normalized[index], normalized[index + 1]);
|
||||
index += 1;
|
||||
} else {
|
||||
mutated = true;
|
||||
}
|
||||
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
||||
mutated = true;
|
||||
} else {
|
||||
builder.push(normalized[index]);
|
||||
}
|
||||
}
|
||||
if (mutated) {
|
||||
devLogger.warn(
|
||||
"[nostrEventSchemas] Dropping unmatched surrogate characters from event content"
|
||||
);
|
||||
normalized = builder.join("");
|
||||
}
|
||||
}
|
||||
|
||||
if (!cachedUtf8Encoder && typeof TextEncoder !== "undefined") {
|
||||
cachedUtf8Encoder = new TextEncoder();
|
||||
}
|
||||
@@ -206,6 +234,20 @@ const BASE_SCHEMAS = {
|
||||
"Reaction payload for the referenced event (e.g., '+', '-', or emoji).",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.VIDEO_COMMENT]: {
|
||||
type: NOTE_TYPES.VIDEO_COMMENT,
|
||||
label: "Video comment",
|
||||
kind: 1,
|
||||
videoEventTagName: "e",
|
||||
videoDefinitionTagName: "a",
|
||||
parentCommentTagName: "e",
|
||||
participantTagName: "p",
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: {
|
||||
format: "text",
|
||||
description: "Plain text comment body sanitized for UTF-8 compatibility.",
|
||||
},
|
||||
},
|
||||
[NOTE_TYPES.WATCH_HISTORY_INDEX]: {
|
||||
type: NOTE_TYPES.WATCH_HISTORY_INDEX,
|
||||
label: "Watch history index",
|
||||
@@ -814,6 +856,114 @@ export function buildReactionEvent({
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCommentEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
videoEventId = "",
|
||||
videoEventRelay = "",
|
||||
videoDefinitionAddress = "",
|
||||
videoDefinitionRelay = "",
|
||||
parentCommentId = "",
|
||||
parentCommentRelay = "",
|
||||
threadParticipantPubkey = "",
|
||||
threadParticipantRelay = "",
|
||||
additionalTags = [],
|
||||
content = "",
|
||||
}) {
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.VIDEO_COMMENT);
|
||||
const tags = [];
|
||||
|
||||
const videoEventTagName = schema?.videoEventTagName || "e";
|
||||
const normalizedVideoEventId =
|
||||
typeof videoEventId === "string" ? videoEventId.trim() : "";
|
||||
const normalizedVideoEventRelay =
|
||||
typeof videoEventRelay === "string" ? videoEventRelay.trim() : "";
|
||||
if (videoEventTagName && normalizedVideoEventId) {
|
||||
if (normalizedVideoEventRelay) {
|
||||
tags.push([videoEventTagName, normalizedVideoEventId, normalizedVideoEventRelay]);
|
||||
} else {
|
||||
tags.push([videoEventTagName, normalizedVideoEventId]);
|
||||
}
|
||||
}
|
||||
|
||||
const videoDefinitionTagName = schema?.videoDefinitionTagName || "a";
|
||||
const normalizedVideoDefinitionAddress =
|
||||
typeof videoDefinitionAddress === "string"
|
||||
? videoDefinitionAddress.trim()
|
||||
: "";
|
||||
const normalizedVideoDefinitionRelay =
|
||||
typeof videoDefinitionRelay === "string" ? videoDefinitionRelay.trim() : "";
|
||||
if (videoDefinitionTagName && normalizedVideoDefinitionAddress) {
|
||||
if (normalizedVideoDefinitionRelay) {
|
||||
tags.push([
|
||||
videoDefinitionTagName,
|
||||
normalizedVideoDefinitionAddress,
|
||||
normalizedVideoDefinitionRelay,
|
||||
]);
|
||||
} else {
|
||||
tags.push([videoDefinitionTagName, normalizedVideoDefinitionAddress]);
|
||||
}
|
||||
}
|
||||
|
||||
const parentCommentTagName = schema?.parentCommentTagName || "e";
|
||||
const normalizedParentCommentId =
|
||||
typeof parentCommentId === "string" ? parentCommentId.trim() : "";
|
||||
const normalizedParentCommentRelay =
|
||||
typeof parentCommentRelay === "string" ? parentCommentRelay.trim() : "";
|
||||
if (parentCommentTagName && normalizedParentCommentId) {
|
||||
if (normalizedParentCommentRelay) {
|
||||
tags.push([
|
||||
parentCommentTagName,
|
||||
normalizedParentCommentId,
|
||||
normalizedParentCommentRelay,
|
||||
]);
|
||||
} else {
|
||||
tags.push([parentCommentTagName, normalizedParentCommentId]);
|
||||
}
|
||||
}
|
||||
|
||||
const participantTagName = schema?.participantTagName || "p";
|
||||
const normalizedThreadParticipantPubkey =
|
||||
typeof threadParticipantPubkey === "string"
|
||||
? threadParticipantPubkey.trim()
|
||||
: "";
|
||||
const normalizedThreadParticipantRelay =
|
||||
typeof threadParticipantRelay === "string"
|
||||
? threadParticipantRelay.trim()
|
||||
: "";
|
||||
if (participantTagName && normalizedThreadParticipantPubkey) {
|
||||
if (normalizedThreadParticipantRelay) {
|
||||
tags.push([
|
||||
participantTagName,
|
||||
normalizedThreadParticipantPubkey,
|
||||
normalizedThreadParticipantRelay,
|
||||
]);
|
||||
} else {
|
||||
tags.push([participantTagName, normalizedThreadParticipantPubkey]);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(additionalTags)) {
|
||||
additionalTags.forEach((tag) => {
|
||||
if (Array.isArray(tag) && tag.length >= 2) {
|
||||
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
const resolvedContent = ensureValidUtf8Content(content);
|
||||
|
||||
return {
|
||||
kind: schema?.kind ?? 1,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: resolvedContent,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWatchHistoryIndexEvent({
|
||||
pubkey,
|
||||
created_at,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { buildCommentEvent } from "../js/nostrEventSchemas.js";
|
||||
|
||||
test("buildCommentEvent produces required tags for top-level comments", () => {
|
||||
const event = buildCommentEvent({
|
||||
pubkey: "commenter",
|
||||
created_at: 1700000200,
|
||||
videoEventId: "video-event-id",
|
||||
videoEventRelay: "wss://relay.example",
|
||||
videoDefinitionAddress: "30078:deadbeefcafebabe:clip-1",
|
||||
threadParticipantPubkey: "deadbeefcafebabe",
|
||||
content: "Great \ud800video!",
|
||||
});
|
||||
|
||||
assert.equal(event.kind, 1);
|
||||
|
||||
const eventTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "e");
|
||||
assert.deepEqual(eventTags, [["e", "video-event-id", "wss://relay.example"]]);
|
||||
|
||||
const addressTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "a");
|
||||
assert.deepEqual(addressTags, [["a", "30078:deadbeefcafebabe:clip-1"]]);
|
||||
|
||||
const participantTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "p");
|
||||
assert.deepEqual(participantTags, [["p", "deadbeefcafebabe"]]);
|
||||
|
||||
assert.equal(event.content, "Great video!");
|
||||
});
|
||||
|
||||
test("buildCommentEvent includes parent pointers for threaded replies", () => {
|
||||
const event = buildCommentEvent({
|
||||
pubkey: "commenter",
|
||||
created_at: 1700000201,
|
||||
videoEventId: "video-event-id",
|
||||
videoDefinitionAddress: "30078:deadbeefcafebabe:clip-1",
|
||||
parentCommentId: "parent-comment-id",
|
||||
threadParticipantPubkey: "parent-author",
|
||||
content: "Replying now",
|
||||
});
|
||||
|
||||
assert.equal(event.kind, 1);
|
||||
|
||||
const eventTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "e");
|
||||
assert.deepEqual(eventTags, [
|
||||
["e", "video-event-id"],
|
||||
["e", "parent-comment-id"],
|
||||
]);
|
||||
|
||||
const addressTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "a");
|
||||
assert.deepEqual(addressTags, [["a", "30078:deadbeefcafebabe:clip-1"]]);
|
||||
|
||||
const participantTags = event.tags.filter((tag) => Array.isArray(tag) && tag[0] === "p");
|
||||
assert.deepEqual(participantTags, [["p", "parent-author"]]);
|
||||
});
|
||||
Reference in new issue
Block a user