Merge branch 'master' into Jukebox
This commit is contained in:
commit
3a8a8f95e9
17 changed files with 122 additions and 39 deletions
|
|
@ -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
|
* Fallback wallet for the LNURL scheme
|
||||||
* Instant wallet for LN demonstrations
|
* 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.
|
See [lnbits.org](https://lnbits.org) for more detailed documentation.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ Download this repo and install the dependencies:
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/lnbits/lnbits.git
|
git clone https://github.com/lnbits/lnbits.git
|
||||||
cd lnbits/
|
cd lnbits/
|
||||||
|
# ensure you have virtualenv installed, on debian/ubuntu 'apt install python3-venv' should work
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
./venv/bin/pip install -r requirements.txt
|
./venv/bin/pip install -r requirements.txt
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import sys
|
import sys
|
||||||
import importlib
|
|
||||||
import warnings
|
import warnings
|
||||||
|
import importlib
|
||||||
|
import traceback
|
||||||
|
|
||||||
from quart import g
|
from quart import g
|
||||||
from quart_trio import QuartTrio
|
from quart_trio import QuartTrio
|
||||||
|
|
@ -23,7 +24,6 @@ from .tasks import (
|
||||||
invoice_listener,
|
invoice_listener,
|
||||||
internal_invoice_listener,
|
internal_invoice_listener,
|
||||||
webhook_handler,
|
webhook_handler,
|
||||||
grab_app_for_later,
|
|
||||||
)
|
)
|
||||||
from .settings import WALLET
|
from .settings import WALLET
|
||||||
|
|
||||||
|
|
@ -48,7 +48,7 @@ def create_app(config_object="lnbits.settings") -> QuartTrio:
|
||||||
register_commands(app)
|
register_commands(app)
|
||||||
register_request_hooks(app)
|
register_request_hooks(app)
|
||||||
register_async_tasks(app)
|
register_async_tasks(app)
|
||||||
grab_app_for_later(app)
|
register_exception_handlers(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
@ -114,10 +114,6 @@ def register_filters(app: QuartTrio):
|
||||||
def register_request_hooks(app: QuartTrio):
|
def register_request_hooks(app: QuartTrio):
|
||||||
"""Open the core db for each request so everything happens in a big transaction"""
|
"""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
|
@app.after_request
|
||||||
async def set_secure_headers(response):
|
async def set_secure_headers(response):
|
||||||
secure_headers.quart(response)
|
secure_headers.quart(response)
|
||||||
|
|
@ -139,3 +135,21 @@ def register_async_tasks(app):
|
||||||
@app.after_serving
|
@app.after_serving
|
||||||
async def stop_listeners():
|
async def stop_listeners():
|
||||||
pass
|
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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -267,11 +267,22 @@ async def get_payments(
|
||||||
async def delete_expired_invoices(
|
async def delete_expired_invoices(
|
||||||
conn: Optional[Connection] = None,
|
conn: Optional[Connection] = None,
|
||||||
) -> 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(
|
rows = await (conn or db).fetchall(
|
||||||
"""
|
"""
|
||||||
SELECT bolt11
|
SELECT bolt11
|
||||||
FROM apipayments
|
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:
|
for (payment_request,) in rows:
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@
|
||||||
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
|
<q-badge v-if="props.row.tag" color="yellow" text-color="black">
|
||||||
<a
|
<a
|
||||||
class="inherit"
|
class="inherit"
|
||||||
:href="['/', props.row.tag, '?usr=', user.id].join('')"
|
:href="['/', props.row.tag, '/?usr=', user.id].join('')"
|
||||||
>
|
>
|
||||||
#{{ props.row.tag }}
|
#{{ props.row.tag }}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import json
|
||||||
import lnurl # type: ignore
|
import lnurl # type: ignore
|
||||||
import httpx
|
import httpx
|
||||||
from urllib.parse import urlparse, urlunparse, urlencode, parse_qs, ParseResult
|
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 http import HTTPStatus
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union
|
||||||
|
|
@ -310,8 +310,8 @@ async def api_payments_sse():
|
||||||
await send_event.send(("keepalive", ""))
|
await send_event.send(("keepalive", ""))
|
||||||
await trio.sleep(25)
|
await trio.sleep(25)
|
||||||
|
|
||||||
g.nursery.start_soon(payment_received)
|
current_app.nursery.start_soon(payment_received)
|
||||||
g.nursery.start_soon(repeat_keepalive)
|
current_app.nursery.start_soon(repeat_keepalive)
|
||||||
|
|
||||||
async def send_events():
|
async def send_events():
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from os import path
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from quart import (
|
from quart import (
|
||||||
g,
|
g,
|
||||||
|
current_app,
|
||||||
abort,
|
abort,
|
||||||
jsonify,
|
jsonify,
|
||||||
request,
|
request,
|
||||||
|
|
@ -154,7 +155,7 @@ async def lnurl_full_withdraw_callback():
|
||||||
async def pay():
|
async def pay():
|
||||||
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
|
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")
|
balance_notify = request.args.get("balanceNotify")
|
||||||
if balance_notify:
|
if balance_notify:
|
||||||
|
|
@ -197,7 +198,7 @@ async def lnurlwallet():
|
||||||
user = await get_user(account.id, conn=conn)
|
user = await get_user(account.id, conn=conn)
|
||||||
wallet = await create_wallet(user_id=user.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,
|
redeem_lnurl_withdraw,
|
||||||
wallet.id,
|
wallet.id,
|
||||||
request.args.get("lightning"),
|
request.args.get("lightning"),
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ async def lnurl_callback(track_id):
|
||||||
if not track:
|
if not track:
|
||||||
return jsonify({"status": "ERROR", "reason": "Couldn't find 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:
|
if amount_received < track.min_sendable:
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ async def api_lnurl_callback(link_id):
|
||||||
min = link.min * 1000
|
min = link.min * 1000
|
||||||
max = link.max * 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:
|
if amount_received < min:
|
||||||
return (
|
return (
|
||||||
jsonify(
|
jsonify(
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,9 @@ async def api_link_create_or_update(link_id=None):
|
||||||
):
|
):
|
||||||
return jsonify({"message": "Must use full satoshis."}), HTTPStatus.BAD_REQUEST
|
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:
|
if link_id:
|
||||||
link = await get_pay_link(link_id)
|
link = await get_pay_link(link_id)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ async def lnurl_callback(item_id):
|
||||||
min = price * 995
|
min = price * 995
|
||||||
max = price * 1010
|
max = price * 1010
|
||||||
|
|
||||||
amount_received = int(request.args.get("amount"))
|
amount_received = int(request.args.get("amount") or 0)
|
||||||
if amount_received < min:
|
if amount_received < min:
|
||||||
return jsonify(
|
return jsonify(
|
||||||
LnurlErrorResponse(
|
LnurlErrorResponse(
|
||||||
|
|
|
||||||
|
|
@ -452,7 +452,10 @@
|
||||||
id: this.domainDialog.data.wallet
|
id: this.domainDialog.data.wallet
|
||||||
})
|
})
|
||||||
var data = this.domainDialog.data
|
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)
|
console.log(this.domainDialog)
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
this.updateDomain(wallet, data)
|
this.updateDomain(wallet, data)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,11 @@ from .models import Users, Wallets
|
||||||
|
|
||||||
|
|
||||||
async def create_usermanager_user(
|
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:
|
) -> Users:
|
||||||
account = await create_account()
|
account = await create_account()
|
||||||
user = await get_user(account.id)
|
user = await get_user(account.id)
|
||||||
|
|
@ -27,10 +31,10 @@ async def create_usermanager_user(
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO users (id, name, admin)
|
INSERT INTO users (id, name, admin, email, password)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(user.id, user_name, admin_id),
|
(user.id, user_name, admin_id, email, password),
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.execute(
|
await db.execute(
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,26 @@
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-expansion-item>
|
</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/<user_id></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/<user_id> -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-expansion-item group="api" dense expand-separator label="GET wallets">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|
@ -114,7 +134,8 @@
|
||||||
</h5>
|
</h5>
|
||||||
<code
|
<code
|
||||||
>{"admin_id": <string>, "user_name": <string>,
|
>{"admin_id": <string>, "user_name": <string>,
|
||||||
"wallet_name": <string>}</code
|
"wallet_name": <string>,"email": <Optional string>
|
||||||
|
,"password": <Optional string>}</code
|
||||||
>
|
>
|
||||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||||
Returns 201 CREATED (application/json)
|
Returns 201 CREATED (application/json)
|
||||||
|
|
@ -128,7 +149,8 @@
|
||||||
<code
|
<code
|
||||||
>curl -X POST {{ request.url_root }}api/v1/users -d '{"admin_id": "{{
|
>curl -X POST {{ request.url_root }}api/v1/users -d '{"admin_id": "{{
|
||||||
g.user.id }}", "wallet_name": <string>, "user_name":
|
g.user.id }}", "wallet_name": <string>, "user_name":
|
||||||
<string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
|
<string>, "email": <Optional string>, "password": <
|
||||||
|
Optional string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H
|
||||||
"Content-type: application/json"
|
"Content-type: application/json"
|
||||||
</code>
|
</code>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,18 @@
|
||||||
v-model.trim="userDialog.data.walname"
|
v-model.trim="userDialog.data.walname"
|
||||||
label="Initial wallet name"
|
label="Initial wallet name"
|
||||||
></q-input>
|
></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
|
<q-btn
|
||||||
unelevated
|
unelevated
|
||||||
|
|
@ -224,7 +236,14 @@
|
||||||
usersTable: {
|
usersTable: {
|
||||||
columns: [
|
columns: [
|
||||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
{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: {
|
pagination: {
|
||||||
rowsPerPage: 10
|
rowsPerPage: 10
|
||||||
|
|
@ -299,7 +318,9 @@
|
||||||
var data = {
|
var data = {
|
||||||
admin_id: this.g.user.id,
|
admin_id: this.g.user.id,
|
||||||
user_name: this.userDialog.data.usrname,
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"])
|
@usermanager_ext.route("/api/v1/users", methods=["POST"])
|
||||||
@api_check_wallet_key(key_type="invoice")
|
@api_check_wallet_key(key_type="invoice")
|
||||||
@api_validate_post_request(
|
@api_validate_post_request(
|
||||||
schema={
|
schema={
|
||||||
"admin_id": {"type": "string", "empty": False, "required": True},
|
|
||||||
"user_name": {"type": "string", "empty": False, "required": True},
|
"user_name": {"type": "string", "empty": False, "required": True},
|
||||||
"wallet_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():
|
async def api_usermanager_users_create():
|
||||||
user = await create_usermanager_user(
|
user = await create_usermanager_user(**g.data)
|
||||||
g.data["user_name"], g.data["wallet_name"], g.data["admin_id"]
|
|
||||||
)
|
|
||||||
return jsonify(user._asdict()), HTTPStatus.CREATED
|
return jsonify(user._asdict()), HTTPStatus.CREATED
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,6 @@ from lnbits.core.crud import (
|
||||||
)
|
)
|
||||||
from lnbits.core.services import redeem_lnurl_withdraw
|
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] = []
|
deferred_async: List[Callable] = []
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue