diff --git a/.env.example b/.env.example index 4edaea97..b7fb9f06 100644 --- a/.env.example +++ b/.env.example @@ -6,14 +6,17 @@ PORT=5000 DEBUG=false +# Allow users and admins by user IDs (comma separated list) LNBITS_ALLOWED_USERS="" LNBITS_ADMIN_USERS="" # Extensions only admin can access LNBITS_ADMIN_EXTENSIONS="ngrok" 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/Dockerfile b/Dockerfile index 6259fe7b..f107f68c 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/lnbits/core/crud.py b/lnbits/core/crud.py index bb1ca0c1..881d1001 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 68a7b7ed..5f26cb03 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 4bf6067c..22fbd05d 100644 --- a/lnbits/core/templates/core/wallet.html +++ b/lnbits/core/templates/core/wallet.html @@ -388,9 +388,14 @@ {% endif %} {% if AD_SPACE %} {% for ADS in AD_SPACE %} {% set AD = ADS.split(';') %} - +
{{ AD_TITLE }}
+ + + + + + {% endfor %} {% endif %} diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 983d5a26..ae3e6a5e 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -12,7 +12,7 @@ from urllib.parse import ParseResult, parse_qs, urlencode, urlparse, urlunparse import async_timeout import httpx import pyqrcode -from fastapi import Depends, Header, Query, Request +from fastapi import Depends, Header, Query, Request, Response from fastapi.exceptions import HTTPException from fastapi.params import Body from loguru import logger @@ -155,30 +155,29 @@ class CreateInvoiceData(BaseModel): async def api_payments_create_invoice(data: CreateInvoiceData, wallet: Wallet): - if data.description_hash: + if data.description_hash or data.unhashed_description: try: - description_hash = binascii.unhexlify(data.description_hash) + description_hash = ( + binascii.unhexlify(data.description_hash) + if data.description_hash + else b"" + ) + unhashed_description = ( + binascii.unhexlify(data.unhashed_description) + if data.unhashed_description + else b"" + ) except binascii.Error: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, - detail="'description_hash' must be a valid hex string", + detail="'description_hash' and 'unhashed_description' must be a valid hex strings", ) - unhashed_description = b"" - memo = "" - elif data.unhashed_description: - try: - unhashed_description = binascii.unhexlify(data.unhashed_description) - except binascii.Error: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, - detail="'unhashed_description' must be a valid hex string", - ) - description_hash = b"" memo = "" else: description_hash = b"" unhashed_description = b"" memo = data.memo or LNBITS_SITE_TITLE + if data.unit == "sat": amount = int(data.amount) else: @@ -585,8 +584,8 @@ class DecodePayment(BaseModel): data: str -@core_app.post("/api/v1/payments/decode") -async def api_payments_decode(data: DecodePayment): +@core_app.post("/api/v1/payments/decode", status_code=HTTPStatus.OK) +async def api_payments_decode(data: DecodePayment, response: Response): payment_str = data.data try: if payment_str[:5] == "LNURL": @@ -607,6 +606,7 @@ async def api_payments_decode(data: DecodePayment): "min_final_cltv_expiry": invoice.min_final_cltv_expiry, } except: + response.status_code = HTTPStatus.BAD_REQUEST return {"message": "Failed to decode"} diff --git a/lnbits/db.py b/lnbits/db.py index f52b0391..321b23d0 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -1,6 +1,7 @@ import asyncio import datetime import os +import re import time from contextlib import asynccontextmanager from typing import Optional @@ -73,18 +74,39 @@ class Connection(Compat): query = query.replace("?", "%s") return query + def rewrite_values(self, values): + # strip html + CLEANR = re.compile("<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});") + + def cleanhtml(raw_html): + if isinstance(raw_html, str): + cleantext = re.sub(CLEANR, "", raw_html) + return cleantext + else: + return raw_html + + # tuple to list and back to tuple + values = tuple([cleanhtml(l) for l in list(values)]) + return values + async def fetchall(self, query: str, values: tuple = ()) -> list: - result = await self.conn.execute(self.rewrite_query(query), values) + result = await self.conn.execute( + self.rewrite_query(query), self.rewrite_values(values) + ) return await result.fetchall() async def fetchone(self, query: str, values: tuple = ()): - result = await self.conn.execute(self.rewrite_query(query), values) + result = await self.conn.execute( + self.rewrite_query(query), self.rewrite_values(values) + ) row = await result.fetchone() await result.close() return row async def execute(self, query: str, values: tuple = ()): - return await self.conn.execute(self.rewrite_query(query), values) + return await self.conn.execute( + self.rewrite_query(query), self.rewrite_values(values) + ) class Database(Compat): 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!

