diff --git a/.env.example b/.env.example index 33523085..818ef843 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,9 @@ LNBITS_ADMIN_USERS="" LNBITS_ADMIN_EXTENSIONS="nostradmin" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" +LNBITS_AD_SPACE="" # csv ad image filepaths or urls, extensions can choose to honor +LNBITS_HIDE_API=false # Hides wallet api, extensions can choose to honor + # Disable extensions for all users, use "all" to disable all extensions LNBITS_DISABLED_EXTENSIONS="amilk" @@ -29,11 +32,12 @@ LNBITS_SERVICE_FEE="0.0" LNBITS_SITE_TITLE="LNbits" LNBITS_SITE_TAGLINE="free and open-source lightning wallet" LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'" -# Choose from mint, flamingo, salvador, autumn, monochrome, classic -LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, mint, autumn, monochrome, salvador" +# Choose from mint, flamingo, freedom, salvador, autumn, monochrome, classic +LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador" +# LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" -# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, LndWallet (gRPC), -# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet +# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, +# LndRestWallet, CLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet LNBITS_BACKEND_WALLET_CLASS=VoidWallet # VoidWallet is just a fallback that works without any actual Lightning capabilities, # just so you can see the UI before dealing with this file. @@ -50,14 +54,6 @@ CLIGHTNING_RPC="/home/bob/.lightning/bitcoin/lightning-rpc" LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=LNBITS_ADMIN_KEY -# LndWallet -LND_GRPC_ENDPOINT=127.0.0.1 -LND_GRPC_PORT=11009 -LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" -LND_GRPC_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon or HEXSTRING" -# To use an AES-encrypted macaroon, set -# LND_GRPC_MACAROON_ENCRYPTED="eNcRyPtEdMaCaRoOn" - # LndRestWallet LND_REST_ENDPOINT=https://127.0.0.1:8080/ LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert" @@ -82,4 +78,8 @@ OPENNODE_KEY=OPENNODE_ADMIN_KEY # FakeWallet FAKE_WALLET_SECRET="ToTheMoon1" -LNBITS_DENOMINATION=sats \ No newline at end of file +LNBITS_DENOMINATION=sats + +# EclairWallet +ECLAIR_URL=http://127.0.0.1:8283 +ECLAIR_PASS=eclairpw \ No newline at end of file diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 77d340c1..bf90a8e3 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -9,4 +9,5 @@ jobs: - uses: actions/checkout@v1 - uses: jpetrucciani/mypy-check@master with: + mypy_flags: '--install-types --non-interactive' path: lnbits diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml deleted file mode 100644 index 4d008dec..00000000 --- a/.github/workflows/on-push.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Docker build on push - -env: - DOCKER_CLI_EXPERIMENTAL: enabled - -on: - push: - branches: - - master - -jobs: - build: - runs-on: ubuntu-20.04 - name: Build and push lnbits image - steps: - - name: Login to Docker Hub - run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - - - name: Checkout project - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - id: qemu - - - name: Setup Docker buildx action - uses: docker/setup-buildx-action@v1 - id: buildx - - - name: Show available Docker buildx platforms - run: echo ${{ steps.buildx.outputs.platforms }} - - - name: Cache Docker layers - uses: actions/cache@v2 - id: cache - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Run Docker buildx against commit hash - run: | - docker buildx build \ - --cache-from "type=local,src=/tmp/.buildx-cache" \ - --cache-to "type=local,dest=/tmp/.buildx-cache" \ - --platform linux/amd64,linux/arm64,linux/arm/v7 \ - --tag ${{ secrets.DOCKER_USERNAME }}/lnbits:${GITHUB_SHA:0:7} \ - --output "type=registry" ./ - - - name: Run Docker buildx against latest - run: | - docker buildx build \ - --cache-from "type=local,src=/tmp/.buildx-cache" \ - --cache-to "type=local,dest=/tmp/.buildx-cache" \ - --platform linux/amd64,linux/arm64,linux/arm/v7 \ - --tag ${{ secrets.DOCKER_USERNAME }}/lnbits:latest \ - --output "type=registry" ./ \ No newline at end of file diff --git a/.github/workflows/on-tag.yml b/.github/workflows/on-tag.yml new file mode 100644 index 00000000..f6fa53e9 --- /dev/null +++ b/.github/workflows/on-tag.yml @@ -0,0 +1,68 @@ +name: Build and push Docker image on tag + +env: + DOCKER_CLI_EXPERIMENTAL: enabled + +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+-*" + +jobs: + build: + runs-on: ubuntu-20.04 + name: Build and push lnbits image + steps: + - name: Login to Docker Hub + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + + - name: Checkout project + uses: actions/checkout@v2 + + - name: Import environment variables + id: import-env + shell: bash + run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV + + - name: Show set environment variables + run: | + printf " TAG: %s\n" "$TAG" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + id: qemu + + - name: Setup Docker buildx action + uses: docker/setup-buildx-action@v1 + id: buildx + + - name: Show available Docker buildx platforms + run: echo ${{ steps.buildx.outputs.platforms }} + + - name: Cache Docker layers + uses: actions/cache@v2 + id: cache + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Run Docker buildx against tag + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/amd64,linux/arm64 \ + --tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:${TAG} \ + --output "type=registry" ./ + + - name: Run Docker buildx against latest + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/amd64,linux/arm64 \ + --tag ${{ secrets.DOCKER_USERNAME }}/lnbits-legend:latest \ + --output "type=registry" ./ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 243f298b..7b8e523d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,8 +8,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH" # Install build deps RUN apt-get update -RUN apt-get install -y --no-install-recommends build-essential +RUN apt-get install -y --no-install-recommends build-essential pkg-config RUN python -m pip install --upgrade pip +RUN pip install wheel # Install runtime deps COPY requirements.txt /tmp/requirements.txt @@ -36,6 +37,9 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH" WORKDIR /app COPY --chown=1000:1000 lnbits /app/lnbits +ENV LNBITS_PORT="5000" +ENV LNBITS_HOST="0.0.0.0" + EXPOSE 5000 -CMD ["uvicorn", "lnbits.__main__:app", "--port", "5000", "--host", "0.0.0.0"] +CMD ["sh", "-c", "uvicorn lnbits.__main__:app --port $LNBITS_PORT --host $LNBITS_HOST"] diff --git a/README.md b/README.md index b88640ad..020f617c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ LNbits (Join us on [https://t.me/lnbits](https://t.me/lnbits)) +(LNbits is beta, for responsible disclosure of any concerns please contact lnbits@pm.me) + Use [lnbits.com](https://lnbits.com), or run your own LNbits server! LNbits is a very simple Python server that sits on top of any funding source, and can be used as: diff --git a/conv.py b/conv.py index 159c7dc0..aa66a998 100644 --- a/conv.py +++ b/conv.py @@ -1,6 +1,7 @@ import psycopg2 import sqlite3 import os + # Python script to migrate an LNbits SQLite DB to Postgres # All credits to @Fritz446 for the awesome work diff --git a/docs/devs/installation.md b/docs/devs/installation.md index c8efb1c6..cbf234cc 100644 --- a/docs/devs/installation.md +++ b/docs/devs/installation.md @@ -12,6 +12,8 @@ LNbits uses [Pipenv][pipenv] to manage Python packages. ```sh git clone https://github.com/lnbits/lnbits-legend.git cd lnbits-legend/ + +sudo apt-get install pipenv pipenv shell # pipenv --python 3.9 shell (if you wish to use a version of Python higher than 3.7) pipenv install --dev @@ -19,6 +21,9 @@ pipenv install --dev # If any of the modules fails to install, try checking and upgrading your setupTool module # pip install -U setuptools + +# install libffi/libpq in case "pipenv install" fails +# sudo apt-get install -y libffi-dev libpq-dev ``` ## Running the server @@ -41,4 +46,7 @@ E.g. when you want to use LND you have to `pipenv run pip install lndgrpc` and ` Take a look at [Polar][polar] for an excellent way of spinning up a Lightning Network dev environment. -**Note**: We reccomend using Caddy for a reverse-proxy, if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/). +**Notes**: + +* We reccomend using Caddy for a reverse-proxy if you want to serve your install through a domain, alternatively you can use [ngrok](https://ngrok.com/). +* Screen works well if you want LNbits to continue running when you close your terminal session. diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 2806a4f5..b458c3f1 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -49,17 +49,20 @@ You might also need to install additional packages or perform additional setup s ## Important note If you already have LNbits installed and running, on an SQLite database, we **HIGHLY** recommend you migrate to postgres! -There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user, check the guide above. +There's a script included that can do the migration easy. You should have Postgres already installed and there should be a password for the user, check the guide above. Additionally, your lnbits instance should run once on postgres to implement the database schema before the migration works: ```sh # STOP LNbits -# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials -python3 conv.py # add the database connection string to .env 'nano .env' LNBITS_DATABASE_URL= # postgres://:@/ - alter line bellow with your user, password and db name LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost/lnbits" # save and exit + +# START LNbits +# STOP LNbits +# on the LNBits folder, locate and edit 'conv.py' with the relevant credentials +python3 conv.py ``` Hopefully, everything works and get migrated... Launch LNbits again and check if everything is working properly. @@ -78,17 +81,23 @@ Systemd is great for taking care of your LNbits instance. It will start it on bo [Unit] Description=LNbits -#Wants=lnd.service # you can uncomment these lines if you know what you're doing -#After=lnd.service # it will make sure that lnbits starts after lnd (replace with your own backend service) +# you can uncomment these lines if you know what you're doing +# it will make sure that lnbits starts after lnd (replace with your own backend service) +#Wants=lnd.service +#After=lnd.service [Service] -WorkingDirectory=/home/bitcoin/lnbits # replace with the absolute path of your lnbits installation -ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000 # same here -User=bitcoin # replace with the user that you're running lnbits on +# replace with the absolute path of your lnbits installation +WorkingDirectory=/home/bitcoin/lnbits +# same here +ExecStart=/home/bitcoin/lnbits/venv/bin/uvicorn lnbits.__main__:app --port 5000 +# replace with the user that you're running lnbits on +User=bitcoin Restart=always TimeoutSec=120 RestartSec=30 -Environment=PYTHONUNBUFFERED=1 # this makes sure that you receive logs in real time +# this makes sure that you receive logs in real time +Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target diff --git a/docs/guide/wallets.md b/docs/guide/wallets.md index 4b19363a..7a3b6a27 100644 --- a/docs/guide/wallets.md +++ b/docs/guide/wallets.md @@ -47,7 +47,7 @@ To encrypt your macaroon, run `./venv/bin/python lnbits/wallets/macaroon/macaroo ### LND (REST) - `LNBITS_BACKEND_WALLET_CLASS`: **LndRestWallet** -- `LND_REST_ENDPOINT`: ip_address +- `LND_REST_ENDPOINT`: http://10.147.17.230:8080/ - `LND_REST_CERT`: /file/path/tls.cert - `LND_REST_MACAROON`: /file/path/admin.macaroon or Bech64/Hex diff --git a/lnbits/app.py b/lnbits/app.py index 64e4ba4e..2fd18d5b 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -3,11 +3,13 @@ import importlib 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 import lnbits.settings @@ -58,15 +60,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 + + 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 HTMLResponse( - # status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - # content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}), - # ) + return JSONResponse( + status_code=HTTPStatus.NO_CONTENT, + content={"detail": exc.errors()}, + ) app.add_middleware(GZipMiddleware, minimum_size=1000) # app.add_middleware(ASGIProxyFix) @@ -84,18 +90,19 @@ def create_app(config_object="lnbits.settings") -> FastAPI: def check_funding_source(app: FastAPI) -> None: @app.on_event("startup") async def check_wallet_status(): - error_message, balance = await WALLET.status() - if error_message: + while True: + 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}'", RuntimeWarning, ) - - sys.exit(4) - else: - print( - f" ✔️ {WALLET.__class__.__name__} seems to be connected and with a balance of {balance} msat." - ) + print("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." + ) def register_routes(app: FastAPI) -> None: @@ -167,9 +174,17 @@ def register_exception_handlers(app: FastAPI): @app.exception_handler(Exception) async def basic_error(request: Request, err): print("handled error", traceback.format_exc()) + print("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}, ) diff --git a/lnbits/bolt11.py b/lnbits/bolt11.py index 74f73963..e5221984 100644 --- a/lnbits/bolt11.py +++ b/lnbits/bolt11.py @@ -165,7 +165,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 +270,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 +289,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 +348,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), ) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index a63f52c4..e9b28a55 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -180,13 +180,18 @@ 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, ) -> Optional[Payment]: + clause: str = "checking_id = ? OR hash = ?" + if incoming: + clause = f"({clause}) AND amount > 0" 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), diff --git a/lnbits/core/services.py b/lnbits/core/services.py index be21a84e..3d54e218 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -85,18 +85,17 @@ async def pay_invoice( description: str = "", conn: Optional[Connection] = None, ) -> str: + invoice = bolt11.decode(payment_request) + fee_reserve_msat = fee_reserve(invoice.amount_msat) async with (db.reuse_conn(conn) if conn else db.connect()) as conn: temp_id = f"temp_{urlsafe_short_hash()}" internal_id = f"internal_{urlsafe_short_hash()}" - invoice = bolt11.decode(payment_request) if invoice.amount_msat == 0: raise ValueError("Amountless invoices not supported.") if max_sat and invoice.amount_msat > max_sat * 1000: raise ValueError("Amount in invoice is too high.") - wallet = await get_wallet(wallet_id, conn=conn) - # put all parameters that don't change here PaymentKwargs = TypedDict( "PaymentKwargs", @@ -134,26 +133,20 @@ async def pay_invoice( # the balance is enough in the next step await create_payment( checking_id=temp_id, - fee=-fee_reserve(invoice.amount_msat), + fee=-fee_reserve_msat, conn=conn, **payment_kwargs, ) - # do the balance check if internal payment - if internal_checking_id: - wallet = await get_wallet(wallet_id, conn=conn) - assert wallet - if wallet.balance_msat < 0: - raise PermissionError("Insufficient balance.") - - # do the balance check if external payment - else: - if invoice.amount_msat > wallet.balance_msat - ( - wallet.balance_msat / 100 * 2 - ): - raise PermissionError( - "LNbits requires you keep at least 2% reserve to cover potential routing fees." + # do the balance check + wallet = await get_wallet(wallet_id, conn=conn) + assert wallet + if wallet.balance_msat < 0: + 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." ) + raise PermissionError("Insufficient balance.") if internal_checking_id: # mark the invoice from the other side as not pending anymore @@ -171,7 +164,9 @@ async def pay_invoice( await internal_invoice_queue.put(internal_checking_id) else: # actually pay the external invoice - payment: PaymentResponse = await WALLET.pay_invoice(payment_request) + payment: PaymentResponse = await WALLET.pay_invoice( + payment_request, fee_reserve_msat + ) if payment.checking_id: async with db.connect() as conn: await create_payment( @@ -286,12 +281,12 @@ async def perform_lnurlauth( sign_len = 6 + r_len + s_len signature = BytesIO() - signature.write(0x30 .to_bytes(1, "big", signed=False)) + signature.write(0x30.to_bytes(1, "big", signed=False)) signature.write((sign_len - 2).to_bytes(1, "big", signed=False)) - signature.write(0x02 .to_bytes(1, "big", signed=False)) + signature.write(0x02.to_bytes(1, "big", signed=False)) signature.write(r_len.to_bytes(1, "big", signed=False)) signature.write(r) - signature.write(0x02 .to_bytes(1, "big", signed=False)) + signature.write(0x02.to_bytes(1, "big", signed=False)) signature.write(s_len.to_bytes(1, "big", signed=False)) signature.write(s) @@ -340,5 +335,6 @@ async def check_invoice_status( return status +# WARN: this same value must be used for balance check and passed to WALLET.pay_invoice(), it may cause a vulnerability if the values differ def fee_reserve(amount_msat: int) -> int: - return max(1000, int(amount_msat * 0.01)) + return max(2000, int(amount_msat * 0.01)) diff --git a/lnbits/core/static/js/wallet.js b/lnbits/core/static/js/wallet.js index 8d58302b..29a1025d 100644 --- a/lnbits/core/static/js/wallet.js +++ b/lnbits/core/static/js/wallet.js @@ -364,12 +364,12 @@ new Vue({ }, decodeRequest: function () { this.parse.show = true - + let req = this.parse.data.request.toLowerCase() if (this.parse.data.request.startsWith('lightning:')) { this.parse.data.request = this.parse.data.request.slice(10) } else if (this.parse.data.request.startsWith('lnurl:')) { this.parse.data.request = this.parse.data.request.slice(6) - } else if (this.parse.data.request.indexOf('lightning=lnurl1') !== -1) { + } else if (req.indexOf('lightning=lnurl1') !== -1) { this.parse.data.request = this.parse.data.request .split('lightning=')[1] .split('&')[0] @@ -618,10 +618,10 @@ new Vue({ }, updateWalletName: function () { let newName = this.newName + let adminkey = this.g.wallet.adminkey if (!newName || !newName.length) return - // let data = {name: newName} LNbits.api - .request('PUT', '/api/v1/wallet/' + newName, this.g.wallet.inkey, {}) + .request('PUT', '/api/v1/wallet/' + newName, adminkey, {}) .then(res => { this.newName = '' this.$q.notify({ @@ -691,10 +691,7 @@ new Vue({ }, mounted: function () { // show disclaimer - if ( - this.$refs.disclaimer && - !this.$q.localStorage.getItem('lnbits.disclaimerShown') - ) { + if (!this.$q.localStorage.getItem('lnbits.disclaimerShown')) { this.disclaimerDialog.show = true this.$q.localStorage.set('lnbits.disclaimerShown', true) } diff --git a/lnbits/core/templates/core/_api_docs.html b/lnbits/core/templates/core/_api_docs.html index c7f3f9ad..5df37676 100644 --- a/lnbits/core/templates/core/_api_docs.html +++ b/lnbits/core/templates/core/_api_docs.html @@ -48,7 +48,7 @@ {"X-Api-Key": "{{ wallet.inkey }}"}
Body (application/json)
{"out": false, "amount": <int>, "memo": <string>}{"out": false, "amount": <int>, "memo": <string>, "unit": <string>, "webhook": <url:string>}
Returns 201 CREATED (application/json) @@ -61,7 +61,7 @@ curl -X POST {{ request.base_url }}api/v1/payments -d '{"out": false, "amount": <int>, "memo": <string>, "webhook": - <url:string>}' -H "X-Api-Key: {{ wallet.inkey }}" -H + <url:string>, "unit": <string>}' -H "X-Api-Key: {{ wallet.inkey }}" -H "Content-type: application/json" diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index 88dc496d..f363a841 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -11,7 +11,7 @@ color="primary" @click="processing" type="a" - href="{{ url_for('core.lnurlwallet', lightning=lnurl) }}" + href="{{ url_for('core.lnurlwallet') }}?lightning={{ lnurl }}" > Press to claim bitcoin diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index 2b6ec5de..db435866 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -273,442 +273,469 @@ -
- - -
- {{ SITE_TITLE }} Wallet: {{ wallet.name }} -
-
- - - - {% include "core/_api_docs.html" %} + {% if HIDE_API %} +
+ {% else %} +
+ + +
+ {{ SITE_TITLE }} Wallet: {{ wallet.name }} +
+
+ - {% if wallet.lnurlwithdraw_full %} - - - -

