mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[search] Decode npub authors to hex — the Author filter matched nothing (npubs)
The Author field (and the author: token) passed values through as-typed, but the matcher compares against video.pubkey — HEX. So entering an npub (the only format users actually have) silently returned zero results; only raw hex worked. New normalizeAuthorValues (searchFilters.js, injectable decoder for tests): npub1* decodes via safeDecodeNpub to hex, hex passes through lowercased, and undecodable values are dropped — surfaced as parse errors on the author: token path. Applied in BOTH the parser and the filter modal's applyFilters, so the Author panel field and the token syntax now genuinely filter by creator. Tests: +2 in tests/search-filters.test.mjs (decode/passthrough/drop with an injected decoder; parser hex passthrough + garbage error). Lint + build green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
a6a6d4b0f7
commit
9afbc0cbbb
3 files changed
+82
-4
No files matched your search
@@ -1,3 +1,32 @@
|
||||
import { safeDecodeNpub } from "../utils/nostrHelpers.js";
|
||||
|
||||
const HEX64 = /^[0-9a-f]{64}$/;
|
||||
|
||||
// Authors arrive as npubs (what users actually have) or hex, but videos carry
|
||||
// HEX pubkeys — without decoding, npub authors silently matched nothing.
|
||||
// Undecodable values are dropped (returned in `dropped` for error surfaces).
|
||||
export function normalizeAuthorValues(values, { decodeNpub = safeDecodeNpub } = {}) {
|
||||
const normalized = [];
|
||||
const dropped = [];
|
||||
for (const raw of Array.isArray(values) ? values : []) {
|
||||
const value = typeof raw === "string" ? raw.trim() : "";
|
||||
if (!value) continue;
|
||||
if (HEX64.test(value.toLowerCase())) {
|
||||
normalized.push(value.toLowerCase());
|
||||
continue;
|
||||
}
|
||||
if (value.toLowerCase().startsWith("npub1")) {
|
||||
const hex = typeof decodeNpub === "function" ? decodeNpub(value) : null;
|
||||
if (hex && HEX64.test(hex.toLowerCase())) {
|
||||
normalized.push(hex.toLowerCase());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
dropped.push(value);
|
||||
}
|
||||
return { normalized, dropped };
|
||||
}
|
||||
|
||||
const DATE_FORMAT_OPTIONS = {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
@@ -176,7 +205,13 @@ export function parseFilterQuery(inputString = "") {
|
||||
errors.push({ token, message: "Author value is empty." });
|
||||
break;
|
||||
}
|
||||
filters.authorPubkeys.push(...values);
|
||||
{
|
||||
const { normalized, dropped } = normalizeAuthorValues(values);
|
||||
filters.authorPubkeys.push(...normalized);
|
||||
for (const value of dropped) {
|
||||
errors.push({ token, message: `Could not decode author "${value}".` });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "tag": {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
buildSearchHashFromState,
|
||||
resetSearchFilters,
|
||||
} from "../../search/searchFilterState.js";
|
||||
import { DEFAULT_FILTERS } from "../../search/searchFilters.js";
|
||||
import { DEFAULT_FILTERS, normalizeAuthorValues } from "../../search/searchFilters.js";
|
||||
import { setHashView } from "../../hashView.js";
|
||||
import { ALLOW_NSFW_CONTENT } from "../../config.js";
|
||||
|
||||
@@ -349,8 +349,9 @@ function applyFilters() {
|
||||
filters.sort = sortSelect.value;
|
||||
|
||||
// Author
|
||||
const authors = authorInput.value.split(/[, ]+/).map(s => s.trim()).filter(Boolean);
|
||||
filters.authorPubkeys = authors;
|
||||
const rawAuthors = authorInput.value.split(/[, ]+/).map(s => s.trim()).filter(Boolean);
|
||||
// Decode npubs to hex — videos carry hex pubkeys, so raw npubs matched nothing.
|
||||
filters.authorPubkeys = normalizeAuthorValues(rawAuthors).normalized;
|
||||
|
||||
// Tags
|
||||
const selectedTags = new Set();
|
||||
|
||||
@@ -116,3 +116,45 @@ test("sort: trending weights views by recency", () => {
|
||||
);
|
||||
assert.deepEqual(ranked, ["fresh-riser", "old-hit"]);
|
||||
});
|
||||
|
||||
// Author values arrive as npubs but videos carry hex pubkeys — without decoding,
|
||||
// npub authors silently matched nothing (the "wired but wrong" filter).
|
||||
//
|
||||
// test_integrity_note:
|
||||
// change_type: ["new_tests"]
|
||||
// scenarios:
|
||||
// - id: SCN-search-author-npub
|
||||
// given: "author values as npub / hex / garbage"
|
||||
// when: "normalizeAuthorValues runs (with an injected decoder)"
|
||||
// then: "npubs decode to hex, hex passes through lowercased, garbage is dropped with errors"
|
||||
// observable_outcomes:
|
||||
// - "npub -> decoded hex; HEX passthrough lowercased; undecodable -> dropped"
|
||||
// - "parser author: token emits decode errors for garbage"
|
||||
// determinism_controls:
|
||||
// - "injected decodeNpub stub (no window.NostrTools dependency)"
|
||||
// anti_cheat_rationale:
|
||||
// prevents: ["hard-coded return value"]
|
||||
// relaxation:
|
||||
// did_relax_any_assertion: false
|
||||
|
||||
test("normalizeAuthorValues decodes npubs, passes hex, drops garbage", async () => {
|
||||
const { normalizeAuthorValues } = await import("../js/search/searchFilters.js");
|
||||
const HEX = "a".repeat(64);
|
||||
const decodeNpub = (v) => (v === "npub1good" ? HEX : null);
|
||||
|
||||
const { normalized, dropped } = normalizeAuthorValues(
|
||||
["npub1good", "B".repeat(64), "npub1bad", "garbage"],
|
||||
{ decodeNpub },
|
||||
);
|
||||
assert.deepEqual(normalized, [HEX, "b".repeat(64)]);
|
||||
assert.deepEqual(dropped, ["npub1bad", "garbage"]);
|
||||
});
|
||||
|
||||
test("author: token with hex passes into filters; garbage surfaces as an error", () => {
|
||||
const HEX = "c".repeat(64);
|
||||
const good = parseFilterQuery(`author:${HEX}`);
|
||||
assert.deepEqual(good.filters.authorPubkeys, [HEX]);
|
||||
const bad = parseFilterQuery("author:not-a-key");
|
||||
assert.equal(bad.filters.authorPubkeys.length, 0);
|
||||
assert.ok(bad.errors.some((e) => /decode author/i.test(e.message)));
|
||||
});
|
||||
Reference in new issue
Block a user