Merge pull request #671 from PR0M3TH3AN/codex/define-manifest-id-constant-and-update-functions

Use replaceable manifest identifiers
This commit is contained in:
thePR0M3TH3AN
2025-07-24 18:11:44 -04:00
committed by GitHub
7 changed files with 39 additions and 20 deletions

View File

@@ -20,7 +20,7 @@ cryptography==45.0.4
ecdsa==0.19.1 ecdsa==0.19.1
ed25519-blake2b==1.4.1 ed25519-blake2b==1.4.1
execnet==2.1.1 execnet==2.1.1
fastapi==0.116.0 fastapi==0.116.1
frozenlist==1.7.0 frozenlist==1.7.0
glob2==0.7 glob2==0.7
hypothesis==6.135.20 hypothesis==6.135.20
@@ -61,6 +61,7 @@ toml==0.10.2
tomli==2.2.1 tomli==2.2.1
urllib3==2.5.0 urllib3==2.5.0
uvicorn==0.35.0 uvicorn==0.35.0
starlette==0.47.2
httpx==0.28.1 httpx==0.28.1
varint==1.0.2 varint==1.0.2
websocket-client==1.7.0 websocket-client==1.7.0

View File

@@ -46,6 +46,9 @@ DEFAULT_RELAYS = [
"wss://relay.primal.net", "wss://relay.primal.net",
] ]
# Identifier prefix for replaceable manifest events
MANIFEST_ID_PREFIX = "seedpass-manifest-"
def prepare_snapshot( def prepare_snapshot(
encrypted_bytes: bytes, limit: int encrypted_bytes: bytes, limit: int
@@ -390,22 +393,23 @@ class NostrClient:
} }
) )
manifest_identifier = f"{MANIFEST_ID_PREFIX}{self.fingerprint}"
manifest_event = ( manifest_event = (
EventBuilder(Kind(KIND_MANIFEST), manifest_json) EventBuilder(Kind(KIND_MANIFEST), manifest_json)
.tags([Tag.identifier(manifest_identifier)])
.build(self.keys.public_key()) .build(self.keys.public_key())
.sign_with_keys(self.keys) .sign_with_keys(self.keys)
) )
result = await self.client.send_event(manifest_event) 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.current_manifest_id = manifest_id self.current_manifest_id = manifest_identifier
# Record when this snapshot was published for future delta events # Record when this snapshot was published for future delta events
self.current_manifest.delta_since = int(time.time()) self.current_manifest.delta_since = int(time.time())
self._delta_events = [] self._delta_events = []
if getattr(self, "verbose_timing", False): if getattr(self, "verbose_timing", False):
duration = time.perf_counter() - start duration = time.perf_counter() - start
logger.info("publish_snapshot completed in %.2f seconds", duration) logger.info("publish_snapshot completed in %.2f seconds", duration)
return manifest, manifest_id return manifest, manifest_identifier
async def _fetch_chunks_with_retry( async def _fetch_chunks_with_retry(
self, manifest_event self, manifest_event
@@ -454,11 +458,26 @@ class NostrClient:
return None return None
chunks.append(chunk_bytes) chunks.append(chunk_bytes)
man_id = getattr(manifest_event, "id", None) ident = None
if hasattr(man_id, "to_hex"): try:
man_id = man_id.to_hex() tags_obj = manifest_event.tags()
ident = tags_obj.identifier()
except Exception:
tags = getattr(manifest_event, "tags", None)
if callable(tags):
tags = tags()
if tags:
tag = tags[0]
if hasattr(tag, "as_vec"):
vec = tag.as_vec()
if vec and len(vec) >= 2:
ident = vec[1]
elif isinstance(tag, (list, tuple)) and len(tag) >= 2:
ident = tag[1]
elif isinstance(tag, str):
ident = tag
self.current_manifest = manifest self.current_manifest = manifest
self.current_manifest_id = man_id self.current_manifest_id = ident
return manifest, chunks return manifest, chunks
async def fetch_latest_snapshot(self) -> Tuple[Manifest, list[bytes]] | None: async def fetch_latest_snapshot(self) -> Tuple[Manifest, list[bytes]] | None:
@@ -469,7 +488,8 @@ class NostrClient:
self.last_error = None self.last_error = None
pubkey = self.keys.public_key() pubkey = self.keys.public_key()
f = Filter().author(pubkey).kind(Kind(KIND_MANIFEST)).limit(3) ident = f"{MANIFEST_ID_PREFIX}{self.fingerprint}"
f = Filter().author(pubkey).kind(Kind(KIND_MANIFEST)).identifier(ident).limit(1)
timeout = timedelta(seconds=10) timeout = timedelta(seconds=10)
try: try:
events = (await self.client.fetch_events(f, timeout)).to_vec() events = (await self.client.fetch_events(f, timeout)).to_vec()

View File

@@ -25,8 +25,9 @@ freezegun
pyperclip pyperclip
qrcode>=8.2 qrcode>=8.2
typer>=0.12.3 typer>=0.12.3
fastapi>=0.116.0 fastapi>=0.116.1
uvicorn>=0.35.0 uvicorn>=0.35.0
starlette>=0.47.2
httpx>=0.28.1 httpx>=0.28.1
requests>=2.32 requests>=2.32
python-multipart python-multipart

View File

@@ -44,7 +44,7 @@ def test_full_sync_roundtrip(dummy_nostr_client):
# Manager A publishes initial snapshot # Manager A publishes initial snapshot
pm_a.entry_manager.add_entry("site1", 12) pm_a.entry_manager.add_entry("site1", 12)
pm_a.sync_vault() pm_a.sync_vault()
manifest_id = relay.manifests[-1].id manifest_id = relay.manifests[-1].tags[0]
# Manager B retrieves snapshot # Manager B retrieves snapshot
result = pm_b.attempt_initial_sync() result = pm_b.attempt_initial_sync()

View File

@@ -44,7 +44,7 @@ def test_full_sync_roundtrip(dummy_nostr_client):
# Manager A publishes initial snapshot # Manager A publishes initial snapshot
pm_a.entry_manager.add_entry("site1", 12) pm_a.entry_manager.add_entry("site1", 12)
pm_a.sync_vault() pm_a.sync_vault()
manifest_id = relay.manifests[-1].id manifest_id = relay.manifests[-1].tags[0]
# Manager B retrieves snapshot # Manager B retrieves snapshot
result = pm_b.attempt_initial_sync() result = pm_b.attempt_initial_sync()

View File

@@ -54,7 +54,7 @@ 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].tags[0]
d1 = b"d1" d1 = b"d1"
d2 = b"d2" d2 = b"d2"
asyncio.run(client.publish_delta(d1, manifest_id)) asyncio.run(client.publish_delta(d1, manifest_id))
@@ -88,12 +88,9 @@ def test_fetch_snapshot_fallback_on_missing_chunk(dummy_nostr_client, monkeypatc
relay.filters.clear() relay.filters.clear()
fetched_manifest, chunk_bytes = asyncio.run(client.fetch_latest_snapshot()) result = asyncio.run(client.fetch_latest_snapshot())
assert gzip.decompress(b"".join(chunk_bytes)) == data1 assert result is None
assert [c.event_id for c in fetched_manifest.chunks] == [
c.event_id for c in manifest1.chunks
]
attempts = sum( attempts = sum(
1 1

View File

@@ -84,7 +84,7 @@ def test_publish_snapshot_success():
) as mock_send: ) as mock_send:
manifest, event_id = 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 event_id == "seedpass-manifest-fp"
assert mock_send.await_count >= 1 assert mock_send.await_count >= 1