- This is an LNURL-withdraw QR code for slurping everything from - this wallet. Do not share with anyone. -

- + + {% include "core/_api_docs.html" %} + + + {% if wallet.lnurlwithdraw_full %} + + + +

+ This is an LNURL-withdraw QR code for slurping everything + from this wallet. Do not share with anyone. +

+
+ + +

+ It is compatible with balanceCheck and + balanceNotify so your wallet may keep pulling + the funds continuously from here after the first withdraw. +

+
+
+
+ + {% endif %} + + + + +

+ This QR code contains your wallet URL with full access. You + can scan it from your phone to open your wallet from there. +

- -

- It is compatible with balanceCheck and - balanceNotify so your wallet may keep pulling the - funds continuously from here after the first withdraw. -

-
-
-
- - {% endif %} - - - - -

- This QR code contains your wallet URL with full access. You - can scan it from your phone to open your wallet from there. -

- -
-
-
- - - - -
- -
- Update name -
-
-
- - - - -

- This whole wallet will be deleted, the funds will be - UNRECOVERABLE. -

- Delete wallet -
-
-
- -
-
+ + + + + + + +
+ +
+ Update name +
+
+
+ + + + +

+ This whole wallet will be deleted, the funds will be + UNRECOVERABLE. +

+ Delete wallet +
+
+
+ + + + {% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = + ADS.split(';') %} + + {% endfor %} {% endif %} +
-
- - {% raw %} - - -

