diff --git a/config.json b/config.json index bbe422f..eef2c1f 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "tasks", - "version": "0.0.2", + "version": "0.0.1", "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 7c56f07..295d473 100644 --- a/nostr_hooks.py +++ b/nostr_hooks.py @@ -14,6 +14,21 @@ 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: @@ -22,16 +37,15 @@ 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 - signer = await resolve_for_wallet(task.wallet) - if signer is None: + keys = await _account_keys(task.wallet) + if not keys: return + pubkey, prvkey = keys nostr_event = await publish_task_to_nostr( - nostr_client, task, signer, delete=delete + nostr_client, task, pubkey, prvkey, delete=delete ) if nostr_event and not delete: task.nostr_event_id = nostr_event.id @@ -48,16 +62,15 @@ 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 - signer = await resolve_for_wallet(task.wallet) - if signer is None: + keys = await _account_keys(task.wallet) + if not keys: return None + pubkey, prvkey = keys nostr_event = await publish_completion_to_nostr( - nostr_client, task.address, completion, signer + nostr_client, task.address, completion, pubkey, prvkey ) return nostr_event.id if nostr_event else None except Exception as exc: @@ -70,16 +83,15 @@ 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 - signer = await resolve_for_wallet(wallet_id) - if signer is None: + keys = await _account_keys(wallet_id) + if not keys: return + pubkey, prvkey = keys await publish_completion_delete_to_nostr( - nostr_client, completion_id, signer + nostr_client, completion_id, pubkey, prvkey ) except Exception as exc: logger.warning(f"[TASKS] Nostr completion delete failed: {exc}") diff --git a/nostr_publisher.py b/nostr_publisher.py index 82c97d4..4198906 100644 --- a/nostr_publisher.py +++ b/nostr_publisher.py @@ -9,16 +9,13 @@ Builds: - kind 5 deletes referencing either an `a` (for tasks) or `e` tag (for completions). -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. +Signs with the wallet owner's Account keypair and publishes via the +NostrClient wrapping nostrclient's WebSocket. """ import time -from lnbits.core.signers import NostrSigner +import coincurve from loguru import logger from .models import Task, TaskCompletion @@ -131,30 +128,17 @@ def build_delete_completion_event( return nostr_event -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 +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 publish_task_to_nostr( nostr_client, task: Task, - signer: NostrSigner, + account_pubkey: str, + account_prvkey: str, *, delete: bool = False, ) -> NostrEvent | None: @@ -163,11 +147,12 @@ async def publish_task_to_nostr( return None try: nostr_event = ( - build_delete_task_event(task, signer.pubkey) + build_delete_task_event(task, account_pubkey) if delete - else build_task_event(task, signer.pubkey) + else build_task_event(task, account_pubkey) ) - await _sign_and_publish(nostr_client, nostr_event, signer) + sign_nostr_event(nostr_event, account_prvkey) + await nostr_client.publish_nostr_event(nostr_event) logger.info( f"[TASKS] Published {'delete' if delete else '31922 task'} " f"{nostr_event.id[:16]}... (kind {nostr_event.kind})" @@ -182,16 +167,18 @@ async def publish_completion_to_nostr( nostr_client, task_address: str, completion: TaskCompletion, - signer: NostrSigner, + account_pubkey: str, + account_prvkey: str, ) -> 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, signer.pubkey + task_address, completion, account_pubkey ) - await _sign_and_publish(nostr_client, nostr_event, signer) + sign_nostr_event(nostr_event, account_prvkey) + await nostr_client.publish_nostr_event(nostr_event) logger.info( f"[TASKS] Published 31925 completion {nostr_event.id[:16]}... " f"(status: {completion.task_status})" @@ -205,13 +192,15 @@ async def publish_completion_to_nostr( async def publish_completion_delete_to_nostr( nostr_client, completion_id: str, - signer: NostrSigner, + account_pubkey: str, + account_prvkey: str, ) -> NostrEvent | None: if not nostr_client: return None try: - nostr_event = build_delete_completion_event(completion_id, signer.pubkey) - await _sign_and_publish(nostr_client, nostr_event, signer) + 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) logger.info( f"[TASKS] Published completion delete {nostr_event.id[:16]}..." )