2021-08-30 19:55:02 +02:00
|
|
|
import asyncio
|
2023-09-11 15:48:49 +02:00
|
|
|
import json
|
2022-07-16 14:23:03 +02:00
|
|
|
import time
|
2021-07-31 00:26:22 +02:00
|
|
|
import traceback
|
2022-10-04 09:51:47 +02:00
|
|
|
import uuid
|
2020-10-06 05:39:54 +02:00
|
|
|
from http import HTTPStatus
|
2024-04-17 10:51:07 +02:00
|
|
|
from typing import Dict, List, Optional
|
2022-07-07 14:30:16 +02:00
|
|
|
|
2022-07-16 14:23:03 +02:00
|
|
|
from loguru import logger
|
2023-09-11 15:48:49 +02:00
|
|
|
from py_vapid import Vapid
|
|
|
|
from pywebpush import WebPushException, webpush
|
2021-09-11 11:02:48 +02:00
|
|
|
|
2021-03-24 04:40:32 +01:00
|
|
|
from lnbits.core.crud import (
|
|
|
|
delete_expired_invoices,
|
2023-09-11 15:48:49 +02:00
|
|
|
delete_webpush_subscriptions,
|
2022-07-16 14:23:03 +02:00
|
|
|
get_payments,
|
|
|
|
get_standalone_payment,
|
2021-03-24 04:40:32 +01:00
|
|
|
)
|
2023-09-11 15:48:49 +02:00
|
|
|
from lnbits.settings import settings
|
2024-04-15 09:02:21 +02:00
|
|
|
from lnbits.wallets import get_funding_source
|
2020-10-06 05:39:54 +02:00
|
|
|
|
2023-08-18 11:25:33 +02:00
|
|
|
tasks: List[asyncio.Task] = []
|
2024-03-18 14:02:04 +01:00
|
|
|
unique_tasks: Dict[str, asyncio.Task] = {}
|
2023-08-18 11:25:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
def create_task(coro):
|
|
|
|
task = asyncio.create_task(coro)
|
|
|
|
tasks.append(task)
|
|
|
|
return task
|
|
|
|
|
|
|
|
|
|
|
|
def create_permanent_task(func):
|
|
|
|
return create_task(catch_everything_and_restart(func))
|
|
|
|
|
|
|
|
|
2024-04-17 10:51:07 +02:00
|
|
|
def create_unique_task(name: str, coro):
|
2024-03-18 14:02:04 +01:00
|
|
|
if unique_tasks.get(name):
|
|
|
|
logger.warning(f"task `{name}` already exists, cancelling it")
|
|
|
|
try:
|
|
|
|
unique_tasks[name].cancel()
|
|
|
|
except Exception as exc:
|
2024-04-17 07:36:22 +02:00
|
|
|
logger.warning(f"error while cancelling task `{name}`: {exc!s}")
|
2024-03-18 14:02:04 +01:00
|
|
|
task = asyncio.create_task(coro)
|
|
|
|
unique_tasks[name] = task
|
|
|
|
return task
|
|
|
|
|
|
|
|
|
2024-04-17 10:51:07 +02:00
|
|
|
def create_permanent_unique_task(name: str, coro):
|
|
|
|
return create_unique_task(name, catch_everything_and_restart(coro, name))
|
2024-03-18 14:02:04 +01:00
|
|
|
|
|
|
|
|
2023-08-18 11:25:33 +02:00
|
|
|
def cancel_all_tasks():
|
|
|
|
for task in tasks:
|
|
|
|
try:
|
|
|
|
task.cancel()
|
|
|
|
except Exception as exc:
|
2024-04-17 07:36:22 +02:00
|
|
|
logger.warning(f"error while cancelling task: {exc!s}")
|
2024-03-18 14:02:04 +01:00
|
|
|
for name, task in unique_tasks.items():
|
|
|
|
try:
|
|
|
|
task.cancel()
|
|
|
|
except Exception as exc:
|
2024-04-17 07:36:22 +02:00
|
|
|
logger.warning(f"error while cancelling task `{name}`: {exc!s}")
|
2023-08-18 11:25:33 +02:00
|
|
|
|
2021-07-31 00:26:22 +02:00
|
|
|
|
2024-04-17 10:51:07 +02:00
|
|
|
async def catch_everything_and_restart(func, name: str = "unnamed"):
|
2021-07-31 00:26:22 +02:00
|
|
|
try:
|
|
|
|
await func()
|
2021-08-30 19:55:02 +02:00
|
|
|
except asyncio.CancelledError:
|
2021-07-31 00:26:22 +02:00
|
|
|
raise # because we must pass this up
|
|
|
|
except Exception as exc:
|
2024-04-17 10:51:07 +02:00
|
|
|
logger.error(f"exception in background task `{name}`:", exc)
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.error(traceback.format_exc())
|
|
|
|
logger.error("will restart the task in 5 seconds.")
|
2021-08-30 19:55:02 +02:00
|
|
|
await asyncio.sleep(5)
|
2024-04-17 10:51:07 +02:00
|
|
|
await catch_everything_and_restart(func, name)
|
2020-10-06 06:50:55 +02:00
|
|
|
|
|
|
|
|
2024-02-16 13:47:52 +01:00
|
|
|
invoice_listeners: Dict[str, asyncio.Queue] = {}
|
2020-10-06 05:39:54 +02:00
|
|
|
|
2022-10-04 09:51:47 +02:00
|
|
|
|
2024-02-16 12:48:50 +01:00
|
|
|
# TODO: name should not be optional
|
|
|
|
# some extensions still dont use a name, but they should
|
2023-02-02 13:58:23 +01:00
|
|
|
def register_invoice_listener(send_chan: asyncio.Queue, name: Optional[str] = None):
|
2020-10-06 05:39:54 +02:00
|
|
|
"""
|
2023-08-24 11:26:09 +02:00
|
|
|
A method intended for extensions (and core/tasks.py) to call when they want to be
|
|
|
|
notified about new invoice payments incoming. Will emit all incoming payments.
|
2020-10-06 05:39:54 +02:00
|
|
|
"""
|
2024-02-20 13:03:29 +01:00
|
|
|
if not name:
|
|
|
|
# fallback to a random name if extension didn't provide one
|
|
|
|
name = f"no_name_{str(uuid.uuid4())[:8]}"
|
|
|
|
|
|
|
|
if invoice_listeners.get(name):
|
|
|
|
logger.warning(f"invoice listener `{name}` already exists, replacing it")
|
|
|
|
|
|
|
|
logger.trace(f"registering invoice listener `{name}`")
|
|
|
|
invoice_listeners[name] = send_chan
|
2020-10-06 05:39:54 +02:00
|
|
|
|
|
|
|
|
2022-07-19 18:51:35 +02:00
|
|
|
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
2020-10-22 20:36:37 +02:00
|
|
|
|
|
|
|
|
2021-07-31 00:26:22 +02:00
|
|
|
async def internal_invoice_listener():
|
2022-10-04 09:51:47 +02:00
|
|
|
"""
|
|
|
|
internal_invoice_queue will be filled directly in core/services.py
|
|
|
|
after the payment was deemed to be settled internally.
|
|
|
|
|
|
|
|
Called by the app startup sequence.
|
|
|
|
"""
|
2024-04-22 11:33:53 +02:00
|
|
|
while settings.lnbits_running:
|
2021-08-30 19:55:02 +02:00
|
|
|
checking_id = await internal_invoice_queue.get()
|
2022-10-04 09:51:47 +02:00
|
|
|
logger.info("> got internal payment notification", checking_id)
|
2024-02-21 08:01:43 +01:00
|
|
|
create_task(invoice_callback_dispatcher(checking_id))
|
2020-10-22 20:36:37 +02:00
|
|
|
|
|
|
|
|
2021-07-31 00:26:22 +02:00
|
|
|
async def invoice_listener():
|
2022-10-04 09:51:47 +02:00
|
|
|
"""
|
|
|
|
invoice_listener will collect all invoices that come directly
|
|
|
|
from the backend wallet.
|
|
|
|
|
|
|
|
Called by the app startup sequence.
|
|
|
|
"""
|
2024-04-15 09:02:21 +02:00
|
|
|
funding_source = get_funding_source()
|
|
|
|
async for checking_id in funding_source.paid_invoices_stream():
|
2022-07-07 14:30:16 +02:00
|
|
|
logger.info("> got a payment notification", checking_id)
|
2024-02-21 08:01:43 +01:00
|
|
|
create_task(invoice_callback_dispatcher(checking_id))
|
2021-03-21 21:57:33 +01:00
|
|
|
|
|
|
|
|
|
|
|
async def check_pending_payments():
|
2022-10-04 09:51:47 +02:00
|
|
|
"""
|
|
|
|
check_pending_payments is called during startup to check for pending payments with
|
|
|
|
the backend and also to delete expired invoices. Incoming payments will be
|
|
|
|
checked only once, outgoing pending payments will be checked regularly.
|
|
|
|
"""
|
2021-03-28 05:11:41 +02:00
|
|
|
outgoing = True
|
|
|
|
incoming = True
|
|
|
|
|
2024-04-22 11:33:53 +02:00
|
|
|
while settings.lnbits_running:
|
2023-09-25 12:32:01 +02:00
|
|
|
logger.info(
|
|
|
|
f"Task: checking all pending payments (incoming={incoming},"
|
|
|
|
f" outgoing={outgoing}) of last 15 days"
|
|
|
|
)
|
|
|
|
start_time = time.time()
|
|
|
|
pending_payments = await get_payments(
|
|
|
|
since=(int(time.time()) - 60 * 60 * 24 * 15), # 15 days ago
|
|
|
|
complete=False,
|
|
|
|
pending=True,
|
|
|
|
outgoing=outgoing,
|
|
|
|
incoming=incoming,
|
|
|
|
exclude_uncheckable=True,
|
|
|
|
)
|
|
|
|
for payment in pending_payments:
|
|
|
|
await payment.check_status()
|
|
|
|
await asyncio.sleep(0.01) # to avoid complete blocking
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
f"Task: pending check finished for {len(pending_payments)} payments"
|
|
|
|
f" (took {time.time() - start_time:0.3f} s)"
|
|
|
|
)
|
|
|
|
# we delete expired invoices once upon the first pending check
|
|
|
|
if incoming:
|
|
|
|
logger.debug("Task: deleting all expired invoices")
|
2023-08-23 21:24:54 +02:00
|
|
|
start_time = time.time()
|
2023-09-25 12:32:01 +02:00
|
|
|
await delete_expired_invoices()
|
2022-11-25 10:32:30 +01:00
|
|
|
logger.info(
|
2023-09-25 12:32:01 +02:00
|
|
|
"Task: expired invoice deletion finished (took"
|
|
|
|
f" {time.time() - start_time:0.3f} s)"
|
2022-09-09 14:02:07 +02:00
|
|
|
)
|
2022-08-30 13:28:58 +02:00
|
|
|
|
2021-03-28 05:11:41 +02:00
|
|
|
# after the first check we will only check outgoing, not incoming
|
|
|
|
# that will be handled by the global invoice listeners, hopefully
|
|
|
|
incoming = False
|
|
|
|
|
2021-08-30 19:55:02 +02:00
|
|
|
await asyncio.sleep(60 * 30) # every 30 minutes
|
2020-10-06 05:39:54 +02:00
|
|
|
|
|
|
|
|
|
|
|
async def invoice_callback_dispatcher(checking_id: str):
|
2022-10-04 09:51:47 +02:00
|
|
|
"""
|
|
|
|
Takes incoming payments, sets pending=False, and dispatches them to
|
|
|
|
invoice_listeners from core and extensions.
|
|
|
|
"""
|
2022-06-03 14:33:31 +02:00
|
|
|
payment = await get_standalone_payment(checking_id, incoming=True)
|
2020-10-06 05:39:54 +02:00
|
|
|
if payment and payment.is_in:
|
2024-02-20 13:03:29 +01:00
|
|
|
logger.trace(
|
|
|
|
f"invoice listeners: sending invoice callback for payment {checking_id}"
|
|
|
|
)
|
2020-11-21 22:04:39 +01:00
|
|
|
await payment.set_pending(False)
|
2024-02-20 13:03:29 +01:00
|
|
|
for name, send_chan in invoice_listeners.items():
|
|
|
|
logger.trace(f"invoice listeners: sending to `{name}`")
|
2021-08-30 19:55:02 +02:00
|
|
|
await send_chan.put(payment)
|
2023-09-11 15:48:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
async def send_push_notification(subscription, title, body, url=""):
|
|
|
|
vapid = Vapid()
|
|
|
|
try:
|
|
|
|
logger.debug("sending push notification")
|
|
|
|
webpush(
|
|
|
|
json.loads(subscription.data),
|
|
|
|
json.dumps({"title": title, "body": body, "url": url}),
|
|
|
|
vapid.from_pem(bytes(settings.lnbits_webpush_privkey, "utf-8")),
|
|
|
|
{"aud": "", "sub": "mailto:alan@lnbits.com"},
|
|
|
|
)
|
|
|
|
except WebPushException as e:
|
|
|
|
if e.response.status_code == HTTPStatus.GONE:
|
|
|
|
# cleanup unsubscribed or expired push subscriptions
|
|
|
|
await delete_webpush_subscriptions(subscription.endpoint)
|
|
|
|
else:
|
|
|
|
logger.error(f"failed sending push notification: {e.response.text}")
|