From ceb2eb28ad658976257db4e8c477c13e28e35515 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:42:53 -0400 Subject: [PATCH] Fix Nostr manifest handling --- src/main.py | 9 +++-- src/seedpass/core/manager.py | 24 +++++++++++--- src/tests/test_nostr_restore_flow.py | 49 ++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/tests/test_nostr_restore_flow.py diff --git a/src/main.py b/src/main.py index 1fa2482..37981ef 100644 --- a/src/main.py +++ b/src/main.py @@ -406,6 +406,7 @@ def handle_retrieve_from_nostr(password_manager: PasswordManager): Handles the action of retrieving the encrypted password index from Nostr. """ try: + password_manager.nostr_client.fingerprint = password_manager.current_fingerprint result = asyncio.run(password_manager.nostr_client.fetch_latest_snapshot()) if result: manifest, chunks = result @@ -423,8 +424,12 @@ def handle_retrieve_from_nostr(password_manager: PasswordManager): print(colored("Encrypted index retrieved and saved successfully.", "green")) logging.info("Encrypted index retrieved and saved successfully from Nostr.") else: - print(colored("Failed to retrieve data from Nostr.", "red")) - logging.error("Failed to retrieve data from Nostr.") + msg = ( + f"No Nostr events found for fingerprint" + f" {password_manager.current_fingerprint}." + ) + print(colored(msg, "red")) + logging.error(msg) except Exception as e: logging.error(f"Failed to retrieve from Nostr: {e}", exc_info=True) print(colored(f"Error: Failed to retrieve from Nostr: {e}", "red")) diff --git a/src/seedpass/core/manager.py b/src/seedpass/core/manager.py index 7205e03..ebe832c 100644 --- a/src/seedpass/core/manager.py +++ b/src/seedpass/core/manager.py @@ -95,7 +95,7 @@ from datetime import datetime from utils.fingerprint_manager import FingerprintManager # Import NostrClient -from nostr.client import NostrClient, DEFAULT_RELAYS +from nostr.client import NostrClient, DEFAULT_RELAYS, MANIFEST_ID_PREFIX from .config_manager import ConfigManager from .state_manager import StateManager @@ -272,6 +272,8 @@ class PasswordManager: def notify(self, message: str, level: str = "INFO") -> None: """Enqueue a notification and set it as the active message.""" note = Notification(message, level) + if not hasattr(self, "notifications"): + self.notifications = queue.Queue() self.notifications.put(note) self._current_notification = note self._notification_expiry = time.time() + NOTIFICATION_DURATION @@ -605,6 +607,8 @@ class PasswordManager: selected_fingerprint = fingerprints[int(choice) - 1] self.fingerprint_manager.current_fingerprint = selected_fingerprint self.current_fingerprint = selected_fingerprint + if not getattr(self, "manifest_id", None): + self.manifest_id = f"{MANIFEST_ID_PREFIX}{selected_fingerprint}" # Update fingerprint directory self.fingerprint_dir = ( @@ -645,7 +649,9 @@ class PasswordManager: config_manager=getattr(self, "config_manager", None), parent_seed=getattr(self, "parent_seed", None), ) - if getattr(self, "manifest_id", None): + if getattr(self, "manifest_id", None) and hasattr( + self.nostr_client, "_state_lock" + ): from nostr.backup_models import Manifest with self.nostr_client._state_lock: @@ -903,6 +909,8 @@ class PasswordManager: self.current_fingerprint = fingerprint self.fingerprint_manager.current_fingerprint = fingerprint self.fingerprint_dir = fingerprint_dir + if not getattr(self, "manifest_id", None): + self.manifest_id = f"{MANIFEST_ID_PREFIX}{fingerprint}" logging.info(f"Current seed profile set to {fingerprint}") try: @@ -1174,7 +1182,9 @@ class PasswordManager: parent_seed=getattr(self, "parent_seed", None), ) - if getattr(self, "manifest_id", None): + if getattr(self, "manifest_id", None) and hasattr( + self.nostr_client, "_state_lock" + ): from nostr.backup_models import Manifest with self.nostr_client._state_lock: @@ -1197,6 +1207,8 @@ class PasswordManager: """Always fetch the latest vault data from Nostr and update the local index.""" start = time.perf_counter() try: + if getattr(self, "current_fingerprint", None): + self.nostr_client.fingerprint = self.current_fingerprint result = await self.nostr_client.fetch_latest_snapshot() if not result: if self.nostr_client.last_error: @@ -1346,6 +1358,8 @@ class PasswordManager: have_data = False start = time.perf_counter() try: + if getattr(self, "current_fingerprint", None): + self.nostr_client.fingerprint = self.current_fingerprint result = await self.nostr_client.fetch_latest_snapshot() if result: manifest, chunks = result @@ -4308,7 +4322,9 @@ class PasswordManager: parent_seed=getattr(self, "parent_seed", None), ) - if getattr(self, "manifest_id", None): + if getattr(self, "manifest_id", None) and hasattr( + self.nostr_client, "_state_lock" + ): from nostr.backup_models import Manifest with self.nostr_client._state_lock: diff --git a/src/tests/test_nostr_restore_flow.py b/src/tests/test_nostr_restore_flow.py new file mode 100644 index 0000000..759f6f8 --- /dev/null +++ b/src/tests/test_nostr_restore_flow.py @@ -0,0 +1,49 @@ +from pathlib import Path + +import main +from helpers import create_vault, dummy_nostr_client, TEST_SEED, TEST_PASSWORD +from seedpass.core.entry_management import EntryManager +from seedpass.core.backup import BackupManager +from seedpass.core.config_manager import ConfigManager +from seedpass.core.manager import PasswordManager, EncryptionMode + + +def _init_pm(dir_path: Path, client) -> PasswordManager: + vault, enc_mgr = create_vault(dir_path, TEST_SEED, TEST_PASSWORD) + cfg_mgr = ConfigManager(vault, dir_path) + backup_mgr = BackupManager(dir_path, cfg_mgr) + entry_mgr = EntryManager(vault, backup_mgr) + + pm = PasswordManager.__new__(PasswordManager) + pm.encryption_mode = EncryptionMode.SEED_ONLY + pm.encryption_manager = enc_mgr + pm.vault = vault + pm.entry_manager = entry_mgr + pm.backup_manager = backup_mgr + pm.config_manager = cfg_mgr + pm.nostr_client = client + pm.fingerprint_dir = dir_path + pm.current_fingerprint = "fp" + pm.is_dirty = False + return pm + + +def test_restore_flow_from_snapshot(monkeypatch, tmp_path): + client, relay = dummy_nostr_client.__wrapped__(tmp_path / "srv", monkeypatch) + + dir_a = tmp_path / "A" + dir_b = tmp_path / "B" + dir_a.mkdir() + dir_b.mkdir() + + pm_a = _init_pm(dir_a, client) + pm_a.entry_manager.add_entry("site1", 12) + pm_a.sync_vault() + assert relay.manifests + + pm_b = _init_pm(dir_b, client) + monkeypatch.setattr(main, "pause", lambda *a, **k: None) + main.handle_retrieve_from_nostr(pm_b) + + labels = [e[1] for e in pm_b.entry_manager.list_entries()] + assert labels == ["site1"]