Merge branch 'main' into diagon-alley

This commit is contained in:
Tiago Vasconcelos 2022-12-22 12:25:30 +00:00
commit 738f392270
18 changed files with 233 additions and 90 deletions

View file

@ -14,9 +14,6 @@ LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
# Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available # Enable Admin GUI, available for the first user in LNBITS_ADMIN_USERS if available
LNBITS_ADMIN_UI=false LNBITS_ADMIN_UI=false
# Restricts access, User IDs seperated by comma
LNBITS_ALLOWED_USERS=""
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
# Ad space description # Ad space description

View file

@ -7,6 +7,7 @@ on:
push: push:
tags: tags:
- "[0-9]+.[0-9]+.[0-9]+" - "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+-*" - "[0-9]+.[0-9]+.[0-9]+-*"
jobs: jobs:

View file

@ -1,4 +1,4 @@
FROM python:3.9-slim FROM python:3.10-slim
RUN apt-get clean RUN apt-get clean
RUN apt-get update RUN apt-get update

View file

@ -40,3 +40,33 @@ Allowed Users
============= =============
enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids enviroment variable: LNBITS_ALLOWED_USERS, comma-seperated list of user ids
By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend. By defining this users, LNbits will no longer be useable by the public, only defined users and admins can then access the LNbits frontend.
How to activate
=============
```
$ sudo systemctl stop lnbits.service
$ cd ~/lnbits-legend
$ sudo nano .env
```
-> set: `LNBITS_ADMIN_UI=true`
Now start LNbits once in the terminal window
```
$ poetry run lnbits
```
It will now show you the Super User Account:
`SUCCESS | ✔️ Access super user account at: https://127.0.0.1:5000/wallet?usr=5711d7..`
The `/wallet?usr=..` is your super user account. You just have to append it to your normal LNbits web domain.
After that you will find the __`Admin` / `Manage Server`__ between `Wallets` and `Extensions`
Here you can design the interface, it has TOPUP to fill wallets and you can restrict access rights to extensions only for admins or generally deactivated for everyone. You can make users admins or set up Allowed Users if you want to restrict access. And of course the classic settings of the .env file, e.g. to change the funding source wallet or set a charge fee.
Do not forget
```
sudo systemctl start lnbits.service
```
A little hint, if you set `RESET TO DEFAULTS`, then a new Super User Account will also be created. The old one is then no longer valid.

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

@ -2,8 +2,10 @@ import asyncio
import json import json
import httpx import httpx
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
@ -48,19 +50,21 @@ async def on_invoice_paid(payment: Payment) -> None:
if pay_link.webhook_headers: if pay_link.webhook_headers:
kwargs["headers"] = json.loads(pay_link.webhook_headers) kwargs["headers"] = json.loads(pay_link.webhook_headers)
r = await client.post(pay_link.webhook_url, **kwargs) r: httpx.Response = await client.post(pay_link.webhook_url, **kwargs)
await mark_webhook_sent(payment, r.status_code) await mark_webhook_sent(
except (httpx.ConnectError, httpx.RequestError): payment, r.status_code, r.is_success, r.reason_phrase, r.text
await mark_webhook_sent(payment, -1) )
except Exception as ex:
logger.error(ex)
await mark_webhook_sent(payment, -1, False, "Unexpected Error", str(ex))
async def mark_webhook_sent(payment: Payment, status: int) -> None: async def mark_webhook_sent(
payment.extra["wh_status"] = status payment: Payment, status: int, is_success: bool, reason_phrase="", text=""
) -> None:
payment.extra["wh_status"] = status # keep for backwards compability
payment.extra["wh_success"] = is_success
payment.extra["wh_message"] = reason_phrase
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