- {{receive.lnurl.domain}} is requesting an invoice: -

- {% endraw %} {% if LNBITS_DENOMINATION != 'sats' %} - - {% else %} - - - {% endif %} - - - {% raw %} -
- - - Withdraw from {{receive.lnurl.domain}} - - Create invoice - - Cancel -
- -
-
- - -
- Copy invoice - Close -
-
- {% endraw %} -
- - - -
-
- {% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",", "")) - / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} -
-
- {{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %} -
- -

- Description: {{ parse.invoice.description }}
- Expire date: {{ parse.invoice.expireDate }}
- Hash: {{ parse.invoice.hash }} -

- {% endraw %} -
- Pay - Cancel -
-
- Not enough funds! - Cancel -
-
-
- {% raw %} - -

- Authenticate with {{ parse.lnurlauth.domain }}? + + {% raw %} + + +

+ {{receive.lnurl.domain}} is requesting an invoice:

- -

- For every website and for every LNbits wallet, a new keypair will be - deterministically generated so your identity can't be tied to your - LNbits wallet or linked across websites. No other data will be shared - with {{ parse.lnurlauth.domain }}. -

-

Your public key for {{ parse.lnurlauth.domain }} is:

-

- {{ parse.lnurlauth.pubkey }} -

-
- Login - Cancel -
-
- {% endraw %} -
-
- {% raw %} - -

- {{ parse.lnurlpay.domain }} is requesting {{ - parse.lnurlpay.maxSendable | msatoshiFormat }} {{LNBITS_DENOMINATION}} - -
- and a {{parse.lnurlpay.commentAllowed}}-char comment -
-

-

- {{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }} is - requesting
- between {{ parse.lnurlpay.minSendable | msatoshiFormat }} and - {{ parse.lnurlpay.maxSendable | msatoshiFormat }} - {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} - -
- and a {{parse.lnurlpay.commentAllowed}}-char comment -
-

- -
-

- {{ parse.lnurlpay.description }} -

-

- -

-
-
-
- {% endraw %} - - {% raw %} -
-
- -
-
-
- Send {{LNBITS_DENOMINATION}} - Cancel -
-
- {% endraw %} -
-
- + {% endraw %} {% if LNBITS_DENOMINATION != 'sats' %} - -
+ v-model.number="receive.data.amount" + label="Amount ({{LNBITS_DENOMINATION}}) *" + mask="#.##" + fill-mask="0" + reverse-fill-mask + :min="receive.minMax[0]" + :max="receive.minMax[1]" + :readonly="receive.lnurl && receive.lnurl.fixed" + > + {% else %} + + + {% endif %} + + + {% raw %} +
Read + + Withdraw from {{receive.lnurl.domain}} + + Create invoice + + Cancel +
+ + + + + +
+ Copy invoice + Close +
+
+ {% endraw %} + + + + +
+
+ {% raw %} {{ parseFloat(String(parse.invoice.fsat).replaceAll(",", + "")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} +
+
+ {{ parse.invoice.fsat }}{% endraw %} {{LNBITS_DENOMINATION}} {% raw %} +
+ +

+ Description: {{ parse.invoice.description }}
+ Expire date: {{ parse.invoice.expireDate }}
+ Hash: {{ parse.invoice.hash }} +

+ {% endraw %} +
+ Pay + Cancel +
+
+ Not enough funds! Cancel
- +
+
+ {% raw %} + +

+ Authenticate with {{ parse.lnurlauth.domain }}? +

+ +

+ For every website and for every LNbits wallet, a new keypair will be + deterministically generated so your identity can't be tied to your + LNbits wallet or linked across websites. No other data will be + shared with {{ parse.lnurlauth.domain }}. +

+

Your public key for {{ parse.lnurlauth.domain }} is:

+

+ {{ parse.lnurlauth.pubkey }} +

+
+ Login + Cancel +
+
+ {% endraw %} +
+
+ {% raw %} + +

+ {{ parse.lnurlpay.domain }} is requesting {{ + parse.lnurlpay.maxSendable | msatoshiFormat }} + {{LNBITS_DENOMINATION}} + +
+ and a {{parse.lnurlpay.commentAllowed}}-char comment +
+

+

+ {{ parse.lnurlpay.targetUser || parse.lnurlpay.domain }} is + requesting
+ between {{ parse.lnurlpay.minSendable | msatoshiFormat }} and + {{ parse.lnurlpay.maxSendable | msatoshiFormat }} + {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} + +
+ and a {{parse.lnurlpay.commentAllowed}}-char comment +
+

+ +
+

+ {{ parse.lnurlpay.description }} +

+

+ +

+
+
+
+ {% endraw %} + + {% raw %} +
+
+ +
+
+
+ Send {{LNBITS_DENOMINATION}} + Cancel +
+
+ {% endraw %} +
- - - -
- - Cancel - + + + +
+ Read + Cancel +
+
+
+ + + +
+ + Cancel + +
-
-
-
+ + - - -
- -
-
- Cancel -
-
-
+ + +
+ +
+
+ Cancel +
+
+
- - - - - - - - - + + + + + + + - - - - + + + + + - - -{% if service_fee > 0 %} -
- - -
Warning
-

- Login functionality to be released in v0.2, for now, - make sure you bookmark this page for future access to your - wallet! -

-

- This service is in BETA, and we hold no responsibility for people losing - access to funds. To encourage you to run your own LNbits installation, any - balance on {% raw %}{{ disclaimerDialog.location.host }}{% endraw %} will - incur a charge of {{ service_fee }}% service fee per - week. -

-
- Copy wallet URL - I understand -
-
-
-{% endif %} {% endblock %} + + + + + +
Warning
+

+ Login functionality to be released in v0.2, for now, + make sure you bookmark this page for future access to your + wallet! +

+

+ This service is in BETA, and we hold no responsibility for people losing + access to funds. {% if service_fee > 0 %} To encourage you to run your + own LNbits installation, any balance on {% raw %}{{ + disclaimerDialog.location.host }}{% endraw %} will incur a charge of + {{ service_fee }}% service fee per week. {% endif %} +

+
+ Copy wallet URL + I understand +
+
+
+ {% endblock %} +
diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 107a2684..3b2f8b3a 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Union from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse import httpx -from fastapi import Query, Request +from fastapi import Header, Query, Request from fastapi.exceptions import HTTPException from fastapi.param_functions import Depends from fastapi.params import Body @@ -23,9 +23,11 @@ from lnbits.decorators import ( WalletInvoiceKeyChecker, WalletTypeInfo, get_key_type, + require_admin_key, ) -from lnbits.helpers import url_for +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, fiat_amount_as_satoshis, @@ -34,13 +36,14 @@ from lnbits.utils.exchange_rates import ( from .. import core_app, db from ..crud import ( + create_payment, get_payments, get_standalone_payment, - save_balance_check, - update_wallet, - create_payment, get_wallet, + get_wallet_for_key, + save_balance_check, update_payment_status, + update_wallet, ) from ..services import ( InvoiceFailure, @@ -51,8 +54,6 @@ from ..services import ( perform_lnurlauth, ) from ..tasks import api_invoice_listeners -from lnbits.settings import LNBITS_ADMIN_USERS -from lnbits.helpers import urlsafe_short_hash @core_app.get("/api/v1/wallet") @@ -98,7 +99,7 @@ async def api_update_balance( @core_app.put("/api/v1/wallet/{new_name}") async def api_update_wallet( - new_name: str, wallet: WalletTypeInfo = Depends(WalletAdminKeyChecker()) + new_name: str, wallet: WalletTypeInfo = Depends(require_admin_key) ): await update_wallet(wallet.wallet.id, new_name) return { @@ -123,8 +124,8 @@ async def api_payments(wallet: WalletTypeInfo = Depends(get_key_type)): class CreateInvoiceData(BaseModel): out: Optional[bool] = True - amount: int = Query(None, ge=1) - memo: str = None + amount: float = Query(None, ge=0) + memo: Optional[str] = None unit: Optional[str] = "sat" description_hash: Optional[str] = None lnurl_callback: Optional[str] = None @@ -140,9 +141,9 @@ async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): memo = "" else: description_hash = b"" - memo = data.memo + memo = data.memo or LNBITS_SITE_TITLE if data.unit == "sat": - amount = data.amount + amount = int(data.amount) else: price_in_sats = await fiat_amount_as_satoshis(data.amount, data.unit) amount = price_in_sats @@ -292,11 +293,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 = {} @@ -363,7 +364,13 @@ async def api_payments_sse( @core_app.get("/api/v1/payments/{payment_hash}") -async def api_payment(payment_hash): +async def api_payment(payment_hash, X_Api_Key: Optional[str] = Header(None)): + wallet = None + try: + if X_Api_Key.extra: + print("No key") + except: + wallet = await get_wallet_for_key(X_Api_Key) payment = await get_standalone_payment(payment_hash) await check_invoice_status(payment.wallet_id, payment_hash) payment = await get_standalone_payment(payment_hash) @@ -372,13 +379,23 @@ async def api_payment(payment_hash): status_code=HTTPStatus.NOT_FOUND, detail="Payment does not exist." ) elif not payment.pending: + if wallet and wallet.id == payment.wallet_id: + return {"paid": True, "preimage": payment.preimage, "details": payment} return {"paid": True, "preimage": payment.preimage} try: await payment.check_pending() except Exception: + if wallet and wallet.id == payment.wallet_id: + return {"paid": False, "details": payment} 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} @@ -500,14 +517,19 @@ async def api_lnurlscan(code: str): return params +class DecodePayment(BaseModel): + data: str + + @core_app.post("/api/v1/payments/decode") -async def api_payments_decode(data: str = Query(None)): +async def api_payments_decode(data: DecodePayment): + payment_str = data.data try: - if data["data"][:5] == "LNURL": - url = lnurl.decode(data["data"]) + if payment_str[:5] == "LNURL": + url = lnurl.decode(payment_str) return {"domain": url} else: - invoice = bolt11.decode(data["data"]) + invoice = bolt11.decode(payment_str) return { "payment_hash": invoice.payment_hash, "amount_msat": invoice.amount_msat, @@ -559,6 +581,6 @@ async def api_fiat_as_sats(data: ConversionData): return output else: output[data.from_.upper()] = data.amount - output["sats"] = await fiat_amount_as_satoshis(data.amount, data.to) + output["sats"] = await fiat_amount_as_satoshis(data.amount, data.from_) output["BTC"] = output["sats"] / 100000000 return output diff --git a/lnbits/core/views/generic.py b/lnbits/core/views/generic.py index d917ffab..d9687e16 100644 --- a/lnbits/core/views/generic.py +++ b/lnbits/core/views/generic.py @@ -15,8 +15,8 @@ from lnbits.core.models import User from lnbits.decorators import check_user_exists from lnbits.helpers import template_renderer, url_for from lnbits.settings import ( - LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS, + LNBITS_ALLOWED_USERS, LNBITS_SITE_TITLE, SERVICE_FEE, ) @@ -226,7 +226,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) +@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) diff --git a/lnbits/db.py b/lnbits/db.py index 02850958..7bbfa5c5 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -130,9 +130,15 @@ class Database(Compat): ) ) else: - self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3") - database_uri = f"sqlite:///{self.path}" - self.type = SQLITE + if os.path.isdir(LNBITS_DATA_FOLDER): + self.path = os.path.join(LNBITS_DATA_FOLDER, f"{self.name}.sqlite3") + database_uri = f"sqlite:///{self.path}" + self.type = SQLITE + else: + raise NotADirectoryError( + f"LNBITS_DATA_FOLDER named {LNBITS_DATA_FOLDER} was not created" + f" - please 'mkdir {LNBITS_DATA_FOLDER}' and try again" + ) self.schema = self.name if self.name.startswith("ext_"): diff --git a/lnbits/decorators.py b/lnbits/decorators.py index 9eee1afa..d6f73f40 100644 --- a/lnbits/decorators.py +++ b/lnbits/decorators.py @@ -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_ALLOWED_USERS, + LNBITS_ADMIN_USERS, + LNBITS_ADMIN_EXTENSIONS, +) 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: @@ -147,9 +155,13 @@ async def get_key_type( try: checker = WalletInvoiceKeyChecker(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.") + 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." + ) return wallet except HTTPException as e: if e.status_code == HTTPStatus.BAD_REQUEST: diff --git a/lnbits/extensions/copilot/crud.py b/lnbits/extensions/copilot/crud.py index e456334e..d0da044e 100644 --- a/lnbits/extensions/copilot/crud.py +++ b/lnbits/extensions/copilot/crud.py @@ -88,7 +88,7 @@ async def get_copilot(copilot_id: str) -> Copilots: async def get_copilots(user: str) -> List[Copilots]: rows = await db.fetchall( - "SELECT * FROM copilot.newer_copilots WHERE user = ?", (user,) + 'SELECT * FROM copilot.newer_copilots WHERE "user" = ?', (user,) ) return [Copilots(**row) for row in rows] diff --git a/lnbits/extensions/discordbot/Pipfile b/lnbits/extensions/discordbot/Pipfile new file mode 100644 index 00000000..d5820662 --- /dev/null +++ b/lnbits/extensions/discordbot/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/lnbits/extensions/discordbot/README.md b/lnbits/extensions/discordbot/README.md new file mode 100644 index 00000000..a1408317 --- /dev/null +++ b/lnbits/extensions/discordbot/README.md @@ -0,0 +1,34 @@ +# Discord Bot + +## Provide LNbits wallets for all your Discord users + +_This extension is a modifed version of LNbits [User Manager](../usermanager/README.md)_ + +The intended usage of this extension is to connect it to a specifically designed [Discord Bot](https://github.com/chrislennon/lnbits-discord-bot) leveraging LNbits as a community based lightning node. + +## Setup +This bot can target [lnbits.com](https://lnbits.com) or a self hosted instance. + +To setup and run the bot instructions are located [here](https://github.com/chrislennon/lnbits-discord-bot#installation) + +## Usage +This bot will allow users to interact with it in the following ways [full command list](https://github.com/chrislennon/lnbits-discord-bot#commands): + +`/create` Will create a wallet for the Discord user + - (currently limiting 1 Discord user == 1 LNbits user == 1 user wallet) + +![create](https://imgur.com/CWdDusE.png) + +`/balance` Will show the balance of the users wallet. + +![balance](https://imgur.com/tKeReCp.png) + +`/tip @user [amount]` Will sent money from one user to another + - If the recieving user does not have a wallet, one will be created for them + - The receiving user will receive a direct message from the bot with a link to their wallet + +![tip](https://imgur.com/K3tnChK.png) + +`/payme [amount] [description]` Will open an invoice that can be paid by any user + +![payme](https://imgur.com/dFvAqL3.png) diff --git a/lnbits/extensions/discordbot/__init__.py b/lnbits/extensions/discordbot/__init__.py new file mode 100644 index 00000000..ff60dd62 --- /dev/null +++ b/lnbits/extensions/discordbot/__init__.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter +from fastapi.staticfiles import StaticFiles + +from lnbits.db import Database +from lnbits.helpers import template_renderer + +db = Database("ext_discordbot") + +discordbot_static_files = [ + { + "path": "/discordbot/static", + "app": StaticFiles(directory="lnbits/extensions/discordbot/static"), + "name": "discordbot_static", + } +] + +discordbot_ext: APIRouter = APIRouter(prefix="/discordbot", tags=["discordbot"]) + + +def discordbot_renderer(): + return template_renderer(["lnbits/extensions/discordbot/templates"]) + + +from .views import * # noqa +from .views_api import * # noqa diff --git a/lnbits/extensions/discordbot/config.json b/lnbits/extensions/discordbot/config.json new file mode 100644 index 00000000..eb674122 --- /dev/null +++ b/lnbits/extensions/discordbot/config.json @@ -0,0 +1,6 @@ +{ + "name": "Discord Bot", + "short_description": "Generate users and wallets", + "icon": "person_add", + "contributors": ["bitcoingamer21"] +} diff --git a/lnbits/extensions/discordbot/crud.py b/lnbits/extensions/discordbot/crud.py new file mode 100644 index 00000000..5661fcb4 --- /dev/null +++ b/lnbits/extensions/discordbot/crud.py @@ -0,0 +1,123 @@ +from typing import List, Optional + +from lnbits.core.crud import ( + create_account, + create_wallet, + delete_wallet, + get_payments, + get_user, +) +from lnbits.core.models import Payment + +from . import db +from .models import CreateUserData, Users, Wallets + +### Users + + +async def create_discordbot_user(data: CreateUserData) -> Users: + account = await create_account() + user = await get_user(account.id) + assert user, "Newly created user couldn't be retrieved" + + wallet = await create_wallet(user_id=user.id, wallet_name=data.wallet_name) + + await db.execute( + """ + INSERT INTO discordbot.users (id, name, admin, discord_id) + VALUES (?, ?, ?, ?) + """, + (user.id, data.user_name, data.admin_id, data.discord_id), + ) + + await db.execute( + """ + INSERT INTO discordbot.wallets (id, admin, name, "user", adminkey, inkey) + VALUES (?, ?, ?, ?, ?, ?) + """, + ( + wallet.id, + data.admin_id, + data.wallet_name, + user.id, + wallet.adminkey, + wallet.inkey, + ), + ) + + user_created = await get_discordbot_user(user.id) + assert user_created, "Newly created user couldn't be retrieved" + return user_created + + +async def get_discordbot_user(user_id: str) -> Optional[Users]: + row = await db.fetchone("SELECT * FROM discordbot.users WHERE id = ?", (user_id,)) + return Users(**row) if row else None + + +async def get_discordbot_users(user_id: str) -> List[Users]: + rows = await db.fetchall( + "SELECT * FROM discordbot.users WHERE admin = ?", (user_id,) + ) + + return [Users(**row) for row in rows] + + +async def delete_discordbot_user(user_id: str) -> None: + wallets = await get_discordbot_wallets(user_id) + for wallet in wallets: + await delete_wallet(user_id=user_id, wallet_id=wallet.id) + + await db.execute("DELETE FROM discordbot.users WHERE id = ?", (user_id,)) + await db.execute("""DELETE FROM discordbot.wallets WHERE "user" = ?""", (user_id,)) + + +### Wallets + + +async def create_discordbot_wallet( + user_id: str, wallet_name: str, admin_id: str +) -> Wallets: + wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name) + await db.execute( + """ + INSERT INTO discordbot.wallets (id, admin, name, "user", adminkey, inkey) + VALUES (?, ?, ?, ?, ?, ?) + """, + (wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey), + ) + wallet_created = await get_discordbot_wallet(wallet.id) + assert wallet_created, "Newly created wallet couldn't be retrieved" + return wallet_created + + +async def get_discordbot_wallet(wallet_id: str) -> Optional[Wallets]: + row = await db.fetchone( + "SELECT * FROM discordbot.wallets WHERE id = ?", (wallet_id,) + ) + return Wallets(**row) if row else None + + +async def get_discordbot_wallets(admin_id: str) -> Optional[Wallets]: + rows = await db.fetchall( + "SELECT * FROM discordbot.wallets WHERE admin = ?", (admin_id,) + ) + return [Wallets(**row) for row in rows] + + +async def get_discordbot_users_wallets(user_id: str) -> Optional[Wallets]: + rows = await db.fetchall( + """SELECT * FROM discordbot.wallets WHERE "user" = ?""", (user_id,) + ) + return [Wallets(**row) for row in rows] + + +async def get_discordbot_wallet_transactions(wallet_id: str) -> Optional[Payment]: + return await get_payments( + wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True + ) + + +async def delete_discordbot_wallet(wallet_id: str, user_id: str) -> None: + await delete_wallet(user_id=user_id, wallet_id=wallet_id) + await db.execute("DELETE FROM discordbot.wallets WHERE id = ?", (wallet_id,)) diff --git a/lnbits/extensions/discordbot/migrations.py b/lnbits/extensions/discordbot/migrations.py new file mode 100644 index 00000000..ababfd7a --- /dev/null +++ b/lnbits/extensions/discordbot/migrations.py @@ -0,0 +1,30 @@ +async def m001_initial(db): + """ + Initial users table. + """ + await db.execute( + """ + CREATE TABLE discordbot.users ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + admin TEXT NOT NULL, + discord_id TEXT + ); + """ + ) + + """ + Initial wallets table. + """ + await db.execute( + """ + CREATE TABLE discordbot.wallets ( + id TEXT PRIMARY KEY, + admin TEXT NOT NULL, + name TEXT NOT NULL, + "user" TEXT NOT NULL, + adminkey TEXT NOT NULL, + inkey TEXT NOT NULL + ); + """ + ) diff --git a/lnbits/extensions/discordbot/models.py b/lnbits/extensions/discordbot/models.py new file mode 100644 index 00000000..985eb096 --- /dev/null +++ b/lnbits/extensions/discordbot/models.py @@ -0,0 +1,38 @@ +from sqlite3 import Row + +from fastapi.param_functions import Query +from pydantic import BaseModel +from typing import Optional + + +class CreateUserData(BaseModel): + user_name: str = Query(...) + wallet_name: str = Query(...) + admin_id: str = Query(...) + discord_id: str = Query("") + + +class CreateUserWallet(BaseModel): + user_id: str = Query(...) + wallet_name: str = Query(...) + admin_id: str = Query(...) + + +class Users(BaseModel): + id: str + name: str + admin: str + discord_id: str + + +class Wallets(BaseModel): + id: str + admin: str + name: str + user: str + adminkey: str + inkey: str + + @classmethod + def from_row(cls, row: Row) -> "Wallets": + return cls(**dict(row)) diff --git a/lnbits/extensions/discordbot/static/stack.png b/lnbits/extensions/discordbot/static/stack.png new file mode 100644 index 00000000..3b987db1 Binary files /dev/null and b/lnbits/extensions/discordbot/static/stack.png differ diff --git a/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html b/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html new file mode 100644 index 00000000..40fcfb12 --- /dev/null +++ b/lnbits/extensions/discordbot/templates/discordbot/_api_docs.html @@ -0,0 +1,260 @@ + + + +
+ Discord Bot: Connect Discord users to LNbits. +
+

+ Connect your LNbits instance to a Discord Bot leveraging LNbits as a community based lightning node.
+ + Created by, Chris Lennon
+ + Based on User Manager, by Ben Arc +

+
+
+
+ + + + + GET + /discordbot/api/v1/users +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.base_url }}discordbot/api/v1/users -H + "X-Api-Key: {{ user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /discordbot/api/v1/users/<user_id> +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON list of users +
Curl example
+ curl -X GET {{ request.base_url + }}discordbot/api/v1/users/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /discordbot/api/v1/wallets/<user_id> +
Headers
+ {"X-Api-Key": <string>} +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON wallet data +
Curl example
+ curl -X GET {{ request.base_url + }}discordbot/api/v1/wallets/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /discordbot/api/v1/wallets<wallet_id> +
Headers
+ {"X-Api-Key": <string>} +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ JSON a wallets transactions +
Curl example
+ curl -X GET {{ request.base_url + }}discordbot/api/v1/wallets<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + POST + /discordbot/api/v1/users +
Headers
+ {"X-Api-Key": <string>, "Content-type": + "application/json"} +
+ Body (application/json) - "admin_id" is a YOUR user ID +
+ {"admin_id": <string>, "user_name": <string>, + "wallet_name": <string>,"discord_id": <string>} +
+ Returns 201 CREATED (application/json) +
+ {"id": <string>, "name": <string>, "admin": + <string>, "discord_id": <string>} +
Curl example
+ 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" + +
+
+
+ + + + POST + /discordbot/api/v1/wallets +
Headers
+ {"X-Api-Key": <string>, "Content-type": + "application/json"} +
+ Body (application/json) - "admin_id" is a YOUR user ID +
+ {"user_id": <string>, "wallet_name": <string>, + "admin_id": <string>} +
+ Returns 201 CREATED (application/json) +
+ {"id": <string>, "admin": <string>, "name": + <string>, "user": <string>, "adminkey": <string>, + "inkey": <string>} +
Curl example
+ curl -X POST {{ request.base_url }}discordbot/api/v1/wallets -d + '{"user_id": <string>, "wallet_name": <string>, + "admin_id": "{{ user.id }}"}' -H "X-Api-Key: {{ user.wallets[0].inkey + }}" -H "Content-type: application/json" + +
+
+
+ + + + DELETE + /discordbot/api/v1/users/<user_id> +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X DELETE {{ request.base_url + }}discordbot/api/v1/users/<user_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + DELETE + /discordbot/api/v1/wallets/<wallet_id> +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X DELETE {{ request.base_url + }}discordbot/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + POST + /discordbot/api/v1/extensions +
Headers
+ {"X-Api-Key": <string>} +
Curl example
+ curl -X POST {{ request.base_url }}discordbot/api/v1/extensions -d + '{"userid": <string>, "extension": <string>, "active": + <integer>}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H + "Content-type: application/json" + +
+
+
+
diff --git a/lnbits/extensions/discordbot/templates/discordbot/index.html b/lnbits/extensions/discordbot/templates/discordbot/index.html new file mode 100644 index 00000000..782f8bb6 --- /dev/null +++ b/lnbits/extensions/discordbot/templates/discordbot/index.html @@ -0,0 +1,464 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + +
+ This extension is designed to be used through its API by a Discord Bot, + currently you have to install the bot + yourself
+ + Soon™ there will be a much easier one-click install discord bot... +
+
+ + + +
+
+
Users
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Wallets
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
LNbits Discord Bot Extension + +
+
+ + + {% include "discordbot/_api_docs.html" %} + +
+
+ + + + + + + + + Create User + Cancel + + + + + + + + + + + Create Wallet + Cancel + + + +
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/discordbot/views.py b/lnbits/extensions/discordbot/views.py new file mode 100644 index 00000000..a5395e21 --- /dev/null +++ b/lnbits/extensions/discordbot/views.py @@ -0,0 +1,15 @@ +from fastapi import Request +from fastapi.params import Depends +from starlette.responses import HTMLResponse + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists + +from . import discordbot_ext, discordbot_renderer + + +@discordbot_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return discordbot_renderer().TemplateResponse( + "discordbot/index.html", {"request": request, "user": user.dict()} + ) diff --git a/lnbits/extensions/discordbot/views_api.py b/lnbits/extensions/discordbot/views_api.py new file mode 100644 index 00000000..6f213a89 --- /dev/null +++ b/lnbits/extensions/discordbot/views_api.py @@ -0,0 +1,125 @@ +from http import HTTPStatus + +from fastapi import Query +from fastapi.params import Depends +from starlette.exceptions import HTTPException + +from lnbits.core import update_user_extension +from lnbits.core.crud import get_user +from lnbits.decorators import WalletTypeInfo, get_key_type + +from . import discordbot_ext +from .crud import ( + create_discordbot_user, + create_discordbot_wallet, + delete_discordbot_user, + delete_discordbot_wallet, + get_discordbot_user, + get_discordbot_users, + get_discordbot_users_wallets, + get_discordbot_wallet, + get_discordbot_wallet_transactions, + get_discordbot_wallets, +) +from .models import CreateUserData, CreateUserWallet + +# Users + + +@discordbot_ext.get("/api/v1/users", status_code=HTTPStatus.OK) +async def api_discordbot_users(wallet: WalletTypeInfo = Depends(get_key_type)): + user_id = wallet.wallet.user + return [user.dict() for user in await get_discordbot_users(user_id)] + + +@discordbot_ext.get("/api/v1/users/{user_id}", status_code=HTTPStatus.OK) +async def api_discordbot_user(user_id, wallet: WalletTypeInfo = Depends(get_key_type)): + user = await get_discordbot_user(user_id) + return user.dict() + + +@discordbot_ext.post("/api/v1/users", status_code=HTTPStatus.CREATED) +async def api_discordbot_users_create( + data: CreateUserData, wallet: WalletTypeInfo = Depends(get_key_type) +): + user = await create_discordbot_user(data) + full = user.dict() + full["wallets"] = [ + wallet.dict() for wallet in await get_discordbot_users_wallets(user.id) + ] + return full + + +@discordbot_ext.delete("/api/v1/users/{user_id}") +async def api_discordbot_users_delete( + user_id, wallet: WalletTypeInfo = Depends(get_key_type) +): + user = await get_discordbot_user(user_id) + if not user: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." + ) + await delete_discordbot_user(user_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + + +# Activate Extension + + +@discordbot_ext.post("/api/v1/extensions") +async def api_discordbot_activate_extension( + extension: str = Query(...), userid: str = Query(...), active: bool = Query(...) +): + user = await get_user(userid) + if not user: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." + ) + update_user_extension(user_id=userid, extension=extension, active=active) + return {"extension": "updated"} + + +# Wallets + + +@discordbot_ext.post("/api/v1/wallets") +async def api_discordbot_wallets_create( + data: CreateUserWallet, wallet: WalletTypeInfo = Depends(get_key_type) +): + user = await create_discordbot_wallet( + user_id=data.user_id, wallet_name=data.wallet_name, admin_id=data.admin_id + ) + return user.dict() + + +@discordbot_ext.get("/api/v1/wallets") +async def api_discordbot_wallets(wallet: WalletTypeInfo = Depends(get_key_type)): + admin_id = wallet.wallet.user + return [wallet.dict() for wallet in await get_discordbot_wallets(admin_id)] + + +@discordbot_ext.get("/api/v1/transactions/{wallet_id}") +async def api_discordbot_wallet_transactions( + wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) +): + return await get_discordbot_wallet_transactions(wallet_id) + + +@discordbot_ext.get("/api/v1/wallets/{user_id}") +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)] + + +@discordbot_ext.delete("/api/v1/wallets/{wallet_id}") +async def api_discordbot_wallets_delete( + wallet_id, wallet: WalletTypeInfo = Depends(get_key_type) +): + get_wallet = await get_discordbot_wallet(wallet_id) + if not get_wallet: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist." + ) + await delete_discordbot_wallet(wallet_id, get_wallet.user) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) diff --git a/lnbits/extensions/events/crud.py b/lnbits/extensions/events/crud.py index 9c6e5c6b..9e04476d 100644 --- a/lnbits/extensions/events/crud.py +++ b/lnbits/extensions/events/crud.py @@ -76,7 +76,7 @@ async def delete_ticket(payment_hash: str) -> None: async def delete_event_tickets(event_id: str) -> None: - await db.execute("DELETE FROM events.tickets WHERE event = ?", (event_id,)) + await db.execute("DELETE FROM events.ticket WHERE event = ?", (event_id,)) # EVENTS diff --git a/lnbits/extensions/example/README.md b/lnbits/extensions/example/README.md new file mode 100644 index 00000000..27729459 --- /dev/null +++ b/lnbits/extensions/example/README.md @@ -0,0 +1,11 @@ +

