Merge pull request #996 from PR0M3TH3AN/codex/expand-video-comment-tag-support

Handle uppercase comment root tags in filters and counts
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-29 15:38:52 -04:00
commit 5f34c6ca8d
4 files changed
+330 -22

No files matched your search

+181 -9
View File
@@ -750,6 +750,48 @@ function createVideoCommentFilters(targetInput, options = {}) {
};
filters.push(applyFilterOptions(eventFilter, options));
const uppercaseFilter = { kinds: [COMMENT_EVENT_KIND] };
let hasUppercasePointer = false;
if (typeof descriptor.rootIdentifier === "string" && descriptor.rootIdentifier) {
uppercaseFilter["#I"] = [descriptor.rootIdentifier];
hasUppercasePointer = true;
} else if (
typeof descriptor.videoDefinitionAddress === "string" &&
descriptor.videoDefinitionAddress
) {
uppercaseFilter["#A"] = [descriptor.videoDefinitionAddress];
hasUppercasePointer = true;
} else if (
typeof descriptor.videoEventId === "string" &&
descriptor.videoEventId
) {
uppercaseFilter["#E"] = [descriptor.videoEventId];
hasUppercasePointer = true;
}
const normalizedRootKind =
typeof descriptor.rootKind === "string"
? descriptor.rootKind
: Number.isFinite(descriptor.rootKind)
? String(Math.floor(descriptor.rootKind))
: "";
if (normalizedRootKind) {
uppercaseFilter["#K"] = [normalizedRootKind];
}
const normalizedRootAuthor =
typeof descriptor.rootAuthorPubkey === "string"
? descriptor.rootAuthorPubkey.trim()
: "";
if (normalizedRootAuthor) {
uppercaseFilter["#P"] = [normalizedRootAuthor];
}
if (hasUppercasePointer) {
filters.push(applyFilterOptions(uppercaseFilter, options));
}
if (
descriptor.parentCommentId &&
descriptor.parentCommentId !== descriptor.videoEventId
@@ -810,21 +852,99 @@ function isVideoCommentEvent(event, descriptor) {
descriptor && typeof descriptor === "object" ? descriptor : {};
const tags = Array.isArray(event.tags) ? event.tags : [];
const requiresParentTag = Boolean(targetDescriptor.parentCommentId);
const requiresAddressMatch = Boolean(targetDescriptor.videoDefinitionAddress);
const requiresRootIdentifierMatch = Boolean(targetDescriptor.rootIdentifier);
const requiresAddressMatch =
!requiresRootIdentifierMatch && Boolean(targetDescriptor.videoDefinitionAddress);
const requiresEventMatch =
!requiresAddressMatch && Boolean(targetDescriptor.videoEventId);
!requiresRootIdentifierMatch && !requiresAddressMatch &&
Boolean(targetDescriptor.videoEventId);
const requiresParentTag = Boolean(targetDescriptor.parentCommentId);
let hasEventTag = !requiresEventMatch;
let hasIdentifierTag = !requiresRootIdentifierMatch;
let hasDefinitionTag = !requiresAddressMatch;
let hasEventTag = !requiresEventMatch;
let hasParentTag = !requiresParentTag;
const expectedRootKind = (() => {
if (typeof targetDescriptor.rootKind === "string") {
return targetDescriptor.rootKind;
}
if (Number.isFinite(targetDescriptor.rootKind)) {
return String(Math.floor(targetDescriptor.rootKind));
}
if (typeof targetDescriptor.videoKind === "string") {
return targetDescriptor.videoKind;
}
if (Number.isFinite(targetDescriptor.videoKind)) {
return String(Math.floor(targetDescriptor.videoKind));
}
return "";
})();
const expectedRootAuthor =
typeof targetDescriptor.rootAuthorPubkey === "string"
? targetDescriptor.rootAuthorPubkey
: typeof targetDescriptor.videoAuthorPubkey === "string"
? targetDescriptor.videoAuthorPubkey
: "";
const expectedParentKind = (() => {
if (typeof targetDescriptor.parentKind === "string") {
return targetDescriptor.parentKind;
}
if (Number.isFinite(targetDescriptor.parentKind)) {
return String(Math.floor(targetDescriptor.parentKind));
}
return "";
})();
const expectedParentAuthor =
typeof targetDescriptor.parentAuthorPubkey === "string"
? targetDescriptor.parentAuthorPubkey
: "";
let sawRootKindTag = false;
let rootKindMatches = !expectedRootKind;
let rootKindMismatch = false;
let sawRootAuthorTag = false;
let rootAuthorMatches = !expectedRootAuthor;
let rootAuthorMismatch = false;
let sawParentKindTag = false;
let parentKindMatches = !expectedParentKind;
let parentKindMismatch = false;
let sawParentAuthorTag = false;
let parentAuthorMatches = !expectedParentAuthor;
let parentAuthorMismatch = false;
for (const tag of tags) {
if (!Array.isArray(tag) || tag.length < 2) {
continue;
}
const [name, value] = tag;
if (name === "e" && typeof value === "string") {
if (typeof value !== "string") {
continue;
}
if (name === "I") {
if (value === targetDescriptor.rootIdentifier) {
hasIdentifierTag = true;
}
} else if (name === "A") {
if (value === targetDescriptor.videoDefinitionAddress) {
hasDefinitionTag = true;
}
} else if (name === "a") {
if (value === targetDescriptor.videoDefinitionAddress) {
hasDefinitionTag = true;
}
} else if (name === "E") {
if (value === targetDescriptor.videoEventId) {
hasEventTag = true;
}
} else if (name === "e") {
if (value === targetDescriptor.videoEventId) {
hasEventTag = true;
}
@@ -834,14 +954,66 @@ function isVideoCommentEvent(event, descriptor) {
) {
hasParentTag = true;
}
} else if (name === "a" && typeof value === "string") {
if (value === targetDescriptor.videoDefinitionAddress) {
hasDefinitionTag = true;
} else if (name === "K") {
sawRootKindTag = true;
if (!expectedRootKind || value === expectedRootKind) {
rootKindMatches = true;
} else {
rootKindMismatch = true;
}
} else if (name === "P") {
sawRootAuthorTag = true;
if (!expectedRootAuthor || value === expectedRootAuthor) {
rootAuthorMatches = true;
} else {
rootAuthorMismatch = true;
}
} else if (name === "k") {
sawParentKindTag = true;
if (!expectedParentKind || value === expectedParentKind) {
parentKindMatches = true;
} else {
parentKindMismatch = true;
}
} else if (name === "p") {
sawParentAuthorTag = true;
if (!expectedParentAuthor || value === expectedParentAuthor) {
parentAuthorMatches = true;
} else {
parentAuthorMismatch = true;
}
}
}
return hasEventTag && hasDefinitionTag && hasParentTag;
if (!hasIdentifierTag || !hasDefinitionTag || !hasEventTag || !hasParentTag) {
return false;
}
if ((sawRootKindTag && rootKindMismatch) || (sawRootAuthorTag && rootAuthorMismatch)) {
return false;
}
if ((sawParentKindTag && parentKindMismatch) || (sawParentAuthorTag && parentAuthorMismatch)) {
return false;
}
if (sawRootKindTag && expectedRootKind && !rootKindMatches) {
return false;
}
if (sawRootAuthorTag && expectedRootAuthor && !rootAuthorMatches) {
return false;
}
if (sawParentKindTag && expectedParentKind && !parentKindMatches) {
return false;
}
if (sawParentAuthorTag && expectedParentAuthor && !parentAuthorMatches) {
return false;
}
return true;
}
export async function publishComment(
+45
View File
@@ -135,6 +135,51 @@ export default class DiscussionCountService {
}
const address = buildVideoAddressPointer(video);
const rootIdentifier =
typeof video.videoRootId === "string" ? video.videoRootId.trim() : "";
const uppercaseFilter = { kinds: [COMMENT_EVENT_KIND] };
let hasUppercasePointer = false;
if (rootIdentifier) {
uppercaseFilter["#I"] = [rootIdentifier];
hasUppercasePointer = true;
} else if (address) {
uppercaseFilter["#A"] = [address];
hasUppercasePointer = true;
} else if (eventId) {
uppercaseFilter["#E"] = [eventId];
hasUppercasePointer = true;
}
const rootKind = (() => {
if (Number.isFinite(video.kind)) {
return String(Math.floor(video.kind));
}
if (typeof video.kind === "string" && video.kind.trim()) {
const parsed = Number(video.kind);
if (Number.isFinite(parsed)) {
return String(Math.floor(parsed));
}
return video.kind.trim();
}
return "";
})();
if (rootKind) {
uppercaseFilter["#K"] = [rootKind];
}
const rootAuthor =
typeof video.pubkey === "string" ? video.pubkey.trim() : "";
if (rootAuthor) {
uppercaseFilter["#P"] = [rootAuthor];
}
if (hasUppercasePointer) {
filters.push(uppercaseFilter);
}
if (address) {
filters.push({ kinds: [COMMENT_EVENT_KIND], "#a": [address] });
}
+39 -5
View File
@@ -6,6 +6,7 @@ import { JSDOM } from "jsdom";
import DiscussionCountService, {
COUNT_UNSUPPORTED_TITLE,
} from "../js/services/discussionCountService.js";
import { COMMENT_EVENT_KIND } from "../js/nostr/commentEvents.js";
function createDomWithCountElement(videoId) {
const dom = new JSDOM(`
@@ -35,7 +36,22 @@ async function testRefreshCountsCachesResults() {
const nostrClient = {
pool: {},
countEventsAcrossRelays: async (filters) => {
assert.equal(filters.length, 2, "should request both id and address filters");
assert.equal(filters.length, 3, "should request id, uppercase, and address filters");
const [eventFilter, uppercaseFilter, addressFilter] = filters;
assert.deepEqual(eventFilter, {
kinds: [COMMENT_EVENT_KIND],
"#e": ["count-video-1"],
});
assert.deepEqual(uppercaseFilter, {
kinds: [COMMENT_EVENT_KIND],
"#A": ["30078:pubkey-1:identifier-1"],
"#K": ["30078"],
"#P": ["pubkey-1"],
});
assert.deepEqual(addressFilter, {
kinds: [COMMENT_EVENT_KIND],
"#a": ["30078:pubkey-1:identifier-1"],
});
return {
total: 5,
perRelay: [{ ok: true }],
@@ -102,10 +118,28 @@ async function testUnsupportedRelaysUpdateDomState() {
const service = new DiscussionCountService();
const nostrClient = {
pool: {},
countEventsAcrossRelays: async () => ({
total: 7,
perRelay: [{ ok: false }],
}),
countEventsAcrossRelays: async (filters) => {
assert.equal(filters.length, 3, "unsupported relays should still receive all filters");
const [eventFilter, uppercaseFilter, addressFilter] = filters;
assert.deepEqual(eventFilter, {
kinds: [COMMENT_EVENT_KIND],
"#e": ["unsupported-video"],
});
assert.deepEqual(uppercaseFilter, {
kinds: [COMMENT_EVENT_KIND],
"#A": ["30078:pubkey-2:identifier-2"],
"#K": ["30078"],
"#P": ["pubkey-2"],
});
assert.deepEqual(addressFilter, {
kinds: [COMMENT_EVENT_KIND],
"#a": ["30078:pubkey-2:identifier-2"],
});
return {
total: 7,
perRelay: [{ ok: false }],
};
},
};
service.refreshCounts([video], {
+65 -8
View File
@@ -308,14 +308,18 @@ test("publishComment emits only video event #e tag when address and parent are a
);
});
test("listVideoComments builds filters with #e primary and #a secondary", async () => {
test("listVideoComments builds filters with uppercase roots plus legacy fallbacks", async () => {
const matchingEventLatest = {
id: "comment-2",
kind: COMMENT_EVENT_KIND,
created_at: 1700000200,
tags: [
["a", "30078:author:clip"],
["A", "30078:author:clip"],
["K", "30078"],
["P", "author"],
["e", "parent-1"],
["k", String(COMMENT_EVENT_KIND)],
["p", "thread-participant"],
],
};
@@ -353,6 +357,8 @@ test("listVideoComments builds filters with #e primary and #a secondary", async
videoEventId: "video-1",
videoDefinitionAddress: "30078:author:clip",
parentCommentId: "parent-1",
parentKind: COMMENT_EVENT_KIND,
parentAuthorPubkey: "thread-participant",
},
{ relays: ["wss://history"], since: 1700000000, limit: 10 },
);
@@ -369,10 +375,11 @@ test("listVideoComments builds filters with #e primary and #a secondary", async
);
assert.equal(
receivedFilters.filters.length,
3,
"event, parent, and address filters should be emitted",
4,
"event, uppercase, parent, and address filters should be emitted",
);
const [eventFilter, parentFilter, definitionFilter] = receivedFilters.filters;
const [eventFilter, uppercaseFilter, parentFilter, definitionFilter] =
receivedFilters.filters;
assert.equal(
eventFilter.kinds[0],
COMMENT_EVENT_KIND,
@@ -383,6 +390,36 @@ test("listVideoComments builds filters with #e primary and #a secondary", async
["video-1"],
"event filter should target the video via #e",
);
assert.equal(
uppercaseFilter.kinds[0],
COMMENT_EVENT_KIND,
"uppercase filter should target comment kind",
);
assert.deepEqual(
uppercaseFilter["#A"],
["30078:author:clip"],
"uppercase filter should target the video definition via #A",
);
assert.deepEqual(
uppercaseFilter["#K"],
["30078"],
"uppercase filter should scope the root kind via #K",
);
assert.deepEqual(
uppercaseFilter["#P"],
["author"],
"uppercase filter should scope the root author via #P",
);
assert.equal(
uppercaseFilter.since,
1700000000,
"since option should propagate to uppercase filter",
);
assert.equal(
uppercaseFilter.limit,
10,
"limit option should propagate to uppercase filter",
);
assert.deepEqual(
parentFilter,
{
@@ -465,8 +502,12 @@ test("listVideoComments supports legacy targets without a definition address", a
kinds: [COMMENT_EVENT_KIND],
"#e": ["legacy-video"],
},
{
kinds: [COMMENT_EVENT_KIND],
"#E": ["legacy-video"],
},
],
"legacy listing should target the video event id via #e",
"legacy listing should target the video event id via #e/#E",
);
});
@@ -503,6 +544,8 @@ test("subscribeVideoComments forwards matching events and cleans up unsubscribe"
videoEventId: "video-1",
videoDefinitionAddress: "30078:author:clip",
parentCommentId: "parent-1",
parentKind: COMMENT_EVENT_KIND,
parentAuthorPubkey: "thread-participant",
},
{
relays: ["wss://live"],
@@ -527,6 +570,12 @@ test("subscribeVideoComments forwards matching events and cleans up unsubscribe"
kinds: [COMMENT_EVENT_KIND],
"#e": ["video-1"],
},
{
kinds: [COMMENT_EVENT_KIND],
"#A": ["30078:author:clip"],
"#K": ["30078"],
"#P": ["author"],
},
{
kinds: [COMMENT_EVENT_KIND],
"#e": ["parent-1"],
@@ -544,8 +593,12 @@ test("subscribeVideoComments forwards matching events and cleans up unsubscribe"
id: "comment-accepted",
kind: COMMENT_EVENT_KIND,
tags: [
["a", "30078:author:clip"],
["A", "30078:author:clip"],
["K", "30078"],
["P", "author"],
["e", "parent-1"],
["k", String(COMMENT_EVENT_KIND)],
["p", "thread-participant"],
],
});
@@ -600,8 +653,12 @@ test("subscribeVideoComments supports video targets without a definition address
kinds: [COMMENT_EVENT_KIND],
"#e": ["legacy-video"],
},
{
kinds: [COMMENT_EVENT_KIND],
"#E": ["legacy-video"],
},
],
"subscription should emit only the video #e filter when address is absent",
"subscription should emit both legacy and uppercase video filters when address is absent",
);
assert.ok(handler, "event handler should be registered");