Merge branch 'main' into SCRUB
This commit is contained in:
+15
-13
@@ -1,36 +1,38 @@
|
||||
import asyncio
|
||||
|
||||
import uvloop
|
||||
from loguru import logger
|
||||
from starlette.requests import Request
|
||||
|
||||
from .commands import bundle_vendored, migrate_databases, transpile_scss
|
||||
from .commands import migrate_databases
|
||||
from .settings import (
|
||||
DEBUG,
|
||||
HOST,
|
||||
LNBITS_COMMIT,
|
||||
LNBITS_DATA_FOLDER,
|
||||
LNBITS_DATABASE_URL,
|
||||
LNBITS_SITE_TITLE,
|
||||
PORT,
|
||||
SERVICE_FEE,
|
||||
WALLET,
|
||||
)
|
||||
|
||||
uvloop.install()
|
||||
|
||||
asyncio.create_task(migrate_databases())
|
||||
transpile_scss()
|
||||
bundle_vendored()
|
||||
|
||||
from .app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
print(
|
||||
f"""Starting LNbits with
|
||||
- git version: {LNBITS_COMMIT}
|
||||
- site title: {LNBITS_SITE_TITLE}
|
||||
- debug: {DEBUG}
|
||||
- data folder: {LNBITS_DATA_FOLDER}
|
||||
- funding source: {WALLET.__class__.__name__}
|
||||
- service fee: {SERVICE_FEE}
|
||||
"""
|
||||
logger.info("Starting LNbits")
|
||||
logger.info(f"Host: {HOST}")
|
||||
logger.info(f"Port: {PORT}")
|
||||
logger.info(f"Debug: {DEBUG}")
|
||||
logger.info(f"Site title: {LNBITS_SITE_TITLE}")
|
||||
logger.info(f"Funding source: {WALLET.__class__.__name__}")
|
||||
logger.info(
|
||||
f"Database: {'PostgreSQL' if LNBITS_DATABASE_URL and LNBITS_DATABASE_URL.startswith('postgres://') else 'CockroachDB' if LNBITS_DATABASE_URL and LNBITS_DATABASE_URL.startswith('cockroachdb://') else 'SQLite'}"
|
||||
)
|
||||
logger.info(f"Data folder: {LNBITS_DATA_FOLDER}")
|
||||
logger.info(f"Git version: {LNBITS_COMMIT}")
|
||||
# logger.info(f"Service fee: {SERVICE_FEE}")
|
||||
|
||||
+83
-20
@@ -1,14 +1,18 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from loguru import logger
|
||||
|
||||
import lnbits.settings
|
||||
from lnbits.core.tasks import register_task_listeners
|
||||
@@ -39,10 +43,21 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||
"""Create application factory.
|
||||
:param config_object: The configuration object to use.
|
||||
"""
|
||||
app = FastAPI()
|
||||
app.mount("/static", StaticFiles(directory="lnbits/static"), name="static")
|
||||
configure_logger()
|
||||
|
||||
app = FastAPI(
|
||||
title="LNbits API",
|
||||
description="API for LNbits, the free and open source bitcoin wallet and accounts system with plugins.",
|
||||
license_info={
|
||||
"name": "MIT License",
|
||||
"url": "https://raw.githubusercontent.com/lnbits/lnbits-legend/main/LICENSE",
|
||||
},
|
||||
)
|
||||
app.mount("/static", StaticFiles(packages=[("lnbits", "static")]), name="static")
|
||||
app.mount(
|
||||
"/core/static", StaticFiles(directory="lnbits/core/static"), name="core_static"
|
||||
"/core/static",
|
||||
StaticFiles(packages=[("lnbits.core", "static")]),
|
||||
name="core_static",
|
||||
)
|
||||
|
||||
origins = ["*"]
|
||||
@@ -58,15 +73,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI:
|
||||
async def validation_exception_handler(
|
||||
request: Request, exc: RequestValidationError
|
||||
):
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html",
|
||||
{"request": request, "err": f"`{exc.errors()}` is not a valid UUID."},
|
||||
)
|
||||
# Only the browser sends "text/html" request
|
||||
# not fail proof, but everything else get's a JSON response
|
||||
|
||||
# return HTMLResponse(
|
||||
# status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
# content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
|
||||
# )
|
||||
if "text/html" in request.headers["accept"]:
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html",
|
||||
{"request": request, "err": f"{exc.errors()} is not a valid UUID."},
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=HTTPStatus.NO_CONTENT,
|
||||
content={"detail": exc.errors()},
|
||||
)
|
||||
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
||||
# app.add_middleware(ASGIProxyFix)
|
||||
@@ -88,14 +107,14 @@ def check_funding_source(app: FastAPI) -> None:
|
||||
error_message, balance = await WALLET.status()
|
||||
if not error_message:
|
||||
break
|
||||
warnings.warn(
|
||||
f" × The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
|
||||
logger.error(
|
||||
f"The backend for {WALLET.__class__.__name__} isn't working properly: '{error_message}'",
|
||||
RuntimeWarning,
|
||||
)
|
||||
print("Retrying connection to backend in 5 seconds...")
|
||||
logger.info("Retrying connection to backend in 5 seconds...")
|
||||
await asyncio.sleep(5)
|
||||
print(
|
||||
f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat."
|
||||
logger.info(
|
||||
f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat."
|
||||
)
|
||||
|
||||
|
||||
@@ -118,9 +137,10 @@ def register_routes(app: FastAPI) -> None:
|
||||
for s in ext_statics:
|
||||
app.mount(s["path"], s["app"], s["name"])
|
||||
|
||||
logger.trace(f"adding route for extension {ext_module}")
|
||||
app.include_router(ext_route)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
logger.error(str(e))
|
||||
raise ImportError(
|
||||
f"Please make sure that the extension `{ext.code}` follows conventions."
|
||||
)
|
||||
@@ -167,10 +187,53 @@ def register_async_tasks(app):
|
||||
def register_exception_handlers(app: FastAPI):
|
||||
@app.exception_handler(Exception)
|
||||
async def basic_error(request: Request, err):
|
||||
print("handled error", traceback.format_exc())
|
||||
logger.error("handled error", traceback.format_exc())
|
||||
logger.error("ERROR:", err)
|
||||
etype, _, tb = sys.exc_info()
|
||||
traceback.print_exception(etype, err, tb)
|
||||
exc = traceback.format_exc()
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": err}
|
||||
|
||||
if "text/html" in request.headers["accept"]:
|
||||
return template_renderer().TemplateResponse(
|
||||
"error.html", {"request": request, "err": err}
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=HTTPStatus.NO_CONTENT,
|
||||
content={"detail": err},
|
||||
)
|
||||
|
||||
|
||||
def configure_logger() -> None:
|
||||
logger.remove()
|
||||
log_level: str = "DEBUG" if lnbits.settings.DEBUG else "INFO"
|
||||
formatter = Formatter()
|
||||
logger.add(sys.stderr, level=log_level, format=formatter.format)
|
||||
|
||||
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
|
||||
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
|
||||
|
||||
|
||||
class Formatter:
|
||||
def __init__(self):
|
||||
self.padding = 0
|
||||
self.minimal_fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level}</level> | <level>{message}</level>\n"
|
||||
if lnbits.settings.DEBUG:
|
||||
self.fmt: str = "<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level: <4}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | <level>{message}</level>\n"
|
||||
else:
|
||||
self.fmt: str = self.minimal_fmt
|
||||
|
||||
def format(self, record):
|
||||
function = "{function}".format(**record)
|
||||
if function == "emit": # uvicorn logs
|
||||
return self.minimal_fmt
|
||||
return self.fmt
|
||||
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
try:
|
||||
level = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
logger.log(level, record.getMessage())
|
||||
|
||||
+14
-13
@@ -1,15 +1,16 @@
|
||||
import bitstring # type: ignore
|
||||
import re
|
||||
import hashlib
|
||||
from typing import List, NamedTuple, Optional
|
||||
from bech32 import bech32_encode, bech32_decode, CHARSET
|
||||
from ecdsa import SECP256k1, VerifyingKey # type: ignore
|
||||
from ecdsa.util import sigdecode_string # type: ignore
|
||||
from binascii import unhexlify
|
||||
import re
|
||||
import time
|
||||
from binascii import unhexlify
|
||||
from decimal import Decimal
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
import bitstring # type: ignore
|
||||
import embit
|
||||
import secp256k1
|
||||
from bech32 import CHARSET, bech32_decode, bech32_encode
|
||||
from ecdsa import SECP256k1, VerifyingKey # type: ignore
|
||||
from ecdsa.util import sigdecode_string # type: ignore
|
||||
|
||||
|
||||
class Route(NamedTuple):
|
||||
@@ -165,7 +166,7 @@ def lnencode(addr, privkey):
|
||||
if addr.amount:
|
||||
amount = Decimal(str(addr.amount))
|
||||
# We can only send down to millisatoshi.
|
||||
if amount * 10 ** 12 % 10:
|
||||
if amount * 10**12 % 10:
|
||||
raise ValueError(
|
||||
"Cannot encode {}: too many decimal places".format(addr.amount)
|
||||
)
|
||||
@@ -270,7 +271,7 @@ class LnAddr(object):
|
||||
def shorten_amount(amount):
|
||||
"""Given an amount in bitcoin, shorten it"""
|
||||
# Convert to pico initially
|
||||
amount = int(amount * 10 ** 12)
|
||||
amount = int(amount * 10**12)
|
||||
units = ["p", "n", "u", "m", ""]
|
||||
for unit in units:
|
||||
if amount % 1000 == 0:
|
||||
@@ -289,7 +290,7 @@ def _unshorten_amount(amount: str) -> int:
|
||||
# * `u` (micro): multiply by 0.000001
|
||||
# * `n` (nano): multiply by 0.000000001
|
||||
# * `p` (pico): multiply by 0.000000000001
|
||||
units = {"p": 10 ** 12, "n": 10 ** 9, "u": 10 ** 6, "m": 10 ** 3}
|
||||
units = {"p": 10**12, "n": 10**9, "u": 10**6, "m": 10**3}
|
||||
unit = str(amount)[-1]
|
||||
|
||||
# BOLT #11:
|
||||
@@ -348,9 +349,9 @@ def _trim_to_bytes(barr):
|
||||
|
||||
def _readable_scid(short_channel_id: int) -> str:
|
||||
return "{blockheight}x{transactionindex}x{outputindex}".format(
|
||||
blockheight=((short_channel_id >> 40) & 0xffffff),
|
||||
transactionindex=((short_channel_id >> 16) & 0xffffff),
|
||||
outputindex=(short_channel_id & 0xffff),
|
||||
blockheight=((short_channel_id >> 40) & 0xFFFFFF),
|
||||
transactionindex=((short_channel_id >> 16) & 0xFFFFFF),
|
||||
outputindex=(short_channel_id & 0xFFFF),
|
||||
)
|
||||
|
||||
|
||||
|
||||
+11
-8
@@ -1,16 +1,19 @@
|
||||
import asyncio
|
||||
import warnings
|
||||
import click
|
||||
import importlib
|
||||
import re
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
|
||||
from .db import SQLITE, POSTGRES, COCKROACH
|
||||
from .core import db as core_db, migrations as core_migrations
|
||||
import click
|
||||
from loguru import logger
|
||||
|
||||
from .core import db as core_db
|
||||
from .core import migrations as core_migrations
|
||||
from .db import COCKROACH, POSTGRES, SQLITE
|
||||
from .helpers import (
|
||||
get_valid_extensions,
|
||||
get_css_vendored,
|
||||
get_js_vendored,
|
||||
get_valid_extensions,
|
||||
url_for_vendored,
|
||||
)
|
||||
from .settings import LNBITS_PATH
|
||||
@@ -69,7 +72,7 @@ async def migrate_databases():
|
||||
if match:
|
||||
version = int(match.group(1))
|
||||
if version > current_versions.get(db_name, 0):
|
||||
print(f"running migration {db_name}.{version}")
|
||||
logger.debug(f"running migration {db_name}.{version}")
|
||||
await migrate(db)
|
||||
|
||||
if db.schema == None:
|
||||
@@ -110,4 +113,4 @@ async def migrate_databases():
|
||||
async with ext_db.connect() as ext_conn:
|
||||
await run_migration(ext_conn, ext_migrations)
|
||||
|
||||
print(" ✔️ All migrations done.")
|
||||
logger.info("✔️ All migrations done.")
|
||||
|
||||
+21
-9
@@ -1,15 +1,15 @@
|
||||
import json
|
||||
import datetime
|
||||
from uuid import uuid4
|
||||
from typing import List, Optional, Dict, Any
|
||||
import json
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.db import Connection, POSTGRES, COCKROACH
|
||||
from lnbits.db import COCKROACH, POSTGRES, Connection
|
||||
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
|
||||
|
||||
from . import db
|
||||
from .models import User, Wallet, Payment, BalanceCheck
|
||||
from .models import BalanceCheck, Payment, User, Wallet
|
||||
|
||||
# accounts
|
||||
# --------
|
||||
@@ -180,16 +180,28 @@ async def get_wallet_for_key(
|
||||
|
||||
|
||||
async def get_standalone_payment(
|
||||
checking_id_or_hash: str, conn: Optional[Connection] = None
|
||||
checking_id_or_hash: str,
|
||||
conn: Optional[Connection] = None,
|
||||
incoming: Optional[bool] = False,
|
||||
wallet_id: Optional[str] = None,
|
||||
) -> Optional[Payment]:
|
||||
clause: str = "checking_id = ? OR hash = ?"
|
||||
values = [checking_id_or_hash, checking_id_or_hash]
|
||||
if incoming:
|
||||
clause = f"({clause}) AND amount > 0"
|
||||
|
||||
if wallet_id:
|
||||
clause = f"({clause}) AND wallet = ?"
|
||||
values.append(wallet_id)
|
||||
|
||||
row = await (conn or db).fetchone(
|
||||
"""
|
||||
f"""
|
||||
SELECT *
|
||||
FROM apipayments
|
||||
WHERE checking_id = ? OR hash = ?
|
||||
WHERE {clause}
|
||||
LIMIT 1
|
||||
""",
|
||||
(checking_id_or_hash, checking_id_or_hash),
|
||||
tuple(values),
|
||||
)
|
||||
|
||||
return Payment.from_row(row) if row else None
|
||||
|
||||
+12
-7
@@ -1,12 +1,15 @@
|
||||
import json
|
||||
import hmac
|
||||
import hashlib
|
||||
from lnbits.helpers import url_for
|
||||
import hmac
|
||||
import json
|
||||
from sqlite3 import Row
|
||||
from typing import Dict, List, NamedTuple, Optional
|
||||
|
||||
from ecdsa import SECP256k1, SigningKey # type: ignore
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from typing import List, NamedTuple, Optional, Dict
|
||||
from sqlite3 import Row
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.helpers import url_for
|
||||
from lnbits.settings import WALLET
|
||||
|
||||
|
||||
@@ -142,10 +145,12 @@ class Payment(BaseModel):
|
||||
status = await WALLET.get_invoice_status(self.checking_id)
|
||||
|
||||
if self.is_out and status.failed:
|
||||
print(f" - deleting outgoing failed payment {self.checking_id}: {status}")
|
||||
logger.info(
|
||||
f" - deleting outgoing failed payment {self.checking_id}: {status}"
|
||||
)
|
||||
await self.delete()
|
||||
elif not status.pending:
|
||||
print(
|
||||
logger.info(
|
||||
f" - marking '{'in' if self.is_in else 'out'}' {self.checking_id} as not pending anymore: {status}"
|
||||
)
|
||||
await self.set_pending(status.pending)
|
||||
|
||||
+34
-10
@@ -6,14 +6,22 @@ from typing import Dict, Optional, Tuple
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import httpx
|
||||
from fastapi import Depends
|
||||
from lnurl import LnurlErrorResponse
|
||||
from lnurl import decode as decode_lnurl # type: ignore
|
||||
from loguru import logger
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.db import Connection
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import WALLET
|
||||
from lnbits.settings import FAKE_WALLET, WALLET
|
||||
from lnbits.wallets.base import PaymentResponse, PaymentStatus
|
||||
|
||||
from . import db
|
||||
@@ -48,15 +56,19 @@ async def create_invoice(
|
||||
description_hash: Optional[bytes] = None,
|
||||
extra: Optional[Dict] = None,
|
||||
webhook: Optional[str] = None,
|
||||
internal: Optional[bool] = False,
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Tuple[str, str]:
|
||||
invoice_memo = None if description_hash else memo
|
||||
|
||||
ok, checking_id, payment_request, error_message = await WALLET.create_invoice(
|
||||
# use the fake wallet if the invoice is for internal use only
|
||||
wallet = FAKE_WALLET if internal else WALLET
|
||||
|
||||
ok, checking_id, payment_request, error_message = await wallet.create_invoice(
|
||||
amount=amount, memo=invoice_memo, description_hash=description_hash
|
||||
)
|
||||
if not ok:
|
||||
raise InvoiceFailure(error_message or "Unexpected backend error.")
|
||||
raise InvoiceFailure(error_message or "unexpected backend error.")
|
||||
|
||||
invoice = bolt11.decode(payment_request)
|
||||
|
||||
@@ -120,6 +132,7 @@ async def pay_invoice(
|
||||
# check_internal() returns the checking_id of the invoice we're waiting for
|
||||
internal_checking_id = await check_internal(invoice.payment_hash, conn=conn)
|
||||
if internal_checking_id:
|
||||
logger.debug(f"creating temporary internal payment with id {internal_id}")
|
||||
# create a new payment from this wallet
|
||||
await create_payment(
|
||||
checking_id=internal_id,
|
||||
@@ -129,6 +142,7 @@ async def pay_invoice(
|
||||
**payment_kwargs,
|
||||
)
|
||||
else:
|
||||
logger.debug(f"creating temporary payment with id {temp_id}")
|
||||
# create a temporary payment here so we can check if
|
||||
# the balance is enough in the next step
|
||||
await create_payment(
|
||||
@@ -142,6 +156,7 @@ async def pay_invoice(
|
||||
wallet = await get_wallet(wallet_id, conn=conn)
|
||||
assert wallet
|
||||
if wallet.balance_msat < 0:
|
||||
logger.debug("balance is too low, deleting temporary payment")
|
||||
if not internal_checking_id and wallet.balance_msat > -fee_reserve_msat:
|
||||
raise PaymentFailure(
|
||||
f"You must reserve at least 1% ({round(fee_reserve_msat/1000)} sat) to cover potential routing fees."
|
||||
@@ -149,6 +164,7 @@ async def pay_invoice(
|
||||
raise PermissionError("Insufficient balance.")
|
||||
|
||||
if internal_checking_id:
|
||||
logger.debug(f"marking temporary payment as not pending {internal_checking_id}")
|
||||
# mark the invoice from the other side as not pending anymore
|
||||
# so the other side only has access to his new money when we are sure
|
||||
# the payer has enough to deduct from
|
||||
@@ -163,11 +179,14 @@ async def pay_invoice(
|
||||
|
||||
await internal_invoice_queue.put(internal_checking_id)
|
||||
else:
|
||||
logger.debug(f"backend: sending payment {temp_id}")
|
||||
# actually pay the external invoice
|
||||
payment: PaymentResponse = await WALLET.pay_invoice(
|
||||
payment_request, fee_reserve_msat
|
||||
)
|
||||
logger.debug(f"backend: pay_invoice finished {temp_id}")
|
||||
if payment.checking_id:
|
||||
logger.debug(f"creating final payment {payment.checking_id}")
|
||||
async with db.connect() as conn:
|
||||
await create_payment(
|
||||
checking_id=payment.checking_id,
|
||||
@@ -177,15 +196,18 @@ async def pay_invoice(
|
||||
conn=conn,
|
||||
**payment_kwargs,
|
||||
)
|
||||
logger.debug(f"deleting temporary payment {temp_id}")
|
||||
await delete_payment(temp_id, conn=conn)
|
||||
else:
|
||||
logger.debug(f"backend payment failed, no checking_id {temp_id}")
|
||||
async with db.connect() as conn:
|
||||
logger.debug(f"deleting temporary payment {temp_id}")
|
||||
await delete_payment(temp_id, conn=conn)
|
||||
raise PaymentFailure(
|
||||
payment.error_message
|
||||
or "Payment failed, but backend didn't give us an error message."
|
||||
)
|
||||
|
||||
logger.debug(f"payment successful {payment.checking_id}")
|
||||
return invoice.payment_hash
|
||||
|
||||
|
||||
@@ -216,7 +238,7 @@ async def redeem_lnurl_withdraw(
|
||||
conn=conn,
|
||||
)
|
||||
except:
|
||||
print(
|
||||
logger.warning(
|
||||
f"failed to create invoice on redeem_lnurl_withdraw from {lnurl}. params: {res}"
|
||||
)
|
||||
return None
|
||||
@@ -243,12 +265,14 @@ async def redeem_lnurl_withdraw(
|
||||
|
||||
|
||||
async def perform_lnurlauth(
|
||||
callback: str, conn: Optional[Connection] = None
|
||||
callback: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Optional[LnurlErrorResponse]:
|
||||
cb = urlparse(callback)
|
||||
|
||||
k1 = unhexlify(parse_qs(cb.query)["k1"][0])
|
||||
key = g().wallet.lnurlauth_key(cb.netloc)
|
||||
key = wallet.wallet.lnurlauth_key(cb.netloc)
|
||||
|
||||
def int_to_bytes_suitable_der(x: int) -> bytes:
|
||||
"""for strict DER we need to encode the integer with some quirks"""
|
||||
@@ -325,11 +349,11 @@ async def check_invoice_status(
|
||||
if not payment.pending:
|
||||
return status
|
||||
if payment.is_out and status.failed:
|
||||
print(f" - deleting outgoing failed payment {payment.checking_id}: {status}")
|
||||
logger.info(f"deleting outgoing failed payment {payment.checking_id}: {status}")
|
||||
await payment.delete()
|
||||
elif not status.pending:
|
||||
print(
|
||||
f" - marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
|
||||
logger.info(
|
||||
f"marking '{'in' if payment.is_in else 'out'}' {payment.checking_id} as not pending anymore: {status}"
|
||||
)
|
||||
await payment.set_pending(status.pending)
|
||||
return status
|
||||
|
||||
@@ -1,4 +1,36 @@
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
data: function () {
|
||||
return {
|
||||
searchTerm: '',
|
||||
filteredExtensions: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.filteredExtensions = this.g.extensions
|
||||
},
|
||||
watch: {
|
||||
searchTerm(term) {
|
||||
// Reset the filter
|
||||
this.filteredExtensions = this.g.extensions
|
||||
if (term !== '') {
|
||||
// Filter the extensions list
|
||||
function extensionNameContains(searchTerm) {
|
||||
return function (extension) {
|
||||
return (
|
||||
extension.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
extension.shortDescription
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.filteredExtensions = this.filteredExtensions.filter(
|
||||
extensionNameContains(term)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
mixins: [windowMixin]
|
||||
})
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
// the cache version gets updated every time there is a new deployment
|
||||
const CACHE_VERSION = 1
|
||||
const CURRENT_CACHE = `lnbits-${CACHE_VERSION}-`
|
||||
|
||||
const getApiKey = request => {
|
||||
return request.headers.get('X-Api-Key') || 'none'
|
||||
}
|
||||
|
||||
// on activation we clean up the previously registered service workers
|
||||
self.addEventListener('activate', evt =>
|
||||
evt.waitUntil(
|
||||
caches.keys().then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
const currentCacheVersion = cacheName.split('-').slice(-2, 2)
|
||||
if (currentCacheVersion !== CACHE_VERSION) {
|
||||
return caches.delete(cacheName)
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// The fetch handler serves responses for same-origin resources from a cache.
|
||||
// If no response is found, it populates the runtime cache with the response
|
||||
// from the network before returning it to the page.
|
||||
self.addEventListener('fetch', event => {
|
||||
// Skip cross-origin requests, like those for Google Analytics.
|
||||
if (
|
||||
event.request.url.startsWith(self.location.origin) &&
|
||||
event.request.method == 'GET'
|
||||
) {
|
||||
// Open the cache
|
||||
event.respondWith(
|
||||
caches.open(CURRENT_CACHE + getApiKey(event.request)).then(cache => {
|
||||
// Go to the network first
|
||||
return fetch(event.request)
|
||||
.then(fetchedResponse => {
|
||||
cache.put(event.request, fetchedResponse.clone())
|
||||
|
||||
return fetchedResponse
|
||||
})
|
||||
.catch(() => {
|
||||
// If the network is unavailable, get
|
||||
return cache.match(event.request.url)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -702,3 +702,11 @@ new Vue({
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
if (navigator.serviceWorker != null) {
|
||||
navigator.serviceWorker
|
||||
.register('/service-worker.js')
|
||||
.then(function (registration) {
|
||||
console.log('Registered events at scope: ', registration.scope)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import asyncio
|
||||
import httpx
|
||||
from typing import List
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
from . import db
|
||||
@@ -20,7 +22,7 @@ async def register_task_listeners():
|
||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
while True:
|
||||
payment = await invoice_paid_queue.get()
|
||||
|
||||
logger.debug("received invoice paid event")
|
||||
# send information to sse channel
|
||||
await dispatch_invoice_listener(payment)
|
||||
|
||||
@@ -44,7 +46,7 @@ async def dispatch_invoice_listener(payment: Payment):
|
||||
try:
|
||||
send_channel.put_nowait(payment)
|
||||
except asyncio.QueueFull:
|
||||
print("removing sse listener", send_channel)
|
||||
logger.debug("removing sse listener", send_channel)
|
||||
api_invoice_listeners.remove(send_channel)
|
||||
|
||||
|
||||
@@ -52,6 +54,7 @@ async def dispatch_webhook(payment: Payment):
|
||||
async with httpx.AsyncClient() as client:
|
||||
data = payment.dict()
|
||||
try:
|
||||
logger.debug("sending webhook", payment.webhook)
|
||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||
await mark_webhook_sent(payment, r.status_code)
|
||||
except (httpx.ConnectError, httpx.RequestError):
|
||||
|
||||
@@ -48,7 +48,9 @@
|
||||
<code>{"X-Api-Key": "<i>{{ wallet.inkey }}</i>"}</code><br />
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Body (application/json)</h5>
|
||||
<code
|
||||
>{"out": false, "amount": <int>, "memo": <string>, "unit": <string>, "webhook": <url:string>}</code
|
||||
>{"out": false, "amount": <int>, "memo": <string>, "unit":
|
||||
<string>, "webhook": <url:string>, "internal":
|
||||
<bool>}</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">
|
||||
Returns 201 CREATED (application/json)
|
||||
@@ -61,8 +63,8 @@
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false,
|
||||
"amount": <int>, "memo": <string>, "webhook":
|
||||
<url:string>, "unit": <string>}' -H "X-Api-Key: <i>{{ wallet.inkey }}</i>" -H
|
||||
"Content-type: application/json"</code
|
||||
<url:string>, "unit": <string>}' -H "X-Api-Key:
|
||||
<i>{{ wallet.inkey }}</i>" -H "Content-type: application/json"</code
|
||||
>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -2,10 +2,23 @@
|
||||
%} {% block scripts %} {{ window_vars(user) }}
|
||||
<script src="/core/static/js/extensions.js"></script>
|
||||
{% endblock %} {% block page %}
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-sm-3 col-xs-8 q-ml-auto">
|
||||
<q-input v-model="searchTerm" label="Search extensions">
|
||||
<q-icon
|
||||
v-if="searchTerm !== ''"
|
||||
name="close"
|
||||
@click="searchTerm = ''"
|
||||
class="cursor-pointer q-mt-lg"
|
||||
/>
|
||||
</q-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row q-col-gutter-md">
|
||||
<div
|
||||
class="col-6 col-md-4 col-lg-3"
|
||||
v-for="extension in g.extensions"
|
||||
v-for="extension in filteredExtensions"
|
||||
:key="extension.code"
|
||||
>
|
||||
<q-card>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
<!---->
|
||||
{% block scripts %} {{ window_vars(user, wallet) }}
|
||||
<script src="/core/static/js/wallet.js"></script>
|
||||
<link rel="manifest" href="/manifest/{{ user.id }}.webmanifest" />
|
||||
{% endblock %}
|
||||
<!---->
|
||||
{% block title %} {{ wallet.name }} - {{ SITE_TITLE }} {% endblock %}
|
||||
@@ -709,7 +708,7 @@
|
||||
|
||||
<q-dialog v-model="disclaimerDialog.show">
|
||||
<q-card class="q-pa-lg">
|
||||
<h6 class="q-my-md text-deep-purple">Warning</h6>
|
||||
<h6 class="q-my-md text-primary">Warning</h6>
|
||||
<p>
|
||||
Login functionality to be released in v0.2, for now,
|
||||
<strong
|
||||
|
||||
+85
-41
@@ -7,26 +7,23 @@ from typing import Dict, List, Optional, Union
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
import httpx
|
||||
from fastapi import Header, Query, Request
|
||||
from fastapi import Depends, Header, Query, Request
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.param_functions import Depends
|
||||
from fastapi.params import Body
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import Field
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from lnbits import bolt11, lnurl
|
||||
from lnbits.bolt11 import Invoice
|
||||
from lnbits.core.models import Payment, Wallet
|
||||
from lnbits.decorators import (
|
||||
WalletAdminKeyChecker,
|
||||
WalletInvoiceKeyChecker,
|
||||
WalletTypeInfo,
|
||||
get_key_type,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import url_for, urlsafe_short_hash
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import LNBITS_ADMIN_USERS, LNBITS_SITE_TITLE
|
||||
from lnbits.utils.exchange_rates import (
|
||||
currencies,
|
||||
@@ -110,16 +107,29 @@ async def api_update_wallet(
|
||||
|
||||
|
||||
@core_app.get("/api/v1/payments")
|
||||
async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
|
||||
async def api_payments(
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
):
|
||||
pendingPayments = await get_payments(
|
||||
wallet_id=wallet.wallet.id, pending=True, exclude_uncheckable=True
|
||||
wallet_id=wallet.wallet.id,
|
||||
pending=True,
|
||||
exclude_uncheckable=True,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
for payment in pendingPayments:
|
||||
await check_invoice_status(
|
||||
wallet_id=payment.wallet_id, payment_hash=payment.payment_hash
|
||||
)
|
||||
return await get_payments(wallet_id=wallet.wallet.id, pending=True, complete=True)
|
||||
return await get_payments(
|
||||
wallet_id=wallet.wallet.id,
|
||||
pending=True,
|
||||
complete=True,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
|
||||
class CreateInvoiceData(BaseModel):
|
||||
@@ -132,6 +142,7 @@ class CreateInvoiceData(BaseModel):
|
||||
lnurl_balance_check: Optional[str] = None
|
||||
extra: Optional[dict] = None
|
||||
webhook: Optional[str] = None
|
||||
internal: Optional[bool] = False
|
||||
bolt11: Optional[str] = None
|
||||
|
||||
|
||||
@@ -145,6 +156,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
if data.unit == "sat":
|
||||
amount = int(data.amount)
|
||||
else:
|
||||
assert data.unit is not None, "unit not set"
|
||||
price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit)
|
||||
amount = price_in_sats
|
||||
|
||||
@@ -157,6 +169,7 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
description_hash=description_hash,
|
||||
extra=data.extra,
|
||||
webhook=data.webhook,
|
||||
internal=data.internal,
|
||||
conn=conn,
|
||||
)
|
||||
except InvoiceFailure as e:
|
||||
@@ -169,6 +182,9 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet):
|
||||
lnurl_response: Union[None, bool, str] = None
|
||||
if data.lnurl_callback:
|
||||
if "lnurl_balance_check" in data:
|
||||
assert (
|
||||
data.lnurl_balance_check is not None
|
||||
), "lnurl_balance_check is required"
|
||||
save_balance_check(wallet.id, data.lnurl_balance_check)
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
@@ -231,12 +247,9 @@ async def api_payments_pay_invoice(bolt11: str, wallet: Wallet):
|
||||
status_code=HTTPStatus.CREATED,
|
||||
)
|
||||
async def api_payments_create(
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
invoiceData: CreateInvoiceData = Body(...),
|
||||
):
|
||||
if wallet.wallet_type < 0 or wallet.wallet_type > 2:
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Key is invalid")
|
||||
|
||||
if invoiceData.out is True and wallet.wallet_type == 0:
|
||||
if not invoiceData.bolt11:
|
||||
raise HTTPException(
|
||||
@@ -246,8 +259,14 @@ async def api_payments_create(
|
||||
return await api_payments_pay_invoice(
|
||||
invoiceData.bolt11, wallet.wallet
|
||||
) # admin key
|
||||
# invoice key
|
||||
return await api_payments_create_invoice(invoiceData, wallet.wallet)
|
||||
elif not invoiceData.out:
|
||||
# invoice key
|
||||
return await api_payments_create_invoice(invoiceData, wallet.wallet)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Invoice (or Admin) key required.",
|
||||
)
|
||||
|
||||
|
||||
class CreateLNURLData(BaseModel):
|
||||
@@ -260,7 +279,7 @@ class CreateLNURLData(BaseModel):
|
||||
|
||||
@core_app.post("/api/v1/payments/lnurl")
|
||||
async def api_payments_pay_lnurl(
|
||||
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
data: CreateLNURLData, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
domain = urlparse(data.callback).netloc
|
||||
|
||||
@@ -286,6 +305,12 @@ async def api_payments_pay_lnurl(
|
||||
detail=f"{domain} said: '{params.get('reason', '')}'",
|
||||
)
|
||||
|
||||
if not params.get("pr"):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"{domain} did not return a payment request.",
|
||||
)
|
||||
|
||||
invoice = bolt11.decode(params["pr"])
|
||||
if invoice.amount_msat != data.amount:
|
||||
raise HTTPException(
|
||||
@@ -293,11 +318,11 @@ async def api_payments_pay_lnurl(
|
||||
detail=f"{domain} returned an invalid invoice. Expected {data.amount} msat, got {invoice.amount_msat}.",
|
||||
)
|
||||
|
||||
# if invoice.description_hash != data.description_hash:
|
||||
# raise HTTPException(
|
||||
# status_code=HTTPStatus.BAD_REQUEST,
|
||||
# detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
|
||||
# )
|
||||
if invoice.description_hash != data.description_hash:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"{domain} returned an invalid invoice. Expected description_hash == {data.description_hash}, got {invoice.description_hash}.",
|
||||
)
|
||||
|
||||
extra = {}
|
||||
|
||||
@@ -305,7 +330,7 @@ async def api_payments_pay_lnurl(
|
||||
extra["success_action"] = params["successAction"]
|
||||
if data.comment:
|
||||
extra["comment"] = data.comment
|
||||
|
||||
assert data.description is not None, "description is required"
|
||||
payment_hash = await pay_invoice(
|
||||
wallet_id=wallet.wallet.id,
|
||||
payment_request=params["pr"],
|
||||
@@ -322,19 +347,20 @@ async def api_payments_pay_lnurl(
|
||||
|
||||
|
||||
async def subscribe(request: Request, wallet: Wallet):
|
||||
this_wallet_id = wallet.wallet.id
|
||||
this_wallet_id = wallet.id
|
||||
|
||||
payment_queue = asyncio.Queue(0)
|
||||
payment_queue: asyncio.Queue[Payment] = asyncio.Queue(0)
|
||||
|
||||
print("adding sse listener", payment_queue)
|
||||
logger.debug("adding sse listener", payment_queue)
|
||||
api_invoice_listeners.append(payment_queue)
|
||||
|
||||
send_queue = asyncio.Queue(0)
|
||||
send_queue: asyncio.Queue[tuple[str, Payment]] = asyncio.Queue(0)
|
||||
|
||||
async def payment_received() -> None:
|
||||
while True:
|
||||
payment: Payment = await payment_queue.get()
|
||||
if payment.wallet_id == this_wallet_id:
|
||||
logger.debug("payment receieved", payment)
|
||||
await send_queue.put(("payment-received", payment))
|
||||
|
||||
asyncio.create_task(payment_received())
|
||||
@@ -359,21 +385,32 @@ async def api_payments_sse(
|
||||
request: Request, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
return EventSourceResponse(
|
||||
subscribe(request, wallet), ping=20, media_type="text/event-stream"
|
||||
subscribe(request, wallet.wallet), ping=20, media_type="text/event-stream"
|
||||
)
|
||||
|
||||
|
||||
@core_app.get("/api/v1/payments/{payment_hash}")
|
||||
async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
||||
# We use X_Api_Key here because we want this call to work with and without keys
|
||||
# If a valid key is given, we also return the field "details", otherwise not
|
||||
wallet = None
|
||||
try:
|
||||
if X_Api_Key.extra:
|
||||
print("No key")
|
||||
logger.warning("No key")
|
||||
except:
|
||||
wallet = await get_wallet_for_key(X_Api_Key)
|
||||
payment = await get_standalone_payment(payment_hash)
|
||||
payment = await get_standalone_payment(
|
||||
payment_hash, wallet_id=wallet.id if wallet else None
|
||||
) # we have to specify the wallet id here, because postgres and sqlite return internal payments in different order
|
||||
# and get_standalone_payment otherwise just fetches the first one, causing unpredictable results
|
||||
if payment is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
||||
)
|
||||
await check_invoice_status(payment.wallet_id, payment_hash)
|
||||
payment = await get_standalone_payment(payment_hash)
|
||||
payment = await get_standalone_payment(
|
||||
payment_hash, wallet_id=wallet.id if wallet else None
|
||||
)
|
||||
if not payment:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist."
|
||||
@@ -391,14 +428,16 @@ async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)):
|
||||
return {"paid": False}
|
||||
|
||||
if wallet and wallet.id == payment.wallet_id:
|
||||
return {"paid": not payment.pending, "preimage": payment.preimage, "details": payment}
|
||||
return {
|
||||
"paid": not payment.pending,
|
||||
"preimage": payment.preimage,
|
||||
"details": payment,
|
||||
}
|
||||
return {"paid": not payment.pending, "preimage": payment.preimage}
|
||||
|
||||
|
||||
@core_app.get(
|
||||
"/api/v1/lnurlscan/{code}", dependencies=[Depends(WalletInvoiceKeyChecker())]
|
||||
)
|
||||
async def api_lnurlscan(code: str):
|
||||
@core_app.get("/api/v1/lnurlscan/{code}")
|
||||
async def api_lnurlscan(code: str, wallet: WalletTypeInfo = Depends(get_key_type)):
|
||||
try:
|
||||
url = lnurl.decode(code)
|
||||
domain = urlparse(url).netloc
|
||||
@@ -426,7 +465,7 @@ async def api_lnurlscan(code: str):
|
||||
params.update(kind="auth")
|
||||
params.update(callback=url) # with k1 already in it
|
||||
|
||||
lnurlauth_key = g().wallet.lnurlauth_key(domain)
|
||||
lnurlauth_key = wallet.wallet.lnurlauth_key(domain)
|
||||
params.update(pubkey=lnurlauth_key.verifying_key.to_string("compressed").hex())
|
||||
else:
|
||||
async with httpx.AsyncClient() as client:
|
||||
@@ -542,14 +581,19 @@ async def api_payments_decode(data: DecodePayment):
|
||||
return {"message": "Failed to decode"}
|
||||
|
||||
|
||||
@core_app.post("/api/v1/lnurlauth", dependencies=[Depends(WalletAdminKeyChecker())])
|
||||
async def api_perform_lnurlauth(callback: str):
|
||||
err = await perform_lnurlauth(callback)
|
||||
class Callback(BaseModel):
|
||||
callback: str = Query(...)
|
||||
|
||||
|
||||
@core_app.post("/api/v1/lnurlauth")
|
||||
async def api_perform_lnurlauth(
|
||||
callback: Callback, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
err = await perform_lnurlauth(callback.callback, wallet=wallet)
|
||||
if err:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE, detail=err.reason
|
||||
)
|
||||
|
||||
return ""
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from fastapi.exceptions import HTTPException
|
||||
from fastapi.params import Depends, Query
|
||||
from fastapi.responses import FileResponse, RedirectResponse
|
||||
from fastapi.routing import APIRouter
|
||||
from loguru import logger
|
||||
from pydantic.types import UUID4
|
||||
from starlette.responses import HTMLResponse, JSONResponse
|
||||
|
||||
@@ -17,10 +18,12 @@ from lnbits.helpers import template_renderer, url_for
|
||||
from lnbits.settings import (
|
||||
LNBITS_ADMIN_USERS,
|
||||
LNBITS_ALLOWED_USERS,
|
||||
LNBITS_CUSTOM_LOGO,
|
||||
LNBITS_SITE_TITLE,
|
||||
SERVICE_FEE,
|
||||
)
|
||||
|
||||
from ...helpers import get_valid_extensions
|
||||
from ..crud import (
|
||||
create_account,
|
||||
create_wallet,
|
||||
@@ -64,11 +67,21 @@ async def extensions(
|
||||
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
|
||||
)
|
||||
|
||||
# check if extension exists
|
||||
if extension_to_enable or extension_to_disable:
|
||||
ext = extension_to_enable or extension_to_disable
|
||||
if ext not in [e.code for e in get_valid_extensions()]:
|
||||
raise HTTPException(
|
||||
HTTPStatus.BAD_REQUEST, f"Extension '{ext}' doesn't exist."
|
||||
)
|
||||
|
||||
if extension_to_enable:
|
||||
logger.info(f"Enabling extension: {extension_to_enable} for user {user.id}")
|
||||
await update_user_extension(
|
||||
user_id=user.id, extension=extension_to_enable, active=True
|
||||
)
|
||||
elif extension_to_disable:
|
||||
logger.info(f"Disabling extension: {extension_to_disable} for user {user.id}")
|
||||
await update_user_extension(
|
||||
user_id=user.id, extension=extension_to_disable, active=False
|
||||
)
|
||||
@@ -108,6 +121,7 @@ async def wallet(
|
||||
|
||||
if not user_id:
|
||||
user = await get_user((await create_account()).id)
|
||||
logger.info(f"Create user {user.id}")
|
||||
else:
|
||||
user = await get_user(user_id)
|
||||
if not user:
|
||||
@@ -125,12 +139,16 @@ async def wallet(
|
||||
wallet = user.wallets[0]
|
||||
else:
|
||||
wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name)
|
||||
logger.info(
|
||||
f"Created new wallet {wallet_name if wallet_name else '(no name)'} for user {user.id}"
|
||||
)
|
||||
|
||||
return RedirectResponse(
|
||||
f"/wallet?usr={user.id}&wal={wallet.id}",
|
||||
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
|
||||
)
|
||||
|
||||
logger.debug(f"Access wallet {wallet_name}{'of user '+ user.id if user else ''}")
|
||||
wallet = user.get_wallet(wallet_id)
|
||||
if not wallet:
|
||||
return template_renderer().TemplateResponse(
|
||||
@@ -144,6 +162,7 @@ async def wallet(
|
||||
"user": user.dict(),
|
||||
"wallet": wallet.dict(),
|
||||
"service_fee": service_fee,
|
||||
"web_manifest": f"/manifest/{user.id}.webmanifest",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -200,13 +219,13 @@ async def lnurl_full_withdraw_callback(request: Request):
|
||||
async def deletewallet(request: Request, wal: str = Query(...), usr: str = Query(...)):
|
||||
user = await get_user(usr)
|
||||
user_wallet_ids = [u.id for u in user.wallets]
|
||||
print("USR", user_wallet_ids)
|
||||
|
||||
if wal not in user_wallet_ids:
|
||||
raise HTTPException(HTTPStatus.FORBIDDEN, "Not your wallet.")
|
||||
else:
|
||||
await delete_wallet(user_id=user.id, wallet_id=wal)
|
||||
user_wallet_ids.remove(wal)
|
||||
logger.debug("Deleted wallet {wal} of user {user.id}")
|
||||
|
||||
if user_wallet_ids:
|
||||
return RedirectResponse(
|
||||
@@ -226,7 +245,9 @@ async def lnurl_balance_notify(request: Request, service: str):
|
||||
redeem_lnurl_withdraw(bc.wallet, bc.url)
|
||||
|
||||
|
||||
@core_html_routes.get("/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet")
|
||||
@core_html_routes.get(
|
||||
"/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet"
|
||||
)
|
||||
async def lnurlwallet(request: Request):
|
||||
async with db.connect() as conn:
|
||||
account = await create_account(conn=conn)
|
||||
@@ -249,6 +270,11 @@ async def lnurlwallet(request: Request):
|
||||
)
|
||||
|
||||
|
||||
@core_html_routes.get("/service-worker.js", response_class=FileResponse)
|
||||
async def service_worker():
|
||||
return FileResponse("lnbits/core/static/js/service-worker.js")
|
||||
|
||||
|
||||
@core_html_routes.get("/manifest/{usr}.webmanifest")
|
||||
async def manifest(usr: str):
|
||||
user = await get_user(usr)
|
||||
@@ -256,21 +282,23 @@ async def manifest(usr: str):
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
|
||||
|
||||
return {
|
||||
"short_name": "LNbits",
|
||||
"name": "LNbits Wallet",
|
||||
"short_name": LNBITS_SITE_TITLE,
|
||||
"name": LNBITS_SITE_TITLE + " Wallet",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
|
||||
"src": LNBITS_CUSTOM_LOGO
|
||||
if LNBITS_CUSTOM_LOGO
|
||||
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@0.3.0/docs/logos/lnbits.png",
|
||||
"type": "image/png",
|
||||
"sizes": "900x900",
|
||||
}
|
||||
],
|
||||
"start_url": "/wallet?usr=" + usr,
|
||||
"background_color": "#3367D6",
|
||||
"description": "Weather forecast information",
|
||||
"start_url": "/wallet?usr=" + usr + "&wal=" + user.wallets[0].id,
|
||||
"background_color": "#1F2234",
|
||||
"description": "Bitcoin Lightning Wallet",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3367D6",
|
||||
"theme_color": "#1F2234",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": wallet.name,
|
||||
|
||||
@@ -4,6 +4,7 @@ from http import HTTPStatus
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from fastapi import HTTPException
|
||||
from loguru import logger
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
@@ -45,7 +46,7 @@ async def api_public_payment_longpolling(payment_hash):
|
||||
|
||||
payment_queue = asyncio.Queue(0)
|
||||
|
||||
print("adding standalone invoice listener", payment_hash, payment_queue)
|
||||
logger.debug("adding standalone invoice listener", payment_hash, payment_queue)
|
||||
api_invoice_listeners.append(payment_queue)
|
||||
|
||||
response = None
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@ import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Optional
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy_aio.base import AsyncConnection
|
||||
from sqlalchemy_aio.strategy import ASYNCIO_STRATEGY # type: ignore
|
||||
@@ -139,7 +140,7 @@ class Database(Compat):
|
||||
f"LNBITS_DATA_FOLDER named {LNBITS_DATA_FOLDER} was not created"
|
||||
f" - please 'mkdir {LNBITS_DATA_FOLDER}' and try again"
|
||||
)
|
||||
|
||||
logger.trace(f"database {self.type} added for {self.name}")
|
||||
self.schema = self.name
|
||||
if self.name.startswith("ext_"):
|
||||
self.schema = self.name[4:]
|
||||
|
||||
+18
-6
@@ -13,7 +13,11 @@ from starlette.requests import Request
|
||||
from lnbits.core.crud import get_user, get_wallet_for_key
|
||||
from lnbits.core.models import User, Wallet
|
||||
from lnbits.requestvars import g
|
||||
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS, LNBITS_ADMIN_EXTENSIONS
|
||||
from lnbits.settings import (
|
||||
LNBITS_ADMIN_EXTENSIONS,
|
||||
LNBITS_ADMIN_USERS,
|
||||
LNBITS_ALLOWED_USERS,
|
||||
)
|
||||
|
||||
|
||||
class KeyChecker(SecurityBase):
|
||||
@@ -122,7 +126,7 @@ async def get_key_type(
|
||||
# 0: admin
|
||||
# 1: invoice
|
||||
# 2: invalid
|
||||
pathname = r['path'].split('/')[1]
|
||||
pathname = r["path"].split("/")[1]
|
||||
|
||||
if not api_key_header and not api_key_query:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
|
||||
@@ -133,8 +137,12 @@ async def get_key_type(
|
||||
checker = WalletAdminKeyChecker(api_key=token)
|
||||
await checker.__call__(r)
|
||||
wallet = WalletTypeInfo(0, checker.wallet)
|
||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
|
||||
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
|
||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
|
||||
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||
)
|
||||
return wallet
|
||||
except HTTPException as e:
|
||||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||
@@ -148,8 +156,12 @@ async def get_key_type(
|
||||
checker = WalletInvoiceKeyChecker(api_key=token)
|
||||
await checker.__call__(r)
|
||||
wallet = WalletTypeInfo(1, checker.wallet)
|
||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
|
||||
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
|
||||
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (
|
||||
LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
|
||||
)
|
||||
return wallet
|
||||
except HTTPException as e:
|
||||
if e.status_code == HTTPStatus.BAD_REQUEST:
|
||||
|
||||
@@ -9,7 +9,7 @@ db = Database("ext_bleskomat")
|
||||
bleskomat_static_files = [
|
||||
{
|
||||
"path": "/bleskomat/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/bleskomat/static"),
|
||||
"app": StaticFiles(packages=[("lnbits", "extensions/bleskomat/static")]),
|
||||
"name": "bleskomat_static",
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import httpx
|
||||
import json
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
fiat_currencies = json.load(
|
||||
open(
|
||||
os.path.join(
|
||||
|
||||
@@ -3,6 +3,7 @@ import math
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
|
||||
from loguru import logger
|
||||
from starlette.requests import Request
|
||||
|
||||
from . import bleskomat_ext
|
||||
@@ -122,7 +123,7 @@ async def api_bleskomat_lnurl(req: Request):
|
||||
except LnurlHttpError as e:
|
||||
return {"status": "ERROR", "reason": str(e)}
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
logger.error(str(e))
|
||||
return {"status": "ERROR", "reason": "Unexpected error"}
|
||||
|
||||
return {"status": "OK"}
|
||||
|
||||
@@ -3,11 +3,12 @@ import time
|
||||
from typing import Dict
|
||||
|
||||
from fastapi.params import Query
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, validator
|
||||
from starlette.requests import Request
|
||||
|
||||
from lnbits import bolt11
|
||||
from lnbits.core.services import pay_invoice, PaymentFailure
|
||||
from lnbits.core.services import PaymentFailure, pay_invoice
|
||||
|
||||
from . import db
|
||||
from .exchange_rates import exchange_rate_providers, fiat_currencies
|
||||
@@ -125,7 +126,7 @@ class BleskomatLnurl(BaseModel):
|
||||
except (ValueError, PermissionError, PaymentFailure) as e:
|
||||
raise LnurlValidationError("Failed to pay invoice: " + str(e))
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
logger.error(str(e))
|
||||
raise LnurlValidationError("Unexpected error")
|
||||
|
||||
async def use(self, conn) -> bool:
|
||||
|
||||
@@ -62,4 +62,5 @@
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/Bleskomat"></q-btn>
|
||||
</q-expansion-item>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Depends, Query
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
@@ -60,7 +61,7 @@ async def api_bleskomat_create_or_update(
|
||||
currency=fiat_currency, provider=exchange_rate_provider
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
logger.error(e)
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"',
|
||||
|
||||
@@ -12,7 +12,7 @@ db = Database("ext_copilot")
|
||||
copilot_static_files = [
|
||||
{
|
||||
"path": "/copilot/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/copilot/static"),
|
||||
"app": StaticFiles(packages=[("lnbits", "extensions/copilot/static")]),
|
||||
"name": "copilot_static",
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
|
||||
from starlette.requests import Request
|
||||
from fastapi.param_functions import Query
|
||||
from typing import Optional, Dict
|
||||
from lnbits.lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from pydantic import BaseModel
|
||||
import json
|
||||
from sqlite3 import Row
|
||||
from typing import Dict, Optional
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
|
||||
from lnbits.lnurl import encode as lnurl_encode # type: ignore
|
||||
|
||||
|
||||
class CreateCopilotData(BaseModel):
|
||||
|
||||
@@ -25,7 +25,7 @@ async def wait_for_paid_invoices():
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
webhook = None
|
||||
data = None
|
||||
if "copilot" != payment.extra.get("tag"):
|
||||
if payment.extra.get("tag") != "copilot":
|
||||
# not an copilot invoice
|
||||
return
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/copilot"></q-btn>
|
||||
<q-expansion-item group="api" dense expand-separator label="Create copilot">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
@@ -31,8 +32,8 @@
|
||||
<code>[<copilot_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}api/v1/copilot -d '{"title":
|
||||
<string>, "animation": <string>,
|
||||
>curl -X POST {{ request.base_url }}copilot/api/v1/copilot -d
|
||||
'{"title": <string>, "animation": <string>,
|
||||
"show_message":<string>, "amount": <integer>,
|
||||
"lnurl_title": <string>}' -H "Content-type: application/json"
|
||||
-H "X-Api-Key: {{user.wallets[0].adminkey }}"
|
||||
@@ -59,11 +60,11 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url
|
||||
}}api/v1/copilot/<copilot_id> -d '{"title": <string>,
|
||||
"animation": <string>, "show_message":<string>,
|
||||
"amount": <integer>, "lnurl_title": <string>}' -H
|
||||
"Content-type: application/json" -H "X-Api-Key:
|
||||
{{user.wallets[0].adminkey }}"
|
||||
}}copilot/api/v1/copilot/<copilot_id> -d '{"title":
|
||||
<string>, "animation": <string>,
|
||||
"show_message":<string>, "amount": <integer>,
|
||||
"lnurl_title": <string>}' -H "Content-type: application/json"
|
||||
-H "X-Api-Key: {{user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -87,8 +88,9 @@
|
||||
<code>[<copilot_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}api/v1/copilot/<copilot_id>
|
||||
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
>curl -X GET {{ request.base_url
|
||||
}}copilot/api/v1/copilot/<copilot_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -110,8 +112,8 @@
|
||||
<code>[<copilot_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}api/v1/copilots -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
>curl -X GET {{ request.base_url }}copilot/api/v1/copilots -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -136,7 +138,7 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.base_url
|
||||
}}api/v1/copilot/<copilot_id> -H "X-Api-Key: {{
|
||||
}}copilot/api/v1/copilot/<copilot_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -161,9 +163,10 @@
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}/api/v1/copilot/ws/<string,
|
||||
copilot_id>/<string, comment>/<string, gif name> -H
|
||||
"X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
>curl -X GET {{ request.base_url
|
||||
}}copilot/api/v1/copilot/ws/<string, copilot_id>/<string,
|
||||
comment>/<string, gif name> -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -9,7 +9,7 @@ db = Database("ext_discordbot")
|
||||
discordbot_static_files = [
|
||||
{
|
||||
"path": "/discordbot/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/discordbot/static"),
|
||||
"app": StaticFiles(packages=[("lnbits", "extensions/discordbot/static")]),
|
||||
"name": "discordbot_static",
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from sqlite3 import Row
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class CreateUserData(BaseModel):
|
||||
@@ -11,6 +11,7 @@ class CreateUserData(BaseModel):
|
||||
admin_id: str = Query(...)
|
||||
discord_id: str = Query("")
|
||||
|
||||
|
||||
class CreateUserWallet(BaseModel):
|
||||
user_id: str = Query(...)
|
||||
wallet_name: str = Query(...)
|
||||
@@ -23,6 +24,7 @@ class Users(BaseModel):
|
||||
admin: str
|
||||
discord_id: str
|
||||
|
||||
|
||||
class Wallets(BaseModel):
|
||||
id: str
|
||||
admin: str
|
||||
|
||||
@@ -10,12 +10,19 @@
|
||||
Discord Bot: Connect Discord users to LNbits.
|
||||
</h5>
|
||||
<p>
|
||||
Connect your LNbits instance to a <a href="https://github.com/chrislennon/lnbits-discord-bot">Discord Bot</a> leveraging LNbits as a community based lightning node.<br />
|
||||
Connect your LNbits instance to a
|
||||
<a href="https://github.com/chrislennon/lnbits-discord-bot"
|
||||
>Discord Bot</a
|
||||
>
|
||||
leveraging LNbits as a community based lightning node.<br />
|
||||
<small>
|
||||
Created by, <a href="https://github.com/chrislennon">Chris Lennon</a></small
|
||||
> <br />
|
||||
Created by,
|
||||
<a href="https://github.com/chrislennon">Chris Lennon</a></small
|
||||
>
|
||||
<br />
|
||||
<small>
|
||||
Based on User Manager, by <a href="https://github.com/benarc">Ben Arc</a></small
|
||||
Based on User Manager, by
|
||||
<a href="https://github.com/benarc">Ben Arc</a></small
|
||||
>
|
||||
</p>
|
||||
</q-card-section>
|
||||
@@ -27,6 +34,7 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/discordbot"></q-btn>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET users">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
@@ -149,8 +157,9 @@
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}discordbot/api/v1/users -d
|
||||
'{"admin_id": "{{ user.id }}", "wallet_name": <string>,
|
||||
"user_name": <string>, "discord_id": <string>}' -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}" -H "Content-type: application/json"
|
||||
"user_name": <string>, "discord_id": <string>}' -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type:
|
||||
application/json"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -136,7 +136,8 @@
|
||||
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="text-subtitle1 q-my-none">LNbits Discord Bot Extension
|
||||
<h6 class="text-subtitle1 q-my-none">
|
||||
LNbits Discord Bot Extension
|
||||
<!--{{SITE_TITLE}} Discord Bot Extension-->
|
||||
</h6>
|
||||
</q-card-section>
|
||||
@@ -236,7 +237,12 @@
|
||||
columns: [
|
||||
{name: 'id', align: 'left', label: 'ID', field: 'id'},
|
||||
{name: 'name', align: 'left', label: 'Username', field: 'name'},
|
||||
{name: 'discord_id', align: 'left', label: 'discord_id', field: 'discord_id'}
|
||||
{
|
||||
name: 'discord_id',
|
||||
align: 'left',
|
||||
label: 'discord_id',
|
||||
field: 'discord_id'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
rowsPerPage: 10
|
||||
|
||||
@@ -109,9 +109,7 @@ async def api_discordbot_wallet_transactions(
|
||||
async def api_discordbot_users_wallets(
|
||||
user_id, wallet: WalletTypeInfo = Depends(get_key_type)
|
||||
):
|
||||
return [
|
||||
s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)
|
||||
]
|
||||
return [s_wallet.dict() for s_wallet in await get_discordbot_users_wallets(user_id)]
|
||||
|
||||
|
||||
@discordbot_ext.delete("/api/v1/wallets/{wallet_id}")
|
||||
|
||||
@@ -16,7 +16,7 @@ async def create_ticket(
|
||||
INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(payment_hash, wallet, event, name, email, False, False),
|
||||
(payment_hash, wallet, event, name, email, False, True),
|
||||
)
|
||||
|
||||
ticket = await get_ticket(payment_hash)
|
||||
|
||||
@@ -20,4 +20,5 @@
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/events"></q-btn>
|
||||
</q-expansion-item>
|
||||
|
||||
@@ -135,15 +135,7 @@
|
||||
var self = this
|
||||
axios
|
||||
|
||||
.post(
|
||||
'/events/api/v1/tickets/' + '{{ event_id }}/{{ event_price }}',
|
||||
{
|
||||
event: '{{ event_id }}',
|
||||
event_name: '{{ event_name }}',
|
||||
name: self.formDialog.data.name,
|
||||
email: self.formDialog.data.email
|
||||
}
|
||||
)
|
||||
.get('/events/api/v1/tickets/' + '{{ event_id }}')
|
||||
.then(function (response) {
|
||||
self.paymentReq = response.data.payment_request
|
||||
self.paymentCheck = response.data.payment_hash
|
||||
@@ -161,7 +153,17 @@
|
||||
|
||||
paymentChecker = setInterval(function () {
|
||||
axios
|
||||
.get('/events/api/v1/tickets/' + self.paymentCheck)
|
||||
.post(
|
||||
'/events/api/v1/tickets/' +
|
||||
'{{ event_id }}/' +
|
||||
self.paymentCheck,
|
||||
{
|
||||
event: '{{ event_id }}',
|
||||
event_name: '{{ event_name }}',
|
||||
name: self.formDialog.data.name,
|
||||
email: self.formDialog.data.email
|
||||
}
|
||||
)
|
||||
.then(function (res) {
|
||||
if (res.data.paid) {
|
||||
clearInterval(paymentChecker)
|
||||
|
||||
@@ -381,10 +381,10 @@
|
||||
getTickets: function () {
|
||||
var self = this
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/tickets?all_wallets=true',
|
||||
this.g.user.wallets[0].inkey
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/tickets?all_wallets=true',
|
||||
this.g.user.wallets[0].inkey
|
||||
)
|
||||
.then(function (response) {
|
||||
console.log(response)
|
||||
|
||||
@@ -133,7 +133,10 @@
|
||||
var self = this
|
||||
|
||||
LNbits.api
|
||||
.request('GET', '/events/api/v1/register/ticket/' + res)
|
||||
.request(
|
||||
'GET',
|
||||
'/events/api/v1/register/ticket/' + res.split('//')[1]
|
||||
)
|
||||
.then(function (response) {
|
||||
self.$q.notify({
|
||||
type: 'positive',
|
||||
|
||||
@@ -13,9 +13,8 @@
|
||||
<br />
|
||||
|
||||
<qrcode
|
||||
:value="'{{ ticket_id }}'"
|
||||
:options="{width: 340}"
|
||||
class="rounded-borders"
|
||||
:value="'ticket://{{ ticket_id }}'"
|
||||
:options="{width: 500}"
|
||||
></qrcode>
|
||||
<br />
|
||||
<q-btn @click="printWindow" color="grey" class="q-ml-auto">
|
||||
|
||||
@@ -97,8 +97,8 @@ async def api_tickets(
|
||||
return [ticket.dict() for ticket in await get_tickets(wallet_ids)]
|
||||
|
||||
|
||||
@events_ext.post("/api/v1/tickets/{event_id}/{sats}")
|
||||
async def api_ticket_make_ticket(event_id, sats, data: CreateTicket):
|
||||
@events_ext.get("/api/v1/tickets/{event_id}")
|
||||
async def api_ticket_make_ticket(event_id):
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
@@ -107,37 +107,35 @@ async def api_ticket_make_ticket(event_id, sats, data: CreateTicket):
|
||||
try:
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=event.wallet,
|
||||
amount=int(sats),
|
||||
amount=event.price_per_ticket,
|
||||
memo=f"{event_id}",
|
||||
extra={"tag": "events"},
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
|
||||
|
||||
ticket = await create_ticket(
|
||||
payment_hash=payment_hash,
|
||||
wallet=event.wallet,
|
||||
event=event_id,
|
||||
name=data.name,
|
||||
email=data.email,
|
||||
)
|
||||
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail=f"Event could not be fetched."
|
||||
)
|
||||
|
||||
return {"payment_hash": payment_hash, "payment_request": payment_request}
|
||||
|
||||
|
||||
@events_ext.get("/api/v1/tickets/{payment_hash}")
|
||||
async def api_ticket_send_ticket(payment_hash):
|
||||
ticket = await get_ticket(payment_hash)
|
||||
|
||||
@events_ext.post("/api/v1/tickets/{event_id}/{payment_hash}")
|
||||
async def api_ticket_send_ticket(event_id, payment_hash, data: CreateTicket):
|
||||
event = await get_event(event_id)
|
||||
try:
|
||||
status = await api_payment(payment_hash)
|
||||
if status["paid"]:
|
||||
await set_ticket_paid(payment_hash=payment_hash)
|
||||
ticket = await create_ticket(
|
||||
payment_hash=payment_hash,
|
||||
wallet=event.wallet,
|
||||
event=event_id,
|
||||
name=data.name,
|
||||
email=data.email,
|
||||
)
|
||||
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail=f"Event could not be fetched."
|
||||
)
|
||||
|
||||
return {"paid": True, "ticket_id": ticket.id}
|
||||
except Exception:
|
||||
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Not paid")
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<h1>Example Extension</h1>
|
||||
<h2>*tagline*</h2>
|
||||
This is an example extension to help you organise and build you own.
|
||||
|
||||
Try to include an image
|
||||
<img src="https://i.imgur.com/9i4xcQB.png">
|
||||
|
||||
|
||||
<h2>If your extension has API endpoints, include useful ones here</h2>
|
||||
|
||||
<code>curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY"</code>
|
||||
@@ -0,0 +1,16 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
|
||||
db = Database("ext_example")
|
||||
|
||||
example_ext: APIRouter = APIRouter(prefix="/example", tags=["example"])
|
||||
|
||||
|
||||
def example_renderer():
|
||||
return template_renderer(["lnbits/extensions/example/templates"])
|
||||
|
||||
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "Build your own!!",
|
||||
"short_description": "Join us, make an extension",
|
||||
"icon": "info",
|
||||
"contributors": ["github_username"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# async def m001_initial(db):
|
||||
# await db.execute(
|
||||
# f"""
|
||||
# CREATE TABLE example.example (
|
||||
# id TEXT PRIMARY KEY,
|
||||
# wallet TEXT NOT NULL,
|
||||
# time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
|
||||
# );
|
||||
# """
|
||||
# )
|
||||
@@ -0,0 +1,5 @@
|
||||
# from pydantic import BaseModel
|
||||
|
||||
# class Example(BaseModel):
|
||||
# id: str
|
||||
# wallet: str
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||
%} {% block page %}
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h5 class="text-subtitle1 q-mt-none q-mb-md">
|
||||
Frameworks used by {{SITE_TITLE}}
|
||||
</h5>
|
||||
<q-list>
|
||||
<q-item
|
||||
v-for="tool in tools"
|
||||
:key="tool.name"
|
||||
tag="a"
|
||||
:href="tool.url"
|
||||
target="_blank"
|
||||
>
|
||||
{% raw %}
|
||||
<!-- with raw Flask won't try to interpret the Vue moustaches -->
|
||||
<q-item-section>
|
||||
<q-item-label>{{ tool.name }}</q-item-label>
|
||||
<q-item-label caption>{{ tool.language }}</q-item-label>
|
||||
</q-item-section>
|
||||
{% endraw %}
|
||||
</q-item>
|
||||
</q-list>
|
||||
<q-separator class="q-my-lg"></q-separator>
|
||||
<p>
|
||||
A magical "g" is always available, with info about the user, wallets and
|
||||
extensions:
|
||||
</p>
|
||||
<code class="text-caption">{% raw %}{{ g }}{% endraw %}</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||
<script>
|
||||
new Vue({
|
||||
el: '#vue',
|
||||
mixins: [windowMixin],
|
||||
data: function () {
|
||||
return {
|
||||
tools: []
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
var self = this
|
||||
|
||||
// axios is available for making requests
|
||||
axios({
|
||||
method: 'GET',
|
||||
url: '/example/api/v1/tools',
|
||||
headers: {
|
||||
'X-example-header': 'not-used'
|
||||
}
|
||||
}).then(function (response) {
|
||||
self.tools = response.data
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.params import Depends
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import example_ext, example_renderer
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
@example_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return example_renderer().TemplateResponse(
|
||||
"example/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
# views_api.py is for you API endpoints that could be hit by another service
|
||||
|
||||
# add your dependencies here
|
||||
|
||||
# import httpx
|
||||
# (use httpx just like requests, except instead of response.ok there's only the
|
||||
# response.is_error that is its inverse)
|
||||
|
||||
from . import example_ext
|
||||
|
||||
# add your endpoints here
|
||||
|
||||
|
||||
@example_ext.get("/api/v1/tools")
|
||||
async def api_example():
|
||||
"""Try to add descriptions for others."""
|
||||
tools = [
|
||||
{
|
||||
"name": "fastAPI",
|
||||
"url": "https://fastapi.tiangolo.com/",
|
||||
"language": "Python",
|
||||
},
|
||||
{
|
||||
"name": "Vue.js",
|
||||
"url": "https://vuejs.org/",
|
||||
"language": "JavaScript",
|
||||
},
|
||||
{
|
||||
"name": "Quasar Framework",
|
||||
"url": "https://quasar.dev/",
|
||||
"language": "JavaScript",
|
||||
},
|
||||
]
|
||||
|
||||
return tools
|
||||
@@ -12,7 +12,7 @@ db = Database("ext_jukebox")
|
||||
jukebox_static_files = [
|
||||
{
|
||||
"path": "/jukebox/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/jukebox/static"),
|
||||
"app": StaticFiles(packages=[("lnbits", "extensions/jukebox/static")]),
|
||||
"name": "jukebox_static",
|
||||
}
|
||||
]
|
||||
|
||||
@@ -41,7 +41,7 @@ async def update_jukebox(
|
||||
q = ", ".join([f"{field[0]} = ?" for field in data])
|
||||
items = [f"{field[1]}" for field in data]
|
||||
items.append(juke_id)
|
||||
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
|
||||
q = q.replace("user", '"user"', 1) # hack to make user be "user"!
|
||||
await db.execute(f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (items))
|
||||
row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,))
|
||||
return Jukebox(**row) if row else None
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from typing import NamedTuple
|
||||
from sqlite3 import Row
|
||||
from typing import NamedTuple, Optional
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from pydantic.main import BaseModel
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
|
||||
class CreateJukeLinkData(BaseModel):
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
Vue.component(VueQrcode.name, VueQrcode)
|
||||
|
||||
var mapJukebox = obj => {
|
||||
if(obj.sp_device){
|
||||
if (obj.sp_device) {
|
||||
obj._data = _.clone(obj)
|
||||
|
||||
|
||||
obj.sp_id = obj._data.id
|
||||
obj.device = obj._data.sp_device.split('-')[0]
|
||||
playlists = obj._data.sp_playlists.split(',')
|
||||
@@ -17,11 +17,9 @@ var mapJukebox = obj => {
|
||||
obj.playlist = playlistsar.join()
|
||||
console.log(obj)
|
||||
return obj
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
new Vue({
|
||||
@@ -87,14 +85,14 @@ new Vue({
|
||||
var link = _.findWhere(this.JukeboxLinks, {id: linkId})
|
||||
|
||||
this.qrCodeDialog.data = _.clone(link)
|
||||
|
||||
|
||||
this.qrCodeDialog.data.url =
|
||||
window.location.protocol + '//' + window.location.host
|
||||
this.qrCodeDialog.show = true
|
||||
},
|
||||
getJukeboxes() {
|
||||
self = this
|
||||
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'GET',
|
||||
@@ -103,8 +101,7 @@ new Vue({
|
||||
)
|
||||
.then(function (response) {
|
||||
self.JukeboxLinks = response.data.map(function (obj) {
|
||||
|
||||
return mapJukebox(obj)
|
||||
return mapJukebox(obj)
|
||||
})
|
||||
console.log(self.JukeboxLinks)
|
||||
})
|
||||
@@ -154,7 +151,7 @@ new Vue({
|
||||
submitSpotifyKeys() {
|
||||
self = this
|
||||
self.jukeboxDialog.data.user = self.g.user.id
|
||||
|
||||
|
||||
LNbits.api
|
||||
.request(
|
||||
'POST',
|
||||
|
||||
@@ -16,7 +16,7 @@ async def wait_for_paid_invoices():
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "jukebox" != payment.extra.get("tag"):
|
||||
if payment.extra.get("tag") != "jukebox":
|
||||
# not a jukebox invoice
|
||||
return
|
||||
await update_jukebox_payment(payment.payment_hash, paid=True)
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/jukebox"></q-btn>
|
||||
|
||||
<q-expansion-item group="api" dense expand-separator label="List jukeboxes">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
@@ -37,8 +39,8 @@
|
||||
<code>[<jukebox_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}api/v1/jukebox -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
>curl -X GET {{ request.base_url }}jukebox/api/v1/jukebox -H
|
||||
"X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -59,8 +61,9 @@
|
||||
<code><jukebox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}api/v1/jukebox/<juke_id> -H
|
||||
"X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
>curl -X GET {{ request.base_url
|
||||
}}jukebox/api/v1/jukebox/<juke_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -86,8 +89,8 @@
|
||||
<code><jukbox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}api/v1/jukebox/ -d '{"user":
|
||||
<string, user_id>, "title": <string>,
|
||||
>curl -X POST {{ request.base_url }}jukebox/api/v1/jukebox/ -d
|
||||
'{"user": <string, user_id>, "title": <string>,
|
||||
"wallet":<string>, "sp_user": <string,
|
||||
spotify_user_account>, "sp_secret": <string,
|
||||
spotify_user_secret>, "sp_access_token": <string,
|
||||
@@ -116,8 +119,9 @@
|
||||
<code><jukebox_object></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.base_url }}api/v1/jukebox/<juke_id>
|
||||
-H "X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
>curl -X DELETE {{ request.base_url
|
||||
}}jukebox/api/v1/jukebox/<juke_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
>
|
||||
<q-step
|
||||
:name="1"
|
||||
title="Pick wallet, price"
|
||||
title="1. Pick Wallet and Price"
|
||||
icon="account_balance_wallet"
|
||||
:done="step > 1"
|
||||
>
|
||||
@@ -170,16 +170,25 @@
|
||||
<br />
|
||||
</q-step>
|
||||
|
||||
<q-step :name="2" title="Add api keys" icon="vpn_key" :done="step > 2">
|
||||
<q-step
|
||||
:name="2"
|
||||
title="2. Add API keys"
|
||||
icon="vpn_key"
|
||||
:done="step > 2"
|
||||
>
|
||||
<img src="/jukebox/static/spotapi.gif" />
|
||||
To use this extension you need a Spotify client ID and client secret.
|
||||
You get these by creating an app in the Spotify developers dashboard
|
||||
<a
|
||||
You get these by creating an app in the Spotify Developer Dashboard
|
||||
<br />
|
||||
<br />
|
||||
<q-btn
|
||||
type="a"
|
||||
target="_blank"
|
||||
style="color: #43a047"
|
||||
color="primary"
|
||||
href="https://developer.spotify.com/dashboard/applications"
|
||||
>here</a
|
||||
>.
|
||||
>Open the Spotify Developer Dashboard</q-btn
|
||||
>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
class="q-pb-md q-pt-md"
|
||||
@@ -231,28 +240,39 @@
|
||||
<br />
|
||||
</q-step>
|
||||
|
||||
<q-step :name="3" title="Add Redirect URI" icon="link" :done="step > 3">
|
||||
<q-step
|
||||
:name="3"
|
||||
title="3. Add Redirect URI"
|
||||
icon="link"
|
||||
:done="step > 3"
|
||||
>
|
||||
<img src="/jukebox/static/spotapi1.gif" />
|
||||
In the app go to edit-settings, set the redirect URI to this link
|
||||
<p>
|
||||
In the app go to edit-settings, set the redirect URI to this link
|
||||
</p>
|
||||
<q-card
|
||||
class="cursor-pointer word-break"
|
||||
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
|
||||
>
|
||||
<q-card-section style="word-break: break-all">
|
||||
{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
|
||||
%}
|
||||
</q-card-section>
|
||||
<q-tooltip> Click to copy URL </q-tooltip>
|
||||
</q-card>
|
||||
<br />
|
||||
<q-btn
|
||||
dense
|
||||
outline
|
||||
unelevated
|
||||
color="primary"
|
||||
size="xs"
|
||||
@click="copyText(locationcb + jukeboxDialog.data.sp_id, 'Link copied to clipboard!')"
|
||||
>{% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw
|
||||
%}<q-tooltip> Click to copy URL </q-tooltip>
|
||||
</q-btn>
|
||||
<br />
|
||||
Settings can be found
|
||||
<a
|
||||
type="a"
|
||||
target="_blank"
|
||||
style="color: #43a047"
|
||||
color="primary"
|
||||
href="https://developer.spotify.com/dashboard/applications"
|
||||
>here</a
|
||||
>.
|
||||
>Open the Spotify Application Settings</q-btn
|
||||
>
|
||||
<br /><br />
|
||||
<p>
|
||||
After adding the redirect URI, click the "Authorise access" button
|
||||
below.
|
||||
</p>
|
||||
|
||||
<div class="row q-mt-md">
|
||||
<div class="col-4">
|
||||
@@ -281,7 +301,7 @@
|
||||
|
||||
<q-step
|
||||
:name="4"
|
||||
title="Select playlists"
|
||||
title="4. Select Device and Playlists"
|
||||
icon="queue_music"
|
||||
active-color="primary"
|
||||
:done="step > 4"
|
||||
|
||||
@@ -455,5 +455,6 @@ async def api_get_jukebox_currently(
|
||||
)
|
||||
except:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong, or no song is playing yet"
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Something went wrong, or no song is playing yet",
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ db = Database("ext_livestream")
|
||||
livestream_static_files = [
|
||||
{
|
||||
"path": "/livestream/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/livestream/static"),
|
||||
"app": StaticFiles(packages=[("lnbits", "extensions/livestream/static")]),
|
||||
"name": "livestream_static",
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
from lnbits.core.crud import create_payment
|
||||
from lnbits.core.models import Payment
|
||||
@@ -20,17 +22,17 @@ async def wait_for_paid_invoices():
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "livestream" != payment.extra.get("tag"):
|
||||
if payment.extra.get("tag") != "livestream":
|
||||
# not a livestream invoice
|
||||
return
|
||||
|
||||
track = await get_track(payment.extra.get("track", -1))
|
||||
if not track:
|
||||
print("this should never happen", payment)
|
||||
logger.error("this should never happen", payment)
|
||||
return
|
||||
|
||||
if payment.extra.get("shared_with"):
|
||||
print("payment was shared already", payment)
|
||||
logger.error("payment was shared already", payment)
|
||||
return
|
||||
|
||||
producer = await get_producer(track.producer)
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/livestream"></q-btn>
|
||||
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
@@ -38,8 +40,8 @@
|
||||
<code>[<livestream_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
>curl -X GET {{ request.base_url }}livestream/api/v1/livestream -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -59,8 +61,8 @@
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X PUT {{ request.url_root
|
||||
}}api/v1/livestream/track/<track_id> -H "X-Api-Key: {{
|
||||
>curl -X PUT {{ request.base_url }}
|
||||
livestream/api/v1/livestream/track/<track_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -81,8 +83,8 @@
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X PUT {{ request.url_root
|
||||
}}api/v1/livestream/fee/<fee_pct> -H "X-Api-Key: {{
|
||||
>curl -X PUT {{ request.base_url }}
|
||||
livestream/api/v1/livestream/fee/<fee_pct> -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -109,11 +111,12 @@
|
||||
</h5>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}api/v1/livestream/tracks -d
|
||||
'{"name": <string>, "download_url": <string>,
|
||||
"price_msat": <integer>, "producer_id": <integer>,
|
||||
"producer_name": <string>}' -H "Content-type: application/json"
|
||||
-H "X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
>curl -X POST {{ request.base_url }}
|
||||
livestream/api/v1/livestream/tracks -d '{"name": <string>,
|
||||
"download_url": <string>, "price_msat": <integer>,
|
||||
"producer_id": <integer>, "producer_name": <string>}' -H
|
||||
"Content-type: application/json" -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -123,6 +126,7 @@
|
||||
dense
|
||||
expand-separator
|
||||
label="Delete a withdraw link"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
@@ -136,8 +140,8 @@
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root
|
||||
}}api/v1/livestream/tracks/<track_id> -H "X-Api-Key: {{
|
||||
>curl -X DELETE {{ request.base_url }}
|
||||
livestream/api/v1/livestream/tracks/<track_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from http import HTTPStatus
|
||||
# from mmap import MAP_DENYWRITE
|
||||
|
||||
from fastapi.param_functions import Depends
|
||||
from fastapi.params import Query
|
||||
@@ -14,6 +13,8 @@ from lnbits.decorators import check_user_exists
|
||||
from . import livestream_ext, livestream_renderer
|
||||
from .crud import get_livestream_by_track, get_track
|
||||
|
||||
# from mmap import MAP_DENYWRITE
|
||||
|
||||
|
||||
@livestream_ext.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
@@ -186,9 +188,9 @@ async def purge_addresses(domain_id: str):
|
||||
) # give user 1 day to topup is address
|
||||
|
||||
if not paid and pay_expire:
|
||||
print("DELETE UNP_PAY_EXP", r["username"])
|
||||
logger.debug("DELETE UNP_PAY_EXP", r["username"])
|
||||
await delete_address(r["id"])
|
||||
|
||||
if paid and expired:
|
||||
print("DELETE PAID_EXP", r["username"])
|
||||
logger.debug("DELETE PAID_EXP", r["username"])
|
||||
await delete_address(r["id"])
|
||||
|
||||
@@ -9,6 +9,7 @@ from lnurl import ( # type: ignore
|
||||
LnurlPayActionResponse,
|
||||
LnurlPayResponse,
|
||||
)
|
||||
from loguru import logger
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
@@ -38,13 +39,12 @@ async def lnurl_response(username: str, domain: str, request: Request):
|
||||
"maxSendable": 1000000000,
|
||||
}
|
||||
|
||||
print("RESP", resp)
|
||||
logger.debug("RESP", resp)
|
||||
return resp
|
||||
|
||||
|
||||
@lnaddress_ext.get("/lnurl/cb/{address_id}", name="lnaddress.lnurl_callback")
|
||||
async def lnurl_callback(address_id, amount: int = Query(...)):
|
||||
print("PING")
|
||||
address = await get_address(address_id)
|
||||
if not address:
|
||||
return LnurlErrorResponse(reason=f"Address not found").dict()
|
||||
|
||||
@@ -43,13 +43,13 @@ async def call_webhook_on_paid(payment_hash):
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "lnaddress" == payment.extra.get("tag"):
|
||||
if payment.extra.get("tag") == "lnaddress":
|
||||
|
||||
await payment.set_pending(False)
|
||||
await set_address_paid(payment_hash=payment.payment_hash)
|
||||
await call_webhook_on_paid(payment_hash=payment.payment_hash)
|
||||
|
||||
elif "renew lnaddress" == payment.extra.get("tag"):
|
||||
elif payment.extra.get("tag") == "renew lnaddress":
|
||||
|
||||
await payment.set_pending(False)
|
||||
await set_address_renewed(
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/lnaddress"></q-btn>
|
||||
<q-expansion-item group="api" dense expand-separator label="GET domains">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
@@ -45,7 +46,7 @@
|
||||
<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 }}lnaddress/api/v1/domains -H
|
||||
>curl -X GET {{ request.base_url }}lnaddress/api/v1/domains -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -81,7 +82,7 @@
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root }}lnaddress/api/v1/domains -d
|
||||
>curl -X POST {{ request.base_url }}lnaddress/api/v1/domains -d
|
||||
'{"wallet": "{{ user.wallets[0].id }}", "domain": <string>,
|
||||
"cf_token": <string>,"cf_zone_id": <string>,"webhook":
|
||||
<Optional string> ,"cost": <integer>}' -H "X-Api-Key: {{
|
||||
@@ -101,7 +102,7 @@
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.url_root
|
||||
>curl -X DELETE {{ request.base_url
|
||||
}}lnaddress/api/v1/domains/<domain_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
</code>
|
||||
@@ -122,7 +123,7 @@
|
||||
<code>JSON list of addresses</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root }}lnaddress/api/v1/addresses -H
|
||||
>curl -X GET {{ request.base_url }}lnaddress/api/v1/addresses -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -142,14 +143,20 @@
|
||||
<code>JSON list of addresses</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.url_root
|
||||
>curl -X GET {{ request.base_url
|
||||
}}lnaddress/api/v1/address/<domain>/<username>/<wallet_key>
|
||||
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
<q-expansion-item group="api" dense expand-separator label="POST address">
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
expand-separator
|
||||
label="POST address"
|
||||
class="q-pb-md"
|
||||
>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
@@ -160,7 +167,7 @@
|
||||
<code>{"X-Api-Key": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.url_root
|
||||
>curl -X POST {{ request.base_url
|
||||
}}lnaddress/api/v1/address/<domain_id> -d '{"domain":
|
||||
<string>, "username": <string>,"email": <Optional
|
||||
string>, "wallet_endpoint": <string>, "wallet_key":
|
||||
|
||||
@@ -372,7 +372,7 @@
|
||||
}
|
||||
data.wallet_endpoint = data.wallet_endpoint ?? '{{ root_url }}'
|
||||
data.duration = parseInt(data.duration)
|
||||
|
||||
|
||||
axios
|
||||
.post('/lnaddress/api/v1/address/{{ domain_id }}', data)
|
||||
.then(response => {
|
||||
|
||||
@@ -191,9 +191,13 @@
|
||||
type="text"
|
||||
label="Cloudflare API token"
|
||||
>
|
||||
<template v-slot:hint>
|
||||
Check extension <a href="https://github.com/lnbits/lnbits-legend/tree/master/lnbits/extensions/lnaddress">documentation!</a>
|
||||
</template>
|
||||
<template v-slot:hint>
|
||||
Check extension
|
||||
<a
|
||||
href="https://github.com/lnbits/lnbits-legend/blob/main/lnbits/extensions/lnaddress/README.md"
|
||||
>documentation!</a
|
||||
>
|
||||
</template>
|
||||
<q-tooltip class="bg-grey-8" anchor="bottom left" self="top left"
|
||||
>Your API key in cloudflare</q-tooltip
|
||||
>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
from base64 import b64decode
|
||||
from fastapi.param_functions import Security
|
||||
|
||||
from fastapi.security.api_key import APIKeyHeader
|
||||
|
||||
from fastapi import Request, status
|
||||
from fastapi.param_functions import Security
|
||||
from fastapi.security.api_key import APIKeyHeader
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.decorators import WalletTypeInfo, get_key_type # type: ignore
|
||||
|
||||
|
||||
api_key_header_auth = APIKeyHeader(
|
||||
name="AUTHORIZATION",
|
||||
auto_error=False,
|
||||
|
||||
@@ -31,5 +31,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
</q-card-section>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/lndhub"></q-btn>
|
||||
</q-card>
|
||||
</q-expansion-item>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from lnbits.decorators import check_user_exists
|
||||
from . import lndhub_ext, lndhub_renderer
|
||||
from fastapi import Request
|
||||
from fastapi.params import Depends
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from . import lndhub_ext, lndhub_renderer
|
||||
|
||||
|
||||
@lndhub_ext.get("/")
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
import time
|
||||
from base64 import urlsafe_b64encode
|
||||
from http import HTTPStatus
|
||||
|
||||
@@ -13,7 +12,7 @@ from lnbits import bolt11
|
||||
from lnbits.core.crud import delete_expired_invoices, get_payments
|
||||
from lnbits.core.services import create_invoice, pay_invoice
|
||||
from lnbits.decorators import WalletTypeInfo
|
||||
from lnbits.settings import WALLET, LNBITS_SITE_TITLE
|
||||
from lnbits.settings import LNBITS_SITE_TITLE, WALLET
|
||||
|
||||
from . import lndhub_ext
|
||||
from .decorators import check_wallet, require_admin_key
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from lnbits.core.models import Wallet
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import httpx
|
||||
|
||||
from lnbits.core.models import Wallet
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import CreateFormData, CreateTicketData, Tickets, Forms
|
||||
import httpx
|
||||
from .models import CreateFormData, CreateTicketData, Forms, Tickets
|
||||
|
||||
|
||||
async def create_ticket(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import asyncio
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
|
||||
@@ -16,13 +18,13 @@ async def wait_for_paid_invoices():
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "lnticket" != payment.extra.get("tag"):
|
||||
if payment.extra.get("tag") != "lnticket":
|
||||
# not a lnticket invoice
|
||||
return
|
||||
|
||||
ticket = await get_ticket(payment.checking_id)
|
||||
if not ticket:
|
||||
print("this should never happen", payment)
|
||||
logger.error("this should never happen", payment)
|
||||
return
|
||||
|
||||
await payment.set_pending(False)
|
||||
|
||||
@@ -19,4 +19,5 @@
|
||||
</p>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/lnticket"></q-btn>
|
||||
</q-expansion-item>
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<q-th auto-width></q-th>
|
||||
<q-th auto-width></q-th>
|
||||
<q-th v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label }}
|
||||
{{ col.label }}
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
@@ -145,11 +145,13 @@
|
||||
icon="launch"
|
||||
:color="($q.dark.isActive) ? 'grey-7' : 'grey-5'"
|
||||
@click="ticketCard(props)"
|
||||
><q-tooltip> Click to show ticket </q-tooltip></q-btn>
|
||||
><q-tooltip> Click to show ticket </q-tooltip></q-btn
|
||||
>
|
||||
</q-td>
|
||||
|
||||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
{{ col.label == "Ticket" ? col.value.length > 20 ? `${col.value.substring(0, 20)}...` : col.value : col.value }}
|
||||
{{ col.label == "Ticket" ? col.value.length > 20 ?
|
||||
`${col.value.substring(0, 20)}...` : col.value : col.value }}
|
||||
</q-td>
|
||||
|
||||
<q-td auto-width>
|
||||
@@ -410,7 +412,7 @@
|
||||
})
|
||||
})
|
||||
},
|
||||
ticketCard(ticket){
|
||||
ticketCard(ticket) {
|
||||
this.ticketDialog.show = true
|
||||
let {date, email, ltext, name} = ticket.row
|
||||
this.ticketDialog.data = {
|
||||
@@ -469,7 +471,7 @@
|
||||
},
|
||||
updateformDialog: function (formId) {
|
||||
var link = _.findWhere(this.forms, {id: formId})
|
||||
console.log("LINK", link)
|
||||
console.log('LINK', link)
|
||||
|
||||
this.formDialog.data.id = link.id
|
||||
this.formDialog.data.wallet = link.wallet
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
from typing import Optional
|
||||
|
||||
from embit import bech32
|
||||
from embit import compact
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import hmac
|
||||
|
||||
from embit import bech32, compact
|
||||
from fastapi import Request
|
||||
from fastapi.param_functions import Query
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||
from lnbits.core.views.api import pay_invoice
|
||||
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||
|
||||
from . import lnurldevice_ext
|
||||
from .crud import (
|
||||
create_lnurldevicepayment,
|
||||
get_lnurldevice,
|
||||
get_lnurldevicepayment,
|
||||
update_lnurldevicepayment,
|
||||
get_lnurlpayload,
|
||||
update_lnurldevicepayment,
|
||||
)
|
||||
|
||||
|
||||
@@ -150,7 +146,7 @@ async def lnurl_v1_params(
|
||||
"defaultDescription": device.title,
|
||||
}
|
||||
price_msat = int(price_msat * ((device.profit / 100) + 1) / 1000)
|
||||
print(price_msat)
|
||||
|
||||
lnurldevicepayment = await create_lnurldevicepayment(
|
||||
deviceid=device.id,
|
||||
payload=p,
|
||||
@@ -204,7 +200,7 @@ async def lnurl_callback(
|
||||
extra={"tag": "withdraw"},
|
||||
)
|
||||
return {"status": "OK"}
|
||||
print(lnurldevicepayment.sats)
|
||||
|
||||
payment_hash, payment_request = await create_invoice(
|
||||
wallet_id=device.wallet,
|
||||
amount=lnurldevicepayment.sats / 1000,
|
||||
|
||||
@@ -38,7 +38,7 @@ async def m001_initial(db):
|
||||
|
||||
async def m002_redux(db):
|
||||
"""
|
||||
Moves everything from lnurlpos to lnurldevices
|
||||
Moves everything from lnurlpos to lnurldevice
|
||||
"""
|
||||
try:
|
||||
for row in [
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
label="Swagger API"
|
||||
type="a"
|
||||
href="../docs#/lnurldevice"
|
||||
></q-btn>
|
||||
<q-expansion-item
|
||||
group="api"
|
||||
dense
|
||||
@@ -39,8 +45,8 @@
|
||||
<code>[<lnurldevice_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}api/v1/lnurldevice -d '{"title":
|
||||
<string>, "message":<string>, "currency":
|
||||
>curl -X POST {{ request.base_url }}lnurldevice/api/v1/lnurlpos -d
|
||||
'{"title": <string>, "message":<string>, "currency":
|
||||
<integer>}' -H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
@@ -71,7 +77,7 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url
|
||||
}}api/v1/lnurlpos/<lnurldevice_id> -d ''{"title":
|
||||
}}lnurldevice/api/v1/lnurlpos/<lnurldevice_id> -d ''{"title":
|
||||
<string>, "message":<string>, "currency":
|
||||
<integer>} -H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{user.wallets[0].adminkey }}"
|
||||
@@ -104,8 +110,8 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url
|
||||
}}api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
}}lnurldevice/api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key:
|
||||
{{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -120,7 +126,7 @@
|
||||
<q-card-section>
|
||||
<code
|
||||
><span class="text-blue">GET</span>
|
||||
/lnurldevice/api/v1/lnurlposs</code
|
||||
/lnurldevice/api/v1/lnurlpos</code
|
||||
>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Headers</h5>
|
||||
<code>{"X-Api-Key": <invoice_key>}</code><br />
|
||||
@@ -133,7 +139,7 @@
|
||||
<code>[<lnurldevice_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}api/v1/lnurldevices -H
|
||||
>curl -X GET {{ request.base_url }}lnurldevice/api/v1/lnurlpos -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
@@ -159,8 +165,8 @@
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.base_url
|
||||
}}api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
}}lnurldevice/api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key:
|
||||
{{ user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -480,12 +480,11 @@
|
||||
updatedData
|
||||
)
|
||||
.then(function (response) {
|
||||
self.lnurldeviceLinks = _.reject(
|
||||
self.lnurldeviceLinks,
|
||||
function (obj) {
|
||||
return obj.id === updatedData.id
|
||||
}
|
||||
)
|
||||
self.lnurldeviceLinks = _.reject(self.lnurldeviceLinks, function (
|
||||
obj
|
||||
) {
|
||||
return obj.id === updatedData.id
|
||||
})
|
||||
self.lnurldeviceLinks.push(maplnurldevice(response.data))
|
||||
self.formDialoglnurldevice.show = false
|
||||
self.clearFormDialoglnurldevice()
|
||||
|
||||
@@ -12,7 +12,7 @@ db = Database("ext_lnurlp")
|
||||
lnurlp_static_files = [
|
||||
{
|
||||
"path": "/lnurlp/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/lnurlp/static"),
|
||||
"app": StaticFiles(packages=[("lnbits", "extensions/lnurlp/static")]),
|
||||
"name": "lnurlp_static",
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from lnbits.db import SQLITE
|
||||
|
||||
from . import db
|
||||
from .models import PayLink, CreatePayLinkData
|
||||
from .models import CreatePayLinkData, PayLink
|
||||
|
||||
|
||||
async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
||||
|
||||
returning = "" if db.type == SQLITE else "RETURNING ID"
|
||||
method = db.execute if db.type == SQLITE else db.fetchone
|
||||
|
||||
result = await (method)(
|
||||
f"""
|
||||
INSERT INTO lnurlp.pay_links (
|
||||
@@ -22,9 +24,10 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
||||
success_text,
|
||||
success_url,
|
||||
comment_chars,
|
||||
currency
|
||||
currency,
|
||||
fiat_base_multiplier
|
||||
)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?)
|
||||
{returning}
|
||||
""",
|
||||
(
|
||||
@@ -37,6 +40,7 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink:
|
||||
data.success_url,
|
||||
data.comment_chars,
|
||||
data.currency,
|
||||
data.fiat_base_multiplier,
|
||||
),
|
||||
)
|
||||
if db.type == SQLITE:
|
||||
|
||||
@@ -33,7 +33,7 @@ async def api_lnurl_response(request: Request, link_id):
|
||||
|
||||
resp = LnurlPayResponse(
|
||||
callback=request.url_for("lnurlp.api_lnurl_callback", link_id=link.id),
|
||||
min_sendable=math.ceil(link.min * rate) * 1000,
|
||||
min_sendable=round(link.min * rate) * 1000,
|
||||
max_sendable=round(link.max * rate) * 1000,
|
||||
metadata=link.lnurlpay_metadata,
|
||||
)
|
||||
|
||||
@@ -50,3 +50,13 @@ async def m003_min_max_comment_fiat(db):
|
||||
await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN max INTEGER;")
|
||||
await db.execute("UPDATE lnurlp.pay_links SET max = min;")
|
||||
await db.execute("DROP TABLE lnurlp.invoices")
|
||||
|
||||
|
||||
async def m004_fiat_base_multiplier(db):
|
||||
"""
|
||||
Store the multiplier for fiat prices. We store the price in cents and
|
||||
remember to multiply by 100 when we use it to convert to Dollars.
|
||||
"""
|
||||
await db.execute(
|
||||
"ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;"
|
||||
)
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import json
|
||||
from urllib.parse import urlparse, urlunparse, parse_qs, urlencode, ParseResult
|
||||
from starlette.requests import Request
|
||||
from fastapi.param_functions import Query
|
||||
from typing import Optional, Dict
|
||||
from lnbits.lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from sqlite3 import Row
|
||||
from typing import Dict, Optional
|
||||
from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
from fastapi.param_functions import Query
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
|
||||
from lnbits.lnurl import encode as lnurl_encode # type: ignore
|
||||
|
||||
|
||||
class CreatePayLinkData(BaseModel):
|
||||
description: str
|
||||
min: int = Query(0.01, ge=0.01)
|
||||
max: int = Query(0.01, ge=0.01)
|
||||
min: float = Query(1, ge=0.01)
|
||||
max: float = Query(1, ge=0.01)
|
||||
currency: str = Query(None)
|
||||
comment_chars: int = Query(0, ge=0, lt=800)
|
||||
webhook_url: str = Query(None)
|
||||
success_text: str = Query(None)
|
||||
success_url: str = Query(None)
|
||||
fiat_base_multiplier: int = Query(100, ge=1)
|
||||
|
||||
|
||||
class PayLink(BaseModel):
|
||||
id: int
|
||||
wallet: str
|
||||
description: str
|
||||
min: int
|
||||
min: float
|
||||
served_meta: int
|
||||
served_pr: int
|
||||
webhook_url: Optional[str]
|
||||
@@ -32,11 +35,15 @@ class PayLink(BaseModel):
|
||||
success_url: Optional[str]
|
||||
currency: Optional[str]
|
||||
comment_chars: int
|
||||
max: int
|
||||
max: float
|
||||
fiat_base_multiplier: int
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, row: Row) -> "PayLink":
|
||||
data = dict(row)
|
||||
if data["currency"] and data["fiat_base_multiplier"]:
|
||||
data["min"] /= data["fiat_base_multiplier"]
|
||||
data["max"] /= data["fiat_base_multiplier"]
|
||||
return cls(**data)
|
||||
|
||||
def lnurl(self, req: Request) -> str:
|
||||
|
||||
@@ -35,6 +35,7 @@ new Vue({
|
||||
rowsPerPage: 10
|
||||
}
|
||||
},
|
||||
nfcTagWriting: false,
|
||||
formDialog: {
|
||||
show: false,
|
||||
fixedAmount: true,
|
||||
@@ -205,6 +206,42 @@ new Vue({
|
||||
.catch(err => {
|
||||
LNbits.utils.notifyApiError(err)
|
||||
})
|
||||
},
|
||||
writeNfcTag: async function (lnurl) {
|
||||
try {
|
||||
if (typeof NDEFReader == 'undefined') {
|
||||
throw {
|
||||
toString: function () {
|
||||
return 'NFC not supported on this device or browser.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ndef = new NDEFReader()
|
||||
|
||||
this.nfcTagWriting = true
|
||||
this.$q.notify({
|
||||
message: 'Tap your NFC tag to write the LNURL-pay link to it.'
|
||||
})
|
||||
|
||||
await ndef.write({
|
||||
records: [{recordType: 'url', data: 'lightning:' + lnurl, lang: 'en'}]
|
||||
})
|
||||
|
||||
this.nfcTagWriting = false
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: 'NFC tag written successfully.'
|
||||
})
|
||||
} catch (error) {
|
||||
this.nfcTagWriting = false
|
||||
this.$q.notify({
|
||||
type: 'negative',
|
||||
message: error
|
||||
? error.toString()
|
||||
: 'An unexpected error has occurred.'
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import httpx
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
@@ -19,7 +20,7 @@ async def wait_for_paid_invoices():
|
||||
|
||||
|
||||
async def on_invoice_paid(payment: Payment) -> None:
|
||||
if "lnurlp" != payment.extra.get("tag"):
|
||||
if payment.extra.get("tag") != "lnurlp":
|
||||
# not an lnurlp invoice
|
||||
return
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/lnurlp"></q-btn>
|
||||
<q-expansion-item group="api" dense expand-separator label="List pay links">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
@@ -17,8 +18,8 @@
|
||||
<code>[<pay_link_object>, ...]</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}api/v1/links -H "X-Api-Key: {{
|
||||
user.wallets[0].inkey }}"
|
||||
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links -H "X-Api-Key:
|
||||
{{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -39,8 +40,8 @@
|
||||
<code>{"lnurl": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X GET {{ request.base_url }}api/v1/links/<pay_id> -H
|
||||
"X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
>curl -X GET {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
|
||||
-H "X-Api-Key: {{ user.wallets[0].inkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -51,6 +52,7 @@
|
||||
expand-separator
|
||||
label="Create a pay link"
|
||||
>
|
||||
<q-btn flat label="Swagger API" type="a" href="../docs#/lnurlp"></q-btn>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code><span class="text-green">POST</span> /lnurlp/api/v1/links</code>
|
||||
@@ -68,11 +70,11 @@
|
||||
<code>{"lnurl": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X POST {{ request.base_url }}api/v1/links -d '{"description":
|
||||
<string>, "amount": <integer>, "max": <integer>,
|
||||
"min": <integer>, "comment_chars": <integer>}' -H
|
||||
"Content-type: application/json" -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
>curl -X POST {{ request.base_url }}lnurlp/api/v1/links -d
|
||||
'{"description": <string>, "amount": <integer>, "max":
|
||||
<integer>, "min": <integer>, "comment_chars":
|
||||
<integer>}' -H "Content-type: application/json" -H "X-Api-Key:
|
||||
{{ user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -99,8 +101,8 @@
|
||||
<code>{"lnurl": <string>}</code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X PUT {{ request.base_url }}api/v1/links/<pay_id> -d
|
||||
'{"description": <string>, "amount": <integer>}' -H
|
||||
>curl -X PUT {{ request.base_url }}lnurlp/api/v1/links/<pay_id>
|
||||
-d '{"description": <string>, "amount": <integer>}' -H
|
||||
"Content-type: application/json" -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
@@ -126,8 +128,9 @@
|
||||
<code></code>
|
||||
<h5 class="text-caption q-mt-sm q-mb-none">Curl example</h5>
|
||||
<code
|
||||
>curl -X DELETE {{ request.base_url }}api/v1/links/<pay_id> -H
|
||||
"X-Api-Key: {{ user.wallets[0].adminkey }}"
|
||||
>curl -X DELETE {{ request.base_url
|
||||
}}lnurlp/api/v1/links/<pay_id> -H "X-Api-Key: {{
|
||||
user.wallets[0].adminkey }}"
|
||||
</code>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -14,10 +14,17 @@
|
||||
</q-responsive>
|
||||
</a>
|
||||
</div>
|
||||
<div class="row q-mt-lg">
|
||||
<div class="row q-mt-lg q-gutter-sm">
|
||||
<q-btn outline color="grey" @click="copyText('{{ lnurl }}')"
|
||||
>Copy LNURL</q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
icon="nfc"
|
||||
@click="writeNfcTag(' {{ lnurl }} ')"
|
||||
:disable="nfcTagWriting"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
||||
@@ -99,7 +99,8 @@
|
||||
@click="openUpdateDialog(props.row.id)"
|
||||
icon="edit"
|
||||
color="light-blue"
|
||||
></q-btn>
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
@@ -153,7 +154,8 @@
|
||||
v-model.trim="formDialog.data.description"
|
||||
type="text"
|
||||
label="Item description *"
|
||||
></q-input>
|
||||
>
|
||||
</q-input>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<q-input
|
||||
filled
|
||||
@@ -171,7 +173,8 @@
|
||||
type="number"
|
||||
:step="formDialog.data.currency && formDialog.data.currency !== 'satoshis' ? '0.01' : '1'"
|
||||
label="Max *"
|
||||
></q-input>
|
||||
>
|
||||
</q-input>
|
||||
</div>
|
||||
<div class="row q-col-gutter-sm">
|
||||
<div class="col">
|
||||
@@ -200,7 +203,8 @@
|
||||
type="number"
|
||||
label="Comment maximum characters"
|
||||
hint="Tell wallets to prompt users for a comment that will be sent along with the payment. LNURLp will store the comment and send it in the webhook."
|
||||
></q-input>
|
||||
>
|
||||
</q-input>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
@@ -224,7 +228,8 @@
|
||||
type="text"
|
||||
label="Success URL (optional)"
|
||||
hint="Will be shown as a clickable link to the user in his wallet after a successful payment, appended by the payment_hash as a query string."
|
||||
></q-input>
|
||||
>
|
||||
</q-input>
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
v-if="formDialog.data.id"
|
||||
@@ -294,6 +299,14 @@
|
||||
@click="copyText(qrCodeDialog.data.pay_url, 'Link copied to clipboard!')"
|
||||
>Shareable link</q-btn
|
||||
>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
icon="nfc"
|
||||
@click="writeNfcTag(qrCodeDialog.data.lnurl)"
|
||||
:disable="nfcTagWriting"
|
||||
>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
outline
|
||||
color="grey"
|
||||
|
||||
@@ -73,13 +73,10 @@ async def api_link_retrieve(
|
||||
@lnurlp_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK)
|
||||
async def api_link_create_or_update(
|
||||
data: CreatePayLinkData,
|
||||
request: Request,
|
||||
link_id=None,
|
||||
wallet: WalletTypeInfo = Depends(get_key_type),
|
||||
):
|
||||
if data.min < 1:
|
||||
raise HTTPException(
|
||||
detail="Min must be more than 1.", status_code=HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
if data.min > data.max:
|
||||
raise HTTPException(
|
||||
@@ -87,12 +84,18 @@ async def api_link_create_or_update(
|
||||
)
|
||||
|
||||
if data.currency == None and (
|
||||
round(data.min) != data.min or round(data.max) != data.max
|
||||
round(data.min) != data.min or round(data.max) != data.max or data.min < 1
|
||||
):
|
||||
raise HTTPException(
|
||||
detail="Must use full satoshis.", status_code=HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
# database only allows int4 entries for min and max. For fiat currencies,
|
||||
# we multiply by data.fiat_base_multiplier (usually 100) to save the value in cents.
|
||||
if data.currency and data.fiat_base_multiplier:
|
||||
data.min *= data.fiat_base_multiplier
|
||||
data.max *= data.fiat_base_multiplier
|
||||
|
||||
if "success_url" in data and data.success_url[:8] != "https://":
|
||||
raise HTTPException(
|
||||
detail="Success URL must be secure https://...",
|
||||
@@ -115,7 +118,7 @@ async def api_link_create_or_update(
|
||||
link = await update_pay_link(**data.dict(), link_id=link_id)
|
||||
else:
|
||||
link = await create_pay_link(data, wallet_id=wallet.wallet.id)
|
||||
return {**link.dict(), "lnurl": link.lnurl}
|
||||
return {**link.dict(), "lnurl": link.lnurl(request)}
|
||||
|
||||
|
||||
@lnurlp_ext.delete("/api/v1/links/{link_id}")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from lnbits.db import Database
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
"name": "LNURLPayout",
|
||||
"short_description": "Autodump wallet funds to LNURLpay",
|
||||
"icon": "exit_to_app",
|
||||
"contributors": ["arcbtc"]
|
||||
"contributors": ["arcbtc","talvasconcelos"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import List, Optional, Union
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from . import db
|
||||
from .models import lnurlpayout, CreateLnurlPayoutData
|
||||
from .models import CreateLnurlPayoutData, lnurlpayout
|
||||
|
||||
|
||||
async def create_lnurlpayout(
|
||||
|
||||
@@ -2,6 +2,7 @@ import asyncio
|
||||
from http import HTTPStatus
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from lnbits.core import db as core_db
|
||||
@@ -27,7 +28,7 @@ async def on_invoice_paid(payment: Payment) -> None:
|
||||
try:
|
||||
# Check its got a payout associated with it
|
||||
lnurlpayout_link = await get_lnurlpayout_from_wallet(payment.wallet_id)
|
||||
print("LNURLpayout", lnurlpayout_link)
|
||||
logger.debug("LNURLpayout", lnurlpayout_link)
|
||||
if lnurlpayout_link:
|
||||
|
||||
# Check the wallet balance is more than the threshold
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
label="API info"
|
||||
:content-inset-level="0.5"
|
||||
>
|
||||
<q-btn
|
||||
flat
|
||||
label="Swagger API"
|
||||
type="a"
|
||||
href="../docs#/lnurlpayout"
|
||||
></q-btn>
|
||||
<q-expansion-item group="api" dense expand-separator label="List lnurlpayout">
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
@@ -32,6 +38,7 @@
|
||||
expand-separator
|
||||
label="Create a lnurlpayout"
|
||||
>
|
||||
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<code
|
||||
|
||||
@@ -9,7 +9,7 @@ db = Database("ext_offlineshop")
|
||||
offlineshop_static_files = [
|
||||
{
|
||||
"path": "/offlineshop/static",
|
||||
"app": StaticFiles(directory="lnbits/extensions/offlineshop/static"),
|
||||
"app": StaticFiles(packages=[("lnbits", "extensions/offlineshop/static")]),
|
||||
"name": "offlineshop_static",
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from lnbits.db import SQLITE
|
||||
|
||||
from . import db
|
||||
from .models import Item, Shop
|
||||
from .wordlists import animals
|
||||
from .models import Shop, Item
|
||||
|
||||
|
||||
async def create_shop(*, wallet_id: str) -> int:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import base64
|
||||
import struct
|
||||
import hmac
|
||||
import struct
|
||||
import time
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ def hotp(key, counter, digits=6, digest="sha1"):
|
||||
key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8))
|
||||
counter = struct.pack(">Q", counter)
|
||||
mac = hmac.new(key, counter, digest).digest()
|
||||
offset = mac[-1] & 0x0f
|
||||
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7fffffff
|
||||
offset = mac[-1] & 0x0F
|
||||
binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF
|
||||
return str(binary)[-digits:].zfill(digits)
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import json
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from typing import Optional, List, Dict
|
||||
from lnurl import encode as lnurl_encode # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore
|
||||
from lnurl.types import LnurlPayMetadata # type: ignore
|
||||
from pydantic import BaseModel
|
||||
from starlette.requests import Request
|
||||
|
||||
from .helpers import totp
|
||||
|
||||
shop_counters: Dict = {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user