mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 02:42:28 +00:00
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:
4 files changed
+330
-22
No files matched your search
+181
-9
@@ -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(
|
||||
|
||||
@@ -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] });
|
||||
}
|
||||
|
||||
@@ -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], {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user