update with block list fix

This commit is contained in:
thePR0M3TH3AN committed 2026-02-21 16:45:16 -05:00
1 parent 5d4a28ef3a
commit aac87db4eb
3 files changed
+266 -11

No files matched your search

+44 -8
View File
@@ -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));
}
+154
View File
@@ -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;
}
});
});
});
+68 -3
View File
@@ -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;
}
})();