Merge pull request #987 from PR0M3TH3AN/codex/fix-comment-kind-and-tag-usage

Use kind 1111 comments with address-first tags
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-29 07:45:57 -04:00
commit 05676118cc
5 files changed
+128 -68

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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,
+3 -2
View File
@@ -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;
+45 -31
View File
@@ -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"],
],