diff --git a/.env.example b/.env.example index 673470b7..32a67f75 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,9 @@ HOST=127.0.0.1 PORT=5000 +# uvicorn variable, allow https behind a proxy +# FORWARDED_ALLOW_IPS="*" + DEBUG=false # User IDs seperated by comma @@ -15,8 +18,10 @@ LNBITS_ALLOWED_USERS="" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" -# csv ad image filepaths or urls, extensions can choose to honor -LNBITS_AD_SPACE="" +# Ad space description +# LNBITS_AD_SPACE_TITLE="Supported by" +# csv ad space, format ";;, ;;", extensions can choose to honor +# LNBITS_AD_SPACE="" # Hides wallet api, extensions can choose to honor LNBITS_HIDE_API=false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..bfaddbeb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - LNbits version: [e.g. 0.9.2 or commit hash] + - Database [e.g. sqlite, postgres] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..4f49a497 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature request]" +labels: feature request +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/something-else.md b/.github/ISSUE_TEMPLATE/something-else.md new file mode 100644 index 00000000..4bd9ec2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/something-else.md @@ -0,0 +1,10 @@ +--- +name: Something else +about: Anything else that you need to say +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/Dockerfile b/Dockerfile index ba116c63..a03e1eb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN curl -sSL https://install.python-poetry.org | python3 - ENV PATH="/root/.local/bin:$PATH" WORKDIR /app +RUN mkdir -p lnbits/data COPY . . diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 6b95f93b..072c4d91 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -48,7 +48,9 @@ poetry run lnbits # Note that you have to add the line DEBUG=true in your .env file, too. ``` -## Option 2: Nix +## Option 2: Nix + +> note: currently not supported while we make some architectural changes on the path to leave beta ```sh git clone https://github.com/lnbits/lnbits-legend.git @@ -155,6 +157,7 @@ kill_timeout = 30 HOST="127.0.0.1" PORT=5000 LNBITS_FORCE_HTTPS=true + FORWARDED_ALLOW_IPS="*" LNBITS_DATA_FOLDER="/data" ${PUT_YOUR_LNBITS_ENV_VARS_HERE} @@ -217,8 +220,8 @@ You need to edit the `.env` file. ```sh # 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" +# postgres://:@:/ - alter line bellow with your user, password and db name +LNBITS_DATABASE_URL="postgres://postgres:postgres@localhost:5432/lnbits" # save and exit ``` diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 7fc21a98..eb0d64de 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -229,6 +229,24 @@ async def get_wallet_payment( return Payment.from_row(row) if row else None +async def get_latest_payments_by_extension(ext_name: str, ext_id: str, limit: int = 5): + rows = await db.fetchall( + f""" + SELECT * FROM apipayments + WHERE pending = 'false' + AND extra LIKE ? + AND extra LIKE ? + ORDER BY time DESC LIMIT {limit} + """, + ( + f"%{ext_name}%", + f"%{ext_id}%", + ), + ) + + return rows + + async def get_payments( *, wallet_id: Optional[str] = None, diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index ebecb5e3..d92f384a 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -51,7 +51,7 @@ async def m001_initial(db): f""" CREATE TABLE IF NOT EXISTS apipayments ( payhash TEXT NOT NULL, - amount INTEGER NOT NULL, + amount {db.big_int} NOT NULL, fee INTEGER NOT NULL DEFAULT 0, wallet TEXT NOT NULL, pending BOOLEAN NOT NULL, diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index 146fc6ad..a28030c0 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -183,6 +183,23 @@
 
