chore: satisfy upstream lint (black, mypy, prettier, ruff)
Some checks failed
lint.yml / chore: satisfy upstream lint (black, mypy, prettier, ruff) (push) Failing after 0s

- black/prettier reformatting across new aio code
- type annotations on db.fetchone/fetchall callsites in crud.py
- explicit dict[str, list[str]] for tag_lists in nostr_sync.py
- type:ignore[attr-defined] on Account.prvkey access — the field is
  added by the aio-fork lnbits.core.models.Account; upstream lnbits
  does not yet have it, so consumers without the fork must add a
  prvkey column to accounts before the Nostr publisher can sign.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-05 20:04:15 +02:00
commit b428b0dca8
11 changed files with 48 additions and 57 deletions

View file

@ -75,9 +75,7 @@ def events_start():
except Exception as exc: except Exception as exc:
logger.error(f"[EVENTS] Nostr sync task failed: {exc}") logger.error(f"[EVENTS] Nostr sync task failed: {exc}")
task3 = create_permanent_unique_task( task3 = create_permanent_unique_task("ext_events_nostr_sync", _sync_nostr_events)
"ext_events_nostr_sync", _sync_nostr_events
)
scheduled_tasks.append(task3) scheduled_tasks.append(task3)

12
crud.py
View file

@ -94,7 +94,7 @@ async def update_ticket(ticket: Ticket) -> Ticket:
async def get_ticket(payment_hash: str) -> Ticket | None: async def get_ticket(payment_hash: str) -> Ticket | None:
row = await db.fetchone( row: dict | None = await db.fetchone(
"SELECT * FROM events.ticket WHERE id = :id", "SELECT * FROM events.ticket WHERE id = :id",
{"id": payment_hash}, {"id": payment_hash},
) )
@ -107,13 +107,15 @@ async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids]) q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})") rows: list[dict] = await db.fetchall(
f"SELECT * FROM events.ticket WHERE wallet IN ({q})"
)
return [Ticket(**_parse_ticket_row(row)) for row in rows] return [Ticket(**_parse_ticket_row(row)) for row in rows]
async def get_tickets_by_user_id(user_id: str) -> list[Ticket]: async def get_tickets_by_user_id(user_id: str) -> list[Ticket]:
"""All tickets owned by the given LNbits user_id.""" """All tickets owned by the given LNbits user_id."""
rows = await db.fetchall( rows: list[dict] = await db.fetchall(
"SELECT * FROM events.ticket WHERE user_id = :user_id ORDER BY time DESC", "SELECT * FROM events.ticket WHERE user_id = :user_id ORDER BY time DESC",
{"user_id": user_id}, {"user_id": user_id},
) )
@ -206,7 +208,7 @@ async def get_pending_events() -> list[Event]:
async def get_settings() -> EventsSettings: async def get_settings() -> EventsSettings:
"""Singleton settings row, seeded by m010.""" """Singleton settings row, seeded by m010."""
row = await db.fetchone("SELECT * FROM events.settings WHERE id = 1") row: dict | None = await db.fetchone("SELECT * FROM events.settings WHERE id = 1")
if row: if row:
return EventsSettings(**dict(row)) return EventsSettings(**dict(row))
return EventsSettings() return EventsSettings()
@ -225,7 +227,7 @@ async def delete_event(event_id: str) -> None:
async def get_event_tickets(event_id: str) -> list[Ticket]: async def get_event_tickets(event_id: str) -> list[Ticket]:
rows = await db.fetchall( rows: list[dict] = await db.fetchall(
"SELECT * FROM events.ticket WHERE event = :event", "SELECT * FROM events.ticket WHERE event = :event",
{"event": event_id}, {"event": event_id},
) )

View file

