diff --git a/config.json b/config.json index eef2c1f..bbe422f 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "tasks", - "version": "0.0.1", + "version": "0.0.2", "name": "Tasks", "repo": "https://git.atitlan.io/aiolabs/tasks", "short_description": "Recurring tasks and chore-tracking, published over Nostr", diff --git a/nostr_hooks.py b/nostr_hooks.py index 295d473..7c56f07 100644 --- a/nostr_hooks.py +++ b/nostr_hooks.py @@ -14,21 +14,6 @@ from .nostr_publisher import ( ) -async def _account_keys(wallet_id: str) -> tuple[str, str] | None: - """Fetch (pubkey, prvkey) for the wallet's owning account. Returns None - when the account is missing keys, so callers can skip cleanly.""" - from lnbits.core.crud.users import get_account - from lnbits.core.crud.wallets import get_wallet - - wallet_obj = await get_wallet(wallet_id) - if not wallet_obj: - return None - account = await get_account(wallet_obj.user) - if not account or not account.pubkey or not account.prvkey: # type: ignore[attr-defined] - return None - return account.pubkey, account.prvkey # type: ignore[attr-defined] - - async def publish_or_delete_task_event( task: Task, *, delete: bool = False ) -> None: @@ -37,15 +22,16 @@ async def publish_or_delete_task_event( Errors are logged and swallowed so a Nostr outage doesn't break the HTTP flow that triggered the publish.""" try: + from lnbits.core.signers import resolve_for_wallet + from . import nostr_client - keys = await _account_keys(task.wallet) - if not keys: + signer = await resolve_for_wallet(task.wallet) + if signer is None: return - pubkey, prvkey = keys nostr_event = await publish_task_to_nostr( - nostr_client, task, pubkey, prvkey, delete=delete + nostr_client, task, signer, delete=delete ) if nostr_event and not delete: task.nostr_event_id = nostr_event.id @@ -62,15 +48,16 @@ async def publish_task_completion( caller can persist it as the completion's primary key, replacing the locally-generated hash from the optimistic insert.""" try: + from lnbits.core.signers import resolve_for_wallet + from . import nostr_client - keys = await _account_keys(task.wallet) - if not keys: + signer = await resolve_for_wallet(task.wallet) + if signer is None: return None - pubkey, prvkey = keys nostr_event = await publish_completion_to_nostr( - nostr_client, task.address, completion, pubkey, prvkey + nostr_client, task.address, completion, signer ) return nostr_event.id if nostr_event else None except Exception as exc: @@ -83,15 +70,16 @@ async def publish_completion_delete( ) -> None: """Publish a NIP-09 delete for a previously-published completion.""" try: + from lnbits.core.signers import resolve_for_wallet + from . import nostr_client - keys = await _account_keys(wallet_id) - if not keys: + signer = await resolve_for_wallet(wallet_id) + if signer is None: return - pubkey, prvkey = keys await publish_completion_delete_to_nostr( - nostr_client, completion_id, pubkey, prvkey + nostr_client, completion_id, signer ) except Exception as exc: logger.warning(f"[TASKS] Nostr completion delete failed: {exc}") diff --git a/nostr_publisher.py b/nostr_publisher.py index 4198906..82c97d4 100644 --- a/nostr_publisher.py +++ b/nostr_publisher.py @@ -9,13 +9,16 @@ Builds: - kind 5 deletes referencing either an `a` (for tasks) or `e` tag (for completions). -Signs with the wallet owner's Account keypair and publishes via the -NostrClient wrapping nostrclient's WebSocket. +Signing routes through the core `NostrSigner` abstraction +(LocalSigner / RemoteBunkerSigner / ClientSideOnlySigner). The signer +fills in `id`, `pubkey`, and `sig` per NIP-01 — same serialization +rules as our local `event_id` property uses, so the returned id +matches what we'd have computed locally. """ import time -import coincurve +from lnbits.core.signers import NostrSigner from loguru import logger from .models import Task, TaskCompletion @@ -128,17 +131,30 @@ def build_delete_completion_event( return nostr_event -def sign_nostr_event(nostr_event: NostrEvent, private_key_hex: str) -> None: - privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex)) - sig = privkey.sign_schnorr(bytes.fromhex(nostr_event.id)) - nostr_event.sig = sig.hex() +async def _sign_and_publish( + nostr_client, nostr_event: NostrEvent, signer: NostrSigner +) -> NostrEvent: + """Hand the unsigned event to the signer — it fills in `id`, `pubkey`, + and `sig` — then publish through the relay client. Mutates `nostr_event` + in place with the canonical signed values.""" + unsigned = { + "kind": nostr_event.kind, + "created_at": nostr_event.created_at, + "tags": nostr_event.tags, + "content": nostr_event.content, + } + signed = await signer.sign_event(unsigned) + nostr_event.id = signed["id"] + nostr_event.pubkey = signed["pubkey"] + nostr_event.sig = signed["sig"] + await nostr_client.publish_nostr_event(nostr_event) + return nostr_event async def publish_task_to_nostr( nostr_client, task: Task, - account_pubkey: str, - account_prvkey: str, + signer: NostrSigner, *, delete: bool = False, ) -> NostrEvent | None: @@ -147,12 +163,11 @@ async def publish_task_to_nostr( return None try: nostr_event = ( - build_delete_task_event(task, account_pubkey) + build_delete_task_event(task, signer.pubkey) if delete - else build_task_event(task, account_pubkey) + else build_task_event(task, signer.pubkey) ) - sign_nostr_event(nostr_event, account_prvkey) - await nostr_client.publish_nostr_event(nostr_event) + await _sign_and_publish(nostr_client, nostr_event, signer) logger.info( f"[TASKS] Published {'delete' if delete else '31922 task'} " f"{nostr_event.id[:16]}... (kind {nostr_event.kind})" @@ -167,18 +182,16 @@ async def publish_completion_to_nostr( nostr_client, task_address: str, completion: TaskCompletion, - account_pubkey: str, - account_prvkey: str, + signer: NostrSigner, ) -> NostrEvent | None: if not nostr_client: logger.debug("[TASKS] No NostrClient available, skipping publish") return None try: nostr_event = build_completion_event( - task_address, completion, account_pubkey + task_address, completion, signer.pubkey ) - sign_nostr_event(nostr_event, account_prvkey) - await nostr_client.publish_nostr_event(nostr_event) + await _sign_and_publish(nostr_client, nostr_event, signer) logger.info( f"[TASKS] Published 31925 completion {nostr_event.id[:16]}... " f"(status: {completion.task_status})" @@ -192,15 +205,13 @@ async def publish_completion_to_nostr( async def publish_completion_delete_to_nostr( nostr_client, completion_id: str, - account_pubkey: str, - account_prvkey: str, + signer: NostrSigner, ) -> NostrEvent | None: if not nostr_client: return None try: - nostr_event = build_delete_completion_event(completion_id, account_pubkey) - sign_nostr_event(nostr_event, account_prvkey) - await nostr_client.publish_nostr_event(nostr_event) + nostr_event = build_delete_completion_event(completion_id, signer.pubkey) + await _sign_and_publish(nostr_client, nostr_event, signer) logger.info( f"[TASKS] Published completion delete {nostr_event.id[:16]}..." )