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",
"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",

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(
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}")

View file

@ -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]}..."
)