@ -232,14 +232,12 @@ async def m010_add_events_settings(db):
Create the extension settings singleton row used by the admin UI to Create the extension settings singleton row used by the admin UI to
toggle e.g. auto_approve. toggle e.g. auto_approve.
""" """
await db.execute( await db.execute("""
"""
CREATE TABLE IF NOT EXISTS events.settings ( CREATE TABLE IF NOT EXISTS events.settings (
id INTEGER PRIMARY KEY DEFAULT 1, id INTEGER PRIMARY KEY DEFAULT 1,
auto_approve BOOLEAN NOT NULL DEFAULT FALSE auto_approve BOOLEAN NOT NULL DEFAULT FALSE
) )
""" """)
)
await db.execute( await db.execute(
"INSERT INTO events.settings (id, auto_approve) " "INSERT INTO events.settings (id, auto_approve) "
"SELECT 1, FALSE WHERE NOT EXISTS " "SELECT 1, FALSE WHERE NOT EXISTS "

View file

@ -118,9 +118,7 @@ class CreateTicket(BaseModel):
email = values.get("email") email = values.get("email")
user_id = values.get("user_id") user_id = values.get("user_id")
if not user_id and not (name and email): if not user_id and not (name and email):
raise ValueError( raise ValueError("Either user_id or both name and email must be provided")
"Either user_id or both name and email must be provided"
)
if user_id and (name or email): if user_id and (name or email):
raise ValueError("Cannot provide both user_id and name/email") raise ValueError("Cannot provide both user_id and name/email")
return values return values

View file

@ -39,8 +39,7 @@ class NostrClient:
async def connect(self) -> WebSocketApp: async def connect(self) -> WebSocketApp:
relay_endpoint = encrypt_internal_message("relay", urlsafe=True) relay_endpoint = encrypt_internal_message("relay", urlsafe=True)
ws_url = ( ws_url = (
f"ws://localhost:{settings.port}" f"ws://localhost:{settings.port}" f"/nostrclient/api/v1/{relay_endpoint}"
f"/nostrclient/api/v1/{relay_endpoint}"
) )
logger.info("[EVENTS] Connecting to nostrclient WebSocket...") logger.info("[EVENTS] Connecting to nostrclient WebSocket...")
@ -58,12 +57,8 @@ class NostrClient:
logger.warning(f"[EVENTS] WebSocket error: {error}") logger.warning(f"[EVENTS] WebSocket error: {error}")
def on_close(_, status_code, message): def on_close(_, status_code, message):
logger.warning( logger.warning(f"[EVENTS] WebSocket closed: {status_code} {message}")
f"[EVENTS] WebSocket closed: {status_code} {message}" self.receive_event_queue.put_nowait(ValueError("WebSocket closed"))
)
self.receive_event_queue.put_nowait(
ValueError("WebSocket closed")
)
ws = WebSocketApp( ws = WebSocketApp(
ws_url, ws_url,
@ -118,9 +113,7 @@ class NostrClient:
async def subscribe(self, filters: list[dict]): async def subscribe(self, filters: list[dict]):
"""Subscribe to events matching the given filters.""" """Subscribe to events matching the given filters."""
self.subscription_id = "events-" + urlsafe_short_hash()[:32] self.subscription_id = "events-" + urlsafe_short_hash()[:32]
await self.send_req_queue.put( await self.send_req_queue.put(["REQ", self.subscription_id, *filters])
["REQ", self.subscription_id, *filters]
)
logger.info( logger.info(
f"[EVENTS] Subscribed to NIP-52 events " f"[EVENTS] Subscribed to NIP-52 events "
f"(sub: {self.subscription_id[:20]}...)" f"(sub: {self.subscription_id[:20]}...)"

View file

@ -29,11 +29,15 @@ async def publish_or_delete_nostr_event(event: Event, *, delete: bool = False) -
if not wallet_obj: if not wallet_obj:
return return
account = await get_account(wallet_obj.user) account = await get_account(wallet_obj.user)
if not account or not account.pubkey or not account.prvkey: if not account or not account.pubkey or not account.prvkey: # type: ignore[attr-defined]
return return
nostr_event = await publish_event_to_nostr( nostr_event = await publish_event_to_nostr(
nostr_client, event, account.pubkey, account.prvkey, delete=delete nostr_client,
event,
account.pubkey,
account.prvkey, # type: ignore[attr-defined]
delete=delete,
) )
if nostr_event and not delete: if nostr_event and not delete:
event.nostr_event_id = nostr_event.id event.nostr_event_id = nostr_event.id

View file

@ -41,7 +41,7 @@ def build_nip52_event(event: Event, pubkey: str) -> NostrEvent:
tags.append(["image", event.banner]) tags.append(["image", event.banner])
if event.location: if event.location:
tags.append(["location", event.location]) tags.append(["location", event.location])
for cat in (event.categories or []): for cat in event.categories or []:
tags.append(["t", cat]) tags.append(["t", cat])
nostr_event = NostrEvent( nostr_event = NostrEvent(

View file

@ -50,7 +50,7 @@ async def _handle_calendar_event(nostr_client: NostrClient, event_data: dict):
return return
tags = {t[0]: t[1] for t in event_data.get("tags", []) if len(t) >= 2} tags = {t[0]: t[1] for t in event_data.get("tags", []) if len(t) >= 2}
tag_lists = {} tag_lists: dict[str, list[str]] = {}
for t in event_data.get("tags", []): for t in event_data.get("tags", []):
if len(t) >= 2: if len(t) >= 2:
tag_lists.setdefault(t[0], []).append(t[1]) tag_lists.setdefault(t[0], []).append(t[1])
@ -137,9 +137,11 @@ async def wait_for_nostr_events(nostr_client: NostrClient):
while True: while True:
try: try:
# Subscribe to NIP-52 calendar events # Subscribe to NIP-52 calendar events
await nostr_client.subscribe([ await nostr_client.subscribe(
[
{"kinds": [31922, 31923]}, {"kinds": [31922, 31923]},
]) ]
)
# Process incoming events # Process incoming events
while True: while True:

View file

@ -187,12 +187,7 @@ window.PageEvents = {
}, },
saveSettings() { saveSettings() {
LNbits.api LNbits.api
.request( .request('PUT', '/events/api/v1/events/settings', null, this.settings)
'PUT',
'/events/api/v1/events/settings',
null,
this.settings
)
.then(() => { .then(() => {
Quasar.Notify.create({type: 'positive', message: 'Settings saved'}) Quasar.Notify.create({type: 'positive', message: 'Settings saved'})
}) })

View file

@ -192,7 +192,13 @@
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
<q-badge <q-badge
v-if="col.name === 'status'" v-if="col.name === 'status'"
:color="col.value === 'approved' ? 'green' : col.value === 'proposed' ? 'orange' : 'red'" :color="
col.value === 'approved'
? 'green'
: col.value === 'proposed'
? 'orange'
: 'red'
"
:label="col.value" :label="col.value"
></q-badge> ></q-badge>
<span v-else v-text="col.value"></span> <span v-else v-text="col.value"></span>
@ -284,7 +290,13 @@
<q-td v-for="col in props.cols" :key="col.name" :props="props"> <q-td v-for="col in props.cols" :key="col.name" :props="props">
<q-badge <q-badge
v-if="col.name === 'status'" v-if="col.name === 'status'"
:color="col.value === 'approved' ? 'green' : col.value === 'proposed' ? 'orange' : 'red'" :color="
col.value === 'approved'
? 'green'
: col.value === 'proposed'
? 'orange'
: 'red'
"
:label="col.value" :label="col.value"
></q-badge> ></q-badge>
<span v-else v-text="col.value"></span> <span v-else v-text="col.value"></span>

View file

@ -139,9 +139,7 @@ async def api_get_event(event_id: str) -> Event:
# closing_date is filled in by create_event (defaults to end_date or # closing_date is filled in by create_event (defaults to end_date or
# start_date) but the field is typed Optional, so guard for the typechecker. # start_date) but the field is typed Optional, so guard for the typechecker.
closing_date = ( closing_date = event.closing_date or event.event_end_date or event.event_start_date
event.closing_date or event.event_end_date or event.event_start_date
)
is_window_open = datetime.now(timezone.utc) < datetime.strptime( is_window_open = datetime.now(timezone.utc) < datetime.strptime(
closing_date, "%Y-%m-%d" closing_date, "%Y-%m-%d"
).replace(tzinfo=timezone.utc) ).replace(tzinfo=timezone.utc)
@ -181,10 +179,7 @@ async def api_event_create(
ext_settings = await get_settings() ext_settings = await get_settings()
user_id = wallet.wallet.user user_id = wallet.wallet.user
is_admin = ( is_admin = user_id == settings.super_user or user_id in settings.lnbits_admin_users
user_id == settings.super_user
or user_id in settings.lnbits_admin_users
)
if not is_admin and not ext_settings.auto_approve: if not is_admin and not ext_settings.auto_approve:
data.status = "proposed" data.status = "proposed"
@ -208,9 +203,7 @@ async def api_event_update(
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist." status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
) )
if event.wallet != wallet.wallet.id: if event.wallet != wallet.wallet.id:
raise HTTPException( raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your event.")
status_code=HTTPStatus.FORBIDDEN, detail="Not your event."
)
for k, v in data.dict().items(): for k, v in data.dict().items():
setattr(event, k, v) setattr(event, k, v)
event = await update_event(event) event = await update_event(event)
@ -351,9 +344,7 @@ async def api_get_ticket(ticket_id: str) -> Ticket:
@tickets_api_router.post("/{event_id}") @tickets_api_router.post("/{event_id}")
async def api_ticket_create( async def api_ticket_create(event_id: str, data: CreateTicket) -> TicketPaymentRequest:
event_id: str, data: CreateTicket
) -> TicketPaymentRequest:
event = await get_event(event_id) event = await get_event(event_id)
if not event: if not event:
raise HTTPException( raise HTTPException(
@ -423,9 +414,7 @@ async def _create_named_ticket(
) )
async def _create_user_id_ticket( async def _create_user_id_ticket(event: Event, user_id: str) -> TicketPaymentRequest:
event: Event, user_id: str
) -> TicketPaymentRequest:
price = event.price_per_ticket price = event.price_per_ticket
extra: dict[str, Any] = {"tag": "events", "user_id": user_id} extra: dict[str, Any] = {"tag": "events", "user_id": user_id}