Merge pull request #161 from PR0M3TH3AN/codex/display-nostr-event-id-after-push

Display event ID after Nostr sync
This commit is contained in:
thePR0M3TH3AN
2025-07-02 20:15:40 -04:00
committed by GitHub
12 changed files with 44 additions and 28 deletions

View File

@@ -226,9 +226,14 @@ def handle_post_to_nostr(
Handles the action of posting the encrypted password index to Nostr. Handles the action of posting the encrypted password index to Nostr.
""" """
try: try:
success = password_manager.sync_vault(alt_summary=alt_summary) event_id = password_manager.sync_vault(alt_summary=alt_summary)
if success: if event_id:
print(colored("\N{WHITE HEAVY CHECK MARK} Sync complete.", "green")) print(
colored(
f"\N{WHITE HEAVY CHECK MARK} Sync complete. Event ID: {event_id}",
"green",
)
)
logging.info("Encrypted index posted to Nostr successfully.") logging.info("Encrypted index posted to Nostr successfully.")
else: else:
print(colored("\N{CROSS MARK} Sync failed…", "red")) print(colored("\N{CROSS MARK} Sync failed…", "red"))

View File

@@ -142,7 +142,7 @@ class NostrClient:
encrypted_json: bytes, encrypted_json: bytes,
to_pubkey: str | None = None, to_pubkey: str | None = None,
alt_summary: str | None = None, alt_summary: str | None = None,
) -> bool: ) -> str | None:
"""Builds and publishes a Kind 1 text note or direct message. """Builds and publishes a Kind 1 text note or direct message.
Parameters Parameters
@@ -177,12 +177,12 @@ class NostrClient:
else str(event_output) else str(event_output)
) )
logger.info(f"Successfully published event with ID: {event_id_hex}") logger.info(f"Successfully published event with ID: {event_id_hex}")
return True return event_id_hex
except Exception as e: except Exception as e:
self.last_error = str(e) self.last_error = str(e)
logger.error(f"Failed to publish JSON to Nostr: {e}") logger.error(f"Failed to publish JSON to Nostr: {e}")
return False return None
def publish_event(self, event): def publish_event(self, event):
"""Publish a prepared event to the configured relays.""" """Publish a prepared event to the configured relays."""
@@ -242,7 +242,7 @@ class NostrClient:
async def publish_snapshot( async def publish_snapshot(
self, encrypted_bytes: bytes, limit: int = 50_000 self, encrypted_bytes: bytes, limit: int = 50_000
) -> Manifest: ) -> tuple[Manifest, str]:
"""Publish a compressed snapshot split into chunks. """Publish a compressed snapshot split into chunks.
Parameters Parameters
@@ -276,10 +276,11 @@ class NostrClient:
.build(self.keys.public_key()) .build(self.keys.public_key())
.sign_with_keys(self.keys) .sign_with_keys(self.keys)
) )
await self.client.send_event(manifest_event) result = await self.client.send_event(manifest_event)
manifest_id = result.id.to_hex() if hasattr(result, "id") else str(result)
self.current_manifest = manifest self.current_manifest = manifest
self._delta_events = [] self._delta_events = []
return manifest return manifest, manifest_id
async def fetch_latest_snapshot(self) -> Tuple[Manifest, list[bytes]] | None: async def fetch_latest_snapshot(self) -> Tuple[Manifest, list[bytes]] | None:
"""Retrieve the latest manifest and all snapshot chunks.""" """Retrieve the latest manifest and all snapshot chunks."""

View File