@ -10,12 +10,10 @@ from lnbits.core.crud import (
from lnbits.core.models import Payment from lnbits.core.models import Payment
from . import db from . import db
from .models import CreateUserData, Users, Wallets from .models import CreateUserData, User, Wallet
### Users
async def create_usermanager_user(data: CreateUserData) -> Users: async def create_usermanager_user(data: CreateUserData) -> User:
account = await create_account() account = await create_account()
user = await get_user(account.id) user = await get_user(account.id)
assert user, "Newly created user couldn't be retrieved" assert user, "Newly created user couldn't be retrieved"
@ -50,17 +48,17 @@ async def create_usermanager_user(data: CreateUserData) -> Users:
return user_created return user_created
async def get_usermanager_user(user_id: str) -> Optional[Users]: async def get_usermanager_user(user_id: str) -> Optional[User]:
row = await db.fetchone("SELECT * FROM usermanager.users WHERE id = ?", (user_id,)) row = await db.fetchone("SELECT * FROM usermanager.users WHERE id = ?", (user_id,))
return Users(**row) if row else None return User(**row) if row else None
async def get_usermanager_users(user_id: str) -> List[Users]: async def get_usermanager_users(user_id: str) -> List[User]:
rows = await db.fetchall( rows = await db.fetchall(
"SELECT * FROM usermanager.users WHERE admin = ?", (user_id,) "SELECT * FROM usermanager.users WHERE admin = ?", (user_id,)
) )
return [Users(**row) for row in rows] return [User(**row) for row in rows]
async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> None: async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> None:
@ -73,12 +71,9 @@ async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> Non
await db.execute("""DELETE FROM usermanager.wallets WHERE "user" = ?""", (user_id,)) await db.execute("""DELETE FROM usermanager.wallets WHERE "user" = ?""", (user_id,))
### Wallets
async def create_usermanager_wallet( async def create_usermanager_wallet(
user_id: str, wallet_name: str, admin_id: str user_id: str, wallet_name: str, admin_id: str
) -> Wallets: ) -> Wallet:
wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name) wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name)
await db.execute( await db.execute(
""" """
@ -92,28 +87,28 @@ async def create_usermanager_wallet(
return wallet_created return wallet_created
async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]: async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallet]:
row = await db.fetchone( row = await db.fetchone(
"SELECT * FROM usermanager.wallets WHERE id = ?", (wallet_id,) "SELECT * FROM usermanager.wallets WHERE id = ?", (wallet_id,)
) )
return Wallets(**row) if row else None return Wallet(**row) if row else None
async def get_usermanager_wallets(admin_id: str) -> Optional[Wallets]: async def get_usermanager_wallets(admin_id: str) -> List[Wallet]:
rows = await db.fetchall( rows = await db.fetchall(
"SELECT * FROM usermanager.wallets WHERE admin = ?", (admin_id,) "SELECT * FROM usermanager.wallets WHERE admin = ?", (admin_id,)
) )
return [Wallets(**row) for row in rows] return [Wallet(**row) for row in rows]
async def get_usermanager_users_wallets(user_id: str) -> Optional[Wallets]: async def get_usermanager_users_wallets(user_id: str) -> List[Wallet]:
rows = await db.fetchall( rows = await db.fetchall(
"""SELECT * FROM usermanager.wallets WHERE "user" = ?""", (user_id,) """SELECT * FROM usermanager.wallets WHERE "user" = ?""", (user_id,)
) )
return [Wallets(**row) for row in rows] return [Wallet(**row) for row in rows]
async def get_usermanager_wallet_transactions(wallet_id: str) -> Optional[Payment]: async def get_usermanager_wallet_transactions(wallet_id: str) -> List[Payment]:
return await get_payments( return await get_payments(
wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True
) )

View file

@ -19,7 +19,7 @@ class CreateUserWallet(BaseModel):
admin_id: str = Query(...) admin_id: str = Query(...)
class Users(BaseModel): class User(BaseModel):
id: str id: str
name: str name: str
admin: str admin: str
@ -27,7 +27,7 @@ class Users(BaseModel):
password: Optional[str] = None password: Optional[str] = None
class Wallets(BaseModel): class Wallet(BaseModel):
id: str id: str
admin: str admin: str
name: str name: str
@ -36,5 +36,5 @@ class Wallets(BaseModel):
inkey: str inkey: str
@classmethod @classmethod
def from_row(cls, row: Row) -> "Wallets": def from_row(cls, row: Row) -> "Wallet":
return cls(**dict(row)) return cls(**dict(row))

View file

@ -9,7 +9,9 @@ from . import usermanager_ext, usermanager_renderer
@usermanager_ext.get("/", response_class=HTMLResponse) @usermanager_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)): async def index(
request: Request, user: User = Depends(check_user_exists) # type: ignore
):
return usermanager_renderer().TemplateResponse( return usermanager_renderer().TemplateResponse(
"usermanager/index.html", {"request": request, "user": user.dict()} "usermanager/index.html", {"request": request, "user": user.dict()}
) )

View file

