mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 21:02:28 +00:00
update
This commit is contained in:
1 parent
1165099397
commit
00f96bbe0e
3 files changed
+260
-9
No files matched your search
@@ -2,6 +2,10 @@
|
||||
|
||||
import watchHistoryService from "./watchHistoryService.js";
|
||||
import { nostrClient } from "./nostrClientFacade.js";
|
||||
import {
|
||||
createFeedEngine,
|
||||
createWatchHistoryFeedDefinition,
|
||||
} from "./feedEngine/index.js";
|
||||
import {
|
||||
updateWatchHistoryListWithDefaultClient as updateWatchHistoryList,
|
||||
} from "./nostrWatchHistoryFacade.js";
|
||||
@@ -86,6 +90,21 @@ function getAppInstance() {
|
||||
return getApplication();
|
||||
}
|
||||
|
||||
let localWatchHistoryFallbackEngine = null;
|
||||
|
||||
function getLocalWatchHistoryFallbackEngine() {
|
||||
if (localWatchHistoryFallbackEngine) {
|
||||
return localWatchHistoryFallbackEngine;
|
||||
}
|
||||
const engine = createFeedEngine();
|
||||
engine.registerFeed(
|
||||
"watch-history",
|
||||
createWatchHistoryFeedDefinition({ service: watchHistoryService }),
|
||||
);
|
||||
localWatchHistoryFallbackEngine = engine;
|
||||
return localWatchHistoryFallbackEngine;
|
||||
}
|
||||
|
||||
function buildWatchHistoryFeedRuntime({
|
||||
actor,
|
||||
cursor = 0,
|
||||
@@ -228,8 +247,17 @@ function normalizeHistoryItems(rawItems) {
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
const baseMetadata =
|
||||
candidate &&
|
||||
typeof candidate === "object" &&
|
||||
candidate.metadata &&
|
||||
typeof candidate.metadata === "object"
|
||||
? { ...candidate.metadata }
|
||||
: {};
|
||||
const watchedAtRaw = Number.isFinite(candidate?.watchedAt)
|
||||
? candidate.watchedAt
|
||||
: Number.isFinite(baseMetadata?.watchedAt)
|
||||
? baseMetadata.watchedAt
|
||||
: Number.isFinite(candidate?.timestamp)
|
||||
? candidate.timestamp
|
||||
: Number.isFinite(pointer?.watchedAt)
|
||||
@@ -237,10 +265,23 @@ function normalizeHistoryItems(rawItems) {
|
||||
: null;
|
||||
const watchedAt =
|
||||
watchedAtRaw !== null ? Math.max(0, Math.floor(watchedAtRaw)) : 0;
|
||||
const video = candidate?.video || baseMetadata.video || null;
|
||||
const profile = candidate?.profile || baseMetadata.profile || null;
|
||||
const metadata = {
|
||||
...baseMetadata,
|
||||
pointerKey: key,
|
||||
watchedAt: Number.isFinite(baseMetadata?.watchedAt)
|
||||
? baseMetadata.watchedAt
|
||||
: watchedAt || null,
|
||||
video: video || null,
|
||||
profile: profile || null,
|
||||
};
|
||||
normalized.push({
|
||||
pointer,
|
||||
pointerKey: key,
|
||||
watchedAt,
|
||||
video: metadata.video,
|
||||
metadata,
|
||||
raw: candidate
|
||||
});
|
||||
}
|
||||
@@ -1439,6 +1480,31 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const runtime = buildWatchHistoryFeedRuntime({
|
||||
actor: actorInput,
|
||||
cursor,
|
||||
forceRefresh,
|
||||
});
|
||||
const fallbackEngine = getLocalWatchHistoryFallbackEngine();
|
||||
const hydrated = await fallbackEngine.run("watch-history", { runtime });
|
||||
return {
|
||||
...(hydrated && typeof hydrated === "object" ? hydrated : {}),
|
||||
metadata: {
|
||||
...(hydrated?.metadata && typeof hydrated.metadata === "object"
|
||||
? hydrated.metadata
|
||||
: {}),
|
||||
engine: "local-feed-fallback",
|
||||
},
|
||||
};
|
||||
} catch (fallbackError) {
|
||||
if (isDevEnv) {
|
||||
devLogger.warn(
|
||||
"[historyView] local feed fallback failed; using raw service data.",
|
||||
fallbackError,
|
||||
);
|
||||
}
|
||||
}
|
||||
const items = await watchHistoryService.loadLatest(actorInput, {
|
||||
allowStale: !forceRefresh,
|
||||
forceRefresh,
|
||||
|
||||
+133
-9
@@ -693,6 +693,101 @@ async function resolveNostrToolkit(deps = {}, cacheRef = null) {
|
||||
return cacheRef?.current || null;
|
||||
}
|
||||
|
||||
async function decryptWatchHistoryCiphertext(
|
||||
{
|
||||
event = null,
|
||||
ciphertext = "",
|
||||
actorKey = "",
|
||||
decryptSigner = null,
|
||||
sessionPrivateKey = "",
|
||||
deps = {},
|
||||
} = {},
|
||||
toolkitCacheRef = null,
|
||||
) {
|
||||
const normalizedCiphertext =
|
||||
typeof ciphertext === "string" ? ciphertext.trim() : "";
|
||||
if (!normalizedCiphertext || !actorKey) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const decryptors = new Map();
|
||||
const registerDecryptor = (scheme, decrypt) => {
|
||||
if (typeof scheme !== "string" || !scheme || typeof decrypt !== "function") {
|
||||
return;
|
||||
}
|
||||
if (!decryptors.has(scheme)) {
|
||||
decryptors.set(scheme, decrypt);
|
||||
}
|
||||
};
|
||||
|
||||
if (decryptSigner && typeof decryptSigner === "object") {
|
||||
if (typeof decryptSigner.nip44Decrypt === "function") {
|
||||
registerDecryptor("nip44", (value) =>
|
||||
decryptSigner.nip44Decrypt(actorKey, value),
|
||||
);
|
||||
}
|
||||
if (typeof decryptSigner.nip04Decrypt === "function") {
|
||||
registerDecryptor("nip04", (value) =>
|
||||
decryptSigner.nip04Decrypt(actorKey, value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedSessionPrivateKey =
|
||||
typeof sessionPrivateKey === "string" ? sessionPrivateKey.trim().toLowerCase() : "";
|
||||
const canUseSessionPrivateKey = /^[0-9a-f]{64}$/.test(normalizedSessionPrivateKey);
|
||||
if (canUseSessionPrivateKey) {
|
||||
const tools = await resolveNostrToolkit(deps, toolkitCacheRef);
|
||||
if (tools?.nip04 && typeof tools.nip04.decrypt === "function") {
|
||||
registerDecryptor("nip04", (value) =>
|
||||
tools.nip04.decrypt(normalizedSessionPrivateKey, actorKey, value),
|
||||
);
|
||||
}
|
||||
const nip44Suite = createNip44CipherSuite(
|
||||
tools,
|
||||
normalizedSessionPrivateKey,
|
||||
actorKey,
|
||||
);
|
||||
if (nip44Suite?.v2 && typeof nip44Suite.v2.decrypt === "function") {
|
||||
registerDecryptor("nip44_v2", (value) => nip44Suite.v2.decrypt(value));
|
||||
}
|
||||
if (nip44Suite?.legacy && typeof nip44Suite.legacy.decrypt === "function") {
|
||||
registerDecryptor("nip44", (value) => nip44Suite.legacy.decrypt(value));
|
||||
}
|
||||
}
|
||||
|
||||
if (!decryptors.size) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const availableSchemes = Array.from(decryptors.keys());
|
||||
const order = determineWatchHistoryDecryptionOrder(
|
||||
event,
|
||||
normalizedCiphertext,
|
||||
availableSchemes,
|
||||
);
|
||||
|
||||
for (const scheme of order) {
|
||||
const decrypt = decryptors.get(scheme);
|
||||
if (typeof decrypt !== "function") {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const plaintext = await decrypt(normalizedCiphertext);
|
||||
if (typeof plaintext === "string" && plaintext.trim()) {
|
||||
return plaintext;
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
`[nostr] Failed to decrypt watch history payload with ${scheme}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function mergeWatchHistoryItemsWithFallback(parsed, fallbackItems) {
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return {
|
||||
@@ -1900,8 +1995,13 @@ class WatchHistoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
const sessionPrivateKey =
|
||||
typeof sessionActor?.privateKey === "string" ? sessionActor.privateKey : "";
|
||||
const canAttemptDecrypt = Boolean(decryptSigner) || Boolean(sessionPrivateKey);
|
||||
const toolkitCacheRef = { current: null };
|
||||
|
||||
let decryptedItems = [];
|
||||
if (chunkIdentifiers.length && decryptSigner) {
|
||||
if (chunkIdentifiers.length && canAttemptDecrypt) {
|
||||
try {
|
||||
const results = await pool.list(readRelays, [
|
||||
{
|
||||
@@ -1920,13 +2020,19 @@ class WatchHistoryManager {
|
||||
if (!ciphertext) {
|
||||
return [];
|
||||
}
|
||||
let plaintext = "";
|
||||
if (typeof decryptSigner.nip04Decrypt === "function") {
|
||||
try {
|
||||
plaintext = await decryptSigner.nip04Decrypt(actorKey, ciphertext);
|
||||
} catch (error) {
|
||||
devLogger.warn("[nostr] Failed to decrypt watch history chunk:", error);
|
||||
}
|
||||
let plaintext = looksLikeJsonStructure(ciphertext) ? ciphertext : "";
|
||||
if (!plaintext) {
|
||||
plaintext = await decryptWatchHistoryCiphertext(
|
||||
{
|
||||
event,
|
||||
ciphertext,
|
||||
actorKey,
|
||||
decryptSigner,
|
||||
sessionPrivateKey,
|
||||
deps: this.deps,
|
||||
},
|
||||
toolkitCacheRef,
|
||||
);
|
||||
}
|
||||
if (!plaintext) {
|
||||
return [];
|
||||
@@ -1962,9 +2068,27 @@ class WatchHistoryManager {
|
||||
const dTag = event.tags.find(t => t[0] === 'd');
|
||||
const eventSnapshotId = dTag ? dTag[1] : "";
|
||||
|
||||
let payloadContent = ciphertext;
|
||||
if (!looksLikeJsonStructure(payloadContent) && canAttemptDecrypt) {
|
||||
const decrypted = await decryptWatchHistoryCiphertext(
|
||||
{
|
||||
event,
|
||||
ciphertext: payloadContent,
|
||||
actorKey,
|
||||
decryptSigner,
|
||||
sessionPrivateKey,
|
||||
deps: this.deps,
|
||||
},
|
||||
toolkitCacheRef,
|
||||
);
|
||||
if (decrypted) {
|
||||
payloadContent = decrypted;
|
||||
}
|
||||
}
|
||||
|
||||
// Try parsing as plaintext first
|
||||
const payload = parseWatchHistoryContentWithFallback(
|
||||
ciphertext,
|
||||
payloadContent,
|
||||
fallbackPointers,
|
||||
{
|
||||
version: 0,
|
||||
|
||||
@@ -311,6 +311,67 @@ test("fetchWatchHistory falls back to pointer payload when nip04 decrypt fails",
|
||||
}
|
||||
});
|
||||
|
||||
test("fetchWatchHistory decrypts encrypted pointer events with nip44 signer support", async () => {
|
||||
const actorHex = "5".repeat(64);
|
||||
const ciphertext = "nip44-ciphertext";
|
||||
const decryptCalls = [];
|
||||
|
||||
const pointerEvent = {
|
||||
id: "pointer-nip44",
|
||||
pubkey: actorHex,
|
||||
created_at: 1_700_200_000,
|
||||
content: ciphertext,
|
||||
tags: [
|
||||
["encrypted", "nip44"],
|
||||
["a", "30078:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:fallback"],
|
||||
["d", "watch-history-month"],
|
||||
],
|
||||
};
|
||||
|
||||
const signer = {
|
||||
nip44Decrypt: async (pubkey, payload) => {
|
||||
decryptCalls.push({ pubkey, payload });
|
||||
assert.equal(pubkey, actorHex, "nip44 decrypt should target the actor pubkey");
|
||||
assert.equal(payload, ciphertext, "nip44 decrypt should receive encrypted content");
|
||||
return JSON.stringify({
|
||||
version: 2,
|
||||
items: [{ type: "e", value: "decrypted-from-pointer", watchedAt: 33 }],
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const pool = {
|
||||
async list() {
|
||||
return [pointerEvent];
|
||||
},
|
||||
};
|
||||
|
||||
const manager = createWatchHistoryManager({
|
||||
getActivePubkey: () => actorHex,
|
||||
resolveActiveSigner: () => signer,
|
||||
shouldRequestExtensionPermissions: () => false,
|
||||
getPool: () => pool,
|
||||
getReadRelays: () => ["wss://relay.example"],
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await fetchWatchHistory(manager, actorHex, { forceRefresh: true });
|
||||
assert.equal(decryptCalls.length, 1, "nip44 decrypt should be attempted once");
|
||||
assert.deepStrictEqual(
|
||||
result.items.map((item) => item.value),
|
||||
["decrypted-from-pointer"],
|
||||
"decrypted pointer event payload should be used",
|
||||
);
|
||||
assert.equal(
|
||||
result.items[0]?.watchedAt,
|
||||
33,
|
||||
"watched timestamp from decrypted payload should be preserved",
|
||||
);
|
||||
} finally {
|
||||
manager.clear();
|
||||
}
|
||||
});
|
||||
|
||||
test("publishWatchHistorySnapshot uses injected nostr-tools helpers when signer cannot encrypt", async () => {
|
||||
const actorPubkey = "f".repeat(64);
|
||||
const sessionPrivateKey = "a".repeat(64);
|
||||
|
||||
Reference in new issue
Block a user