mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 21:02:28 +00:00
update with block list fix
This commit is contained in:
1 parent
5d4a28ef3a
commit
aac87db4eb
3 files changed
+266
-11
No files matched your search
+44
-8
@@ -1214,10 +1214,10 @@ class UserBlockListManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const applyEvents = async (
|
||||
events,
|
||||
{ skipIfEmpty = false, source = "fast" } = {},
|
||||
) => {
|
||||
const applyEvents = async (
|
||||
events,
|
||||
{ skipIfEmpty = false, source = "fast" } = {},
|
||||
) => {
|
||||
const previousState = {
|
||||
blockedPubkeys: new Set(this.blockedPubkeys),
|
||||
privateBlocks: new Set(this._privateBlocks),
|
||||
@@ -1302,15 +1302,43 @@ class UserBlockListManager {
|
||||
this.blockEventId = newestOverall?.id || null;
|
||||
this.blockEventCreatedAt = newestCreatedAt;
|
||||
|
||||
const looksLikeJsonPayload = (value) => {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
const first = trimmed[0];
|
||||
return first === "[" || first === "{";
|
||||
};
|
||||
|
||||
// Helper to decrypt a single event
|
||||
const decryptEvent = async (ev, meta = {}) => {
|
||||
if (!ev) return [];
|
||||
const isContentEmpty = !ev.content || !ev.content.trim();
|
||||
const trimmedContent =
|
||||
typeof ev.content === "string" ? ev.content.trim() : "";
|
||||
const isContentEmpty = !trimmedContent;
|
||||
|
||||
if (isContentEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const hasEncryptionHints = extractEncryptionHints(ev).length > 0;
|
||||
const isLegacyBlockFormat = isTaggedBlockListEvent(ev) || ev.kind === 30002;
|
||||
const optionalDecrypt = !hasEncryptionHints && !isLegacyBlockFormat;
|
||||
|
||||
if (
|
||||
!hasEncryptionHints &&
|
||||
!isLegacyBlockFormat &&
|
||||
looksLikeJsonPayload(trimmedContent)
|
||||
) {
|
||||
return parseBlockListPlaintext(trimmedContent, normalized, {
|
||||
limit: MAX_BLOCKLIST_ENTRIES,
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptors, order, sources, permissionError, signerStatus } =
|
||||
await resolveDecryptors(ev);
|
||||
meta.signerStatus = signerStatus;
|
||||
@@ -1319,6 +1347,12 @@ class UserBlockListManager {
|
||||
);
|
||||
|
||||
if (!decryptors.size) {
|
||||
if (optionalDecrypt) {
|
||||
// NIP-51 allows descriptive plaintext in content for standard
|
||||
// mute lists. If decryptors are unavailable and encryption was
|
||||
// not explicitly indicated, keep public tags and continue.
|
||||
return [];
|
||||
}
|
||||
if (permissionError) {
|
||||
throw permissionError;
|
||||
}
|
||||
@@ -1436,12 +1470,14 @@ class UserBlockListManager {
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
// Only apply the newest list to avoid zombie data from stale lists
|
||||
if (newestOverall === newestStandard && newestStandard) {
|
||||
// Merge the newest standard mute snapshot with legacy/private lists so
|
||||
// users retain full cross-client compatibility during migration.
|
||||
if (newestStandard) {
|
||||
const publicTags = extractPubkeysFromTags(newestStandard.tags, normalized, { limit: MAX_BLOCKLIST_ENTRIES });
|
||||
publicTags.forEach((pk) => nextPublicMutes.add(pk));
|
||||
standardDecrypted.forEach((pk) => nextPrivateBlocks.add(pk));
|
||||
} else if (newestLegacy) {
|
||||
}
|
||||
if (newestLegacy) {
|
||||
legacyDecrypted.forEach((pk) => nextPrivateBlocks.add(pk));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import nostrService from "../js/services/nostrService.js";
|
||||
import { userBlocks } from "../js/userBlocks.js";
|
||||
import { nostrClient } from "../js/nostrClientFacade.js";
|
||||
import { relayManager } from "../js/relayManager.js";
|
||||
import {
|
||||
BLOCK_LIST_IDENTIFIER,
|
||||
KIND_MUTE_LIST,
|
||||
} from "../js/nostrEventSchemas.js";
|
||||
|
||||
const ACTOR_PUBKEY = "aaaa".repeat(16);
|
||||
const BLOCKED_PUBKEY = "bbbb".repeat(16);
|
||||
@@ -170,4 +178,150 @@ describe("DM block/mute filtering", () => {
|
||||
assert.equal(nostrService._isDmRemoteBlocked(message), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration with loaded user block list", () => {
|
||||
it("should block DM contact for pubkeys loaded from both standard and legacy lists", async () => {
|
||||
const actor = ACTOR_PUBKEY;
|
||||
const legacyBlockedPubkey = "dddd".repeat(16);
|
||||
const standardBlockedPubkey = "eeee".repeat(16);
|
||||
|
||||
const UserBlockListManager = userBlocks.constructor;
|
||||
const manager = new UserBlockListManager();
|
||||
|
||||
const originalNostr = globalThis.window?.nostr;
|
||||
const originalRelays = Array.isArray(nostrClient.relays)
|
||||
? [...nostrClient.relays]
|
||||
: nostrClient.relays;
|
||||
const originalFetchListIncrementally = nostrClient.fetchListIncrementally;
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
|
||||
const relayUrls = ["wss://dm-block-regression.example"];
|
||||
relayManager.setEntries(
|
||||
relayUrls.map((url) => ({ url, mode: "both" })),
|
||||
{ allowEmpty: false, updateClient: false },
|
||||
);
|
||||
nostrClient.relays = relayUrls;
|
||||
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
}
|
||||
|
||||
globalThis.window.nostr = {
|
||||
...(originalNostr || {}),
|
||||
nip04: {
|
||||
...((originalNostr && originalNostr.nip04) || {}),
|
||||
decrypt: async () =>
|
||||
JSON.stringify({
|
||||
blockedPubkeys: [legacyBlockedPubkey],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const standardEvent = {
|
||||
id: "event-standard-mute",
|
||||
kind: KIND_MUTE_LIST,
|
||||
created_at: 2_000,
|
||||
pubkey: actor,
|
||||
content: "",
|
||||
tags: [["p", standardBlockedPubkey]],
|
||||
};
|
||||
|
||||
const legacyEvent = {
|
||||
id: "event-legacy-blocks",
|
||||
kind: KIND_MUTE_LIST,
|
||||
created_at: 1_000,
|
||||
pubkey: actor,
|
||||
content: "legacy-encrypted",
|
||||
tags: [["d", BLOCK_LIST_IDENTIFIER]],
|
||||
};
|
||||
|
||||
nostrClient.fetchListIncrementally = async ({
|
||||
kind,
|
||||
dTag,
|
||||
} = {}) => {
|
||||
if (kind === KIND_MUTE_LIST && dTag === BLOCK_LIST_IDENTIFIER) {
|
||||
return [legacyEvent];
|
||||
}
|
||||
if (kind === KIND_MUTE_LIST && !dTag) {
|
||||
return [standardEvent];
|
||||
}
|
||||
if (kind === 30002 && dTag === BLOCK_LIST_IDENTIFIER) {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
try {
|
||||
await manager.loadBlocks(actor, { allowPermissionPrompt: false });
|
||||
|
||||
const waitForMergedBlocks = async (timeoutMs = 250) => {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
if (
|
||||
manager.isBlocked(legacyBlockedPubkey) &&
|
||||
manager.isBlocked(standardBlockedPubkey)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
};
|
||||
await waitForMergedBlocks();
|
||||
|
||||
nostrService.setDmBlockChecker((pubkey) => manager.isBlocked(pubkey));
|
||||
|
||||
nostrService.applyDirectMessage(
|
||||
makeDmMessage({
|
||||
senderPubkey: legacyBlockedPubkey,
|
||||
receiverPubkey: actor,
|
||||
eventId: "blocked-legacy",
|
||||
direction: "incoming",
|
||||
}),
|
||||
{ reason: "integration-test" },
|
||||
);
|
||||
|
||||
nostrService.applyDirectMessage(
|
||||
makeDmMessage({
|
||||
senderPubkey: standardBlockedPubkey,
|
||||
receiverPubkey: actor,
|
||||
eventId: "blocked-standard",
|
||||
direction: "incoming",
|
||||
}),
|
||||
{ reason: "integration-test" },
|
||||
);
|
||||
|
||||
nostrService.applyDirectMessage(
|
||||
makeDmMessage({
|
||||
senderPubkey: ALLOWED_PUBKEY,
|
||||
receiverPubkey: actor,
|
||||
eventId: "allowed-contact",
|
||||
direction: "incoming",
|
||||
}),
|
||||
{ reason: "integration-test" },
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
nostrService.dmMessages.length,
|
||||
1,
|
||||
"only non-blocked contacts should remain in DM message state",
|
||||
);
|
||||
assert.equal(
|
||||
nostrService.dmMessages[0]?.remotePubkey,
|
||||
ALLOWED_PUBKEY,
|
||||
"allowed contact should pass block filter",
|
||||
);
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, {
|
||||
allowEmpty: false,
|
||||
updateClient: false,
|
||||
});
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.fetchListIncrementally = originalFetchListIncrementally;
|
||||
if (typeof globalThis.window === "undefined") {
|
||||
globalThis.window = {};
|
||||
}
|
||||
globalThis.window.nostr = originalNostr;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1333,7 +1333,7 @@ await (async () => {
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
// Test: Merge 'd=user-blocks' tagged lists with plain lists
|
||||
// Test: Merge legacy tagged/private lists with standard mute lists
|
||||
const actor = "a".repeat(64);
|
||||
const taggedBlocked = "b".repeat(64);
|
||||
const plainBlocked = "c".repeat(64);
|
||||
@@ -1386,8 +1386,14 @@ await (async () => {
|
||||
await userBlocks.loadBlocks(actor);
|
||||
|
||||
const blocked = userBlocks.getBlockedPubkeys();
|
||||
assert.ok(!blocked.includes(taggedBlocked), "Should NOT include pubkey from older tagged list (superseded)");
|
||||
assert.ok(blocked.includes(plainBlocked), "Should include pubkey from newer plain list");
|
||||
assert.ok(
|
||||
blocked.includes(taggedBlocked),
|
||||
"should include pubkey from legacy tagged/private list",
|
||||
);
|
||||
assert.ok(
|
||||
blocked.includes(plainBlocked),
|
||||
"should include pubkey from standard mute list",
|
||||
);
|
||||
} finally {
|
||||
relayManager.setEntries(originalRelayEntries, { allowEmpty: false, updateClient: false });
|
||||
nostrClient.relays = originalRelays;
|
||||
@@ -1395,3 +1401,62 @@ await (async () => {
|
||||
window.nostr = originalNostr;
|
||||
}
|
||||
})();
|
||||
|
||||
await (async () => {
|
||||
// Test: standard mute lists may contain non-encrypted text content
|
||||
const actor = "d".repeat(64);
|
||||
const blockedHex = "e".repeat(64);
|
||||
|
||||
const UserBlockListManager = userBlocks.constructor;
|
||||
const manager = new UserBlockListManager();
|
||||
|
||||
const originalRelays = Array.isArray(nostrClient.relays)
|
||||
? [...nostrClient.relays]
|
||||
: nostrClient.relays;
|
||||
const originalPool = nostrClient.pool;
|
||||
const originalNostr = window.nostr;
|
||||
|
||||
const relayUrls = ["wss://plain-content-mute.example"];
|
||||
const originalRelayEntries = relayManager.getEntries();
|
||||
relayManager.setEntries(
|
||||
relayUrls.map((url) => ({ url, mode: "both" })),
|
||||
{ allowEmpty: false, updateClient: false },
|
||||
);
|
||||
|
||||
window.nostr = {
|
||||
...(originalNostr || {}),
|
||||
nip04: undefined,
|
||||
nip44: undefined,
|
||||
};
|
||||
|
||||
nostrClient.relays = relayUrls;
|
||||
nostrClient.pool = {
|
||||
list: async () => [
|
||||
{
|
||||
id: "event-standard-note",
|
||||
kind: 10000,
|
||||
created_at: 13_000,
|
||||
pubkey: actor,
|
||||
content: "mute list synced from another client",
|
||||
tags: [["p", blockedHex]],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
try {
|
||||
await manager.loadBlocks(actor, { allowPermissionPrompt: false });
|
||||
assert.equal(
|
||||
manager.blockedPubkeys.has(blockedHex),
|
||||
true,
|
||||
"non-encrypted content should not block p-tag mute ingestion",
|
||||
);
|
||||
} finally {
|
||||
relayManager.setEntries(
|
||||
originalRelayEntries,
|
||||
{ allowEmpty: false, updateClient: false },
|
||||
);
|
||||
nostrClient.relays = originalRelays;
|
||||
nostrClient.pool = originalPool;
|
||||
window.nostr = originalNostr;
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user