mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Use kind 1111 comments with address-first tags
This commit is contained in:
1 parent
a42dbb45e2
commit
d1d4fa150c
5 files changed
+128
-68
No files matched your search
@@ -138,7 +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 |
|
||||
| Video comment (`NOTE_TYPES.VIDEO_COMMENT`) | `1111` | Mandatory address pointer `['a', <kind:pubkey:identifier>, <relay?>?]`, optional parent pointers `['e', <parent comment id>, <relay?>?]`, and optional participant markers `['p', <pubkey>, <relay?>?]`; legacy clients may still emit `['e', <video event id>, <relay?>?]` and the ingestion pipeline accepts them; 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 |
|
||||
|
||||
+63
-23
@@ -11,7 +11,7 @@ import { devLogger, userLogger } from "../utils/logger.js";
|
||||
const COMMENT_EVENT_SCHEMA = getNostrEventSchema(NOTE_TYPES.VIDEO_COMMENT);
|
||||
export const COMMENT_EVENT_KIND = Number.isFinite(COMMENT_EVENT_SCHEMA?.kind)
|
||||
? COMMENT_EVENT_SCHEMA.kind
|
||||
: 1;
|
||||
: 1111;
|
||||
|
||||
function sanitizeRelayList(primary, fallback) {
|
||||
if (Array.isArray(primary) && primary.length) {
|
||||
@@ -239,41 +239,77 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function createVideoCommentFilters(targetInput, options = {}) {
|
||||
const descriptor = normalizeCommentTarget(targetInput, options);
|
||||
if (!descriptor) {
|
||||
throw new Error("Invalid video comment target supplied.");
|
||||
function applyFilterOptions(filter, options = {}) {
|
||||
if (!filter || typeof filter !== "object") {
|
||||
return filter;
|
||||
}
|
||||
|
||||
const filter = {
|
||||
kinds: [COMMENT_EVENT_KIND],
|
||||
"#e": [descriptor.videoEventId],
|
||||
"#a": [descriptor.videoDefinitionAddress],
|
||||
};
|
||||
|
||||
if (descriptor.parentCommentId) {
|
||||
if (!filter["#e"].includes(descriptor.parentCommentId)) {
|
||||
filter["#e"].push(descriptor.parentCommentId);
|
||||
}
|
||||
}
|
||||
const result = { ...filter };
|
||||
|
||||
if (
|
||||
typeof options.limit === "number" &&
|
||||
Number.isFinite(options.limit) &&
|
||||
options.limit > 0
|
||||
) {
|
||||
filter.limit = Math.floor(options.limit);
|
||||
result.limit = Math.floor(options.limit);
|
||||
}
|
||||
|
||||
if (typeof options.since === "number" && Number.isFinite(options.since)) {
|
||||
filter.since = Math.floor(options.since);
|
||||
result.since = Math.floor(options.since);
|
||||
}
|
||||
|
||||
if (typeof options.until === "number" && Number.isFinite(options.until)) {
|
||||
filter.until = Math.floor(options.until);
|
||||
result.until = Math.floor(options.until);
|
||||
}
|
||||
|
||||
return { descriptor, filters: [filter] };
|
||||
return result;
|
||||
}
|
||||
|
||||
function createVideoCommentFilters(targetInput, options = {}) {
|
||||
const descriptor = normalizeCommentTarget(targetInput, options);
|
||||
if (!descriptor) {
|
||||
throw new Error("Invalid video comment target supplied.");
|
||||
}
|
||||
|
||||
const filters = [];
|
||||
const baseFilter = { kinds: [COMMENT_EVENT_KIND] };
|
||||
|
||||
if (descriptor.videoDefinitionAddress) {
|
||||
baseFilter["#a"] = [descriptor.videoDefinitionAddress];
|
||||
}
|
||||
|
||||
if (descriptor.parentCommentId) {
|
||||
baseFilter["#e"] = [descriptor.parentCommentId];
|
||||
}
|
||||
|
||||
if (!descriptor.videoDefinitionAddress && descriptor.videoEventId) {
|
||||
baseFilter["#e"] = baseFilter["#e"] || [];
|
||||
if (!baseFilter["#e"].includes(descriptor.videoEventId)) {
|
||||
baseFilter["#e"].push(descriptor.videoEventId);
|
||||
}
|
||||
}
|
||||
|
||||
filters.push(applyFilterOptions(baseFilter, options));
|
||||
|
||||
const shouldIncludeLegacyFilter =
|
||||
Boolean(descriptor.videoDefinitionAddress) && Boolean(descriptor.videoEventId);
|
||||
|
||||
if (shouldIncludeLegacyFilter) {
|
||||
const legacyFilter = {
|
||||
kinds: [COMMENT_EVENT_KIND],
|
||||
"#e": [descriptor.videoEventId],
|
||||
};
|
||||
|
||||
if (descriptor.parentCommentId) {
|
||||
if (!legacyFilter["#e"].includes(descriptor.parentCommentId)) {
|
||||
legacyFilter["#e"].push(descriptor.parentCommentId);
|
||||
}
|
||||
}
|
||||
|
||||
filters.push(applyFilterOptions(legacyFilter, options));
|
||||
}
|
||||
|
||||
return { descriptor, filters };
|
||||
}
|
||||
|
||||
function flattenListResults(input) {
|
||||
@@ -306,9 +342,13 @@ function isVideoCommentEvent(event, descriptor) {
|
||||
}
|
||||
|
||||
const tags = Array.isArray(event.tags) ? event.tags : [];
|
||||
let hasEventTag = false;
|
||||
let hasDefinitionTag = false;
|
||||
let hasParentTag = !descriptor.parentCommentId;
|
||||
const requiresParentTag = Boolean(descriptor.parentCommentId);
|
||||
const requiresAddressMatch = Boolean(descriptor.videoDefinitionAddress);
|
||||
const requiresEventMatch = !requiresAddressMatch && Boolean(descriptor.videoEventId);
|
||||
|
||||
let hasEventTag = !requiresEventMatch;
|
||||
let hasDefinitionTag = !requiresAddressMatch;
|
||||
let hasParentTag = !requiresParentTag;
|
||||
|
||||
for (const tag of tags) {
|
||||
if (!Array.isArray(tag) || tag.length < 2) {
|
||||
|
||||
+16
-11
@@ -238,7 +238,7 @@ const BASE_SCHEMAS = {
|
||||
[NOTE_TYPES.VIDEO_COMMENT]: {
|
||||
type: NOTE_TYPES.VIDEO_COMMENT,
|
||||
label: "Video comment",
|
||||
kind: 1,
|
||||
kind: 1111,
|
||||
videoEventTagName: "e",
|
||||
videoDefinitionTagName: "a",
|
||||
parentCommentTagName: "e",
|
||||
@@ -889,19 +889,10 @@ export function buildCommentEvent({
|
||||
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"
|
||||
@@ -909,6 +900,20 @@ export function buildCommentEvent({
|
||||
: "";
|
||||
const normalizedVideoDefinitionRelay =
|
||||
typeof videoDefinitionRelay === "string" ? videoDefinitionRelay.trim() : "";
|
||||
const includeVideoEventTag =
|
||||
!normalizedVideoDefinitionAddress &&
|
||||
Boolean(schema?.videoEventTagName || "e") &&
|
||||
Boolean(normalizedVideoEventId);
|
||||
|
||||
if (includeVideoEventTag) {
|
||||
const videoEventTagName = schema?.videoEventTagName || "e";
|
||||
if (normalizedVideoEventRelay) {
|
||||
tags.push([videoEventTagName, normalizedVideoEventId, normalizedVideoEventRelay]);
|
||||
} else {
|
||||
tags.push([videoEventTagName, normalizedVideoEventId]);
|
||||
}
|
||||
}
|
||||
|
||||
if (videoDefinitionTagName && normalizedVideoDefinitionAddress) {
|
||||
if (normalizedVideoDefinitionRelay) {
|
||||
tags.push([
|
||||
@@ -972,7 +977,7 @@ export function buildCommentEvent({
|
||||
const resolvedContent = ensureValidUtf8Content(content);
|
||||
|
||||
return {
|
||||
kind: schema?.kind ?? 1,
|
||||
kind: schema?.kind ?? 1111,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
import { buildVideoAddressPointer } from "../utils/videoPointer.js";
|
||||
import { COMMENT_EVENT_KIND } from "../nostr/commentEvents.js";
|
||||
|
||||
const DEFAULT_MAX_DISCUSSION_COUNT_VIDEOS = 24;
|
||||
const COUNT_UNSUPPORTED_TITLE = "Relay does not support NIP-45 COUNT queries.";
|
||||
@@ -130,12 +131,12 @@ export default class DiscussionCountService {
|
||||
const filters = [];
|
||||
const eventId = typeof video.id === "string" ? video.id.trim() : "";
|
||||
if (eventId) {
|
||||
filters.push({ kinds: [1], "#e": [eventId] });
|
||||
filters.push({ kinds: [COMMENT_EVENT_KIND], "#e": [eventId] });
|
||||
}
|
||||
|
||||
const address = buildVideoAddressPointer(video);
|
||||
if (address) {
|
||||
filters.push({ kinds: [1], "#a": [address] });
|
||||
filters.push({ kinds: [COMMENT_EVENT_KIND], "#a": [address] });
|
||||
}
|
||||
|
||||
return filters;
|
||||
|
||||
@@ -135,11 +135,8 @@ test("publishComment prefers active signer when available", async () => {
|
||||
const aTags = eventTags.filter((tag) => Array.isArray(tag) && tag[0] === "a");
|
||||
assert.deepEqual(
|
||||
eTags,
|
||||
[
|
||||
["e", "video-event-id", "wss://video-relay"],
|
||||
["e", "parent-comment-id"],
|
||||
],
|
||||
"comment event should include both video and parent pointers",
|
||||
[["e", "parent-comment-id"]],
|
||||
"comment event should include parent pointer but omit the addressable video id",
|
||||
);
|
||||
assert.deepEqual(
|
||||
aTags,
|
||||
@@ -189,13 +186,12 @@ test("publishComment falls back to session signer when active signer is missing"
|
||||
assert.equal(publishCalls.length, 1, "publish should still occur once");
|
||||
});
|
||||
|
||||
test("listVideoComments builds filters with both #e and #a tags", async () => {
|
||||
test("listVideoComments builds filters that prefer #a pointers", async () => {
|
||||
const matchingEventLatest = {
|
||||
id: "comment-2",
|
||||
kind: COMMENT_EVENT_KIND,
|
||||
created_at: 1700000200,
|
||||
tags: [
|
||||
["e", "video-1"],
|
||||
["a", "30078:author:clip"],
|
||||
["e", "parent-1"],
|
||||
],
|
||||
@@ -206,7 +202,6 @@ test("listVideoComments builds filters with both #e and #a tags", async () => {
|
||||
kind: COMMENT_EVENT_KIND,
|
||||
created_at: 1690000000,
|
||||
tags: [
|
||||
["e", "video-1"],
|
||||
["a", "30078:author:clip"],
|
||||
["e", "parent-1"],
|
||||
],
|
||||
@@ -216,7 +211,7 @@ test("listVideoComments builds filters with both #e and #a tags", async () => {
|
||||
id: "comment-3",
|
||||
kind: COMMENT_EVENT_KIND,
|
||||
created_at: 1700000100,
|
||||
tags: [["e", "video-1"]],
|
||||
tags: [["a", "30078:author:clip"]],
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -250,21 +245,38 @@ test("listVideoComments builds filters with both #e and #a tags", async () => {
|
||||
["wss://history"],
|
||||
"list should use caller provided relays",
|
||||
);
|
||||
assert.equal(receivedFilters.filters.length, 1, "exactly one filter should be emitted");
|
||||
const [filter] = receivedFilters.filters;
|
||||
assert.equal(filter.kinds[0], COMMENT_EVENT_KIND, "filter should target comment kind");
|
||||
assert.deepEqual(
|
||||
filter["#e"],
|
||||
["video-1", "parent-1"],
|
||||
"filter should bind the video and parent ids via #e",
|
||||
assert.equal(receivedFilters.filters.length, 2, "legacy and preferred filters should be emitted");
|
||||
const [primaryFilter, legacyFilter] = receivedFilters.filters;
|
||||
assert.equal(
|
||||
primaryFilter.kinds[0],
|
||||
COMMENT_EVENT_KIND,
|
||||
"primary filter should target comment kind",
|
||||
);
|
||||
assert.deepEqual(
|
||||
filter["#a"],
|
||||
primaryFilter["#a"],
|
||||
["30078:author:clip"],
|
||||
"filter should bind the video definition via #a",
|
||||
"primary filter should bind the video definition via #a",
|
||||
);
|
||||
assert.equal(filter.since, 1700000000, "since option should propagate to filter");
|
||||
assert.equal(filter.limit, 10, "limit option should propagate to filter");
|
||||
assert.deepEqual(
|
||||
primaryFilter["#e"],
|
||||
["parent-1"],
|
||||
"primary filter should include the parent pointer via #e",
|
||||
);
|
||||
assert.equal(primaryFilter.since, 1700000000, "since option should propagate to primary filter");
|
||||
assert.equal(primaryFilter.limit, 10, "limit option should propagate to primary filter");
|
||||
|
||||
assert.equal(
|
||||
legacyFilter.kinds[0],
|
||||
COMMENT_EVENT_KIND,
|
||||
"legacy filter should target comment kind",
|
||||
);
|
||||
assert.deepEqual(
|
||||
legacyFilter["#e"],
|
||||
["video-1", "parent-1"],
|
||||
"legacy filter should target the video id for backward compatibility",
|
||||
);
|
||||
assert.equal(legacyFilter.since, 1700000000, "since option should propagate to legacy filter");
|
||||
assert.equal(legacyFilter.limit, 10, "limit option should propagate to legacy filter");
|
||||
});
|
||||
|
||||
test("subscribeVideoComments forwards matching events and cleans up unsubscribe", async () => {
|
||||
@@ -317,24 +329,26 @@ test("subscribeVideoComments forwards matching events and cleans up unsubscribe"
|
||||
["wss://live"],
|
||||
"subscription should use caller provided relays",
|
||||
);
|
||||
assert.equal(subscriptionArgs.filters.length, 1, "subscription should emit a single filter");
|
||||
const [subFilter] = subscriptionArgs.filters;
|
||||
assert.deepEqual(
|
||||
subFilter["#e"],
|
||||
["video-1", "parent-1"],
|
||||
"subscription filter should bind video and parent ids",
|
||||
);
|
||||
assert.deepEqual(
|
||||
subFilter["#a"],
|
||||
["30078:author:clip"],
|
||||
"subscription filter should bind the video definition",
|
||||
subscriptionArgs.filters,
|
||||
[
|
||||
{
|
||||
kinds: [COMMENT_EVENT_KIND],
|
||||
"#a": ["30078:author:clip"],
|
||||
"#e": ["parent-1"],
|
||||
},
|
||||
{
|
||||
kinds: [COMMENT_EVENT_KIND],
|
||||
"#e": ["video-1", "parent-1"],
|
||||
},
|
||||
],
|
||||
"subscription should emit preferred and legacy filters",
|
||||
);
|
||||
|
||||
handler({
|
||||
id: "comment-accepted",
|
||||
kind: COMMENT_EVENT_KIND,
|
||||
tags: [
|
||||
["e", "video-1"],
|
||||
["a", "30078:author:clip"],
|
||||
["e", "parent-1"],
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user