@@ -1129,26 +1129,26 @@ class PasswordManager:
# Re-raise the exception to inform the calling function of the failure # Re-raise the exception to inform the calling function of the failure
raise raise
def sync_vault(self, alt_summary: str | None = None) -> bool: def sync_vault(self, alt_summary: str | None = None) -> str | None:
"""Publish the current vault contents to Nostr.""" """Publish the current vault contents to Nostr."""
try: try:
encrypted = self.get_encrypted_data() encrypted = self.get_encrypted_data()
if not encrypted: if not encrypted:
return False return None
pub_snap = getattr(self.nostr_client, "publish_snapshot", None) pub_snap = getattr(self.nostr_client, "publish_snapshot", None)
if callable(pub_snap): if callable(pub_snap):
if asyncio.iscoroutinefunction(pub_snap): if asyncio.iscoroutinefunction(pub_snap):
asyncio.run(pub_snap(encrypted)) _, event_id = asyncio.run(pub_snap(encrypted))
else: else:
pub_snap(encrypted) _, event_id = pub_snap(encrypted)
else: else:
# Fallback for tests using simplified stubs # Fallback for tests using simplified stubs
self.nostr_client.publish_json_to_nostr(encrypted) event_id = self.nostr_client.publish_json_to_nostr(encrypted)
self.is_dirty = False self.is_dirty = False
return True return event_id
except Exception as e: except Exception as e:
logging.error(f"Failed to sync vault: {e}", exc_info=True) logging.error(f"Failed to sync vault: {e}", exc_info=True)
return False return None
def backup_database(self) -> None: def backup_database(self) -> None:
""" """

View File

@@ -22,6 +22,7 @@ class FakeNostrClient:
def publish_snapshot(self, data: bytes): def publish_snapshot(self, data: bytes):
self.published.append(data) self.published.append(data)
return None, "abcd"
def test_manager_workflow(monkeypatch): def test_manager_workflow(monkeypatch):

View File

