Compare commits

..

No commits in common. "signer-abstraction" and "main" have entirely different histories.

3 changed files with 51 additions and 50 deletions

View file

@ -1,6 +1,6 @@
{ {
"id": "tasks", "id": "tasks",
"version": "0.0.2", "version": "0.0.1",
"name": "Tasks", "name": "Tasks",
"repo": "https://git.atitlan.io/aiolabs/tasks", "repo": "https://git.atitlan.io/aiolabs/tasks",
"short_description": "Recurring tasks and chore-tracking, published over Nostr", "short_description": "Recurring tasks and chore-tracking, published over Nostr",

View file

@ -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( async def publish_or_delete_task_event(
task: Task, *, delete: bool = False task: Task, *, delete: bool = False
) -> None: ) -> 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 Errors are logged and swallowed so a Nostr outage doesn't break the
HTTP flow that triggered the publish.""" HTTP flow that triggered the publish."""
try: try:
from lnbits.core.signers import resolve_for_wallet
from . import nostr_client from . import nostr_client
signer = await resolve_for_wallet(task.wallet) keys = await _account_keys(task.wallet)
if signer is None: if not keys:
return return
pubkey, prvkey = keys
nostr_event = await publish_task_to_nostr( 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: if nostr_event and not delete:
task.nostr_event_id = nostr_event.id 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 caller can persist it as the completion's primary key, replacing the
locally-generated hash from the optimistic insert.""" locally-generated hash from the optimistic insert."""
try: try:
from lnbits.core.signers import resolve_for_wallet
from . import nostr_client from . import nostr_client
signer = await resolve_for_wallet(task.wallet) keys = await _account_keys(task.wallet)
if signer is None: if not keys:
return None return None
pubkey, prvkey = keys
nostr_event = await publish_completion_to_nostr( 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 return nostr_event.id if nostr_event else None
except Exception as exc: except Exception as exc:
@ -70,16 +83,15 @@ async def publish_completion_delete(
) -> None: ) -> None:
"""Publish a NIP-09 delete for a previously-published completion.""" """Publish a NIP-09 delete for a previously-published completion."""
try: try:
from lnbits.core.signers import resolve_for_wallet
from . import nostr_client from . import nostr_client
signer = await resolve_for_wallet(wallet_id) keys = await _account_keys(wallet_id)
if signer is None: if not keys:
return return
pubkey, prvkey = keys
await publish_completion_delete_to_nostr( await publish_completion_delete_to_nostr(
nostr_client, completion_id, signer nostr_client, completion_id, pubkey, prvkey
) )
except Exception as exc: except Exception as exc:
logger.warning(f"[TASKS] Nostr completion delete failed: {exc}") logger.warning(f"[TASKS] Nostr completion delete failed: {exc}")

View file

@ -9,16 +9,13 @@ Builds:
- kind 5 deletes referencing either an `a` (for tasks) or `e` tag - kind 5 deletes referencing either an `a` (for tasks) or `e` tag
(for completions). (for completions).
Signing routes through the core `NostrSigner` abstraction Signs with the wallet owner's Account keypair and publishes via the
(LocalSigner / RemoteBunkerSigner / ClientSideOnlySigner). The signer NostrClient wrapping nostrclient's WebSocket.
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 time
from lnbits.core.signers import NostrSigner import coincurve
from loguru import logger from loguru import logger
from .models import Task, TaskCompletion from .models import Task, TaskCompletion
@ -131,30 +128,17 @@ def build_delete_completion_event(
return nostr_event return nostr_event
async def _sign_and_publish( def sign_nostr_event(nostr_event: NostrEvent, private_key_hex: str) -> None:
nostr_client, nostr_event: NostrEvent, signer: NostrSigner privkey = coincurve.PrivateKey(bytes.fromhex(private_key_hex))
) -> NostrEvent: sig = privkey.sign_schnorr(bytes.fromhex(nostr_event.id))
"""Hand the unsigned event to the signer — it fills in `id`, `pubkey`, nostr_event.sig = sig.hex()
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( async def publish_task_to_nostr(
nostr_client, nostr_client,
task: Task, task: Task,
signer: NostrSigner, account_pubkey: str,
account_prvkey: str,
*, *,
delete: bool = False, delete: bool = False,
) -> NostrEvent | None: ) -> NostrEvent | None:
@ -163,11 +147,12 @@ async def publish_task_to_nostr(
return None return None
try: try:
nostr_event = ( nostr_event = (
build_delete_task_event(task, signer.pubkey) build_delete_task_event(task, account_pubkey)
if delete 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( logger.info(
f"[TASKS] Published {'delete' if delete else '31922 task'} " f"[TASKS] Published {'delete' if delete else '31922 task'} "
f"{nostr_event.id[:16]}... (kind {nostr_event.kind})" f"{nostr_event.id[:16]}... (kind {nostr_event.kind})"
@ -182,16 +167,18 @@ async def publish_completion_to_nostr(
nostr_client, nostr_client,
task_address: str, task_address: str,
completion: TaskCompletion, completion: TaskCompletion,
signer: NostrSigner, account_pubkey: str,
account_prvkey: str,
) -> NostrEvent | None: ) -> NostrEvent | None:
if not nostr_client: if not nostr_client:
logger.debug("[TASKS] No NostrClient available, skipping publish") logger.debug("[TASKS] No NostrClient available, skipping publish")
return None return None
try: try:
nostr_event = build_completion_event( 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( logger.info(
f"[TASKS] Published 31925 completion {nostr_event.id[:16]}... " f"[TASKS] Published 31925 completion {nostr_event.id[:16]}... "
f"(status: {completion.task_status})" f"(status: {completion.task_status})"
@ -205,13 +192,15 @@ async def publish_completion_to_nostr(
async def publish_completion_delete_to_nostr( async def publish_completion_delete_to_nostr(
nostr_client, nostr_client,
completion_id: str, completion_id: str,
signer: NostrSigner, account_pubkey: str,
account_prvkey: str,
) -> NostrEvent | None: ) -> NostrEvent | None:
if not nostr_client: if not nostr_client:
return None return None
try: try:
nostr_event = build_delete_completion_event(completion_id, signer.pubkey) nostr_event = build_delete_completion_event(completion_id, 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( logger.info(
f"[TASKS] Published completion delete {nostr_event.id[:16]}..." f"[TASKS] Published completion delete {nostr_event.id[:16]}..."
) )