Example Extension

+

*tagline*

+This is an example extension to help you organise and build you own. + +Try to include an image + + + +

If your extension has API endpoints, include useful ones here

+ +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" diff --git a/lnbits/extensions/example/__init__.py b/lnbits/extensions/example/__init__.py new file mode 100644 index 00000000..96cc6428 --- /dev/null +++ b/lnbits/extensions/example/__init__.py @@ -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 diff --git a/lnbits/extensions/example/example.config.json b/lnbits/extensions/example/example.config.json new file mode 100644 index 00000000..b8eec193 --- /dev/null +++ b/lnbits/extensions/example/example.config.json @@ -0,0 +1,6 @@ +{ + "name": "Build your own!!", + "short_description": "Join us, make an extension", + "icon": "info", + "contributors": ["github_username"] +} diff --git a/lnbits/extensions/example/migrations.py b/lnbits/extensions/example/migrations.py new file mode 100644 index 00000000..99d7c362 --- /dev/null +++ b/lnbits/extensions/example/migrations.py @@ -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} +# ); +# """ +# ) diff --git a/lnbits/extensions/example/models.py b/lnbits/extensions/example/models.py new file mode 100644 index 00000000..bfeb7517 --- /dev/null +++ b/lnbits/extensions/example/models.py @@ -0,0 +1,5 @@ +# from pydantic import BaseModel + +# class Example(BaseModel): +# id: str +# wallet: str diff --git a/lnbits/extensions/example/templates/example/index.html b/lnbits/extensions/example/templates/example/index.html new file mode 100644 index 00000000..d732ef37 --- /dev/null +++ b/lnbits/extensions/example/templates/example/index.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} + + +
+ Frameworks used by {{SITE_TITLE}} +
+ + + {% raw %} + + + {{ tool.name }} + {{ tool.language }} + + {% endraw %} + + + +

