Merge branch 'master' into Jukebox

This commit is contained in:
Ben Arc 2021-05-31 09:13:30 +01:00
commit 3a8a8f95e9
17 changed files with 122 additions and 39 deletions

View file

@ -22,7 +22,7 @@ LNbits is a very simple Python server that sits on top of any funding source, an
* Fallback wallet for the LNURL scheme
* Instant wallet for LN demonstrations
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularily.
LNbits can run on top of any lightning-network funding source, currently there is support for LND, c-lightning, Spark, LNpay, OpenNode, lntxbot, with more being added regularly.
See [lnbits.org](https://lnbits.org) for more detailed documentation.

View file

@ -13,6 +13,7 @@ Download this repo and install the dependencies:
```sh
git clone https://github.com/lnbits/lnbits.git
cd lnbits/
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
cp .env.example .env

View file

@ -1,6 +1,7 @@
import sys
import importlib
import warnings
import importlib
import traceback
from quart import g
from quart_trio import QuartTrio
@ -23,7 +24,6 @@ from .tasks import (
invoice_listener,
internal_invoice_listener,
webhook_handler,
grab_app_for_later,
)
from .settings import WALLET
@ -48,7 +48,7 @@ def create_app(config_object="lnbits.settings") -> QuartTrio:
register_commands(app)
register_request_hooks(app)
register_async_tasks(app)
grab_app_for_later(app)
register_exception_handlers(app)
return app
@ -114,10 +114,6 @@ def register_filters(app: QuartTrio):
def register_request_hooks(app: QuartTrio):
"""Open the core db for each request so everything happens in a big transaction"""
@app.before_request
async def before_request():
g.nursery = app.nursery
@app.after_request
async def set_secure_headers(response):
secure_headers.quart(response)
@ -139,3 +135,21 @@ def register_async_tasks(app):
@app.after_serving
async def stop_listeners():
pass
def register_exception_handlers(app):
@app.errorhandler(Exception)
async def basic_error(err):
etype, value, tb = sys.exc_info()
traceback.print_exception(etype, err, tb)
exc = traceback.format_exc()
return (
"\n\n".join(
[
"LNbits internal error!",
exc,
"If you believe this shouldn't be an error please bring it up on https://t.me/lnbits",
]
),
500,
)

View file

@ -267,12 +267,23 @@ async def get_payments(
async def delete_expired_invoices(
conn: Optional[Connection] = None,
) -> None:
# first we delete all invoices older than one month
await (conn or db).execute(
"""
DELETE FROM apipayments
WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 2592000
"""
)
# then we delete all expired invoices, checking one by one
rows = await (conn or db).fetchall(
"""
SELECT bolt11
FROM apipayments
WHERE pending = 1 AND amount > 0 AND time < strftime('%s', 'now') - 86400
"""
WHERE pending = 1
AND bolt11 IS NOT NULL
AND amount > 0 AND time < strftime('%s', 'now') - 86400
"""
)
for (payment_request,) in rows:
try:

View file

@ -129,7 +129,7 @@
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
<a
class="inherit"
:href="['/', props.row.tag, '?usr=', user.id].join('')"
:href="['/', props.row.tag, '/?usr=', user.id].join('')"
>
#{{ props.row.tag }}
</a>

View file

@ -3,7 +3,7 @@ import json
import lnurl # type: ignore
import httpx
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
from quart import g, jsonify, make_response, url_for
from quart import g, current_app, jsonify, make_response, url_for
from http import HTTPStatus
from binascii import unhexlify
from typing import Dict, Union
@ -310,8 +310,8 @@ async def api_payments_sse():
await send_event.send(("keepalive", ""))
await trio.sleep(25)
g.nursery.start_soon(payment_received)
g.nursery.start_soon(repeat_keepalive)
current_app.nursery.start_soon(payment_received)
current_app.nursery.start_soon(repeat_keepalive)
async def send_events():
try:

View file

@ -2,6 +2,7 @@ from os import path
from http import HTTPStatus
from quart import (
g,
current_app,
abort,
jsonify,
request,
@ -154,7 +155,7 @@ async def lnurl_full_withdraw_callback():
async def pay():
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
g.nursery.start_soon(pay)
current_app.nursery.start_soon(pay)
balance_notify = request.args.get("balanceNotify")
if balance_notify:
@ -197,7 +198,7 @@ async def lnurlwallet():
user = await get_user(account.id, conn=conn)
wallet = await create_wallet(user_id=user.id, conn=conn)
g.nursery.start_soon(
current_app.nursery.start_soon(
redeem_lnurl_withdraw,
wallet.id,
request.args.get("lightning"),

View file

@ -61,7 +61,7 @@ async def lnurl_callback(track_id):
if not track:
return jsonify({"status": "ERROR", "reason": "Couldn't find track."})
amount_received = int(request.args.get("amount"))
amount_received = int(request.args.get("amount") or 0)
if amount_received < track.min_sendable:
return (

View file

@ -54,7 +54,7 @@ async def api_lnurl_callback(link_id):
min = link.min * 1000
max = link.max * 1000
amount_received = int(request.args.get("amount"))
amount_received = int(request.args.get("amount") or 0)
if amount_received < min:
return (
jsonify(

View file

@ -87,6 +87,9 @@ async def api_link_create_or_update(link_id=None):
round(g.data["min"]) != g.data["min"] or round(g.data["max"]) != g.data["max"]
):
return jsonify({"message": "Must use full satoshis."}), HTTPStatus.BAD_REQUEST
if g.data["success_url"][:8] != "https://":
return jsonify({"message": "Success URL must be secure https://..."}), HTTPStatus.BAD_REQUEST
if link_id:
link = await get_pay_link(link_id)

View file

@ -49,7 +49,7 @@ async def lnurl_callback(item_id):
min = price * 995
max = price * 1010
amount_received = int(request.args.get("amount"))
amount_received = int(request.args.get("amount") or 0)
if amount_received < min:
return jsonify(
LnurlErrorResponse(

View file

@ -452,7 +452,10 @@
id: this.domainDialog.data.wallet
})
var data = this.domainDialog.data
data.allowed_record_types = data.allowed_record_types.join(', ')
data.allowed_record_types =
typeof data.allowed_record_types === 'string'
? data.allowed_record_types
: data.allowed_record_types.join(', ')
console.log(this.domainDialog)
if (data.id) {
this.updateDomain(wallet, data)

View file

@ -17,7 +17,11 @@ from .models import Users, Wallets
async def create_usermanager_user(
user_name: str, wallet_name: str, admin_id: str
user_name: str,
wallet_name: str,
admin_id: str,
email: Optional[str] = None,
password: Optional[str] = None,
) -> Users:
account = await create_account()
user = await get_user(account.id)
@ -27,10 +31,10 @@ async def create_usermanager_user(
await db.execute(
"""
INSERT INTO users (id, name, admin)
VALUES (?, ?, ?)
INSERT INTO users (id, name, admin, email, password)
VALUES (?, ?, ?, ?, ?)
""",
(user.id, user_name, admin_id),
(user.id, user_name, admin_id, email, password),
)
await db.execute(

View file

@ -48,6 +48,26 @@
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET user">
<q-card>
<q-card-section>
<code
><span class="text-light-blue">GET</span>
/usermanager/api/v1/users/&lt;user_id&gt;</code
>
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
</h5>
<code>JSON list of users</code>
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
<code
>curl -X GET {{ request.url_root }}api/v1/users/&lt;user_id&gt; -H
"X-Api-Key: {{ g.user.wallets[0].inkey }}"
</code>
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="api" dense expand-separator label="GET wallets">
<q-card>
<q-card-section>
@ -114,7 +134,8 @@
</h5>
<code
>{"admin_id": &lt;string&gt;, "user_name": &lt;string&gt;,
"wallet_name": &lt;string&gt;}</code
"wallet_name": &lt;string&gt;,"email": &lt;Optional string&gt;
,"password": &lt;Optional string&gt;}</code
>
<h5 class="text-caption q-mt-sm q-mb-none">
Returns 201 CREATED (application/json)
@ -128,7 +149,8 @@
<code
>curl -X POST {{ request.url_root }}api/v1/users -d '{"admin_id": "{{
g.user.id }}", "wallet_name": &lt;string&gt;, "user_name":
&lt;string&gt;}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
&lt;string&gt;, "email": &lt;Optional string&gt;, "password": &lt;
Optional string&gt;}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
"Content-type: application/json"
</code>
</q-card-section>

View file

@ -157,6 +157,18 @@
v-model.trim="userDialog.data.walname"
label="Initial wallet name"
></q-input>
<q-input
filled
dense
v-model.trim="userDialog.data.email"
label="Email"
></q-input>
<q-input
filled
dense
v-model.trim="userDialog.data.password"
label="Password"
></q-input>
<q-btn
unelevated
@ -224,7 +236,14 @@
usersTable: {
columns: [
{name: 'id', align: 'left', label: 'ID', field: 'id'},
{name: 'name', align: 'left', label: 'Username', field: 'name'}
{name: 'name', align: 'left', label: 'Username', field: 'name'},
{name: 'email', align: 'left', label: 'Email', field: 'email'},
{
name: 'password',
align: 'left',
label: 'Password',
field: 'password'
}
],
pagination: {
rowsPerPage: 10
@ -299,7 +318,9 @@
var data = {
admin_id: this.g.user.id,
user_name: this.userDialog.data.usrname,
wallet_name: this.userDialog.data.walname
wallet_name: this.userDialog.data.walname,
email: this.userDialog.data.email,
password: this.userDialog.data.password
}
}

View file

@ -33,19 +33,29 @@ async def api_usermanager_users():
)
@usermanager_ext.route("/api/v1/users/<user_id>", methods=["GET"])
@api_check_wallet_key(key_type="invoice")
async def api_usermanager_user(user_id):
user = await get_usermanager_user(user_id)
return (
jsonify(user._asdict()),
HTTPStatus.OK,
)
@usermanager_ext.route("/api/v1/users", methods=["POST"])
@api_check_wallet_key(key_type="invoice")
@api_validate_post_request(
schema={
"admin_id": {"type": "string", "empty": False, "required": True},
"user_name": {"type": "string", "empty": False, "required": True},
"wallet_name": {"type": "string", "empty": False, "required": True},
"admin_id": {"type": "string", "empty": False, "required": True},
"email": {"type": "string", "required": False},
"password": {"type": "string", "required": False},
}
)
async def api_usermanager_users_create():
user = await create_usermanager_user(
g.data["user_name"], g.data["wallet_name"], g.data["admin_id"]
)
user = await create_usermanager_user(**g.data)
return jsonify(user._asdict()), HTTPStatus.CREATED

View file

@ -13,13 +13,6 @@ from lnbits.core.crud import (
)
from lnbits.core.services import redeem_lnurl_withdraw
main_app: Optional[QuartTrio] = None
def grab_app_for_later(app: QuartTrio):
global main_app
main_app = app
deferred_async: List[Callable] = []