mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 18:32:27 +00:00
Encrypt watch history snapshots and decrypt on fetch
This commit is contained in:
1 parent
789e0eff26
commit
befa1ad5ae
1 file changed
+250
-22
+250
-22
@@ -185,19 +185,6 @@ function normalizePointerInput(pointer) {
|
||||
return { type, value: trimmed, relay: null };
|
||||
}
|
||||
|
||||
function pointerToTag(pointer) {
|
||||
const normalized = clonePointerItem(pointer);
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tag = [normalized.type, normalized.value];
|
||||
if (normalized.relay) {
|
||||
tag.push(normalized.relay);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
function extractPointerItemsFromEvent(event) {
|
||||
if (!event || typeof event !== "object") {
|
||||
return [];
|
||||
@@ -225,6 +212,53 @@ function extractPointerItemsFromEvent(event) {
|
||||
return items;
|
||||
}
|
||||
|
||||
function normalizePointersFromPayload(payload) {
|
||||
const normalized = [];
|
||||
const seen = new Set();
|
||||
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const source = Array.isArray(payload.items) ? payload.items : [];
|
||||
for (const candidate of source) {
|
||||
const pointer = normalizePointerInput(candidate);
|
||||
if (!pointer) {
|
||||
continue;
|
||||
}
|
||||
const key = pointerKey(pointer);
|
||||
if (!key || seen.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
normalized.push(pointer);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function parseWatchHistoryPayload(plaintext) {
|
||||
if (typeof plaintext !== "string") {
|
||||
return { version: 0, items: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(plaintext);
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return { version: 0, items: [] };
|
||||
}
|
||||
|
||||
const version = Number.isFinite(parsed.version) ? parsed.version : 0;
|
||||
const items = normalizePointersFromPayload(parsed);
|
||||
return { version, items };
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn("[nostr] Failed to parse watch history payload:", error);
|
||||
}
|
||||
return { version: 0, items: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function eventToAddressPointer(event) {
|
||||
if (!event || typeof event !== "object") {
|
||||
return "";
|
||||
@@ -935,6 +969,176 @@ class NostrClient {
|
||||
}
|
||||
}
|
||||
|
||||
async encryptWatchHistoryPayload(actorPubkey, payload) {
|
||||
const normalizedActor =
|
||||
typeof actorPubkey === "string" && actorPubkey.trim()
|
||||
? actorPubkey.trim().toLowerCase()
|
||||
: "";
|
||||
if (!normalizedActor) {
|
||||
return { ok: false, error: "invalid-actor" };
|
||||
}
|
||||
|
||||
let normalizedPayload = payload;
|
||||
if (!normalizedPayload || typeof normalizedPayload !== "object") {
|
||||
normalizedPayload = {};
|
||||
}
|
||||
|
||||
const items = Array.isArray(normalizedPayload.items)
|
||||
? normalizedPayload.items
|
||||
: [];
|
||||
|
||||
const prepared = {
|
||||
version: Number.isFinite(normalizedPayload.version)
|
||||
? normalizedPayload.version
|
||||
: 1,
|
||||
items,
|
||||
};
|
||||
|
||||
const plaintext = JSON.stringify(prepared);
|
||||
|
||||
let ciphertext = "";
|
||||
const normalizedLogged =
|
||||
typeof this.pubkey === "string" ? this.pubkey.toLowerCase() : "";
|
||||
|
||||
if (
|
||||
normalizedActor &&
|
||||
normalizedActor === normalizedLogged &&
|
||||
window?.nostr?.nip04?.encrypt
|
||||
) {
|
||||
try {
|
||||
ciphertext = await window.nostr.nip04.encrypt(
|
||||
normalizedActor,
|
||||
plaintext
|
||||
);
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Failed to encrypt watch history with extension:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ciphertext) {
|
||||
try {
|
||||
if (!this.sessionActor || this.sessionActor.pubkey !== normalizedActor) {
|
||||
await this.ensureSessionActor(true);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Failed to ensure session actor while encrypting watch history:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const privateKey = this.sessionActor?.privateKey;
|
||||
if (privateKey && window?.NostrTools?.nip04?.encrypt) {
|
||||
try {
|
||||
ciphertext = await window.NostrTools.nip04.encrypt(
|
||||
privateKey,
|
||||
normalizedActor,
|
||||
plaintext
|
||||
);
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Failed to encrypt watch history with session key:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ciphertext) {
|
||||
return { ok: false, error: "encryption-unavailable" };
|
||||
}
|
||||
|
||||
return { ok: true, ciphertext };
|
||||
}
|
||||
|
||||
async decryptWatchHistoryEvent(pointerEvent, actorPubkey) {
|
||||
const normalizedActor =
|
||||
typeof actorPubkey === "string" && actorPubkey.trim()
|
||||
? actorPubkey.trim().toLowerCase()
|
||||
: "";
|
||||
if (!normalizedActor) {
|
||||
return { version: 0, items: extractPointerItemsFromEvent(pointerEvent) };
|
||||
}
|
||||
|
||||
const ciphertext =
|
||||
pointerEvent && typeof pointerEvent.content === "string"
|
||||
? pointerEvent.content.trim()
|
||||
: "";
|
||||
|
||||
if (!ciphertext) {
|
||||
return { version: 0, items: extractPointerItemsFromEvent(pointerEvent) };
|
||||
}
|
||||
|
||||
const normalizedLogged =
|
||||
typeof this.pubkey === "string" ? this.pubkey.toLowerCase() : "";
|
||||
|
||||
if (
|
||||
normalizedActor &&
|
||||
normalizedActor === normalizedLogged &&
|
||||
window?.nostr?.nip04?.decrypt
|
||||
) {
|
||||
try {
|
||||
const plaintext = await window.nostr.nip04.decrypt(
|
||||
normalizedActor,
|
||||
ciphertext
|
||||
);
|
||||
const parsed = parseWatchHistoryPayload(plaintext);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Failed to decrypt watch history with extension:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!this.sessionActor || this.sessionActor.pubkey !== normalizedActor) {
|
||||
await this.ensureSessionActor(true);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Failed to ensure session actor while decrypting watch history:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const privateKey = this.sessionActor?.privateKey;
|
||||
if (privateKey && window?.NostrTools?.nip04?.decrypt) {
|
||||
try {
|
||||
const plaintext = await window.NostrTools.nip04.decrypt(
|
||||
privateKey,
|
||||
normalizedActor,
|
||||
ciphertext
|
||||
);
|
||||
const parsed = parseWatchHistoryPayload(plaintext);
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Failed to decrypt watch history with session key:",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { version: 0, items: extractPointerItemsFromEvent(pointerEvent) };
|
||||
}
|
||||
|
||||
cancelWatchHistoryRepublish(actor) {
|
||||
const key =
|
||||
typeof actor === "string" && actor.trim()
|
||||
@@ -1037,20 +1241,37 @@ class NostrClient {
|
||||
? normalizedItems.slice(0, WATCH_HISTORY_MAX_ITEMS)
|
||||
: normalizedItems;
|
||||
|
||||
const tags = [["d", WATCH_HISTORY_LIST_IDENTIFIER]];
|
||||
for (const item of trimmedItems) {
|
||||
const tag = pointerToTag(item);
|
||||
if (tag) {
|
||||
tags.push(tag);
|
||||
const payloadItems = trimmedItems.map((item) => ({
|
||||
type: item.type,
|
||||
value: item.value,
|
||||
relay: item.relay || null,
|
||||
}));
|
||||
|
||||
const encryptionResult = await this.encryptWatchHistoryPayload(
|
||||
normalizedActor,
|
||||
{ version: 1, items: payloadItems }
|
||||
);
|
||||
if (!encryptionResult.ok) {
|
||||
if (isDevMode) {
|
||||
console.warn(
|
||||
"[nostr] Unable to encrypt watch history snapshot:",
|
||||
encryptionResult.error
|
||||
);
|
||||
}
|
||||
return encryptionResult;
|
||||
}
|
||||
|
||||
const tags = [
|
||||
["d", WATCH_HISTORY_LIST_IDENTIFIER],
|
||||
["encrypted", "nip04"],
|
||||
];
|
||||
|
||||
const event = {
|
||||
kind: WATCH_HISTORY_KIND,
|
||||
pubkey: normalizedActor,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags,
|
||||
content: "",
|
||||
content: encryptionResult.ciphertext,
|
||||
};
|
||||
|
||||
let signedEvent;
|
||||
@@ -1518,8 +1739,11 @@ class NostrClient {
|
||||
}
|
||||
|
||||
const previousEntry = this.watchHistoryCache.get(actor) || null;
|
||||
const remoteItems = pointerEvent
|
||||
? extractPointerItemsFromEvent(pointerEvent)
|
||||
const decryptedPayload = pointerEvent
|
||||
? await this.decryptWatchHistoryEvent(pointerEvent, actor)
|
||||
: { version: 0, items: [] };
|
||||
const remoteItems = Array.isArray(decryptedPayload.items)
|
||||
? decryptedPayload.items
|
||||
: [];
|
||||
|
||||
let items = remoteItems;
|
||||
@@ -1535,7 +1759,11 @@ class NostrClient {
|
||||
pointerForEntry = storedPointerEvent;
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
const shouldFallback =
|
||||
(!pointerEvent && !items.length) ||
|
||||
(pointerEvent && decryptedPayload.version === 0 && !items.length);
|
||||
|
||||
if (shouldFallback) {
|
||||
if (previousEntry?.items?.length) {
|
||||
items = previousEntry.items;
|
||||
usedFallback = true;
|
||||
|
||||
Reference in new issue
Block a user