+ + {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %} +
+ {{ AD_TITLE }} + + + + + +
+ {% endfor %} {% endif %} diff --git a/lnbits/core/templates/core/wallet.html b/lnbits/core/templates/core/wallet.html index 5393007d..22fbd05d 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -385,8 +385,17 @@ - {% endif %} {% if AD_SPACE %} {% for AD in AD_SPACE %} - + +
{{ AD_TITLE }}
+
+ + + + + {% endfor %} {% endif %} diff --git a/lnbits/extensions/bleskomat/views_api.py b/lnbits/extensions/bleskomat/views_api.py index e29e3fe7..b6e417bb 100644 --- a/lnbits/extensions/bleskomat/views_api.py +++ b/lnbits/extensions/bleskomat/views_api.py @@ -95,4 +95,4 @@ async def api_bleskomat_delete( ) await delete_bleskomat(bleskomat_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/boltcards/templates/boltcards/index.html b/lnbits/extensions/boltcards/templates/boltcards/index.html index 55cc1e5e..6398c20e 100644 --- a/lnbits/extensions/boltcards/templates/boltcards/index.html +++ b/lnbits/extensions/boltcards/templates/boltcards/index.html @@ -380,7 +380,11 @@ Lock key: {{ qrCodeDialog.data.k0 }}
Meta key: {{ qrCodeDialog.data.k1 }}
File key: {{ qrCodeDialog.data.k2 }}
+
+ Always backup all keys that you're trying to write on the card. Without + them you may not be able to change them in the future!

+
None: + # (avoid loops) + if ( + "events" == payment.extra.get("tag") + and payment.extra.get("name") + and payment.extra.get("email") + ): + CreateTicket.name = str(payment.extra.get("name")) + CreateTicket.email = str(payment.extra.get("email")) + await api_ticket_send_ticket(payment.memo, payment.payment_hash, CreateTicket) + return diff --git a/lnbits/extensions/events/templates/events/display.html b/lnbits/extensions/events/templates/events/display.html index 4589c578..e65b4e61 100644 --- a/lnbits/extensions/events/templates/events/display.html +++ b/lnbits/extensions/events/templates/events/display.html @@ -135,7 +135,14 @@ var self = this axios - .get('/events/api/v1/tickets/' + '{{ event_id }}') + .get( + '/events/api/v1/tickets/' + + '{{ event_id }}' + + '/' + + self.formDialog.data.name + + '/' + + self.formDialog.data.email + ) .then(function (response) { self.paymentReq = response.data.payment_request self.paymentCheck = response.data.payment_hash diff --git a/lnbits/extensions/events/templates/events/index.html b/lnbits/extensions/events/templates/events/index.html index 7f34a3e2..21258930 100644 --- a/lnbits/extensions/events/templates/events/index.html +++ b/lnbits/extensions/events/templates/events/index.html @@ -260,7 +260,7 @@ dense v-model.number="formDialog.data.price_per_ticket" type="number" - label="Price per ticket " + label="Sats per ticket " > diff --git a/lnbits/extensions/events/views_api.py b/lnbits/extensions/events/views_api.py index 9cb18f04..668e7f77 100644 --- a/lnbits/extensions/events/views_api.py +++ b/lnbits/extensions/events/views_api.py @@ -2,6 +2,7 @@ from http import HTTPStatus from fastapi.param_functions import Query from fastapi.params import Depends +from loguru import logger from starlette.exceptions import HTTPException from starlette.requests import Request @@ -78,7 +79,7 @@ async def api_form_delete(event_id, wallet: WalletTypeInfo = Depends(get_key_typ await delete_event(event_id) await delete_event_tickets(event_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT #########Tickets########## @@ -96,8 +97,8 @@ async def api_tickets( return [ticket.dict() for ticket in await get_tickets(wallet_ids)] -@events_ext.get("/api/v1/tickets/{event_id}") -async def api_ticket_make_ticket(event_id): +@events_ext.get("/api/v1/tickets/{event_id}/{name}/{email}") +async def api_ticket_make_ticket(event_id, name, email): event = await get_event(event_id) if not event: raise HTTPException( @@ -108,11 +109,10 @@ async def api_ticket_make_ticket(event_id): wallet_id=event.wallet, amount=event.price_per_ticket, memo=f"{event_id}", - extra={"tag": "events"}, + extra={"tag": "events", "name": name, "email": email}, ) except Exception as e: raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) - return {"payment_hash": payment_hash, "payment_request": payment_request} @@ -156,7 +156,7 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_ ) await delete_ticket(ticket_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT # Event Tickets diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py index d160daee..9d6548b6 100644 --- a/lnbits/extensions/jukebox/crud.py +++ b/lnbits/extensions/jukebox/crud.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash @@ -6,11 +6,9 @@ from . import db from .models import CreateJukeboxPayment, CreateJukeLinkData, Jukebox, JukeboxPayment -async def create_jukebox( - data: CreateJukeLinkData, inkey: Optional[str] = "" -) -> Jukebox: +async def create_jukebox(data: CreateJukeLinkData) -> Jukebox: juke_id = urlsafe_short_hash() - result = await db.execute( + 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) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) @@ -36,13 +34,13 @@ async def create_jukebox( async def update_jukebox( - data: CreateJukeLinkData, juke_id: Optional[str] = "" + data: Union[CreateJukeLinkData, Jukebox], juke_id: str = "" ) -> Optional[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)) + 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 @@ -72,7 +70,7 @@ async def delete_jukebox(juke_id: str): """ DELETE FROM jukebox.jukebox WHERE id = ? """, - (juke_id), + (juke_id,), ) @@ -80,7 +78,7 @@ async def delete_jukebox(juke_id: str): async def create_jukebox_payment(data: CreateJukeboxPayment) -> JukeboxPayment: - result = await db.execute( + await db.execute( """ INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid) VALUES (?, ?, ?, ?) diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py index 90984b03..70cf6523 100644 --- a/lnbits/extensions/jukebox/models.py +++ b/lnbits/extensions/jukebox/models.py @@ -1,6 +1,3 @@ -from sqlite3 import Row -from typing import NamedTuple, Optional - from fastapi.param_functions import Query from pydantic import BaseModel from pydantic.main import BaseModel @@ -20,19 +17,19 @@ class CreateJukeLinkData(BaseModel): class Jukebox(BaseModel): - id: Optional[str] - user: Optional[str] - title: Optional[str] - wallet: Optional[str] - inkey: Optional[str] - sp_user: Optional[str] - sp_secret: Optional[str] - sp_access_token: Optional[str] - sp_refresh_token: Optional[str] - sp_device: Optional[str] - sp_playlists: Optional[str] - price: Optional[int] - profit: Optional[int] + id: str + user: str + title: str + wallet: str + inkey: str + sp_user: str + sp_secret: str + sp_access_token: str + sp_refresh_token: str + sp_device: str + sp_playlists: str + price: int + profit: int class JukeboxPayment(BaseModel): diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py index 5614d926..8a68fd27 100644 --- a/lnbits/extensions/jukebox/tasks.py +++ b/lnbits/extensions/jukebox/tasks.py @@ -17,7 +17,8 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: - if payment.extra.get("tag") != "jukebox": - # not a jukebox invoice - return - await update_jukebox_payment(payment.payment_hash, paid=True) + if payment.extra: + if payment.extra.get("tag") != "jukebox": + # not a jukebox invoice + return + await update_jukebox_payment(payment.payment_hash, paid=True) diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py index 56774394..28359a9a 100644 --- a/lnbits/extensions/jukebox/views.py +++ b/lnbits/extensions/jukebox/views.py @@ -17,7 +17,9 @@ templates = Jinja2Templates(directory="templates") @jukebox_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): +async def index( + request: Request, user: User = Depends(check_user_exists) # type: ignore +): return jukebox_renderer().TemplateResponse( "jukebox/index.html", {"request": request, "user": user.dict()} ) @@ -31,6 +33,7 @@ async def connect_to_jukebox(request: Request, juke_id): status_code=HTTPStatus.NOT_FOUND, detail="Jukebox does not exist." ) devices = await api_get_jukebox_device_check(juke_id) + deviceConnected = False for device in devices["devices"]: if device["id"] == jukebox.sp_device.split("-")[1]: deviceConnected = True @@ -48,5 +51,5 @@ async def connect_to_jukebox(request: Request, juke_id): else: return jukebox_renderer().TemplateResponse( "jukebox/error.html", - {"request": request, "jukebox": jukebox.jukebox(req=request)}, + {"request": request, "jukebox": jukebox.dict()}, ) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py index 1f3723a7..5cf1a83b 100644 --- a/lnbits/extensions/jukebox/views_api.py +++ b/lnbits/extensions/jukebox/views_api.py @@ -3,7 +3,6 @@ import json from http import HTTPStatus import httpx -from fastapi import Request from fastapi.param_functions import Query from fastapi.params import Depends from starlette.exceptions import HTTPException @@ -29,9 +28,7 @@ from .models import CreateJukeboxPayment, CreateJukeLinkData @jukebox_ext.get("/api/v1/jukebox") async def api_get_jukeboxs( - req: Request, - wallet: WalletTypeInfo = Depends(require_admin_key), - all_wallets: bool = Query(False), + wallet: WalletTypeInfo = Depends(require_admin_key), # type: ignore ): wallet_user = wallet.wallet.user @@ -53,54 +50,52 @@ async def api_check_credentials_callbac( access_token: str = Query(None), refresh_token: str = Query(None), ): - sp_code = "" - sp_access_token = "" - sp_refresh_token = "" - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(detail="No Jukebox", status_code=HTTPStatus.FORBIDDEN) if code: jukebox.sp_access_token = code - jukebox = await update_jukebox(jukebox, juke_id=juke_id) + await update_jukebox(jukebox, juke_id=juke_id) if access_token: jukebox.sp_access_token = access_token jukebox.sp_refresh_token = refresh_token - jukebox = await update_jukebox(jukebox, juke_id=juke_id) + await update_jukebox(jukebox, juke_id=juke_id) return "

Success!

You can close this window

" -@jukebox_ext.get("/api/v1/jukebox/{juke_id}") -async def api_check_credentials_check( - juke_id: str = Query(None), wallet: WalletTypeInfo = Depends(require_admin_key) -): +@jukebox_ext.get("/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)]) +async def api_check_credentials_check(juke_id: str = Query(None)): jukebox = await get_jukebox(juke_id) return jukebox -@jukebox_ext.post("/api/v1/jukebox", status_code=HTTPStatus.CREATED) +@jukebox_ext.post( + "/api/v1/jukebox", + status_code=HTTPStatus.CREATED, + dependencies=[Depends(require_admin_key)], +) @jukebox_ext.put("/api/v1/jukebox/{juke_id}", status_code=HTTPStatus.OK) async def api_create_update_jukebox( - data: CreateJukeLinkData, - juke_id: str = Query(None), - wallet: WalletTypeInfo = Depends(require_admin_key), + data: CreateJukeLinkData, juke_id: str = Query(None) ): if juke_id: jukebox = await update_jukebox(data, juke_id=juke_id) else: - jukebox = await create_jukebox(data, inkey=wallet.wallet.inkey) + jukebox = await create_jukebox(data) return jukebox -@jukebox_ext.delete("/api/v1/jukebox/{juke_id}") +@jukebox_ext.delete( + "/api/v1/jukebox/{juke_id}", dependencies=[Depends(require_admin_key)] +) async def api_delete_item( - juke_id=None, wallet: WalletTypeInfo = Depends(require_admin_key) + juke_id: str = Query(None), ): await delete_jukebox(juke_id) - try: - return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)] - except: - raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox") + # try: + # return [{**jukebox} for jukebox in await get_jukeboxs(wallet.wallet.user)] + # except: + # raise HTTPException(status_code=HTTPStatus.NO_CONTENT, detail="No Jukebox") ################JUKEBOX ENDPOINTS################## @@ -114,9 +109,8 @@ async def api_get_jukebox_song( sp_playlist: str = Query(None), retry: bool = Query(False), ): - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes") tracks = [] async with httpx.AsyncClient() as client: @@ -152,14 +146,13 @@ async def api_get_jukebox_song( } ) except: - something = None + pass return [track for track in tracks] -async def api_get_token(juke_id=None): - try: - jukebox = await get_jukebox(juke_id) - except: +async def api_get_token(juke_id): + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes") async with httpx.AsyncClient() as client: @@ -187,7 +180,7 @@ async def api_get_token(juke_id=None): jukebox.sp_access_token = r.json()["access_token"] await update_jukebox(jukebox, juke_id=juke_id) except: - something = None + pass return True @@ -198,9 +191,8 @@ async def api_get_token(juke_id=None): async def api_get_jukebox_device_check( juke_id: str = Query(None), retry: bool = Query(False) ): - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No Jukeboxes") async with httpx.AsyncClient() as client: rDevice = await client.get( @@ -221,7 +213,7 @@ async def api_get_jukebox_device_check( status_code=HTTPStatus.FORBIDDEN, detail="Failed to get auth" ) else: - return api_get_jukebox_device_check(juke_id, retry=True) + return await api_get_jukebox_device_check(juke_id, retry=True) else: raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="No device connected" @@ -233,10 +225,8 @@ async def api_get_jukebox_device_check( @jukebox_ext.get("/api/v1/jukebox/jb/invoice/{juke_id}/{song_id}") async def api_get_jukebox_invoice(juke_id, song_id): - try: - jukebox = await get_jukebox(juke_id) - - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox") try: @@ -266,8 +256,7 @@ async def api_get_jukebox_invoice(juke_id, song_id): invoice=invoice[1], payment_hash=payment_hash, juke_id=juke_id, song_id=song_id ) jukebox_payment = await create_jukebox_payment(data) - - return data + return jukebox_payment @jukebox_ext.get("/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}") @@ -296,13 +285,12 @@ async def api_get_jukebox_invoice_paid( pay_hash: str = Query(None), retry: bool = Query(False), ): - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox") await api_get_jukebox_invoice_check(pay_hash, juke_id) jukebox_payment = await get_jukebox_payment(pay_hash) - if jukebox_payment.paid: + if jukebox_payment and jukebox_payment.paid: async with httpx.AsyncClient() as client: r = await client.get( "https://api.spotify.com/v1/me/player/currently-playing?market=ES", @@ -407,9 +395,8 @@ async def api_get_jukebox_invoice_paid( async def api_get_jukebox_currently( retry: bool = Query(False), juke_id: str = Query(None) ): - try: - jukebox = await get_jukebox(juke_id) - except: + jukebox = await get_jukebox(juke_id) + if not jukebox: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="No jukebox") async with httpx.AsyncClient() as client: try: diff --git a/lnbits/extensions/livestream/views_api.py b/lnbits/extensions/livestream/views_api.py index cc173a66..0c169a71 100644 --- a/lnbits/extensions/livestream/views_api.py +++ b/lnbits/extensions/livestream/views_api.py @@ -60,14 +60,14 @@ async def api_update_track(track_id, g: WalletTypeInfo = Depends(get_key_type)): ls = await get_or_create_livestream_by_wallet(g.wallet.id) await update_current_track(ls.id, id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT @livestream_ext.put("/api/v1/livestream/fee/{fee_pct}") async def api_update_fee(fee_pct, g: WalletTypeInfo = Depends(get_key_type)): ls = await get_or_create_livestream_by_wallet(g.wallet.id) await update_livestream_fee(ls.id, int(fee_pct)) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT @livestream_ext.post("/api/v1/livestream/tracks") @@ -93,8 +93,8 @@ async def api_add_track( return -@livestream_ext.route("/api/v1/livestream/tracks/{track_id}") +@livestream_ext.delete("/api/v1/livestream/tracks/{track_id}") async def api_delete_track(track_id, g: WalletTypeInfo = Depends(get_key_type)): ls = await get_or_create_livestream_by_wallet(g.wallet.id) await delete_track_from_livestream(ls.id, track_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/lnaddress/views_api.py b/lnbits/extensions/lnaddress/views_api.py index 8f403a38..46ef6b99 100644 --- a/lnbits/extensions/lnaddress/views_api.py +++ b/lnbits/extensions/lnaddress/views_api.py @@ -93,7 +93,7 @@ async def api_domain_delete(domain_id, g: WalletTypeInfo = Depends(get_key_type) raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your domain") await delete_domain(domain_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT # ADDRESSES @@ -253,4 +253,4 @@ async def api_address_delete(address_id, g: WalletTypeInfo = Depends(get_key_typ ) await delete_address(address_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py index 7c9eb52c..cf6145b3 100644 --- a/lnbits/extensions/lnticket/views_api.py +++ b/lnbits/extensions/lnticket/views_api.py @@ -78,7 +78,7 @@ async def api_form_delete(form_id, wallet: WalletTypeInfo = Depends(get_key_type await delete_form(form_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT #########tickets########## @@ -160,4 +160,4 @@ async def api_ticket_delete(ticket_id, wallet: WalletTypeInfo = Depends(get_key_ raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.") await delete_ticket(ticket_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index 4c25e4cb..e02d23b8 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -23,9 +23,22 @@ async def create_lnurldevice( currency, device, profit, - amount + amount, + pin, + profit1, + amount1, + pin1, + profit2, + amount2, + pin2, + profit3, + amount3, + pin3, + profit4, + amount4, + pin4 ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( lnurldevice_id, @@ -36,6 +49,19 @@ async def create_lnurldevice( data.device, data.profit, data.amount, + data.pin, + data.profit1, + data.amount1, + data.pin1, + data.profit2, + data.amount2, + data.pin2, + data.profit3, + data.amount3, + data.pin3, + data.profit4, + data.amount4, + data.pin4, ), ) return await get_lnurldevice(lnurldevice_id) diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index 79892b78..dd8dcb08 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -8,6 +8,7 @@ from typing import Optional from embit import bech32, compact from fastapi import Request from fastapi.param_functions import Query +from loguru import logger from starlette.exceptions import HTTPException from lnbits.core.services import create_invoice @@ -91,6 +92,9 @@ async def lnurl_v1_params( device_id: str = Query(None), p: str = Query(None), atm: str = Query(None), + gpio: str = Query(None), + profit: str = Query(None), + amount: str = Query(None), ): device = await get_lnurldevice(device_id) if not device: @@ -101,20 +105,28 @@ async def lnurl_v1_params( paymentcheck = await get_lnurlpayload(p) if device.device == "atm": if paymentcheck: - return {"status": "ERROR", "reason": f"Payment already claimed"} + if paymentcheck.payhash != "payment_hash": + return {"status": "ERROR", "reason": f"Payment already claimed"} if device.device == "switch": - price_msat = ( - await fiat_amount_as_satoshis(float(device.profit), device.currency) + await fiat_amount_as_satoshis(float(profit), device.currency) if device.currency != "sat" else amount_in_cent ) * 1000 + # Check they're not trying to trick the switch! + check = False + for switch in device.switches(request): + if switch[0] == gpio and switch[1] == profit and switch[2] == amount: + check = True + if not check: + return {"status": "ERROR", "reason": f"Switch params wrong"} + lnurldevicepayment = await create_lnurldevicepayment( deviceid=device.id, - payload="bla", + payload=amount, sats=price_msat, - pin=1, + pin=gpio, payhash="bla", ) if not lnurldevicepayment: @@ -126,7 +138,7 @@ async def lnurl_v1_params( ), "minSendable": price_msat, "maxSendable": price_msat, - "metadata": await device.lnurlpay_metadata(), + "metadata": device.lnurlpay_metadata, } if len(p) % 4 > 0: p += "=" * (4 - (len(p) % 4)) @@ -165,7 +177,7 @@ async def lnurl_v1_params( "callback": request.url_for( "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id ), - "k1": lnurldevicepayment.id, + "k1": p, "minWithdrawable": price_msat * 1000, "maxWithdrawable": price_msat * 1000, "defaultDescription": device.title, @@ -188,7 +200,7 @@ async def lnurl_v1_params( ), "minSendable": price_msat * 1000, "maxSendable": price_msat * 1000, - "metadata": await device.lnurlpay_metadata(), + "metadata": device.lnurlpay_metadata, } @@ -215,14 +227,13 @@ async def lnurl_callback( status_code=HTTPStatus.FORBIDDEN, detail="No payment request" ) else: - if lnurldevicepayment.id != k1: + if lnurldevicepayment.payload != k1: return {"status": "ERROR", "reason": "Bad K1"} if lnurldevicepayment.payhash != "payment_hash": return {"status": "ERROR", "reason": f"Payment already claimed"} lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload ) - await pay_invoice( wallet_id=device.wallet, payment_request=pr, @@ -233,11 +244,17 @@ async def lnurl_callback( if device.device == "switch": payment_hash, payment_request = await create_invoice( wallet_id=device.wallet, - amount=lnurldevicepayment.sats / 1000, - memo=device.title + "-" + lnurldevicepayment.id, - unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"), - extra={"tag": "Switch", "id": paymentid, "time": device.amount}, + amount=int(lnurldevicepayment.sats / 1000), + memo=device.id + " PIN " + str(lnurldevicepayment.pin), + unhashed_description=device.lnurlpay_metadata.encode("utf-8"), + extra={ + "tag": "Switch", + "pin": str(lnurldevicepayment.pin), + "amount": str(lnurldevicepayment.payload), + "id": paymentid, + }, ) + lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment_id=paymentid, payhash=payment_hash ) @@ -248,9 +265,9 @@ async def lnurl_callback( payment_hash, payment_request = await create_invoice( wallet_id=device.wallet, - amount=lnurldevicepayment.sats / 1000, + amount=int(lnurldevicepayment.sats / 1000), memo=device.title, - unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"), + unhashed_description=device.lnurlpay_metadata.encode("utf-8"), extra={"tag": "PoS"}, ) lnurldevicepayment = await update_lnurldevicepayment( diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py index 7305cceb..1df04075 100644 --- a/lnbits/extensions/lnurldevice/migrations.py +++ b/lnbits/extensions/lnurldevice/migrations.py @@ -88,3 +88,52 @@ async def m003_redux(db): await db.execute( "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;" ) + + +async def m004_redux(db): + """ + Add 'meta' for storing various metadata about the wallet + """ + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin INT DEFAULT 0" + ) + + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit1 FLOAT DEFAULT 0" + ) + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount1 INT DEFAULT 0" + ) + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin1 INT DEFAULT 0" + ) + + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit2 FLOAT DEFAULT 0" + ) + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount2 INT DEFAULT 0" + ) + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin2 INT DEFAULT 0" + ) + + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit3 FLOAT DEFAULT 0" + ) + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount3 INT DEFAULT 0" + ) + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin3 INT DEFAULT 0" + ) + + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN profit4 FLOAT DEFAULT 0" + ) + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount4 INT DEFAULT 0" + ) + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN pin4 INT DEFAULT 0" + ) diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py index 01bcc2ba..c27470b7 100644 --- a/lnbits/extensions/lnurldevice/models.py +++ b/lnbits/extensions/lnurldevice/models.py @@ -1,12 +1,13 @@ import json from sqlite3 import Row -from typing import Optional +from typing import List, Optional from fastapi import Request from lnurl import Lnurl from lnurl import encode as lnurl_encode # type: ignore from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore +from loguru import logger from pydantic import BaseModel from pydantic.main import BaseModel @@ -18,6 +19,19 @@ class createLnurldevice(BaseModel): device: str profit: float amount: int + pin: int = 0 + profit1: float = 0 + amount1: int = 0 + pin1: int = 0 + profit2: float = 0 + amount2: int = 0 + pin2: int = 0 + profit3: float = 0 + amount3: int = 0 + pin3: int = 0 + profit4: float = 0 + amount4: int = 0 + pin4: int = 0 class lnurldevices(BaseModel): @@ -29,18 +43,122 @@ class lnurldevices(BaseModel): device: str profit: float amount: int + pin: int + profit1: float + amount1: int + pin1: int + profit2: float + amount2: int + pin2: int + profit3: float + amount3: int + pin3: int + profit4: float + amount4: int + pin4: int timestamp: str def from_row(cls, row: Row) -> "lnurldevices": return cls(**dict(row)) - def lnurl(self, req: Request) -> Lnurl: - url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id) - return lnurl_encode(url) - - async def lnurlpay_metadata(self) -> LnurlPayMetadata: + @property + def lnurlpay_metadata(self) -> LnurlPayMetadata: return LnurlPayMetadata(json.dumps([["text/plain", self.title]])) + def switches(self, req: Request) -> List: + switches = [] + if self.profit > 0: + url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id) + switches.append( + [ + str(self.pin), + str(self.profit), + str(self.amount), + lnurl_encode( + url + + "?gpio=" + + str(self.pin) + + "&profit=" + + str(self.profit) + + "&amount=" + + str(self.amount) + ), + ] + ) + if self.profit1 > 0: + url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id) + switches.append( + [ + str(self.pin1), + str(self.profit1), + str(self.amount1), + lnurl_encode( + url + + "?gpio=" + + str(self.pin1) + + "&profit=" + + str(self.profit1) + + "&amount=" + + str(self.amount1) + ), + ] + ) + if self.profit2 > 0: + url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id) + switches.append( + [ + str(self.pin2), + str(self.profit2), + str(self.amount2), + lnurl_encode( + url + + "?gpio=" + + str(self.pin2) + + "&profit=" + + str(self.profit2) + + "&amount=" + + str(self.amount2) + ), + ] + ) + if self.profit3 > 0: + url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id) + switches.append( + [ + str(self.pin3), + str(self.profit3), + str(self.amount3), + lnurl_encode( + url + + "?gpio=" + + str(self.pin3) + + "&profit=" + + str(self.profit3) + + "&amount=" + + str(self.amount3) + ), + ] + ) + if self.profit4 > 0: + url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id) + switches.append( + [ + str(self.pin4), + str(self.profit4), + str(self.amount4), + lnurl_encode( + url + + "?gpio=" + + str(self.pin4) + + "&profit=" + + str(self.profit4) + + "&amount=" + + str(self.amount4) + ), + ] + ) + return switches + class lnurldevicepayment(BaseModel): id: str diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index c8f3db04..d3248ad5 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -36,5 +36,9 @@ async def on_invoice_paid(payment: Payment) -> None: lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment_id=payment.extra.get("id"), payhash="used" ) - return await updater(lnurldevicepayment.deviceid) + return await updater( + lnurldevicepayment.deviceid, + lnurldevicepayment.pin, + lnurldevicepayment.payload, + ) return diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index 028dd94b..b0b223ff 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -105,7 +105,7 @@ @click="openQrCodeDialog(props.row.id)" > LNURLs only work over HTTPS view LNURL
view LNURLS
- - -
+ + +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
- {% raw %} -

- ID: {{ qrCodeDialog.data.id }}
-

- {% endraw %} + Copy LNURL +
Copy LNURL + color="primary" + :label="'Switch PIN:' + switch_[0]" + @click="lnurlValueFetch(switch_[3])" + > Close
@@ -333,11 +527,14 @@ mixins: [windowMixin], data: function () { return { + tab: 'mails', protocol: window.location.protocol, location: window.location.hostname, wslocation: window.location.hostname, filter: '', currency: 'USD', + lnurlValue: '', + switches: 0, lnurldeviceLinks: [], lnurldeviceLinksObj: [], devices: [ @@ -386,12 +583,6 @@ label: 'device', field: 'device' }, - { - name: 'profit', - align: 'left', - label: 'profit', - field: 'profit' - }, { name: 'currency', align: 'left', @@ -440,8 +631,20 @@ this.qrCodeDialog.data = _.clone(lnurldevice) this.qrCodeDialog.data.url = window.location.protocol + '//' + window.location.host + this.lnurlValueFetch(this.qrCodeDialog.data.switches[0][3]) this.qrCodeDialog.show = true }, + lnurlValueFetch: function (lnurl) { + this.lnurlValue = lnurl + }, + addSwitch: function () { + var self = this + self.switches = self.switches + 1 + }, + removeSwitch: function () { + var self = this + self.switches = self.switches - 1 + }, cancellnurldevice: function (data) { var self = this self.formDialoglnurldevice.show = false @@ -498,7 +701,9 @@ .then(function (response) { if (response.data) { self.lnurldeviceLinks = response.data.map(maplnurldevice) + console.log('response.data') console.log(response.data) + console.log('response.data') } }) .catch(function (error) { diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index 5c6eba24..f435931b 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -103,8 +103,10 @@ async def websocket_endpoint(websocket: WebSocket, lnurldevice_id: str): manager.disconnect(websocket) -async def updater(lnurldevice_id): +async def updater(lnurldevice_id, lnurldevice_pin, lnurldevice_amount): lnurldevice = await get_lnurldevice(lnurldevice_id) if not lnurldevice: return - await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) + return await manager.send_personal_message( + f"{lnurldevice_pin}-{lnurldevice_amount}", lnurldevice_id + ) diff --git a/lnbits/extensions/lnurldevice/views_api.py b/lnbits/extensions/lnurldevice/views_api.py index c034f66e..c6766423 100644 --- a/lnbits/extensions/lnurldevice/views_api.py +++ b/lnbits/extensions/lnurldevice/views_api.py @@ -39,10 +39,10 @@ async def api_lnurldevice_create_or_update( ): if not lnurldevice_id: lnurldevice = await create_lnurldevice(data) - return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} + return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} else: lnurldevice = await update_lnurldevice(data, lnurldevice_id=lnurldevice_id) - return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} + return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} @lnurldevice_ext.get("/api/v1/lnurlpos") @@ -52,7 +52,7 @@ async def api_lnurldevices_retrieve( wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids try: return [ - {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} + {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} for lnurldevice in await get_lnurldevices(wallet_ids) ] except: @@ -78,7 +78,7 @@ async def api_lnurldevice_retrieve( ) if not lnurldevice.lnurl_toggle: return {**lnurldevice.dict()} - return {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} + return {**lnurldevice.dict(), **{"switches": lnurldevice.switches(req)}} @lnurldevice_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}") diff --git a/lnbits/extensions/lnurlp/crud.py b/lnbits/extensions/lnurlp/crud.py index 9cb01fde..d02ae80e 100644 --- a/lnbits/extensions/lnurlp/crud.py +++ b/lnbits/extensions/lnurlp/crud.py @@ -21,13 +21,15 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: served_meta, served_pr, webhook_url, + webhook_headers, + webhook_body, success_text, success_url, comment_chars, currency, fiat_base_multiplier ) - VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?, ?) {returning} """, ( @@ -36,6 +38,8 @@ async def create_pay_link(data: CreatePayLinkData, wallet_id: str) -> PayLink: data.min, data.max, data.webhook_url, + data.webhook_headers, + data.webhook_body, data.success_text, data.success_url, data.comment_chars, diff --git a/lnbits/extensions/lnurlp/migrations.py b/lnbits/extensions/lnurlp/migrations.py index 81dd62f8..5258471d 100644 --- a/lnbits/extensions/lnurlp/migrations.py +++ b/lnbits/extensions/lnurlp/migrations.py @@ -60,3 +60,11 @@ async def m004_fiat_base_multiplier(db): await db.execute( "ALTER TABLE lnurlp.pay_links ADD COLUMN fiat_base_multiplier INTEGER DEFAULT 1;" ) + + +async def m005_webhook_headers_and_body(db): + """ + Add headers and body to webhooks + """ + await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_headers TEXT;") + await db.execute("ALTER TABLE lnurlp.pay_links ADD COLUMN webhook_body TEXT;") diff --git a/lnbits/extensions/lnurlp/models.py b/lnbits/extensions/lnurlp/models.py index 4bd438a4..2cb4d0ab 100644 --- a/lnbits/extensions/lnurlp/models.py +++ b/lnbits/extensions/lnurlp/models.py @@ -18,6 +18,8 @@ class CreatePayLinkData(BaseModel): currency: str = Query(None) comment_chars: int = Query(0, ge=0, lt=800) webhook_url: str = Query(None) + webhook_headers: str = Query(None) + webhook_body: str = Query(None) success_text: str = Query(None) success_url: str = Query(None) fiat_base_multiplier: int = Query(100, ge=1) @@ -31,6 +33,8 @@ class PayLink(BaseModel): served_meta: int served_pr: int webhook_url: Optional[str] + webhook_headers: Optional[str] + webhook_body: Optional[str] success_text: Optional[str] success_url: Optional[str] currency: Optional[str] diff --git a/lnbits/extensions/lnurlp/tasks.py b/lnbits/extensions/lnurlp/tasks.py index 86f1579a..23f312cb 100644 --- a/lnbits/extensions/lnurlp/tasks.py +++ b/lnbits/extensions/lnurlp/tasks.py @@ -33,17 +33,22 @@ async def on_invoice_paid(payment: Payment) -> None: if pay_link and pay_link.webhook_url: async with httpx.AsyncClient() as client: try: - r = await client.post( - pay_link.webhook_url, - json={ + kwargs = { + "json": { "payment_hash": payment.payment_hash, "payment_request": payment.bolt11, "amount": payment.amount, "comment": payment.extra.get("comment"), "lnurlp": pay_link.id, }, - timeout=40, - ) + "timeout": 40, + } + if pay_link.webhook_body: + kwargs["json"]["body"] = json.loads(pay_link.webhook_body) + if pay_link.webhook_headers: + kwargs["headers"] = json.loads(pay_link.webhook_headers) + + r = await client.post(pay_link.webhook_url, **kwargs) await mark_webhook_sent(payment, r.status_code) except (httpx.ConnectError, httpx.RequestError): await mark_webhook_sent(payment, -1) diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/index.html b/lnbits/extensions/lnurlp/templates/lnurlp/index.html index de90f5af..eb594cec 100644 --- a/lnbits/extensions/lnurlp/templates/lnurlp/index.html +++ b/lnbits/extensions/lnurlp/templates/lnurlp/index.html @@ -213,6 +213,24 @@ label="Webhook URL (optional)" hint="A URL to be called whenever this link receives a payment." > + + List[satsdiceL return [satsdiceLink(**row) for row in rows] -async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]: +async def update_satsdice_pay(link_id: str, **kwargs) -> satsdiceLink: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?", @@ -85,10 +84,10 @@ async def update_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]: row = await db.fetchone( "SELECT * FROM satsdice.satsdice_pay WHERE id = ?", (link_id,) ) - return satsdiceLink(**row) if row else None + return satsdiceLink(**row) -async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLink]: +async def increment_satsdice_pay(link_id: str, **kwargs) -> Optional[satsdiceLink]: q = ", ".join([f"{field[0]} = {field[0]} + ?" for field in kwargs.items()]) await db.execute( f"UPDATE satsdice.satsdice_pay SET {q} WHERE id = ?", @@ -100,7 +99,7 @@ async def increment_satsdice_pay(link_id: int, **kwargs) -> Optional[satsdiceLin return satsdiceLink(**row) if row else None -async def delete_satsdice_pay(link_id: int) -> None: +async def delete_satsdice_pay(link_id: str) -> None: await db.execute("DELETE FROM satsdice.satsdice_pay WHERE id = ?", (link_id,)) @@ -119,9 +118,15 @@ async def create_satsdice_payment(data: CreateSatsDicePayment) -> satsdicePaymen ) VALUES (?, ?, ?, ?, ?) """, - (data["payment_hash"], data["satsdice_pay"], data["value"], False, False), + ( + data.payment_hash, + data.satsdice_pay, + data.value, + False, + False, + ), ) - payment = await get_satsdice_payment(data["payment_hash"]) + payment = await get_satsdice_payment(data.payment_hash) assert payment, "Newly created withdraw couldn't be retrieved" return payment @@ -134,9 +139,7 @@ async def get_satsdice_payment(payment_hash: str) -> Optional[satsdicePayment]: return satsdicePayment(**row) if row else None -async def update_satsdice_payment( - payment_hash: int, **kwargs -) -> Optional[satsdicePayment]: +async def update_satsdice_payment(payment_hash: str, **kwargs) -> satsdicePayment: q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) await db.execute( @@ -147,7 +150,7 @@ async def update_satsdice_payment( "SELECT * FROM satsdice.satsdice_payment WHERE payment_hash = ?", (payment_hash,), ) - return satsdicePayment(**row) if row else None + return satsdicePayment(**row) ##################SATSDICE WITHDRAW LINKS @@ -168,16 +171,16 @@ async def create_satsdice_withdraw(data: CreateSatsDiceWithdraw) -> satsdiceWith VALUES (?, ?, ?, ?, ?, ?, ?) """, ( - data["payment_hash"], - data["satsdice_pay"], - data["value"], + data.payment_hash, + data.satsdice_pay, + data.value, urlsafe_short_hash(), urlsafe_short_hash(), int(datetime.now().timestamp()), - data["used"], + data.used, ), ) - withdraw = await get_satsdice_withdraw(data["payment_hash"], 0) + withdraw = await get_satsdice_withdraw(data.payment_hash, 0) assert withdraw, "Newly created withdraw couldn't be retrieved" return withdraw @@ -247,7 +250,7 @@ async def delete_satsdice_withdraw(withdraw_id: str) -> None: ) -async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck: +async def create_withdraw_hash_check(the_hash: str, lnurl_id: str): await db.execute( """ INSERT INTO satsdice.hash_checkw ( @@ -262,19 +265,15 @@ async def create_withdraw_hash_check(the_hash: str, lnurl_id: str) -> HashCheck: return hashCheck -async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: +async def get_withdraw_hash_checkw(the_hash: str, lnurl_id: str): rowid = await db.fetchone( "SELECT * FROM satsdice.hash_checkw WHERE id = ?", (the_hash,) ) rowlnurl = await db.fetchone( "SELECT * FROM satsdice.hash_checkw WHERE lnurl_id = ?", (lnurl_id,) ) - if not rowlnurl: + if not rowlnurl or not rowid: await create_withdraw_hash_check(the_hash, lnurl_id) return {"lnurl": True, "hash": False} else: - if not rowid: - await create_withdraw_hash_check(the_hash, lnurl_id) - return {"lnurl": True, "hash": False} - else: - return {"lnurl": True, "hash": True} + return {"lnurl": True, "hash": True} diff --git a/lnbits/extensions/satsdice/lnurl.py b/lnbits/extensions/satsdice/lnurl.py index caafc3a4..a9b3cf08 100644 --- a/lnbits/extensions/satsdice/lnurl.py +++ b/lnbits/extensions/satsdice/lnurl.py @@ -1,4 +1,3 @@ -import hashlib import json import math from http import HTTPStatus @@ -83,15 +82,18 @@ async def api_lnurlp_callback( success_action = link.success_action(payment_hash=payment_hash, req=req) - data: CreateSatsDicePayment = { - "satsdice_pay": link.id, - "value": amount_received / 1000, - "payment_hash": payment_hash, - } + data = CreateSatsDicePayment( + satsdice_pay=link.id, + value=amount_received / 1000, + payment_hash=payment_hash, + ) await create_satsdice_payment(data) - payResponse = {"pr": payment_request, "successAction": success_action, "routes": []} - + payResponse: dict = { + "pr": payment_request, + "successAction": success_action, + "routes": [], + } return json.dumps(payResponse) @@ -133,9 +135,7 @@ async def api_lnurlw_response(req: Request, unique_hash: str = Query(None)): name="satsdice.api_lnurlw_callback", ) async def api_lnurlw_callback( - req: Request, unique_hash: str = Query(None), - k1: str = Query(None), pr: str = Query(None), ): @@ -146,12 +146,13 @@ async def api_lnurlw_callback( return {"status": "ERROR", "reason": "spent"} paylink = await get_satsdice_pay(link.satsdice_pay) - await update_satsdice_withdraw(link.id, used=1) - await pay_invoice( - wallet_id=paylink.wallet, - payment_request=pr, - max_sat=link.value, - extra={"tag": "withdraw"}, - ) + if paylink: + await update_satsdice_withdraw(link.id, used=1) + await pay_invoice( + wallet_id=paylink.wallet, + payment_request=pr, + max_sat=link.value, + extra={"tag": "withdraw"}, + ) - return {"status": "OK"} + return {"status": "OK"} diff --git a/lnbits/extensions/satsdice/models.py b/lnbits/extensions/satsdice/models.py index fd9af74f..2537f8d7 100644 --- a/lnbits/extensions/satsdice/models.py +++ b/lnbits/extensions/satsdice/models.py @@ -4,7 +4,7 @@ from typing import Dict, Optional from fastapi import Request from fastapi.param_functions import Query -from lnurl import Lnurl, LnurlWithdrawResponse +from lnurl import Lnurl from lnurl import encode as lnurl_encode # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore from pydantic import BaseModel @@ -80,8 +80,7 @@ class satsdiceWithdraw(BaseModel): def is_spent(self) -> bool: return self.used >= 1 - @property - def lnurl_response(self, req: Request) -> LnurlWithdrawResponse: + def lnurl_response(self, req: Request): url = req.url_for("satsdice.api_lnurlw_callback", unique_hash=self.unique_hash) withdrawResponse = { "tag": "withdrawRequest", @@ -99,7 +98,7 @@ class HashCheck(BaseModel): lnurl_id: str @classmethod - def from_row(cls, row: Row) -> "Hash": + def from_row(cls, row: Row): return cls(**dict(row)) diff --git a/lnbits/extensions/satsdice/views.py b/lnbits/extensions/satsdice/views.py index 72e24867..d2b5e601 100644 --- a/lnbits/extensions/satsdice/views.py +++ b/lnbits/extensions/satsdice/views.py @@ -1,6 +1,8 @@ import random from http import HTTPStatus +from io import BytesIO +import pyqrcode from fastapi import Request from fastapi.param_functions import Query from fastapi.params import Depends @@ -20,13 +22,15 @@ from .crud import ( get_satsdice_withdraw, update_satsdice_payment, ) -from .models import CreateSatsDiceWithdraw, satsdiceLink +from .models import CreateSatsDiceWithdraw templates = Jinja2Templates(directory="templates") @satsdice_ext.get("/", response_class=HTMLResponse) -async def index(request: Request, user: User = Depends(check_user_exists)): +async def index( + request: Request, user: User = Depends(check_user_exists) # type: ignore +): return satsdice_renderer().TemplateResponse( "satsdice/index.html", {"request": request, "user": user.dict()} ) @@ -67,7 +71,7 @@ async def displaywin( ) withdrawLink = await get_satsdice_withdraw(payment_hash) payment = await get_satsdice_payment(payment_hash) - if payment.lost: + if not payment or payment.lost: return satsdice_renderer().TemplateResponse( "satsdice/error.html", {"request": request, "link": satsdicelink.id, "paid": False, "lost": True}, @@ -96,13 +100,18 @@ async def displaywin( ) await update_satsdice_payment(payment_hash, paid=1) paylink = await get_satsdice_payment(payment_hash) + if not paylink: + return satsdice_renderer().TemplateResponse( + "satsdice/error.html", + {"request": request, "link": satsdicelink.id, "paid": False, "lost": True}, + ) - data: CreateSatsDiceWithdraw = { - "satsdice_pay": satsdicelink.id, - "value": paylink.value * satsdicelink.multiplier, - "payment_hash": payment_hash, - "used": 0, - } + data = CreateSatsDiceWithdraw( + satsdice_pay=satsdicelink.id, + value=paylink.value * satsdicelink.multiplier, + payment_hash=payment_hash, + used=0, + ) withdrawLink = await create_satsdice_withdraw(data) return satsdice_renderer().TemplateResponse( @@ -121,9 +130,12 @@ async def displaywin( @satsdice_ext.get("/img/{link_id}", response_class=HTMLResponse) async def img(link_id): - link = await get_satsdice_pay(link_id) or abort( - HTTPStatus.NOT_FOUND, "satsdice link does not exist." - ) + link = await get_satsdice_pay(link_id) + if not link: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="satsdice link does not exist." + ) + qr = pyqrcode.create(link.lnurl) stream = BytesIO() qr.svg(stream, scale=3) diff --git a/lnbits/extensions/satsdice/views_api.py b/lnbits/extensions/satsdice/views_api.py index bccaa5ff..d33b76b8 100644 --- a/lnbits/extensions/satsdice/views_api.py +++ b/lnbits/extensions/satsdice/views_api.py @@ -15,9 +15,10 @@ from .crud import ( delete_satsdice_pay, get_satsdice_pay, get_satsdice_pays, + get_withdraw_hash_checkw, update_satsdice_pay, ) -from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink +from .models import CreateSatsDiceLink ################LNURL pay @@ -25,13 +26,15 @@ from .models import CreateSatsDiceLink, CreateSatsDiceWithdraws, satsdiceLink @satsdice_ext.get("/api/v1/links") async def api_links( request: Request, - wallet: WalletTypeInfo = Depends(get_key_type), + wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore all_wallets: bool = Query(False), ): wallet_ids = [wallet.wallet.id] if all_wallets: - wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + user = await get_user(wallet.wallet.user) + if user: + wallet_ids = user.wallet_ids try: links = await get_satsdice_pays(wallet_ids) @@ -46,7 +49,7 @@ async def api_links( @satsdice_ext.get("/api/v1/links/{link_id}") async def api_link_retrieve( - link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) + link_id: str = Query(None), wallet: WalletTypeInfo = Depends(get_key_type) # type: ignore ): link = await get_satsdice_pay(link_id) @@ -67,7 +70,7 @@ async def api_link_retrieve( @satsdice_ext.put("/api/v1/links/{link_id}", status_code=HTTPStatus.OK) async def api_link_create_or_update( data: CreateSatsDiceLink, - wallet: WalletTypeInfo = Depends(get_key_type), + wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore link_id: str = Query(None), ): if data.min_bet > data.max_bet: @@ -95,10 +98,10 @@ async def api_link_create_or_update( @satsdice_ext.delete("/api/v1/links/{link_id}") async def api_link_delete( - wallet: WalletTypeInfo = Depends(get_key_type), link_id: str = Query(None) + wallet: WalletTypeInfo = Depends(get_key_type), # type: ignore + link_id: str = Query(None), ): link = await get_satsdice_pay(link_id) - if not link: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="Pay link does not exist." @@ -117,11 +120,12 @@ async def api_link_delete( ##########LNURL withdraw -@satsdice_ext.get("/api/v1/withdraws/{the_hash}/{lnurl_id}") +@satsdice_ext.get( + "/api/v1/withdraws/{the_hash}/{lnurl_id}", dependencies=[Depends(get_key_type)] +) async def api_withdraw_hash_retrieve( - wallet: WalletTypeInfo = Depends(get_key_type), lnurl_id: str = Query(None), the_hash: str = Query(None), ): - hashCheck = await get_withdraw_hash_check(the_hash, lnurl_id) + hashCheck = await get_withdraw_hash_checkw(the_hash, lnurl_id) return hashCheck diff --git a/lnbits/extensions/satspay/README.md b/lnbits/extensions/satspay/README.md index d52547ae..7a12feb3 100644 --- a/lnbits/extensions/satspay/README.md +++ b/lnbits/extensions/satspay/README.md @@ -18,7 +18,7 @@ Easilly create invoices that support Lightning Network and on-chain BTC payment. ![charge form](https://i.imgur.com/F10yRiW.png) 3. The charge will appear on the _Charges_ section\ ![charges](https://i.imgur.com/zqHpVxc.png) -4. Your costumer/payee will get the payment page +4. Your customer/payee will get the payment page - they can choose to pay on LN\ ![offchain payment](https://i.imgur.com/4191SMV.png) - or pay on chain\ diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py index 73c87e7c..e1b87c41 100644 --- a/lnbits/extensions/satspay/views_api.py +++ b/lnbits/extensions/satspay/views_api.py @@ -94,7 +94,7 @@ async def api_charge_delete(charge_id, wallet: WalletTypeInfo = Depends(get_key_ ) await delete_charge(charge_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT #############################BALANCE########################## diff --git a/lnbits/extensions/scrub/views_api.py b/lnbits/extensions/scrub/views_api.py index 3714a304..cc55c15d 100644 --- a/lnbits/extensions/scrub/views_api.py +++ b/lnbits/extensions/scrub/views_api.py @@ -109,4 +109,4 @@ async def api_link_delete(link_id, wallet: WalletTypeInfo = Depends(require_admi ) await delete_scrub_link(link_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/splitpayments/templates/splitpayments/index.html b/lnbits/extensions/splitpayments/templates/splitpayments/index.html index 5862abc1..1cceb7ba 100644 --- a/lnbits/extensions/splitpayments/templates/splitpayments/index.html +++ b/lnbits/extensions/splitpayments/templates/splitpayments/index.html @@ -31,14 +31,20 @@ style="flex-wrap: nowrap" v-for="(target, t) in targets" > - + option-label="name" + style="width: 1000px" + new-value-mode="add-unique" + use-input + input-debounce="0" + emit-value + > Hit enter to add values + >Hit enter to add values + +
-

{% raw %}{{ famount }}{% endraw %}

+

{% raw %}{{ amountFormatted }}{% endraw %}

{% raw %}{{ fsat }}{% endraw %} sat
@@ -148,6 +148,14 @@
+ + +
-

{% raw %}{{ famount }}{% endraw %}

+

+ {% raw %}{{ amountWithTipFormatted }}{% endraw %} +

{% raw %}{{ fsat }} sat ( + {{ tipAmountSat }} tip)( + {{ tipAmountFormatted }} tip) {% endraw %}
@@ -204,19 +214,48 @@ style="padding: 10px; margin: 3px" unelevated @click="processTipSelection(tip)" - size="xl" + size="lg" :outline="!($q.dark.isActive)" rounded color="primary" - v-for="tip in this.tip_options" + v-for="tip in tip_options.filter(f => f != 'Round')" :key="tip" >{% raw %}{{ tip }}{% endraw %}% -
-
-

No, thanks

+ +
+ + + Ok +
+ No, thanks Close
@@ -256,6 +295,38 @@ style="font-size: min(90vw, 40em)" >
+ + + + + + + + + + + No paid invoices + + + + {%raw%} + + {{payment.amount / 1000}} sats + Hash: {{payment.checking_id.slice(0, 30)}}... + + + {{payment.dateFrom}} + + + {%endraw%} + + + + {% endblock %} {% block styles %} @@ -294,8 +365,13 @@ exchangeRate: null, stack: [], tipAmount: 0.0, + tipRounding: null, hasNFC: false, nfcTagReading: false, + lastPaymentsDialog: { + show: false, + data: [] + }, invoiceDialog: { show: false, data: null, @@ -310,32 +386,81 @@ }, complete: { show: false - } + }, + rounding: false } }, computed: { amount: function () { if (!this.stack.length) return 0.0 - return (Number(this.stack.join('')) / 100).toFixed(2) + return Number(this.stack.join('') / 100) }, - famount: function () { - return LNbits.utils.formatCurrency(this.amount, this.currency) + amountFormatted: function () { + return LNbits.utils.formatCurrency( + this.amount.toFixed(2), + this.currency + ) + }, + amountWithTipFormatted: function () { + return LNbits.utils.formatCurrency( + (this.amount + this.tipAmount).toFixed(2), + this.currency + ) }, sat: function () { if (!this.exchangeRate) return 0 - return Math.ceil( - ((this.amount - this.tipAmount) / this.exchangeRate) * 100000000 - ) + return Math.ceil((this.amount / this.exchangeRate) * 100000000) }, tipAmountSat: function () { if (!this.exchangeRate) return 0 return Math.ceil((this.tipAmount / this.exchangeRate) * 100000000) }, + tipAmountFormatted: function () { + return LNbits.utils.formatSat(this.tipAmountSat) + }, fsat: function () { return LNbits.utils.formatSat(this.sat) + }, + isRoundValid() { + return this.tipRounding > this.amount + }, + roundToSugestion() { + switch (true) { + case this.amount > 50: + toNext = 10 + break + case this.amount > 6: + toNext = 5 + break + case this.amount > 2.5: + toNext = 1 + break + default: + toNext = 0.5 + break + } + + return Math.ceil(this.amount / toNext) * toNext } }, methods: { + setRounding() { + this.rounding = true + this.tipRounding = this.roundToSugestion + this.$nextTick(() => this.$refs.inputRounding.focus()) + }, + calculatePercent() { + let change = ((this.tipRounding - this.amount) / this.amount) * 100 + if (change < 0) { + this.$q.notify({ + type: 'warning', + message: 'Amount with tip must be greater than initial amount.' + }) + this.tipRounding = this.roundToSugestion + return + } + this.processTipSelection(change) + }, closeInvoiceDialog: function () { this.stack = [] this.tipAmount = 0.0 @@ -348,30 +473,18 @@ processTipSelection: function (selectedTipOption) { this.tipDialog.show = false - if (selectedTipOption) { - const tipAmount = parseFloat( - parseFloat((selectedTipOption / 100) * this.amount) - ) - const subtotal = parseFloat(this.amount) - const grandTotal = parseFloat((tipAmount + subtotal).toFixed(2)) - const totalString = grandTotal.toFixed(2).toString() - - this.stack = [] - for (var i = 0; i < totalString.length; i++) { - const char = totalString[i] - - if (char !== '.') { - this.stack.push(char) - } - } - - this.tipAmount = tipAmount + if (!selectedTipOption) { + this.tipAmount = 0.0 + return this.showInvoice() } + this.tipAmount = (selectedTipOption / 100) * this.amount this.showInvoice() }, submitForm: function () { if (this.tip_options && this.tip_options.length) { + this.rounding = false + this.tipRounding = null this.showTipModal() } else { this.showInvoice() @@ -520,6 +633,24 @@ self.exchangeRate = response.data.data['BTC' + self.currency][self.currency] }) + }, + getLastPayments() { + return axios + .get(`/tpos/api/v1/tposs/${this.tposId}/invoices`) + .then(res => { + if (res.data && res.data.length) { + let last = [...res.data] + this.lastPaymentsDialog.data = last.map(obj => { + obj.dateFrom = moment(obj.time * 1000).fromNow() + return obj + }) + } + }) + .catch(e => console.error(e)) + }, + showLastPayments() { + this.getLastPayments() + this.lastPaymentsDialog.show = true } }, created: function () { @@ -529,10 +660,26 @@ '{{ tpos.tip_options | tojson }}' == 'null' ? null : JSON.parse('{{ tpos.tip_options }}') + + if ('{{ tpos.tip_wallet }}') { + this.tip_options.push('Round') + } setInterval(function () { getRates() }, 120000) } }) + {% endblock %} diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index b7f14b98..fe63a247 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -7,7 +7,8 @@ from lnurl import decode as decode_lnurl from loguru import logger from starlette.exceptions import HTTPException -from lnbits.core.crud import get_user +from lnbits.core.crud import get_latest_payments_by_extension, get_user +from lnbits.core.models import Payment from lnbits.core.services import create_invoice from lnbits.core.views.api import api_payment from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key @@ -51,7 +52,7 @@ async def api_tpos_delete( raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your TPoS.") await delete_tpos(tpos_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT @tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices", status_code=HTTPStatus.CREATED) @@ -81,6 +82,30 @@ async def api_tpos_create_invoice( return {"payment_hash": payment_hash, "payment_request": payment_request} +@tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices") +async def api_tpos_get_latest_invoices(tpos_id: str = None): + try: + payments = [ + Payment.from_row(row) + for row in await get_latest_payments_by_extension( + ext_name="tpos", ext_id=tpos_id + ) + ] + + except Exception as e: + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + + return [ + { + "checking_id": payment.checking_id, + "amount": payment.amount, + "time": payment.time, + "pending": payment.pending, + } + for payment in payments + ] + + @tpos_ext.post( "/api/v1/tposs/{tpos_id}/invoices/{payment_request}/pay", status_code=HTTPStatus.OK ) diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py index 1ce66d4f..649888a8 100644 --- a/lnbits/extensions/usermanager/crud.py +++ b/lnbits/extensions/usermanager/crud.py @@ -63,10 +63,11 @@ async def get_usermanager_users(user_id: str) -> List[Users]: return [Users(**row) for row in rows] -async def delete_usermanager_user(user_id: str) -> None: - wallets = await get_usermanager_wallets(user_id) - for wallet in wallets: - await delete_wallet(user_id=user_id, wallet_id=wallet.id) +async def delete_usermanager_user(user_id: str, delete_core: bool = True) -> None: + if delete_core: + wallets = await get_usermanager_wallets(user_id) + for wallet in wallets: + await delete_wallet(user_id=user_id, wallet_id=wallet.id) await db.execute("DELETE FROM usermanager.users WHERE id = ?", (user_id,)) await db.execute("""DELETE FROM usermanager.wallets WHERE "user" = ?""", (user_id,)) diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html index 886589e6..ff3ba85a 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -38,13 +38,13 @@ >
Body (application/json)
- Returns 201 CREATED (application/json) + Returns 200 OK (application/json)
JSON list of users
Curl example
curl -X GET {{ request.base_url }}usermanager/api/v1/users -H - "X-Api-Key: {{ user.wallets[0].inkey }}" + "X-Api-Key: {{ user.wallets[0].adminkey }}" @@ -57,10 +57,16 @@ /usermanager/api/v1/users/<user_id>
Body (application/json)
+
- Returns 201 CREATED (application/json) + Returns 200 OK (application/json)
- JSON list of users + {"id": <string>, "name": <string>, "admin": + <string>, "email": <string>, "password": + <string>} +
Curl example
curl -X GET {{ request.base_url @@ -75,20 +81,19 @@ GET - /usermanager/api/v1/wallets/<user_id>
Headers
{"X-Api-Key": <string>}
Body (application/json)
- Returns 201 CREATED (application/json) + Returns 200 OK (application/json)
JSON wallet data
Curl example
curl -X GET {{ request.base_url - }}usermanager/api/v1/wallets/<user_id> -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + >curl -X GET {{ request.base_url }}usermanager/api/v1/wallets -H + "X-Api-Key: {{ user.wallets[0].adminkey }}"
@@ -104,7 +109,7 @@ {"X-Api-Key": <string>}
Body (application/json)
- Returns 201 CREATED (application/json) + Returns 200 OK (application/json)
JSON a wallets transactions
Curl example
@@ -215,7 +220,7 @@ curl -X DELETE {{ request.base_url }}usermanager/api/v1/users/<user_id> -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + user.wallets[0].adminkey }}" @@ -233,7 +238,7 @@ curl -X DELETE {{ request.base_url }}usermanager/api/v1/wallets/<wallet_id> -H "X-Api-Key: {{ - user.wallets[0].inkey }}" + user.wallets[0].adminkey }}" @@ -254,11 +259,15 @@ {"X-Api-Key": <string>}
Curl example
curl -X POST {{ request.base_url }}usermanager/api/v1/extensions -d - '{"userid": <string>, "extension": <string>, "active": - <integer>}' -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H - "Content-type: application/json" + >curl -X POST {{ request.base_url + }}usermanager/api/v1/extensions?extension=withdraw&userid=user_id&active=true + -H "X-Api-Key: {{ user.wallets[0].inkey }}" -H "Content-type: + application/json" +
+ Returns 200 OK (application/json) +
+ {"extension": "updated"} diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py index 7e7b7653..b1bf8ef8 100644 --- a/lnbits/extensions/usermanager/views_api.py +++ b/lnbits/extensions/usermanager/views_api.py @@ -52,15 +52,17 @@ async def api_usermanager_users_create( @usermanager_ext.delete("/api/v1/users/{user_id}") async def api_usermanager_users_delete( - user_id, wallet: WalletTypeInfo = Depends(require_admin_key) + user_id, + delete_core: bool = Query(True), + wallet: WalletTypeInfo = Depends(require_admin_key), ): user = await get_usermanager_user(user_id) if not user: raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="User does not exist." ) - await delete_usermanager_user(user_id) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + await delete_usermanager_user(user_id, delete_core) + return "", HTTPStatus.NO_CONTENT # Activate Extension @@ -124,4 +126,4 @@ async def api_usermanager_wallets_delete( status_code=HTTPStatus.NOT_FOUND, detail="Wallet does not exist." ) await delete_usermanager_wallet(wallet_id, get_wallet.user) - raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html b/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html index c65ad1c4..0df5bebf 100644 --- a/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html +++ b/lnbits/extensions/watchonly/static/components/fee-rate/fee-rate.html @@ -6,6 +6,7 @@ filled dense v-model.number="feeRate" + step="any" :rules="[val => !!val || 'Field is required']" type="number" label="sats/vbyte" diff --git a/lnbits/extensions/withdraw/lnurl.py b/lnbits/extensions/withdraw/lnurl.py index 18a99599..5737e54f 100644 --- a/lnbits/extensions/withdraw/lnurl.py +++ b/lnbits/extensions/withdraw/lnurl.py @@ -9,7 +9,7 @@ from fastapi import HTTPException from fastapi.param_functions import Query from loguru import logger from starlette.requests import Request -from starlette.responses import HTMLResponse # type: ignore +from starlette.responses import HTMLResponse from lnbits.core.services import pay_invoice @@ -51,10 +51,24 @@ async def api_lnurl_response(request: Request, unique_hash): # CALLBACK -@withdraw_ext.get("/api/v1/lnurl/cb/{unique_hash}", name="withdraw.api_lnurl_callback") +@withdraw_ext.get( + "/api/v1/lnurl/cb/{unique_hash}", + name="withdraw.api_lnurl_callback", + summary="lnurl withdraw callback", + description=""" + This enpoints allows you to put unique_hash, k1 + and a payment_request to get your payment_request paid. + """, + response_description="JSON with status", + responses={ + 200: {"description": "status: OK"}, + 400: {"description": "k1 is wrong or link open time or withdraw not working."}, + 404: {"description": "withdraw link not found."}, + 405: {"description": "withdraw link is spent."}, + }, +) async def api_lnurl_callback( unique_hash, - request: Request, k1: str = Query(...), pr: str = Query(...), id_unique_hash=None, @@ -63,49 +77,53 @@ async def api_lnurl_callback( now = int(datetime.now().timestamp()) if not link: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found" + status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found." ) if link.is_spent: raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="Withdraw is spent." + status_code=HTTPStatus.METHOD_NOT_ALLOWED, detail="withdraw is spent." ) if link.k1 != k1: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Bad request.") + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="k1 is wrong.") if now < link.open_time: - return {"status": "ERROR", "reason": f"Wait {link.open_time - now} seconds."} + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"wait link open_time {link.open_time - now} seconds.", + ) usescsv = "" + + for x in range(1, link.uses - link.used): + usecv = link.usescsv.split(",") + usescsv += "," + str(usecv[x]) + usecsvback = usescsv + + found = False + if id_unique_hash is not None: + useslist = link.usescsv.split(",") + for ind, x in enumerate(useslist): + tohash = link.id + link.unique_hash + str(x) + if id_unique_hash == shortuuid.uuid(name=tohash): + found = True + useslist.pop(ind) + usescsv = ",".join(useslist) + if not found: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="withdraw not found." + ) + else: + usescsv = usescsv[1:] + + changesback = { + "open_time": link.wait_time, + "used": link.used, + "usescsv": usecsvback, + } + try: - for x in range(1, link.uses - link.used): - usecv = link.usescsv.split(",") - usescsv += "," + str(usecv[x]) - usecsvback = usescsv - - found = False - if id_unique_hash is not None: - useslist = link.usescsv.split(",") - for ind, x in enumerate(useslist): - tohash = link.id + link.unique_hash + str(x) - if id_unique_hash == shortuuid.uuid(name=tohash): - found = True - useslist.pop(ind) - usescsv = ",".join(useslist) - if not found: - raise HTTPException( - status_code=HTTPStatus.NOT_FOUND, detail="LNURL-withdraw not found." - ) - else: - usescsv = usescsv[1:] - - changesback = { - "open_time": link.wait_time, - "used": link.used, - "usescsv": usecsvback, - } - changes = { "open_time": link.wait_time + now, "used": link.used + 1, @@ -143,7 +161,9 @@ async def api_lnurl_callback( except Exception as e: await update_withdraw_link(link.id, **changesback) logger.error(traceback.format_exc()) - return {"status": "ERROR", "reason": "Link not working"} + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail=f"withdraw not working. {str(e)}" + ) # FOR LNURLs WHICH ARE UNIQUE diff --git a/lnbits/extensions/withdraw/static/js/index.js b/lnbits/extensions/withdraw/static/js/index.js index 943e9024..a3eaa593 100644 --- a/lnbits/extensions/withdraw/static/js/index.js +++ b/lnbits/extensions/withdraw/static/js/index.js @@ -290,8 +290,12 @@ new Vue({ }) } }, - exportCSV: function () { - LNbits.utils.exportCSV(this.paywallsTable.columns, this.paywalls) + exportCSV() { + LNbits.utils.exportCSV( + this.withdrawLinksTable.columns, + this.withdrawLinks, + 'withdraw-links' + ) } }, created: function () { diff --git a/lnbits/server.py b/lnbits/server.py index ecf7ff62..3d209961 100644 --- a/lnbits/server.py +++ b/lnbits/server.py @@ -19,14 +19,21 @@ from lnbits.settings import set_cli_settings, settings ) @click.option("--port", default=settings.port, help="Port to listen on") @click.option("--host", default=settings.host, help="Host to run LNBits on") -@click.option("--reload", is_flag=True, help="Reload LNBits on changes in code") +@click.option("--forwarded-allow-ips", default=settings.forwarded_allow_ips, help="Allowed proxy servers") @click.option("--ssl-keyfile", default=None, help="Path to SSL keyfile") @click.option("--ssl-certfile", default=None, help="Path to SSL certificate") @click.pass_context -def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str, reload: bool): +def main( + ctx, + port: int, + host: str, + forwarded_allow_ips: str, + ssl_keyfile: str, + ssl_certfile: str, +): """Launched with `poetry run lnbits` at root level""" - set_cli_settings(host=host, port=port) + set_cli_settings(host=host, port=port, forwarded_allow_ips=forwarded_allow_ips) # this beautiful beast parses all command line arguments and passes them to the uvicorn server d = dict() @@ -48,8 +55,7 @@ def main(ctx, port: int, host: str, ssl_keyfile: str, ssl_certfile: str, reload: loop="uvloop", port=port, host=host, - reload=reload, - forwarded_allow_ips="*", + forwarded_allow_ips=forwarded_allow_ips, ssl_keyfile=ssl_keyfile, ssl_certfile=ssl_certfile, **d diff --git a/lnbits/settings.py b/lnbits/settings.py index 61dbd6f2..0e58f8c7 100644 --- a/lnbits/settings.py +++ b/lnbits/settings.py @@ -32,6 +32,7 @@ class Settings(BaseSettings): debug: bool = Field(default=False) host: str = Field(default="127.0.0.1") port: int = Field(default=5000) + forwarded_allow_ips: str = Field(default="*") lnbits_path: str = Field(default=".") lnbits_commit: str = Field(default="unknown") diff --git a/lnbits/static/images/lnbits-shop-dark.png b/lnbits/static/images/lnbits-shop-dark.png new file mode 100644 index 00000000..3dd677dc Binary files /dev/null and b/lnbits/static/images/lnbits-shop-dark.png differ diff --git a/lnbits/static/images/lnbits-shop-light.png b/lnbits/static/images/lnbits-shop-light.png new file mode 100644 index 00000000..96607cb4 Binary files /dev/null and b/lnbits/static/images/lnbits-shop-light.png differ diff --git a/lnbits/templates/base.html b/lnbits/templates/base.html index 67241bb5..ef270371 100644 --- a/lnbits/templates/base.html +++ b/lnbits/templates/base.html @@ -199,6 +199,18 @@ > + + API DOCS + View LNbits Swagger API docs +