+
diff --git a/lnbits/extensions/events/views_api.py b/lnbits/extensions/events/views_api.py index 08651f5d..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 @@ -10,7 +11,6 @@ from lnbits.core.services import create_invoice from lnbits.core.views.api import api_payment from lnbits.decorators import WalletTypeInfo, get_key_type from lnbits.extensions.events.models import CreateEvent, CreateTicket -from loguru import logger from . import events_ext from .crud import ( @@ -79,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########## 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/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index b0b223ff..25dcf8c9 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -487,6 +487,17 @@ @click="copyText(lnurlValue, 'LNURL copied to clipboard!')" >Copy LNURL
+ {% raw %}{{ wsMessage }}{% endraw %} + {% raw %}{{ wsMessage }}{% endraw %}
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..c4edd3aa 100644 --- a/lnbits/extensions/lnurlp/migrations.py +++ b/lnbits/extensions/lnurlp/migrations.py @@ -8,7 +8,7 @@ async def m001_initial(db): id {db.serial_primary_key}, wallet TEXT NOT NULL, description TEXT NOT NULL, - amount INTEGER NOT NULL, + amount {db.big_int} NOT NULL, served_meta INTEGER NOT NULL, served_pr INTEGER NOT NULL ); @@ -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/migrations.py b/lnbits/extensions/satsdice/migrations.py index 61298241..82ab35ba 100644 --- a/lnbits/extensions/satsdice/migrations.py +++ b/lnbits/extensions/satsdice/migrations.py @@ -3,14 +3,14 @@ async def m001_initial(db): Creates an improved satsdice table and migrates the existing data. """ await db.execute( - """ + f""" CREATE TABLE satsdice.satsdice_pay ( id TEXT PRIMARY KEY, wallet TEXT, title TEXT, min_bet INTEGER, max_bet INTEGER, - amount INTEGER DEFAULT 0, + amount {db.big_int} DEFAULT 0, served_meta INTEGER NOT NULL, served_pr INTEGER NOT NULL, multiplier FLOAT, @@ -28,11 +28,11 @@ async def m002_initial(db): Creates an improved satsdice table and migrates the existing data. """ await db.execute( - """ + f""" CREATE TABLE satsdice.satsdice_withdraw ( id TEXT PRIMARY KEY, satsdice_pay TEXT, - value INTEGER DEFAULT 1, + value {db.big_int} DEFAULT 1, unique_hash TEXT UNIQUE, k1 TEXT, open_time INTEGER, @@ -47,11 +47,11 @@ async def m003_initial(db): Creates an improved satsdice table and migrates the existing data. """ await db.execute( - """ + f""" CREATE TABLE satsdice.satsdice_payment ( payment_hash TEXT PRIMARY KEY, satsdice_pay TEXT, - value INTEGER, + value {db.big_int}, paid BOOL DEFAULT FALSE, lost BOOL DEFAULT FALSE ); 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/config.json b/lnbits/extensions/satspay/config.json index beb0071c..fe9e3df4 100644 --- a/lnbits/extensions/satspay/config.json +++ b/lnbits/extensions/satspay/config.json @@ -2,7 +2,5 @@ "name": "SatsPay Server", "short_description": "Create onchain and LN charges", "icon": "payment", - "contributors": [ - "arcbtc" - ] + "contributors": ["arcbtc"] } diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py index 23d391b7..7e34f6f8 100644 --- a/lnbits/extensions/satspay/crud.py +++ b/lnbits/extensions/satspay/crud.py @@ -1,16 +1,16 @@ +import json from typing import List, Optional -import httpx +from loguru import logger from lnbits.core.services import create_invoice from lnbits.core.views.api import api_payment from lnbits.helpers import urlsafe_short_hash from ..watchonly.crud import get_config, get_fresh_address - -# from lnbits.db import open_ext_db from . import db -from .models import Charges, CreateCharge +from .helpers import fetch_onchain_balance +from .models import Charges, CreateCharge, SatsPayThemes ###############CHARGES########################## @@ -18,6 +18,10 @@ from .models import Charges, CreateCharge async def create_charge(user: str, data: CreateCharge) -> Charges: charge_id = urlsafe_short_hash() if data.onchainwallet: + config = await get_config(user) + data.extra = json.dumps( + {"mempool_endpoint": config.mempool_endpoint, "network": config.network} + ) onchain = await get_fresh_address(data.onchainwallet) onchainaddress = onchain.address else: @@ -48,9 +52,11 @@ async def create_charge(user: str, data: CreateCharge) -> Charges: completelinktext, time, amount, - balance + balance, + extra, + custom_css ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( charge_id, @@ -67,6 +73,8 @@ async def create_charge(user: str, data: CreateCharge) -> Charges: data.time, data.amount, 0, + data.extra, + data.custom_css, ), ) return await get_charge(charge_id) @@ -98,34 +106,118 @@ async def delete_charge(charge_id: str) -> None: await db.execute("DELETE FROM satspay.charges WHERE id = ?", (charge_id,)) -async def check_address_balance(charge_id: str) -> List[Charges]: +async def check_address_balance(charge_id: str) -> Optional[Charges]: charge = await get_charge(charge_id) + if not charge.paid: if charge.onchainaddress: - config = await get_charge_config(charge_id) try: - async with httpx.AsyncClient() as client: - r = await client.get( - config.mempool_endpoint - + "/api/address/" - + charge.onchainaddress - ) - respAmount = r.json()["chain_stats"]["funded_txo_sum"] - if respAmount > charge.balance: - await update_charge(charge_id=charge_id, balance=respAmount) - except Exception: - pass + respAmount = await fetch_onchain_balance(charge) + if respAmount > charge.balance: + await update_charge(charge_id=charge_id, balance=respAmount) + except Exception as e: + logger.warning(e) if charge.lnbitswallet: invoice_status = await api_payment(charge.payment_hash) if invoice_status["paid"]: return await update_charge(charge_id=charge_id, balance=charge.amount) - row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,)) - return Charges.from_row(row) if row else None + return await get_charge(charge_id) -async def get_charge_config(charge_id: str): - row = await db.fetchone( - """SELECT "user" FROM satspay.charges WHERE id = ?""", (charge_id,) +################## SETTINGS ################### + + +async def save_theme(data: SatsPayThemes, css_id: str = None): + # insert or update + if css_id: + await db.execute( + """ + UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ? + """, + (data.custom_css, data.title, css_id), + ) + else: + css_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO satspay.themes ( + css_id, + title, + user, + custom_css + ) + VALUES (?, ?, ?, ?) + """, + ( + css_id, + data.title, + data.user, + data.custom_css, + ), + ) + return await get_theme(css_id) + + +async def get_theme(css_id: str) -> SatsPayThemes: + row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,)) + return SatsPayThemes.from_row(row) if row else None + + +async def get_themes(user_id: str) -> List[SatsPayThemes]: + rows = await db.fetchall( + """SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "timestamp" DESC """, + (user_id,), ) return await get_config(row.user) + + +################## SETTINGS ################### + + +async def save_theme(data: SatsPayThemes, css_id: str = None): + # insert or update + if css_id: + await db.execute( + """ + UPDATE satspay.themes SET custom_css = ?, title = ? WHERE css_id = ? + """, + (data.custom_css, data.title, css_id), + ) + else: + css_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO satspay.themes ( + css_id, + title, + "user", + custom_css + ) + VALUES (?, ?, ?, ?) + """, + ( + css_id, + data.title, + data.user, + data.custom_css, + ), + ) + return await get_theme(css_id) + + +async def get_theme(css_id: str) -> SatsPayThemes: + row = await db.fetchone("SELECT * FROM satspay.themes WHERE css_id = ?", (css_id,)) + return SatsPayThemes.from_row(row) if row else None + + +async def get_themes(user_id: str) -> List[SatsPayThemes]: + rows = await db.fetchall( + """SELECT * FROM satspay.themes WHERE "user" = ? ORDER BY "title" DESC """, + (user_id,), + ) + return [SatsPayThemes.from_row(row) for row in rows] + + +async def delete_theme(theme_id: str) -> None: + await db.execute("DELETE FROM satspay.themes WHERE css_id = ?", (theme_id,)) diff --git a/lnbits/extensions/satspay/helpers.py b/lnbits/extensions/satspay/helpers.py index 2d15b557..60c5ba4a 100644 --- a/lnbits/extensions/satspay/helpers.py +++ b/lnbits/extensions/satspay/helpers.py @@ -1,8 +1,11 @@ +import httpx +from loguru import logger + from .models import Charges -def compact_charge(charge: Charges): - return { +def public_charge(charge: Charges): + c = { "id": charge.id, "description": charge.description, "onchainaddress": charge.onchainaddress, @@ -13,5 +16,40 @@ def compact_charge(charge: Charges): "balance": charge.balance, "paid": charge.paid, "timestamp": charge.timestamp, - "completelink": charge.completelink, # should be secret? + "time_elapsed": charge.time_elapsed, + "time_left": charge.time_left, + "paid": charge.paid, + "custom_css": charge.custom_css, } + + if charge.paid: + c["completelink"] = charge.completelink + c["completelinktext"] = charge.completelinktext + + return c + + +async def call_webhook(charge: Charges): + async with httpx.AsyncClient() as client: + try: + r = await client.post( + charge.webhook, + json=public_charge(charge), + timeout=40, + ) + return {"webhook_success": r.is_success, "webhook_message": r.reason_phrase} + except Exception as e: + logger.warning(f"Failed to call webhook for charge {charge.id}") + logger.warning(e) + return {"webhook_success": False, "webhook_message": str(e)} + + +async def fetch_onchain_balance(charge: Charges): + endpoint = ( + f"{charge.config.mempool_endpoint}/testnet" + if charge.config.network == "Testnet" + else charge.config.mempool_endpoint + ) + async with httpx.AsyncClient() as client: + r = await client.get(endpoint + "/api/address/" + charge.onchainaddress) + return r.json()["chain_stats"]["funded_txo_sum"] diff --git a/lnbits/extensions/satspay/migrations.py b/lnbits/extensions/satspay/migrations.py index 87446c80..e23bd413 100644 --- a/lnbits/extensions/satspay/migrations.py +++ b/lnbits/extensions/satspay/migrations.py @@ -4,7 +4,7 @@ async def m001_initial(db): """ await db.execute( - """ + f""" CREATE TABLE satspay.charges ( id TEXT NOT NULL PRIMARY KEY, "user" TEXT, @@ -18,11 +18,47 @@ async def m001_initial(db): completelink TEXT, completelinktext TEXT, time INTEGER, - amount INTEGER, - balance INTEGER DEFAULT 0, + amount {db.big_int}, + balance {db.big_int} DEFAULT 0, timestamp TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """ ); """ ) + + +async def m002_add_charge_extra_data(db): + """ + Add 'extra' column for storing various config about the charge (JSON format) + """ + await db.execute( + """ALTER TABLE satspay.charges + ADD COLUMN extra TEXT DEFAULT '{"mempool_endpoint": "https://mempool.space", "network": "Mainnet"}'; + """ + ) + + +async def m003_add_themes_table(db): + """ + Themes table + """ + + await db.execute( + """ + CREATE TABLE satspay.themes ( + css_id TEXT NOT NULL PRIMARY KEY, + "user" TEXT, + title TEXT, + custom_css TEXT + ); + """ + ) + + +async def m004_add_custom_css_to_charges(db): + """ + Add custom css option column to the 'charges' table + """ + + await db.execute("ALTER TABLE satspay.charges ADD COLUMN custom_css TEXT;") diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py index daf63f42..cfb3c7ac 100644 --- a/lnbits/extensions/satspay/models.py +++ b/lnbits/extensions/satspay/models.py @@ -1,3 +1,4 @@ +import json from datetime import datetime, timedelta from sqlite3 import Row from typing import Optional @@ -13,8 +14,17 @@ class CreateCharge(BaseModel): webhook: str = Query(None) completelink: str = Query(None) completelinktext: str = Query(None) + custom_css: Optional[str] time: int = Query(..., ge=1) amount: int = Query(..., ge=1) + extra: str = "{}" + + +class ChargeConfig(BaseModel): + mempool_endpoint: Optional[str] + network: Optional[str] + webhook_success: Optional[bool] = False + webhook_message: Optional[str] class Charges(BaseModel): @@ -28,6 +38,8 @@ class Charges(BaseModel): webhook: Optional[str] completelink: Optional[str] completelinktext: Optional[str] = "Back to Merchant" + extra: str = "{}" + custom_css: Optional[str] time: int amount: int balance: int @@ -54,3 +66,22 @@ class Charges(BaseModel): return True else: return False + + @property + def config(self) -> ChargeConfig: + charge_config = json.loads(self.extra) + return ChargeConfig(**charge_config) + + def must_call_webhook(self): + return self.webhook and self.paid and self.config.webhook_success == False + + +class SatsPayThemes(BaseModel): + css_id: str = Query(None) + title: str = Query(None) + custom_css: str = Query(None) + user: Optional[str] + + @classmethod + def from_row(cls, row: Row) -> "SatsPayThemes": + return cls(**dict(row)) diff --git a/lnbits/extensions/satspay/static/js/utils.js b/lnbits/extensions/satspay/static/js/utils.js index 9b4abbfc..2b1be8bd 100644 --- a/lnbits/extensions/satspay/static/js/utils.js +++ b/lnbits/extensions/satspay/static/js/utils.js @@ -14,18 +14,22 @@ const retryWithDelay = async function (fn, retryCount = 0) { } const mapCharge = (obj, oldObj = {}) => { - const charge = _.clone(obj) + const charge = {...oldObj, ...obj} charge.progress = obj.time_left < 0 ? 1 : 1 - obj.time_left / obj.time charge.time = minutesToTime(obj.time) charge.timeLeft = minutesToTime(obj.time_left) - charge.expanded = false charge.displayUrl = ['/satspay/', obj.id].join('') - charge.expanded = oldObj.expanded + charge.expanded = oldObj.expanded || false charge.pendingBalance = oldObj.pendingBalance || 0 return charge } +const mapCSS = (obj, oldObj = {}) => { + const theme = _.clone(obj) + return theme +} + const minutesToTime = min => min > 0 ? new Date(min * 1000).toISOString().substring(14, 19) : '' diff --git a/lnbits/extensions/satspay/tasks.py b/lnbits/extensions/satspay/tasks.py index 46c16bbc..ce54b44a 100644 --- a/lnbits/extensions/satspay/tasks.py +++ b/lnbits/extensions/satspay/tasks.py @@ -1,4 +1,5 @@ import asyncio +import json from loguru import logger @@ -7,7 +8,8 @@ from lnbits.extensions.satspay.crud import check_address_balance, get_charge from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener -# from .crud import get_ticket, set_ticket_paid +from .crud import update_charge +from .helpers import call_webhook async def wait_for_paid_invoices(): @@ -30,4 +32,9 @@ async def on_invoice_paid(payment: Payment) -> None: return await payment.set_pending(False) - await check_address_balance(charge_id=charge.id) + charge = await check_address_balance(charge_id=charge.id) + + if charge.must_call_webhook(): + resp = await call_webhook(charge) + extra = {**charge.config.dict(), **resp} + await update_charge(charge_id=charge.id, extra=json.dumps(extra)) diff --git a/lnbits/extensions/satspay/templates/satspay/_api_docs.html b/lnbits/extensions/satspay/templates/satspay/_api_docs.html index ed658735..6d5ae661 100644 --- a/lnbits/extensions/satspay/templates/satspay/_api_docs.html +++ b/lnbits/extensions/satspay/templates/satspay/_api_docs.html @@ -5,7 +5,13 @@ WatchOnly extension, we highly reccomend using a fresh extended public Key specifically for SatsPayServer!
- Created by, Ben ArcBen Arc, + motorina0