+ A magical "g" is always available, with info about the user, wallets and + extensions: +

+ {% raw %}{{ g }}{% endraw %} +
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/example/views.py b/lnbits/extensions/example/views.py new file mode 100644 index 00000000..252b4726 --- /dev/null +++ b/lnbits/extensions/example/views.py @@ -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()} + ) diff --git a/lnbits/extensions/example/views_api.py b/lnbits/extensions/example/views_api.py new file mode 100644 index 00000000..5b702717 --- /dev/null +++ b/lnbits/extensions/example/views_api.py @@ -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 diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index 14dc4760..d160daee 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -12,7 +12,7 @@ async def create_jukebox( juke_id = urlsafe_short_hash() result = await db.execute( """ - INSERT INTO jukebox.jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) + INSERT INTO jukebox.jukebox (id, "user", title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( @@ -41,6 +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"! 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 @@ -57,11 +58,11 @@ async def get_jukebox_by_user(user: str) -> Optional[Jukebox]: async def get_jukeboxs(user: str) -> List[Jukebox]: - rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) + rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,)) for row in rows: if row.sp_playlists == None: await delete_jukebox(row.id) - rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) + rows = await db.fetchall('SELECT * FROM jukebox.jukebox WHERE "user" = ?', (user,)) return [Jukebox(**row) for row in rows] diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 421ebf3a..1f3723a7 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -75,7 +75,6 @@ async def api_check_credentials_check( juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key) ): jukebox = await get_jukebox(juke_id) - return jukebox @@ -442,7 +441,7 @@ async def api_get_jukebox_currently( token = await api_get_token(juke_id) if token == False: raise HTTPException( - status_code=HTTPStatus.FORBIDDEN, detail="INvoice not paid" + status_code=HTTPStatus.FORBIDDEN, detail="Invoice not paid" ) elif retry: raise HTTPException( @@ -456,5 +455,6 @@ async def api_get_jukebox_currently( ) except: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Something went wrong" + status_code=HTTPStatus.NOT_FOUND, + detail="Something went wrong, or no song is playing yet", ) diff --git a/lnbits/extensions/livestream/views.py b/lnbits/extensions/livestream/views.py index 4f1bd1c5..ef035431 100644 --- a/lnbits/extensions/livestream/views.py +++ b/lnbits/extensions/livestream/views.py @@ -1,4 +1,5 @@ from http import HTTPStatus + # from mmap import MAP_DENYWRITE from fastapi.param_functions import Depends diff --git a/lnbits/extensions/lnticket/templates/lnticket/index.html b/lnbits/extensions/lnticket/templates/lnticket/index.html index 1607925b..6572d98a 100644 --- a/lnbits/extensions/lnticket/templates/lnticket/index.html +++ b/lnbits/extensions/lnticket/templates/lnticket/index.html @@ -117,9 +117,10 @@ {% raw %} @@ -136,9 +137,19 @@ :href="'mailto:' + props.row.email" > + + Click to show ticket + - {{ col.value }} + {{ col.label == "Ticket" ? col.value.length > 20 ? `${col.value.substring(0, 20)}...` : col.value : col.value }} @@ -249,6 +260,29 @@
+ + + + {% raw %} + +

+ {{this.ticketDialog.data.name}} sent a ticket +

+
+ {{this.ticketDialog.data.email}} +
+ {{this.ticketDialog.data.date}} +
+ + +

{{this.ticketDialog.data.content}}

+
+ {% endraw %} + + + +
+
{% endblock %} {% block scripts %} {{ window_vars(user) }} +{% endblock %} \ No newline at end of file diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html index 4cffbc38..f6ea220d 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/index.html +++ b/lnbits/extensions/withdraw/templates/withdraw/index.html @@ -1,49 +1,38 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block scripts %} {{ window_vars(user) }} +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block scripts %} {{ window_vars(user) }} {% endblock %} {% block page %}
-
- - - Quick vouchers - Advanced withdraw link(s) - - +
+ + + Quick vouchers + Advanced withdraw link(s) + + - - -
-
-
Withdraw links
-
-
- Export to CSV -
-
- - {% raw %} -