Merge pull request #1227 from motorina0/withdraw_callback_extra

Withdraw webhook improvements
This commit is contained in:
calle
2022-12-21 14:52:07 +01:00
committed by GitHub
9 changed files with 130 additions and 27 deletions

View File

@ -451,6 +451,34 @@ async def update_payment_details(
return 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: async def delete_payment(checking_id: str, conn: Optional[Connection] = None) -> None:
await (conn or db).execute( await (conn or db).execute(
"DELETE FROM apipayments WHERE checking_id = ?", (checking_id,) "DELETE FROM apipayments WHERE checking_id = ?", (checking_id,)

View File

@ -214,7 +214,8 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
lnurl_response = resp["reason"] lnurl_response = resp["reason"]
else: else:
lnurl_response = True lnurl_response = True
except (httpx.ConnectError, httpx.RequestError): except (httpx.ConnectError, httpx.RequestError) as ex:
logger.error(ex)
lnurl_response = False lnurl_response = False
return { return {

View File

@ -5,6 +5,7 @@ import httpx
from loguru import logger from loguru import logger
from lnbits.core import db as core_db from lnbits.core import db as core_db
from lnbits.core.crud import update_payment_extra
from lnbits.core.models import Payment from lnbits.core.models import Payment
from lnbits.helpers import get_current_extension_name from lnbits.helpers import get_current_extension_name
from lnbits.tasks import register_invoice_listener 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_message"] = reason_phrase
payment.extra["wh_response"] = text payment.extra["wh_response"] = text
await core_db.execute( await update_payment_extra(payment.payment_hash, payment.extra)
"""
UPDATE apipayments SET extra = ?
WHERE hash = ?
""",
(json.dumps(payment.extra), payment.payment_hash),
)

View File

@ -27,9 +27,11 @@ async def create_withdraw_link(
open_time, open_time,
usescsv, usescsv,
webhook_url, webhook_url,
webhook_headers,
webhook_body,
custom_url custom_url
) )
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", """,
( (
link_id, link_id,
@ -45,6 +47,8 @@ async def create_withdraw_link(
int(datetime.now().timestamp()) + data.wait_time, int(datetime.now().timestamp()) + data.wait_time,
usescsv, usescsv,
data.webhook_url, data.webhook_url,
data.webhook_headers,
data.webhook_body,
data.custom_url, data.custom_url,
), ),
) )

View File

@ -11,6 +11,7 @@ from loguru import logger
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
from lnbits.core.crud import update_payment_extra
from lnbits.core.services import pay_invoice from lnbits.core.services import pay_invoice
from . import withdraw_ext from . import withdraw_ext
@ -44,7 +45,11 @@ async def api_lnurl_response(request: Request, unique_hash):
"minWithdrawable": link.min_withdrawable * 1000, "minWithdrawable": link.min_withdrawable * 1000,
"maxWithdrawable": link.max_withdrawable * 1000, "maxWithdrawable": link.max_withdrawable * 1000,
"defaultDescription": link.title, "defaultDescription": link.title,
"webhook_url": link.webhook_url,
"webhook_headers": link.webhook_headers,
"webhook_body": link.webhook_body,
} }
return json.dumps(withdrawResponse) return json.dumps(withdrawResponse)
@ -56,7 +61,7 @@ async def api_lnurl_response(request: Request, unique_hash):
name="withdraw.api_lnurl_callback", name="withdraw.api_lnurl_callback",
summary="lnurl withdraw callback", summary="lnurl withdraw callback",
description=""" 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. and a payment_request to get your payment_request paid.
""", """,
response_description="JSON with status", response_description="JSON with status",
@ -143,18 +148,37 @@ async def api_lnurl_callback(
if link.webhook_url: if link.webhook_url:
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
r = await client.post( kwargs = {
link.webhook_url, "json": {
json={
"payment_hash": payment_hash, "payment_hash": payment_hash,
"payment_request": payment_request, "payment_request": payment_request,
"lnurlw": link.id, "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: except Exception as exc:
# webhook fails shouldn't cause the lnurlw to fail since invoice is already paid # 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"} return {"status": "OK"}

View File

@ -122,3 +122,13 @@ async def m005_add_custom_print_design(db):
Adds custom print design Adds custom print design
""" """
await db.execute("ALTER TABLE withdraw.withdraw_link ADD COLUMN custom_url TEXT;") 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;")

View File

@ -16,6 +16,8 @@ class CreateWithdrawData(BaseModel):
wait_time: int = Query(..., ge=1) wait_time: int = Query(..., ge=1)
is_unique: bool is_unique: bool
webhook_url: str = Query(None) webhook_url: str = Query(None)
webhook_headers: str = Query(None)
webhook_body: str = Query(None)
custom_url: str = Query(None) custom_url: str = Query(None)
@ -35,6 +37,8 @@ class WithdrawLink(BaseModel):
usescsv: str = Query(None) usescsv: str = Query(None)
number: int = Query(0) number: int = Query(0)
webhook_url: str = Query(None) webhook_url: str = Query(None)
webhook_headers: str = Query(None)
webhook_body: str = Query(None)
custom_url: str = Query(None) custom_url: str = Query(None)
@property @property

View File

@ -63,7 +63,8 @@ new Vue({
secondMultiplierOptions: ['seconds', 'minutes', 'hours'], secondMultiplierOptions: ['seconds', 'minutes', 'hours'],
data: { data: {
is_unique: false, is_unique: false,
use_custom: false use_custom: false,
has_webhook: false
} }
}, },
simpleformDialog: { simpleformDialog: {
@ -188,23 +189,35 @@ new Vue({
}, },
updateWithdrawLink: function (wallet, data) { updateWithdrawLink: function (wallet, data) {
var self = this 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 LNbits.api
.request( .request(
'PUT', 'PUT',
'/withdraw/api/v1/links/' + data.id, '/withdraw/api/v1/links/' + data.id,
wallet.adminkey, wallet.adminkey,
_.pick( body
data,
'title',
'min_withdrawable',
'max_withdrawable',
'uses',
'wait_time',
'is_unique',
'webhook_url',
'custom_url'
)
) )
.then(function (response) { .then(function (response) {
self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) { self.withdrawLinks = _.reject(self.withdrawLinks, function (obj) {

View File

@ -209,7 +209,13 @@
</q-select> </q-select>
</div> </div>
</div> </div>
<q-toggle
label="Webhook"
color="secodary"
v-model="formDialog.data.has_webhook"
></q-toggle>
<q-input <q-input
v-if="formDialog.data.has_webhook"
filled filled
dense dense
v-model="formDialog.data.webhook_url" v-model="formDialog.data.webhook_url"
@ -217,6 +223,24 @@
label="Webhook URL (optional)" label="Webhook URL (optional)"
hint="A URL to be called whenever this link gets used." hint="A URL to be called whenever this link gets used."
></q-input> ></q-input>
<q-input
v-if="formDialog.data.has_webhook"
filled
dense
v-model="formDialog.data.webhook_headers"
type="text"
label="Webhook Headers (optional)"
hint="Custom data as JSON string, send headers along with the webhook."
></q-input>
<q-input
v-if="formDialog.data.has_webhook"
filled
dense
v-model="formDialog.data.webhook_body"
type="text"
label="Webhook custom data (optional)"
hint="Custom data as JSON string, will get posted along with webhook 'body' field."
></q-input>
<q-list> <q-list>
<q-item tag="label" class="rounded-borders"> <q-item tag="label" class="rounded-borders">
<q-item-section avatar> <q-item-section avatar>