diff --git a/lnbits/extensions/satspay/templates/satspay/display.html b/lnbits/extensions/satspay/templates/satspay/display.html index 12288c80..8ea218bd 100644 --- a/lnbits/extensions/satspay/templates/satspay/display.html +++ b/lnbits/extensions/satspay/templates/satspay/display.html @@ -109,7 +109,7 @@ @@ -131,7 +131,7 @@ @@ -170,13 +170,17 @@ name="check" style="color: green; font-size: 21.4em" > - +
+
+ +
+
@@ -218,7 +222,7 @@
@@ -289,7 +297,17 @@
- +{% endblock %} {% block styles %} + + {% endblock %} {% block scripts %} @@ -303,7 +321,8 @@ data() { return { charge: JSON.parse('{{charge_data | tojson}}'), - mempool_endpoint: '{{mempool_endpoint}}', + mempoolEndpoint: '{{mempool_endpoint}}', + network: '{{network}}', pendingFunds: 0, ws: null, newProgress: 0.4, @@ -316,19 +335,19 @@ cancelListener: () => {} } }, + computed: { + mempoolHostname: function () { + let hostname = new URL(this.mempoolEndpoint).hostname + if (this.network === 'Testnet') { + hostname += '/testnet' + } + return hostname + } + }, methods: { - startPaymentNotifier() { - this.cancelListener() - if (!this.lnbitswallet) return - this.cancelListener = LNbits.events.onInvoicePaid( - this.wallet, - payment => { - this.checkInvoiceBalance() - } - ) - }, checkBalances: async function () { - if (this.charge.hasStaleBalance) return + if (!this.charge.payment_request && this.charge.hasOnchainStaleBalance) + return try { const {data} = await LNbits.api.request( 'GET', @@ -345,7 +364,7 @@ const { bitcoin: {addresses: addressesAPI} } = mempoolJS({ - hostname: new URL(this.mempool_endpoint).hostname + hostname: new URL(this.mempoolEndpoint).hostname }) try { @@ -353,7 +372,8 @@ address: this.charge.onchainaddress }) const newBalance = utxos.reduce((t, u) => t + u.value, 0) - this.charge.hasStaleBalance = this.charge.balance === newBalance + this.charge.hasOnchainStaleBalance = + this.charge.balance === newBalance this.pendingFunds = utxos .filter(u => !u.status.confirmed) @@ -388,10 +408,10 @@ const { bitcoin: {websocket} } = mempoolJS({ - hostname: new URL(this.mempool_endpoint).hostname + hostname: new URL(this.mempoolEndpoint).hostname }) - this.ws = new WebSocket('wss://mempool.space/api/v1/ws') + this.ws = new WebSocket(`wss://${this.mempoolHostname}/api/v1/ws`) this.ws.addEventListener('open', x => { if (this.charge.onchainaddress) { this.trackAddress(this.charge.onchainaddress) @@ -428,13 +448,14 @@ } }, created: async function () { - if (this.charge.lnbitswallet) this.payInvoice() + // Remove a user defined theme + if (this.charge.custom_css) { + document.body.setAttribute('data-theme', '') + } + if (this.charge.payment_request) this.payInvoice() else this.payOnchain() - await this.checkBalances() - // empty for onchain - this.wallet.inkey = '{{ wallet_inkey }}' - this.startPaymentNotifier() + await this.checkBalances() if (!this.charge.paid) { this.loopRefresh() diff --git a/lnbits/extensions/satspay/templates/satspay/index.html b/lnbits/extensions/satspay/templates/satspay/index.html index 396200cf..602b1a28 100644 --- a/lnbits/extensions/satspay/templates/satspay/index.html +++ b/lnbits/extensions/satspay/templates/satspay/index.html @@ -8,6 +8,26 @@ New charge + + New CSS Theme + + New CSS Theme + For security reason, custom css is only available to server + admins. @@ -203,9 +223,14 @@ :href="props.row.webhook" target="_blank" style="color: unset; text-decoration: none" - >{{props.row.webhook || props.row.webhook}}
{{props.row.webhook}}
+
+ + {{props.row.webhook_message }} + +
ID:
@@ -254,6 +279,63 @@ + + + +
+
+
Themes
+
+
+ + {% raw %} + + + + {% endraw %} + +
+
@@ -298,32 +380,6 @@ > - - - - - - - -
@@ -372,6 +428,52 @@ label="Wallet *" > + +
+
+ + + + + + + + + +
+
+ + + + + + + +
+ Update CSS theme + Save CSS theme + Cancel +
+
+
+
{% endblock %} {% block scripts %} {{ window_vars(user) }}