diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 1c8c71ad..7d9f2a5b 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -451,6 +451,34 @@ async def update_payment_details( return +async def update_payment_extra( + payment_hash: str, + extra: dict, + conn: Optional[Connection] = None, +) -> None: + """ + Only update the `extra` field for the payment. + Old values in the `extra` JSON object will be kept unless the new `extra` overwrites them. + """ + + row = await (conn or db).fetchone( + "SELECT hash, extra from apipayments WHERE hash = ?", + (payment_hash,), + ) + if not row: + return + db_extra = json.loads(row["extra"] if row["extra"] else "{}") + db_extra.update(extra) + + await (conn or db).execute( + """ + UPDATE apipayments SET extra = ? + WHERE hash = ? + """, + (json.dumps(db_extra), payment_hash), + ) + + async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None: await (conn or db).execute( "DELETE FROM apipayments WHERE checking_id = ?", (checking_id,) diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index ebce4b85..2bd19978 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -214,7 +214,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): lnurl_response = resp["reason"] else: lnurl_response = True - except (httpx.ConnectError, httpx.RequestError): + except (httpx.ConnectError, httpx.RequestError) as ex: + logger.error(ex) lnurl_response = False return { diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index 5de47f2e..72805603 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -5,6 +5,7 @@ import httpx from loguru import logger from lnbits.core import db as core_db +from lnbits.core.crud import update_payment_extra from lnbits.core.models import Payment from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener @@ -66,10 +67,4 @@ async def mark_webhook_sent( payment.extra["wh_message"] = reason_phrase payment.extra["wh_response"] = text - await core_db.execute( - """ - UPDATE apipayments SET extra = ? - WHERE hash = ? - """, - (json.dumps(payment.extra), payment.payment_hash), - ) + await update_payment_extra(payment.payment_hash, payment.extra) diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index 9868b057..83404c62 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -27,9 +27,11 @@ async def create_withdraw_link( open_time, usescsv, webhook_url, + webhook_headers, + webhook_body, custom_url ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( link_id, @@ -45,6 +47,8 @@ async def create_withdraw_link( int(datetime.now().timestamp()) + data.wait_time, usescsv, data.webhook_url, + data.webhook_headers, + data.webhook_body, data.custom_url, ), ) diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 5737e54f..7260df1e 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -11,6 +11,7 @@ from loguru import logger from starlette.requests import Request from starlette.responses import HTMLResponse +from lnbits.core.crud import update_payment_extra from lnbits.core.services import pay_invoice from . import withdraw_ext @@ -44,7 +45,11 @@ async def api_lnurl_response(request: Request, unique_hash): "minWithdrawable": link.min_withdrawable * 1000, "maxWithdrawable": link.max_withdrawable * 1000, "defaultDescription": link.title, + "webhook_url": link.webhook_url, + "webhook_headers": link.webhook_headers, + "webhook_body": link.webhook_body, } + return json.dumps(withdrawResponse) @@ -56,7 +61,7 @@ async def api_lnurl_response(request: Request, unique_hash): name="withdraw.api_lnurl_callback", summary="lnurl withdraw callback", description=""" - This enpoints allows you to put unique_hash, k1 + This endpoints allows you to put unique_hash, k1 and a payment_request to get your payment_request paid. """, response_description="JSON with status", @@ -143,18 +148,37 @@ async def api_lnurl_callback( if link.webhook_url: async with httpx.AsyncClient() as client: try: - r = await client.post( - link.webhook_url, - json={ + kwargs = { + "json": { "payment_hash": payment_hash, "payment_request": payment_request, "lnurlw": link.id, }, - timeout=40, + "timeout": 40, + } + if link.webhook_body: + kwargs["json"]["body"] = json.loads(link.webhook_body) + if link.webhook_headers: + kwargs["headers"] = json.loads(link.webhook_headers) + + r: httpx.Response = await client.post(link.webhook_url, **kwargs) + await update_payment_extra( + payment_hash, + { + "wh_success": r.is_success, + "wh_message": r.reason_phrase, + "wh_response": r.text, + }, ) except Exception as exc: # webhook fails shouldn't cause the lnurlw to fail since invoice is already paid - logger.error("Caught exception when dispatching webhook url:", exc) + logger.error( + "Caught exception when dispatching webhook url: " + str(exc) + ) + await update_payment_extra( + payment_hash, + {"wh_success": False, "wh_message": str(exc)}, + ) return {"status": "OK"} diff --git a/lnbits/extensions/withdraw/migrations.py b/lnbits/extensions/withdraw/migrations.py index 0c6ed4fc..95805ae7 100644 --- a/lnbits/extensions/withdraw/migrations.py +++ b/lnbits/extensions/withdraw/migrations.py @@ -122,3 +122,13 @@ async def m005_add_custom_print_design(db): Adds custom print design """ await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN custom_url TEXT;") + + +async def m006_webhook_headers_and_body(db): + """ + Add headers and body to webhooks + """ + await db.execute( + "ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_headers TEXT;" + ) + await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN webhook_body TEXT;") diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 2672537f..51c6a1cf 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -16,6 +16,8 @@ class CreateWithdrawData(BaseModel): wait_time: int = Query(..., ge=1) is_unique: bool webhook_url: str = Query(None) + webhook_headers: str = Query(None) + webhook_body: str = Query(None) custom_url: str = Query(None) @@ -35,6 +37,8 @@ class WithdrawLink(BaseModel): usescsv: str = Query(None) number: int = Query(0) webhook_url: str = Query(None) + webhook_headers: str = Query(None) + webhook_body: str = Query(None) custom_url: str = Query(None) @property diff --git a/lnbits/extensions/withdraw/static/js/index.js b/lnbits/extensions/withdraw/static/js/index.js index a3eaa593..ced78439 100644 --- a/lnbits/extensions/withdraw/static/js/index.js +++ b/lnbits/extensions/withdraw/static/js/index.js @@ -63,7 +63,8 @@ new Vue({ secondMultiplierOptions: ['seconds', 'minutes', 'hours'], data: { is_unique: false, - use_custom: false + use_custom: false, + has_webhook: false } }, simpleformDialog: { @@ -188,23 +189,35 @@ new Vue({ }, updateWithdrawLink: function (wallet, data) { var self = this + const body = _.pick( + data, + 'title', + 'min_withdrawable', + 'max_withdrawable', + 'uses', + 'wait_time', + 'is_unique', + 'webhook_url', + 'webhook_headers', + 'webhook_body', + 'custom_url' + ) + + if (data.has_webhook) { + body = { + ...body, + webhook_url: data.webhook_url, + webhook_headers: data.webhook_headers, + webhook_body: data.webhook_body + } + } LNbits.api .request( 'PUT', '/withdraw/api/v1/links/' + data.id, wallet.adminkey, - _.pick( - data, - 'title', - 'min_withdrawable', - 'max_withdrawable', - 'uses', - 'wait_time', - 'is_unique', - 'webhook_url', - 'custom_url' - ) + body ) .then(function (response) { self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) { diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html index 27684f6b..3ae244e6 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/index.html +++ b/lnbits/extensions/withdraw/templates/withdraw/index.html @@ -209,7 +209,13 @@ + + +