Add NSFW and kids flags to video schema

This commit is contained in:
thePR0M3TH3AN committed 2025-10-09 22:22:38 -04:00
1 parent f33f56c923
commit 259b493a20
6 files changed
+127 -10

No files matched your search

+1 -1
View File
@@ -72,7 +72,7 @@ timestamps.
| Note | Kind (default) | Tags | Content format |
| --- | --- | --- | --- |
| 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`, `enableComments`, `ws`, `xs`) |
| 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 |
| 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. |
+38 -8
View File
@@ -800,7 +800,13 @@ function extractVideoPublishPayload(rawPayload) {
videoData = {};
}
return { videoData, nip71Metadata };
const normalizedVideoData = { ...videoData };
const isNsfw = videoData.isNsfw === true;
normalizedVideoData.isNsfw = isNsfw;
normalizedVideoData.isForKids =
videoData.isForKids === true && !isNsfw;
return { videoData: normalizedVideoData, nip71Metadata };
}
function buildVideoPointerValue(pubkey, videoRootId) {
@@ -2916,6 +2922,8 @@ function convertEventToVideo(event = {}) {
const mode = rawMode || "live";
const deleted = parsedContent.deleted === true;
const isPrivate = parsedContent.isPrivate === true;
const isNsfw = parsedContent.isNsfw === true;
const isForKids = parsedContent.isForKids === true && !isNsfw;
const videoRootId = safeTrim(parsedContent.videoRootId) || event.id;
const wsField = safeTrim(parsedContent.ws);
const xsField = safeTrim(parsedContent.xs);
@@ -3018,6 +3026,8 @@ function convertEventToVideo(event = {}) {
videoRootId,
version,
isPrivate,
isNsfw,
isForKids,
title,
url,
magnet,
@@ -6367,6 +6377,9 @@ export class NostrClient {
const finalEnableComments =
videoData.enableComments === false ? false : true;
const finalIsNsfw = videoData.isNsfw === true;
const finalIsForKids =
videoData.isForKids === true && !finalIsNsfw;
const finalWs =
typeof videoData.ws === "string" ? videoData.ws.trim() : "";
const finalXs =
@@ -6383,6 +6396,8 @@ export class NostrClient {
videoRootId,
deleted: false,
isPrivate: videoData.isPrivate ?? false,
isNsfw: finalIsNsfw,
isForKids: finalIsForKids,
enableComments: finalEnableComments,
};
@@ -6493,6 +6508,8 @@ export class NostrClient {
thumbnail: contentObject.thumbnail,
mode: contentObject.mode,
isPrivate: contentObject.isPrivate,
isNsfw: contentObject.isNsfw,
isForKids: contentObject.isForKids,
};
if (contentObject.ws) {
@@ -6666,6 +6683,10 @@ export class NostrClient {
// Determine if the updated note should be private
const wantPrivate = updatedData.isPrivate ?? baseEvent.isPrivate ?? false;
const wantNsfw = updatedData.isNsfw ?? baseEvent.isNsfw ?? false;
const wantForKids = updatedData.isForKids ?? baseEvent.isForKids ?? false;
const finalIsNsfw = wantNsfw === true;
const finalIsForKids = finalIsNsfw ? false : wantForKids === true;
// Use the new magnet if provided; otherwise, fall back to the decrypted old magnet
const magnetEdited = updatedData.magnetEdited === true;
@@ -6709,6 +6730,8 @@ export class NostrClient {
version: updatedData.version ?? baseEvent.version ?? 2,
deleted: false,
isPrivate: wantPrivate,
isNsfw: finalIsNsfw,
isForKids: finalIsForKids,
title: updatedData.title ?? baseEvent.title,
url: finalUrl,
magnet: finalMagnet,
@@ -6833,13 +6856,15 @@ export class NostrClient {
baseEvent = {
id: fetched.id,
pubkey: fetched.pubkey,
content: JSON.stringify({
version: fetched.version,
deleted: fetched.deleted,
isPrivate: fetched.isPrivate,
title: fetched.title,
url: fetched.url,
magnet: fetched.magnet,
content: JSON.stringify({
version: fetched.version,
deleted: fetched.deleted,
isPrivate: fetched.isPrivate,
isNsfw: fetched.isNsfw,
isForKids: fetched.isForKids,
title: fetched.title,
url: fetched.url,
magnet: fetched.magnet,
thumbnail: fetched.thumbnail,
description: fetched.description,
mode: fetched.mode,
@@ -6869,11 +6894,16 @@ export class NostrClient {
? `LEGACY:${baseEvent.pubkey}:${existingD}`
: baseEvent.id);
const oldIsNsfw = oldContent.isNsfw === true;
const oldIsForKids = oldContent.isForKids === true && !oldIsNsfw;
const contentObject = {
videoRootId: finalRootId,
version: oldVersion,
deleted: true,
isPrivate: oldContent.isPrivate ?? false,
isNsfw: oldIsNsfw,
isForKids: oldIsForKids,
title: oldContent.title || "",
url: "",
magnet: "",
+2
View File
@@ -118,6 +118,8 @@ const BASE_SCHEMAS = {
{ key: "videoRootId", type: "string", required: true },
{ key: "deleted", type: "boolean", required: false },
{ key: "isPrivate", type: "boolean", required: false },
{ key: "isNsfw", type: "boolean", required: false },
{ key: "isForKids", type: "boolean", required: false },
{ key: "enableComments", type: "boolean", required: false },
{ key: "ws", type: "string", required: false },
{ key: "xs", type: "string", required: false },
+2
View File
@@ -302,6 +302,8 @@ export class VideoListView {
: Number(video?.created_at) || 0,
deleted: Boolean(video?.deleted),
isPrivate: Boolean(video?.isPrivate),
isNsfw: Boolean(video?.isNsfw),
isForKids: Boolean(video?.isForKids),
thumbnail: typeof video?.thumbnail === "string" ? video.thumbnail : "",
url: typeof video?.url === "string" ? video.url : "",
magnet: typeof video?.magnet === "string" ? video.magnet : "",
+6 -1
View File
@@ -33,11 +33,17 @@ test("30078 events carry nip71 metadata tags and hydrate fallback metadata", ()
videoRootId: "root-123",
deleted: false,
isPrivate: false,
isNsfw: false,
isForKids: true,
enableComments: true,
},
additionalTags,
});
const parsedContent = JSON.parse(event.content);
assert.equal(parsedContent.isNsfw, false);
assert.equal(parsedContent.isForKids, true);
const hashtagValues = event.tags
.filter((tag) => tag[0] === "t")
.map((tag) => tag[1])
@@ -58,7 +64,6 @@ test("30078 events carry nip71 metadata tags and hydrate fallback metadata", ()
assert.deepEqual(referenceTag, ["r", "https://example.com/info"]);
const client = new NostrClient();
const parsedContent = JSON.parse(event.content);
const video = {
id: "event-123",
videoRootId: parsedContent.videoRootId,
@@ -0,0 +1,78 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
NOTE_TYPES,
getNostrEventSchema,
} from "../js/nostrEventSchemas.js";
import { convertEventToVideo } from "../js/nostr.js";
test("video post schema documents nsfw and kids flags", () => {
const schema = getNostrEventSchema(NOTE_TYPES.VIDEO_POST);
const fieldKeys = Array.isArray(schema?.content?.fields)
? schema.content.fields.map((field) => field?.key)
: [];
assert.ok(
fieldKeys.includes("isNsfw"),
"schema should include isNsfw field"
);
assert.ok(
fieldKeys.includes("isForKids"),
"schema should include isForKids field"
);
});
test("convertEventToVideo normalizes nsfw and kids booleans", () => {
const nsfwEvent = {
id: "evt-nsfw",
content: JSON.stringify({
version: 3,
title: "Flagged clip",
url: "https://cdn.example/nsfw.mp4",
videoRootId: "root-nsfw",
isNsfw: true,
isForKids: true,
}),
tags: [],
};
const nsfwParsed = convertEventToVideo(nsfwEvent);
assert.equal(nsfwParsed.isNsfw, true);
assert.equal(
nsfwParsed.isForKids,
false,
"isForKids should be suppressed when nsfw is true"
);
const kidsEvent = {
id: "evt-kids",
content: JSON.stringify({
version: 3,
title: "Kids clip",
url: "https://cdn.example/kids.mp4",
videoRootId: "root-kids",
isForKids: true,
}),
tags: [],
};
const kidsParsed = convertEventToVideo(kidsEvent);
assert.equal(kidsParsed.isNsfw, false);
assert.equal(kidsParsed.isForKids, true);
const defaultEvent = {
id: "evt-default",
content: JSON.stringify({
version: 3,
title: "Default clip",
url: "https://cdn.example/default.mp4",
videoRootId: "root-default",
}),
tags: [],
};
const defaultParsed = convertEventToVideo(defaultEvent);
assert.equal(defaultParsed.isNsfw, false);
assert.equal(defaultParsed.isForKids, false);
});