@ -23,25 +23,31 @@ from .crud import (
) )
from .models import CreateUserData, CreateUserWallet from .models import CreateUserData, CreateUserWallet
# Users
@usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK) @usermanager_ext.get("/api/v1/users", status_code=HTTPStatus.OK)
async def api_usermanager_users(wallet: WalletTypeInfo = Depends(require_admin_key)): async def api_usermanager_users(
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
):
user_id = wallet.wallet.user user_id = wallet.wallet.user
return [user.dict() for user in await get_usermanager_users(user_id)] return [user.dict() for user in await get_usermanager_users(user_id)]
@usermanager_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK) @usermanager_ext.get(
async def api_usermanager_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)): "/api/v1/users/{user_id}",
status_code=HTTPStatus.OK,
dependencies=[Depends(get_key_type)],
)
async def api_usermanager_user(user_id):
user = await get_usermanager_user(user_id) user = await get_usermanager_user(user_id)
return user.dict() return user.dict() if user else None
@usermanager_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED) @usermanager_ext.post(
async def api_usermanager_users_create( "/api/v1/users",
data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) status_code=HTTPStatus.CREATED,
): dependencies=[Depends(get_key_type)],
)
async def api_usermanager_users_create(data: CreateUserData):
user = await create_usermanager_user(data) user = await create_usermanager_user(data)
full = user.dict() full = user.dict()
full["wallets"] = [ full["wallets"] = [
@ -50,11 +56,12 @@ async def api_usermanager_users_create(
return full return full
@usermanager_ext.delete("/api/v1/users/{user_id}") @usermanager_ext.delete(
"/api/v1/users/{user_id}", dependencies=[Depends(require_admin_key)]
)
async def api_usermanager_users_delete( async def api_usermanager_users_delete(
user_id, user_id,
delete_core: bool = Query(True), delete_core: bool = Query(True),
wallet: WalletTypeInfo = Depends(require_admin_key),
): ):
user = await get_usermanager_user(user_id) user = await get_usermanager_user(user_id)
if not user: if not user:
@ -84,10 +91,8 @@ async def api_usermanager_activate_extension(
# Wallets # Wallets
@usermanager_ext.post("/api/v1/wallets") @usermanager_ext.post("/api/v1/wallets", dependencies=[Depends(get_key_type)])
async def api_usermanager_wallets_create( async def api_usermanager_wallets_create(data: CreateUserWallet):
data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type)
):
user = await create_usermanager_wallet( user = await create_usermanager_wallet(
user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id
) )
@ -95,31 +100,33 @@ async def api_usermanager_wallets_create(
@usermanager_ext.get("/api/v1/wallets") @usermanager_ext.get("/api/v1/wallets")
async def api_usermanager_wallets(wallet: WalletTypeInfo = Depends(require_admin_key)): async def api_usermanager_wallets(
wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore
):
admin_id = wallet.wallet.user admin_id = wallet.wallet.user
return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)] return [wallet.dict() for wallet in await get_usermanager_wallets(admin_id)]
@usermanager_ext.get("/api/v1/transactions/{wallet_id}") @usermanager_ext.get(
async def api_usermanager_wallet_transactions( "/api/v1/transactions/{wallet_id}", dependencies=[Depends(get_key_type)]
wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) )
): async def api_usermanager_wallet_transactions(wallet_id):
return await get_usermanager_wallet_transactions(wallet_id) return await get_usermanager_wallet_transactions(wallet_id)
@usermanager_ext.get("/api/v1/wallets/{user_id}") @usermanager_ext.get(
async def api_usermanager_users_wallets( "/api/v1/wallets/{user_id}", dependencies=[Depends(require_admin_key)]
user_id, wallet: WalletTypeInfo = Depends(require_admin_key) )
): async def api_usermanager_users_wallets(user_id):
return [ return [
s_wallet.dict() for s_wallet in await get_usermanager_users_wallets(user_id) s_wallet.dict() for s_wallet in await get_usermanager_users_wallets(user_id)
] ]
@usermanager_ext.delete("/api/v1/wallets/{wallet_id}") @usermanager_ext.delete(
async def api_usermanager_wallets_delete( "/api/v1/wallets/{wallet_id}", dependencies=[Depends(require_admin_key)]
wallet_id, wallet: WalletTypeInfo = Depends(require_admin_key) )
): async def api_usermanager_wallets_delete(wallet_id):
get_wallet = await get_usermanager_wallet(wallet_id) get_wallet = await get_usermanager_wallet(wallet_id)
if not get_wallet: if not get_wallet:
raise HTTPException( raise HTTPException(

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>

View file

@ -110,7 +110,6 @@ exclude = """(?x)(
| ^lnbits/extensions/streamalerts. | ^lnbits/extensions/streamalerts.
| ^lnbits/extensions/tipjar. | ^lnbits/extensions/tipjar.
| ^lnbits/extensions/tpos. | ^lnbits/extensions/tpos.
| ^lnbits/extensions/usermanager.
| ^lnbits/extensions/watchonly. | ^lnbits/extensions/watchonly.
| ^lnbits/extensions/withdraw. | ^lnbits/extensions/withdraw.
| ^lnbits/wallets/lnd_grpc_files. | ^lnbits/wallets/lnd_grpc_files.