mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge branch 'unstable' into refactor/remove-execCommand-9205199539301717530
This commit is contained in:
7 files changed
+209
-77
No files matched your search
+4
-4
@@ -15,11 +15,13 @@
|
||||
| **NIP-46** | Nostr Remote Signing | `js/nostr/nip46Client.js` | Compliant | Full client implementation for remote signing. |
|
||||
| **NIP-51** | Lists | `js/userBlocks.js`, `js/subscriptions.js` | Compliant | Mute list (Kind 10000) and Subscription list (Kind 30000) logic aligns with spec. |
|
||||
| **NIP-56** | Reporting | `js/services/moderationService.js`, `docs/nips/56.md` | Compliant | `buildReportEvent` implements Kind 1984. |
|
||||
| **NIP-57** | Zaps | `js/payments/zapRequests.js`, `js/payments/zapSplit.js` | Compliant | Implements Kind 9734/9735 flow. |
|
||||
| **NIP-59** | Gift Wrap | `js/nostr/client.js` | Compliant | Implemented as part of NIP-17. |
|
||||
| **NIP-65** | Relay List Metadata | `js/relayManager.js` | Compliant | Verified Kind 10002 loading and parsing in `nip65_compliance.test.mjs`. |
|
||||
| **NIP-71** | Video Events | `js/nostr/nip71.js` | Compliant | Verified tag generation for Kind 30078 in `tests/nostr-specs/kind30078.test.mjs`. |
|
||||
| **NIP-65** | Relay List Metadata | `js/relayManager.js` | Partial | Code supports Kind 10002. Test `nip65_compliance.test.mjs` is missing. |
|
||||
| **NIP-71** | Video Events | `js/nostr/nip71.js` | Partial | Kinds 21/22 supported in schema, but Kind 30078 is primary. Verified tag generation in `tests/nostr-specs/kind30078.test.mjs`. |
|
||||
| **NIP-78** | Application-specific data | `js/services/hashtagPreferencesService.js` | Compliant | Used for hashtag preferences (Kind 30015). |
|
||||
| **NIP-94** | File Metadata | `js/nostr/videoPublisher.js` | Compliant | `buildVideoMirrorEvent` implements Kind 1063. |
|
||||
| **NIP-96** | HTTP File Storage Integration | `js/nostr/videoPublisher.js` | Partial | References `service nip96` in NIP-71 tags. |
|
||||
| **NIP-98** | HTTP Auth | `js/services/r2Service.js` | Compliant | Used for R2 service authentication (Kind 27235). |
|
||||
| **Kind 21** | NIP-71 Video | `js/nostr/nip71.js` | Compliant | Implemented. |
|
||||
| **Kind 22** | NIP-71 Short Video | `js/nostr/nip71.js` | Compliant | Implemented. |
|
||||
@@ -33,5 +35,3 @@
|
||||
| **NIP-25** | Reactions | `js/nostr/reactionEvents.js`, `js/nostrEventSchemas.js` | Compliant | Implements Kind 7 reaction events. |
|
||||
| **NIP-42** | Authentication of clients to relays | `js/nostr/client.js` | Unknown | Needs verification of AUTH command handling. |
|
||||
| **NIP-47** | Wallet Connect | `js/payments/nwcClient.js` | Compliant | Implements NWC client for zaps. |
|
||||
| **NIP-57** | Zaps | `js/payments/zapRequests.js`, `js/payments/zapSplit.js` | Compliant | Implements Kind 9734/9735 flow. |
|
||||
| **NIP-96** | HTTP File Storage Integration | `js/nostr/videoPublisher.js` | Partial | References `service nip96` in NIP-71 tags. |
|
||||
@@ -0,0 +1 @@
|
||||
Completed daily NIP research task. Updated NIP_INVENTORY.md with new findings (NIP-65 missing test, NIP-71/30078 usage) and generated ai/reports/nip-report-2026-02-17.md.
|
||||
@@ -25,8 +25,6 @@ import {
|
||||
DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
|
||||
FEED_TYPES,
|
||||
} from "./constants.js";
|
||||
import { attachHealthBadges } from "./gridHealth.js";
|
||||
import { attachUrlHealthBadges } from "./urlHealthObserver.js";
|
||||
import { updateVideoCardSourceVisibility } from "./utils/cardSourceVisibility.js";
|
||||
import { collectVideoTags } from "./utils/videoTags.js";
|
||||
import { normalizeHashtag } from "./utils/hashtagNormalization.js";
|
||||
|
||||
+69
-67
@@ -2889,80 +2889,82 @@ export class NostrClient {
|
||||
const revertSummaries = [];
|
||||
const revertEvents = [];
|
||||
|
||||
for (const vid of matchingEvents.values()) {
|
||||
const baseRoot =
|
||||
(typeof vid.videoRootId === "string" && vid.videoRootId) ||
|
||||
inferredRoot ||
|
||||
(targetVideo && typeof targetVideo.videoRootId === "string"
|
||||
? targetVideo.videoRootId
|
||||
: "") ||
|
||||
(targetVideo ? targetVideo.id : "") ||
|
||||
vid.id;
|
||||
await Promise.all(
|
||||
Array.from(matchingEvents.values()).map(async (vid) => {
|
||||
const baseRoot =
|
||||
(typeof vid.videoRootId === "string" && vid.videoRootId) ||
|
||||
inferredRoot ||
|
||||
(targetVideo && typeof targetVideo.videoRootId === "string"
|
||||
? targetVideo.videoRootId
|
||||
: "") ||
|
||||
(targetVideo ? targetVideo.id : "") ||
|
||||
vid.id;
|
||||
|
||||
const contentPayload = {
|
||||
version: Number.isFinite(vid.version) ? vid.version : 3,
|
||||
deleted: true,
|
||||
isPrivate: vid.isPrivate === true,
|
||||
isNsfw: vid.isNsfw === true,
|
||||
isForKids: vid.isForKids === true && vid.isNsfw !== true,
|
||||
title: typeof vid.title === "string" ? vid.title : "",
|
||||
url: typeof vid.url === "string" ? vid.url : "",
|
||||
magnet: typeof vid.magnet === "string" ? vid.magnet : "",
|
||||
thumbnail: typeof vid.thumbnail === "string" ? vid.thumbnail : "",
|
||||
description: typeof vid.description === "string" ? vid.description : "",
|
||||
mode: typeof vid.mode === "string" ? vid.mode : "live",
|
||||
videoRootId: baseRoot,
|
||||
};
|
||||
const contentPayload = {
|
||||
version: Number.isFinite(vid.version) ? vid.version : 3,
|
||||
deleted: true,
|
||||
isPrivate: vid.isPrivate === true,
|
||||
isNsfw: vid.isNsfw === true,
|
||||
isForKids: vid.isForKids === true && vid.isNsfw !== true,
|
||||
title: typeof vid.title === "string" ? vid.title : "",
|
||||
url: typeof vid.url === "string" ? vid.url : "",
|
||||
magnet: typeof vid.magnet === "string" ? vid.magnet : "",
|
||||
thumbnail: typeof vid.thumbnail === "string" ? vid.thumbnail : "",
|
||||
description: typeof vid.description === "string" ? vid.description : "",
|
||||
mode: typeof vid.mode === "string" ? vid.mode : "live",
|
||||
videoRootId: baseRoot,
|
||||
};
|
||||
|
||||
const revertResult = await this.revertVideo(
|
||||
{
|
||||
id: vid.id,
|
||||
pubkey: vid.pubkey,
|
||||
content: JSON.stringify(contentPayload),
|
||||
tags: Array.isArray(vid.tags) ? vid.tags : [],
|
||||
},
|
||||
pubkey
|
||||
);
|
||||
const revertResult = await this.revertVideo(
|
||||
{
|
||||
id: vid.id,
|
||||
pubkey: vid.pubkey,
|
||||
content: JSON.stringify(contentPayload),
|
||||
tags: Array.isArray(vid.tags) ? vid.tags : [],
|
||||
},
|
||||
pubkey
|
||||
);
|
||||
|
||||
const revertEvent = revertResult?.event || null;
|
||||
const revertSummary =
|
||||
revertResult?.summary ||
|
||||
summarizePublishResults(revertResult?.publishResults || []);
|
||||
const revertPublishResults = Array.isArray(revertResult?.publishResults)
|
||||
? revertResult.publishResults
|
||||
: [];
|
||||
const revertEvent = revertResult?.event || null;
|
||||
const revertSummary =
|
||||
revertResult?.summary ||
|
||||
summarizePublishResults(revertResult?.publishResults || []);
|
||||
const revertPublishResults = Array.isArray(revertResult?.publishResults)
|
||||
? revertResult.publishResults
|
||||
: [];
|
||||
|
||||
revertSummaries.push({
|
||||
targetId: vid.id || "",
|
||||
event: revertEvent,
|
||||
publishResults: revertPublishResults,
|
||||
summary: revertSummary,
|
||||
});
|
||||
revertSummaries.push({
|
||||
targetId: vid.id || "",
|
||||
event: revertEvent,
|
||||
publishResults: revertPublishResults,
|
||||
summary: revertSummary,
|
||||
});
|
||||
|
||||
if (revertEvent?.id) {
|
||||
revertEvents.push(revertEvent);
|
||||
this.rawEvents.set(revertEvent.id, revertEvent);
|
||||
}
|
||||
if (revertEvent?.id) {
|
||||
revertEvents.push(revertEvent);
|
||||
this.rawEvents.set(revertEvent.id, revertEvent);
|
||||
}
|
||||
|
||||
const cached = this.allEvents.get(vid.id) || vid;
|
||||
cached.deleted = true;
|
||||
cached.url = "";
|
||||
cached.magnet = "";
|
||||
cached.thumbnail = "";
|
||||
cached.description = "This version was deleted by the creator.";
|
||||
cached.videoRootId = baseRoot;
|
||||
this.allEvents.set(vid.id, cached);
|
||||
this.dirtyEventIds.add(vid.id);
|
||||
const cached = this.allEvents.get(vid.id) || vid;
|
||||
cached.deleted = true;
|
||||
cached.url = "";
|
||||
cached.magnet = "";
|
||||
cached.thumbnail = "";
|
||||
cached.description = "This version was deleted by the creator.";
|
||||
cached.videoRootId = baseRoot;
|
||||
this.allEvents.set(vid.id, cached);
|
||||
this.dirtyEventIds.add(vid.id);
|
||||
|
||||
const activeKey = getActiveKey(cached);
|
||||
if (activeKey) {
|
||||
this.activeMap.delete(activeKey);
|
||||
const revertCreatedAt = Number.isFinite(revertEvent?.created_at)
|
||||
? Math.floor(revertEvent.created_at)
|
||||
: Math.floor(Date.now() / 1000);
|
||||
this.recordTombstone(activeKey, revertCreatedAt);
|
||||
}
|
||||
}
|
||||
const activeKey = getActiveKey(cached);
|
||||
if (activeKey) {
|
||||
this.activeMap.delete(activeKey);
|
||||
const revertCreatedAt = Number.isFinite(revertEvent?.created_at)
|
||||
? Math.floor(revertEvent.created_at)
|
||||
: Math.floor(Date.now() / 1000);
|
||||
this.recordTombstone(activeKey, revertCreatedAt);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return { revertSummaries, revertEvents };
|
||||
}
|
||||
|
||||
+1
-2
@@ -5,8 +5,7 @@ import {
|
||||
requestDefaultExtensionPermissions,
|
||||
} from "./nostrClientFacade.js";
|
||||
import { getActiveSigner } from "./nostr/index.js";
|
||||
import { buildRelayListEvent, NOTE_TYPES } from "./nostrEventSchemas.js";
|
||||
import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js";
|
||||
import { buildRelayListEvent } from "./nostrEventSchemas.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
import {
|
||||
publishEventToRelays,
|
||||
|
||||
@@ -5,14 +5,12 @@ import {
|
||||
} from "./nostrClientFacade.js";
|
||||
import { getActiveSigner } from "./nostr/index.js";
|
||||
import { isSessionActor } from "./nostr/sessionActor.js";
|
||||
import { normalizeNostrPubkey } from "./nostr/nip46Client.js";
|
||||
import {
|
||||
buildBlockListEvent,
|
||||
buildMuteListEvent,
|
||||
BLOCK_LIST_IDENTIFIER,
|
||||
KIND_MUTE_LIST,
|
||||
} from "./nostrEventSchemas.js";
|
||||
import { CACHE_POLICIES, STORAGE_TIERS } from "./nostr/cachePolicies.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
import { STANDARD_TIMEOUT_MS, MAX_BLOCKLIST_ENTRIES } from "./constants.js";
|
||||
import {
|
||||
|
||||
@@ -110,6 +110,18 @@ beforeEach(() => {
|
||||
};
|
||||
global.matchMedia = window.matchMedia;
|
||||
|
||||
// Mock navigator
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: {
|
||||
clipboard: {
|
||||
writeText: async () => {}
|
||||
},
|
||||
userAgent: 'node'
|
||||
},
|
||||
writable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
container = document.getElementById('modalMount');
|
||||
container.innerHTML = loginModalHtml;
|
||||
});
|
||||
@@ -133,6 +145,7 @@ afterEach(() => {
|
||||
delete global.MutationObserver;
|
||||
delete global.requestAnimationFrame;
|
||||
delete global.matchMedia;
|
||||
delete global.navigator;
|
||||
|
||||
if (dom) {
|
||||
dom.window.close();
|
||||
@@ -272,3 +285,124 @@ test('LoginModalController shows NIP-46 handshake panel', async (t) => {
|
||||
assert.ok(handshakePanel, 'handshake panel should be present');
|
||||
assert.ok(!handshakePanel.classList.contains('hidden'), 'handshake panel should not be hidden');
|
||||
});
|
||||
|
||||
test('LoginModalController NIP-46 copy button uses navigator.clipboard', async (t) => {
|
||||
const providers = [{ id: 'nip46', label: 'Remote Signer', login: async () => {} }];
|
||||
|
||||
// Mock auth service to provide URI
|
||||
const authService = {
|
||||
requestLogin: async (opts) => {
|
||||
if (opts.onHandshakePrepared) {
|
||||
// simulate async URI generation
|
||||
setTimeout(() => {
|
||||
opts.onHandshakePrepared({ uri: 'nostr:connect:test' });
|
||||
}, 10);
|
||||
}
|
||||
return new Promise(() => {}); // hang forever (simulating pending login)
|
||||
}
|
||||
};
|
||||
|
||||
controller = createController({ providers, services: { authService } });
|
||||
controller.initialize();
|
||||
|
||||
const providerButton = container.querySelector('[data-provider-id="nip46"]');
|
||||
providerButton.dispatchEvent(new dom.window.Event('click', { bubbles: true }));
|
||||
|
||||
await waitForAnimationFrame(window, 5);
|
||||
// Wait for autoStart and requestLogin response
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await waitForAnimationFrame(window, 5);
|
||||
|
||||
const form = container.querySelector('[data-nip46-form]');
|
||||
assert.ok(form, 'Form should be present');
|
||||
|
||||
const copyButton = form.querySelector('[data-nip46-copy-uri]');
|
||||
const handshakeInput = form.querySelector('[data-nip46-handshake-uri]');
|
||||
|
||||
assert.equal(handshakeInput.value, 'nostr:connect:test', 'Input should have URI');
|
||||
assert.equal(copyButton.disabled, false, 'Copy button should be enabled');
|
||||
|
||||
// Setup clipboard mock verification
|
||||
let writeTextCalled = false;
|
||||
let clipboardText = '';
|
||||
global.navigator.clipboard.writeText = async (text) => {
|
||||
writeTextCalled = true;
|
||||
clipboardText = text;
|
||||
};
|
||||
|
||||
// Spy on execCommand
|
||||
let execCommandCalled = false;
|
||||
document.execCommand = () => { execCommandCalled = true; return false; };
|
||||
|
||||
copyButton.dispatchEvent(new dom.window.Event('click', { bubbles: true }));
|
||||
|
||||
// Wait for async writeText
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
assert.equal(writeTextCalled, true, 'Should use navigator.clipboard');
|
||||
assert.equal(clipboardText, 'nostr:connect:test', 'Should copy correct text');
|
||||
assert.equal(execCommandCalled, false, 'Should NOT use execCommand');
|
||||
assert.equal(copyButton.textContent, 'Copied!', 'Button should show success feedback');
|
||||
});
|
||||
|
||||
test('LoginModalController NIP-46 copy button selects text on failure', async (t) => {
|
||||
const providers = [{ id: 'nip46', label: 'Remote Signer', login: async () => {} }];
|
||||
|
||||
const authService = {
|
||||
requestLogin: async (opts) => {
|
||||
if (opts.onHandshakePrepared) {
|
||||
setTimeout(() => {
|
||||
opts.onHandshakePrepared({ uri: 'nostr:connect:test' });
|
||||
}, 10);
|
||||
}
|
||||
return new Promise(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
controller = createController({ providers, services: { authService } });
|
||||
controller.initialize();
|
||||
|
||||
const providerButton = container.querySelector('[data-provider-id="nip46"]');
|
||||
providerButton.dispatchEvent(new dom.window.Event('click', { bubbles: true }));
|
||||
|
||||
await waitForAnimationFrame(window, 5);
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
await waitForAnimationFrame(window, 5);
|
||||
|
||||
const form = container.querySelector('[data-nip46-form]');
|
||||
const copyButton = form.querySelector('[data-nip46-copy-uri]');
|
||||
const handshakeInput = form.querySelector('[data-nip46-handshake-uri]');
|
||||
|
||||
// Mock clipboard failure
|
||||
global.navigator.clipboard.writeText = async () => {
|
||||
throw new Error('Clipboard error');
|
||||
};
|
||||
|
||||
let execCommandCalled = false;
|
||||
document.execCommand = () => { execCommandCalled = true; return false; };
|
||||
|
||||
let selectCalled = false;
|
||||
// JSDOM input element supports select?
|
||||
if (!handshakeInput.select) {
|
||||
handshakeInput.select = () => { selectCalled = true; };
|
||||
} else {
|
||||
// Wrap it
|
||||
const originalSelect = handshakeInput.select.bind(handshakeInput);
|
||||
handshakeInput.select = () => {
|
||||
selectCalled = true;
|
||||
originalSelect();
|
||||
};
|
||||
}
|
||||
|
||||
copyButton.dispatchEvent(new dom.window.Event('click', { bubbles: true }));
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
assert.equal(selectCalled, true, 'Should select input text on fallback');
|
||||
assert.equal(execCommandCalled, false, 'Should NOT use execCommand on fallback');
|
||||
|
||||
// Check status message
|
||||
const statusNode = form.querySelector('[data-nip46-status]');
|
||||
assert.ok(!statusNode.classList.contains('hidden'), 'Status should be visible');
|
||||
assert.ok(statusNode.textContent.includes('fallback'), 'Status should mention fallback');
|
||||
});
|
||||
Reference in new issue
Block a user