@@ -25,7 +25,7 @@ def test_backup_and_publish_to_nostr():
with patch( with patch(
"nostr.client.NostrClient.publish_snapshot", "nostr.client.NostrClient.publish_snapshot",
AsyncMock(return_value=None), AsyncMock(return_value=(None, "abcd")),
) as mock_publish, patch("nostr.client.ClientBuilder"), patch( ) as mock_publish, patch("nostr.client.ClientBuilder"), patch(
"nostr.client.KeyManager" "nostr.client.KeyManager"
), patch.object( ), patch.object(
@@ -38,4 +38,4 @@ def test_backup_and_publish_to_nostr():
result = asyncio.run(nostr_client.publish_snapshot(encrypted_index)) result = asyncio.run(nostr_client.publish_snapshot(encrypted_index))
mock_publish.assert_awaited_with(encrypted_index) mock_publish.assert_awaited_with(encrypted_index)
assert result is None assert result == (None, "abcd")

View File

@@ -29,7 +29,7 @@ def test_retrieve_multi_chunk_snapshot(dummy_nostr_client):
client, relay = dummy_nostr_client client, relay = dummy_nostr_client
data = os.urandom(120000) data = os.urandom(120000)
manifest = asyncio.run(client.publish_snapshot(data, limit=50000)) manifest, _ = asyncio.run(client.publish_snapshot(data, limit=50000))
assert len(manifest.chunks) > 1 assert len(manifest.chunks) > 1
fetched_manifest, chunk_bytes = asyncio.run(client.fetch_latest_snapshot()) fetched_manifest, chunk_bytes = asyncio.run(client.fetch_latest_snapshot())
assert len(chunk_bytes) == len(manifest.chunks) assert len(chunk_bytes) == len(manifest.chunks)
@@ -40,7 +40,7 @@ def test_retrieve_multi_chunk_snapshot(dummy_nostr_client):
def test_publish_and_fetch_deltas(dummy_nostr_client): def test_publish_and_fetch_deltas(dummy_nostr_client):
client, relay = dummy_nostr_client client, relay = dummy_nostr_client
base = b"base" base = b"base"
manifest = asyncio.run(client.publish_snapshot(base)) manifest, _ = asyncio.run(client.publish_snapshot(base))
manifest_id = relay.manifests[-1].id manifest_id = relay.manifests[-1].id
d1 = b"d1" d1 = b"d1"
d2 = b"d2" d2 = b"d2"

View File

@@ -43,7 +43,7 @@ def test_change_password_triggers_nostr_backup(monkeypatch):
with patch("password_manager.manager.NostrClient") as MockClient: with patch("password_manager.manager.NostrClient") as MockClient:
mock_instance = MockClient.return_value mock_instance = MockClient.return_value
mock_instance.publish_snapshot = AsyncMock(return_value=None) mock_instance.publish_snapshot = AsyncMock(return_value=(None, "abcd"))
pm.nostr_client = mock_instance pm.nostr_client = mock_instance
pm.change_password() pm.change_password()
mock_instance.publish_snapshot.assert_called_once() mock_instance.publish_snapshot.assert_called_once()

View File

@@ -54,7 +54,9 @@ def test_password_change_and_unlock(monkeypatch):
pm.fingerprint_dir = fp pm.fingerprint_dir = fp
pm.current_fingerprint = "fp" pm.current_fingerprint = "fp"
pm.parent_seed = SEED pm.parent_seed = SEED
pm.nostr_client = SimpleNamespace(publish_snapshot=lambda *a, **k: None) pm.nostr_client = SimpleNamespace(
publish_snapshot=lambda *a, **k: (None, "abcd")
)
monkeypatch.setattr( monkeypatch.setattr(
"password_manager.manager.prompt_existing_password", lambda *_: old_pw "password_manager.manager.prompt_existing_password", lambda *_: old_pw
@@ -64,7 +66,9 @@ def test_password_change_and_unlock(monkeypatch):
) )
monkeypatch.setattr( monkeypatch.setattr(
"password_manager.manager.NostrClient", "password_manager.manager.NostrClient",
lambda *a, **kw: SimpleNamespace(publish_snapshot=lambda *a, **k: None), lambda *a, **kw: SimpleNamespace(
publish_snapshot=lambda *a, **k: (None, "abcd")
),
) )
pm.change_password() pm.change_password()

View File

@@ -9,16 +9,17 @@ import main
def test_handle_post_success(capsys): def test_handle_post_success(capsys):
pm = SimpleNamespace( pm = SimpleNamespace(
sync_vault=lambda alt_summary=None: True, sync_vault=lambda alt_summary=None: "abcd",
) )
main.handle_post_to_nostr(pm) main.handle_post_to_nostr(pm)
out = capsys.readouterr().out out = capsys.readouterr().out
assert "✅ Sync complete." in out assert "✅ Sync complete." in out
assert "abcd" in out
def test_handle_post_failure(capsys): def test_handle_post_failure(capsys):
pm = SimpleNamespace( pm = SimpleNamespace(
sync_vault=lambda alt_summary=None: False, sync_vault=lambda alt_summary=None: None,
) )
main.handle_post_to_nostr(pm) main.handle_post_to_nostr(pm)
out = capsys.readouterr().out out = capsys.readouterr().out

View File

@@ -62,7 +62,10 @@ def test_add_and_delete_entry(monkeypatch):
published = [] published = []
pm.nostr_client = SimpleNamespace( pm.nostr_client = SimpleNamespace(
publish_snapshot=lambda data, alt_summary=None: published.append(data) publish_snapshot=lambda data, alt_summary=None: (
published.append(data),
(None, "abcd"),
)[1]
) )
inputs = iter([str(index)]) inputs = iter([str(index)])

View File

@@ -81,8 +81,9 @@ def test_publish_snapshot_success():
with patch.object( with patch.object(
client.client, "send_event", side_effect=fake_send client.client, "send_event", side_effect=fake_send
) as mock_send: ) as mock_send:
manifest = asyncio.run(client.publish_snapshot(b"data")) manifest, event_id = asyncio.run(client.publish_snapshot(b"data"))
assert isinstance(manifest, Manifest) assert isinstance(manifest, Manifest)
assert event_id == "abcd"
assert mock_send.await_count >= 1 assert mock_send.await_count >= 1

View File

@@ -33,7 +33,7 @@ def setup_pm(tmp_path, monkeypatch):
relays=list(DEFAULT_RELAYS), relays=list(DEFAULT_RELAYS),
close_client_pool=lambda: None, close_client_pool=lambda: None,
initialize_client_pool=lambda: None, initialize_client_pool=lambda: None,
publish_snapshot=lambda data, alt_summary=None: None, publish_snapshot=lambda data, alt_summary=None: (None, "abcd"),
key_manager=SimpleNamespace(get_npub=lambda: "npub"), key_manager=SimpleNamespace(get_npub=lambda: "npub"),
) )