diff --git a/lnbits/extensions/amilk/README.md b/lnbits/extensions/amilk/README.md deleted file mode 100644 index 277294592..000000000 --- a/lnbits/extensions/amilk/README.md +++ /dev/null @@ -1,11 +0,0 @@ -

Example Extension

-

*tagline*

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

If your extension has API endpoints, include useful ones here

- -curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY" diff --git a/lnbits/extensions/amilk/__init__.py b/lnbits/extensions/amilk/__init__.py deleted file mode 100644 index 0cdd8727f..000000000 --- a/lnbits/extensions/amilk/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_amilk") - -amilk_ext: Blueprint = Blueprint( - "amilk", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/amilk/config.json b/lnbits/extensions/amilk/config.json deleted file mode 100644 index 09faf8af8..000000000 --- a/lnbits/extensions/amilk/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "AMilk", - "short_description": "Assistant Faucet Milker", - "icon": "room_service", - "contributors": ["arcbtc"] -} diff --git a/lnbits/extensions/amilk/crud.py b/lnbits/extensions/amilk/crud.py deleted file mode 100644 index 859d2fa84..000000000 --- a/lnbits/extensions/amilk/crud.py +++ /dev/null @@ -1,42 +0,0 @@ -from base64 import urlsafe_b64encode -from uuid import uuid4 -from typing import List, Optional, Union - -from . import db -from .models import AMilk - - -async def create_amilk(*, wallet_id: str, lnurl: str, atime: int, amount: int) -> AMilk: - amilk_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") - await db.execute( - """ - INSERT INTO amilk.amilks (id, wallet, lnurl, atime, amount) - VALUES (?, ?, ?, ?, ?) - """, - (amilk_id, wallet_id, lnurl, atime, amount), - ) - - amilk = await get_amilk(amilk_id) - assert amilk, "Newly created amilk_id couldn't be retrieved" - return amilk - - -async def get_amilk(amilk_id: str) -> Optional[AMilk]: - row = await db.fetchone("SELECT * FROM amilk.amilks WHERE id = ?", (amilk_id,)) - return AMilk(**row) if row else None - - -async def get_amilks(wallet_ids: Union[str, List[str]]) -> List[AMilk]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM amilk.amilks WHERE wallet IN ({q})", (*wallet_ids,) - ) - - return [AMilk(**row) for row in rows] - - -async def delete_amilk(amilk_id: str) -> None: - await db.execute("DELETE FROM amilk.amilks WHERE id = ?", (amilk_id,)) diff --git a/lnbits/extensions/amilk/migrations.py b/lnbits/extensions/amilk/migrations.py deleted file mode 100644 index 596a86335..000000000 --- a/lnbits/extensions/amilk/migrations.py +++ /dev/null @@ -1,15 +0,0 @@ -async def m001_initial(db): - """ - Initial amilks table. - """ - await db.execute( - """ - CREATE TABLE amilk.amilks ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - lnurl TEXT NOT NULL, - atime INTEGER NOT NULL, - amount INTEGER NOT NULL - ); - """ - ) diff --git a/lnbits/extensions/amilk/models.py b/lnbits/extensions/amilk/models.py deleted file mode 100644 index 647cc530e..000000000 --- a/lnbits/extensions/amilk/models.py +++ /dev/null @@ -1,9 +0,0 @@ -from pydantic import BaseModel - - -class AMilk(BaseModel): - id: str - wallet: str - lnurl: str - atime: int - amount: int diff --git a/lnbits/extensions/amilk/templates/amilk/_api_docs.html b/lnbits/extensions/amilk/templates/amilk/_api_docs.html deleted file mode 100644 index f1c27a1ba..000000000 --- a/lnbits/extensions/amilk/templates/amilk/_api_docs.html +++ /dev/null @@ -1,24 +0,0 @@ - - - -
Assistant Faucet Milker
-

- Milking faucets with software, known as "assmilking", seems at first to - be black-hat, although in fact there might be some unexplored use cases. - An LNURL withdraw gives someone the right to pull funds, which can be - done over time. An LNURL withdraw could be used outside of just faucets, - to provide money streaming and repeat payments.
Paste or scan an - LNURL withdraw, enter the amount for the AMilk to pull and the frequency - for it to be pulled.
- - Created by, Ben Arc -

-
-
-
diff --git a/lnbits/extensions/amilk/templates/amilk/index.html b/lnbits/extensions/amilk/templates/amilk/index.html deleted file mode 100644 index bb332e276..000000000 --- a/lnbits/extensions/amilk/templates/amilk/index.html +++ /dev/null @@ -1,250 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New AMilk - - - - - -
-
-
AMilks
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
- -
- - -
- LNbits Assistant Faucet Milker Extension -
-
- - - {% include "amilk/_api_docs.html" %} - -
-
- - - - - - - - - - Create amilk - Cancel - - - -
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/amilk/views.py b/lnbits/extensions/amilk/views.py deleted file mode 100644 index 2f61df77b..000000000 --- a/lnbits/extensions/amilk/views.py +++ /dev/null @@ -1,23 +0,0 @@ -from quart import g, abort, render_template -from http import HTTPStatus - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import amilk_ext -from .crud import get_amilk - - -@amilk_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(): - return await render_template("amilk/index.html", user=g.user) - - -@amilk_ext.route("/") -async def wall(amilk_id): - amilk = await get_amilk(amilk_id) - if not amilk: - abort(HTTPStatus.NOT_FOUND, "AMilk does not exist.") - - return await render_template("amilk/wall.html", amilk=amilk) diff --git a/lnbits/extensions/amilk/views_api.py b/lnbits/extensions/amilk/views_api.py deleted file mode 100644 index 4b8cad182..000000000 --- a/lnbits/extensions/amilk/views_api.py +++ /dev/null @@ -1,105 +0,0 @@ -import httpx -from quart import g, jsonify, request, abort -from http import HTTPStatus -from lnurl import LnurlWithdrawResponse, handle as handle_lnurl # type: ignore -from lnurl.exceptions import LnurlException # type: ignore -from time import sleep - -from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from lnbits.core.services import create_invoice, check_invoice_status - -from . import amilk_ext -from .crud import create_amilk, get_amilk, get_amilks, delete_amilk - - -@amilk_ext.route("/api/v1/amilk", methods=["GET"]) -@api_check_wallet_key("invoice") -async def api_amilks(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - return ( - jsonify([amilk._asdict() for amilk in await get_amilks(wallet_ids)]), - HTTPStatus.OK, - ) - - -@amilk_ext.route("/api/v1/amilk/milk/", methods=["GET"]) -async def api_amilkit(amilk_id): - milk = await get_amilk(amilk_id) - memo = milk.id - - try: - withdraw_res = handle_lnurl(milk.lnurl, response_class=LnurlWithdrawResponse) - except LnurlException: - abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") - - try: - payment_hash, payment_request = await create_invoice( - wallet_id=milk.wallet, - amount=withdraw_res.max_sats, - memo=memo, - extra={"tag": "amilk"}, - ) - except Exception as e: - return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - - r = httpx.get( - withdraw_res.callback.base, - params={ - **withdraw_res.callback.query_params, - **{"k1": withdraw_res.k1, "pr": payment_request}, - }, - ) - - if r.is_error: - abort(HTTPStatus.INTERNAL_SERVER_ERROR, "Could not process withdraw LNURL.") - - for i in range(10): - sleep(i) - invoice_status = await check_invoice_status(milk.wallet, payment_hash) - if invoice_status.paid: - return jsonify({"paid": True}), HTTPStatus.OK - else: - continue - - return jsonify({"paid": False}), HTTPStatus.OK - - -@amilk_ext.route("/api/v1/amilk", methods=["POST"]) -@api_check_wallet_key("invoice") -@api_validate_post_request( - schema={ - "lnurl": {"type": "string", "empty": False, "required": True}, - "atime": {"type": "integer", "min": 0, "required": True}, - "amount": {"type": "integer", "min": 0, "required": True}, - } -) -async def api_amilk_create(): - amilk = await create_amilk( - wallet_id=g.wallet.id, - lnurl=g.data["lnurl"], - atime=g.data["atime"], - amount=g.data["amount"], - ) - - return jsonify(amilk._asdict()), HTTPStatus.CREATED - - -@amilk_ext.route("/api/v1/amilk/", methods=["DELETE"]) -@api_check_wallet_key("invoice") -async def api_amilk_delete(amilk_id): - amilk = await get_amilk(amilk_id) - - if not amilk: - return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND - - if amilk.wallet != g.wallet.id: - return jsonify({"message": "Not your amilk."}), HTTPStatus.FORBIDDEN - - await delete_amilk(amilk_id) - - return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/bleskomat/README.md b/lnbits/extensions/bleskomat/README.md deleted file mode 100644 index 97c70700a..000000000 --- a/lnbits/extensions/bleskomat/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Bleskomat Extension for lnbits - -This extension allows you to connect a Bleskomat ATM to an lnbits wallet. It will work with both the [open-source DIY Bleskomat ATM project](https://github.com/samotari/bleskomat) as well as the [commercial Bleskomat ATM](https://www.bleskomat.com/). - - -## Connect Your Bleskomat ATM - -* Click the "Add Bleskomat" button on this page to begin. -* Choose a wallet. This will be the wallet that is used to pay satoshis to your ATM customers. -* Choose the fiat currency. This should match the fiat currency that your ATM accepts. -* Pick an exchange rate provider. This is the API that will be used to query the fiat to satoshi exchange rate at the time your customer attempts to withdraw their funds. -* Set your ATM's fee percentage. -* Click the "Done" button. -* Find the new Bleskomat in the list and then click the export icon to download a new configuration file for your ATM. -* Copy the configuration file ("bleskomat.conf") to your ATM's SD card. -* Restart Your Bleskomat ATM. It should automatically reload the configurations from the SD card. - - -## How Does It Work? - -Since the Bleskomat ATMs are designed to be offline, a cryptographic signing scheme is used to verify that the URL was generated by an authorized device. When one of your customers inserts fiat money into the device, a signed URL (lnurl-withdraw) is created and displayed as a QR code. Your customer scans the QR code with their lnurl-supporting mobile app, their mobile app communicates with the web API of lnbits to verify the signature, the fiat currency amount is converted to sats, the customer accepts the withdrawal, and finally lnbits will pay the customer from your lnbits wallet. diff --git a/lnbits/extensions/bleskomat/__init__.py b/lnbits/extensions/bleskomat/__init__.py deleted file mode 100644 index 42f9bb460..000000000 --- a/lnbits/extensions/bleskomat/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_bleskomat") - -bleskomat_ext: Blueprint = Blueprint( - "bleskomat", __name__, static_folder="static", template_folder="templates" -) - -from .lnurl_api import * # noqa -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/bleskomat/config.json b/lnbits/extensions/bleskomat/config.json deleted file mode 100644 index 99244df14..000000000 --- a/lnbits/extensions/bleskomat/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Bleskomat", - "short_description": "Connect a Bleskomat ATM to an lnbits", - "icon": "money", - "contributors": ["chill117"] -} diff --git a/lnbits/extensions/bleskomat/crud.py b/lnbits/extensions/bleskomat/crud.py deleted file mode 100644 index 1cc445769..000000000 --- a/lnbits/extensions/bleskomat/crud.py +++ /dev/null @@ -1,119 +0,0 @@ -import secrets -import time -from uuid import uuid4 -from typing import List, Optional, Union -from . import db -from .models import Bleskomat, BleskomatLnurl -from .helpers import generate_bleskomat_lnurl_hash - - -async def create_bleskomat( - *, - wallet_id: str, - name: str, - fiat_currency: str, - exchange_rate_provider: str, - fee: str, -) -> Bleskomat: - bleskomat_id = uuid4().hex - api_key_id = secrets.token_hex(8) - api_key_secret = secrets.token_hex(32) - api_key_encoding = "hex" - await db.execute( - """ - INSERT INTO bleskomat.bleskomats (id, wallet, api_key_id, api_key_secret, api_key_encoding, name, fiat_currency, exchange_rate_provider, fee) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - bleskomat_id, - wallet_id, - api_key_id, - api_key_secret, - api_key_encoding, - name, - fiat_currency, - exchange_rate_provider, - fee, - ), - ) - bleskomat = await get_bleskomat(bleskomat_id) - assert bleskomat, "Newly created bleskomat couldn't be retrieved" - return bleskomat - - -async def get_bleskomat(bleskomat_id: str) -> Optional[Bleskomat]: - row = await db.fetchone( - "SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,) - ) - return Bleskomat(**row) if row else None - - -async def get_bleskomat_by_api_key_id(api_key_id: str) -> Optional[Bleskomat]: - row = await db.fetchone( - "SELECT * FROM bleskomat.bleskomats WHERE api_key_id = ?", (api_key_id,) - ) - return Bleskomat(**row) if row else None - - -async def get_bleskomats(wallet_ids: Union[str, List[str]]) -> List[Bleskomat]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM bleskomat.bleskomats WHERE wallet IN ({q})", (*wallet_ids,) - ) - return [Bleskomat(**row) for row in rows] - - -async def update_bleskomat(bleskomat_id: str, **kwargs) -> Optional[Bleskomat]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE bleskomat.bleskomats SET {q} WHERE id = ?", - (*kwargs.values(), bleskomat_id), - ) - row = await db.fetchone( - "SELECT * FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,) - ) - return Bleskomat(**row) if row else None - - -async def delete_bleskomat(bleskomat_id: str) -> None: - await db.execute("DELETE FROM bleskomat.bleskomats WHERE id = ?", (bleskomat_id,)) - - -async def create_bleskomat_lnurl( - *, bleskomat: Bleskomat, secret: str, tag: str, params: str, uses: int = 1 -) -> BleskomatLnurl: - bleskomat_lnurl_id = uuid4().hex - hash = generate_bleskomat_lnurl_hash(secret) - now = int(time.time()) - await db.execute( - """ - INSERT INTO bleskomat.bleskomat_lnurls (id, bleskomat, wallet, hash, tag, params, api_key_id, initial_uses, remaining_uses, created_time, updated_time) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - bleskomat_lnurl_id, - bleskomat.id, - bleskomat.wallet, - hash, - tag, - params, - bleskomat.api_key_id, - uses, - uses, - now, - now, - ), - ) - bleskomat_lnurl = await get_bleskomat_lnurl(secret) - assert bleskomat_lnurl, "Newly created bleskomat LNURL couldn't be retrieved" - return bleskomat_lnurl - - -async def get_bleskomat_lnurl(secret: str) -> Optional[BleskomatLnurl]: - hash = generate_bleskomat_lnurl_hash(secret) - row = await db.fetchone( - "SELECT * FROM bleskomat.bleskomat_lnurls WHERE hash = ?", (hash,) - ) - return BleskomatLnurl(**row) if row else None diff --git a/lnbits/extensions/bleskomat/exchange_rates.py b/lnbits/extensions/bleskomat/exchange_rates.py deleted file mode 100644 index 928a28231..000000000 --- a/lnbits/extensions/bleskomat/exchange_rates.py +++ /dev/null @@ -1,79 +0,0 @@ -import httpx -import json -import os - -fiat_currencies = json.load( - open( - os.path.join( - os.path.dirname(os.path.realpath(__file__)), "fiat_currencies.json" - ), - "r", - ) -) - -exchange_rate_providers = { - "bitfinex": { - "name": "Bitfinex", - "domain": "bitfinex.com", - "api_url": "https://api.bitfinex.com/v1/pubticker/{from}{to}", - "getter": lambda data, replacements: data["last_price"], - }, - "bitstamp": { - "name": "Bitstamp", - "domain": "bitstamp.net", - "api_url": "https://www.bitstamp.net/api/v2/ticker/{from}{to}/", - "getter": lambda data, replacements: data["last"], - }, - "coinbase": { - "name": "Coinbase", - "domain": "coinbase.com", - "api_url": "https://api.coinbase.com/v2/exchange-rates?currency={FROM}", - "getter": lambda data, replacements: data["data"]["rates"][replacements["TO"]], - }, - "coinmate": { - "name": "CoinMate", - "domain": "coinmate.io", - "api_url": "https://coinmate.io/api/ticker?currencyPair={FROM}_{TO}", - "getter": lambda data, replacements: data["data"]["last"], - }, - "kraken": { - "name": "Kraken", - "domain": "kraken.com", - "api_url": "https://api.kraken.com/0/public/Ticker?pair=XBT{TO}", - "getter": lambda data, replacements: data["result"][ - "XXBTZ" + replacements["TO"] - ]["c"][0], - }, -} - -exchange_rate_providers_serializable = {} -for ref, exchange_rate_provider in exchange_rate_providers.items(): - exchange_rate_provider_serializable = {} - for key, value in exchange_rate_provider.items(): - if not callable(value): - exchange_rate_provider_serializable[key] = value - exchange_rate_providers_serializable[ref] = exchange_rate_provider_serializable - - -async def fetch_fiat_exchange_rate(currency: str, provider: str): - - replacements = { - "FROM": "BTC", - "from": "btc", - "TO": currency.upper(), - "to": currency.lower(), - } - - url = exchange_rate_providers[provider]["api_url"] - for key in replacements.keys(): - url = url.replace("{" + key + "}", replacements[key]) - - getter = exchange_rate_providers[provider]["getter"] - - async with httpx.AsyncClient() as client: - r = await client.get(url) - r.raise_for_status() - data = r.json() - rate = float(getter(data, replacements)) - - return rate diff --git a/lnbits/extensions/bleskomat/fiat_currencies.json b/lnbits/extensions/bleskomat/fiat_currencies.json deleted file mode 100644 index ff831f3ec..000000000 --- a/lnbits/extensions/bleskomat/fiat_currencies.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "AED": "United Arab Emirates Dirham", - "AFN": "Afghan Afghani", - "ALL": "Albanian Lek", - "AMD": "Armenian Dram", - "ANG": "Netherlands Antillean Gulden", - "AOA": "Angolan Kwanza", - "ARS": "Argentine Peso", - "AUD": "Australian Dollar", - "AWG": "Aruban Florin", - "AZN": "Azerbaijani Manat", - "BAM": "Bosnia and Herzegovina Convertible Mark", - "BBD": "Barbadian Dollar", - "BDT": "Bangladeshi Taka", - "BGN": "Bulgarian Lev", - "BHD": "Bahraini Dinar", - "BIF": "Burundian Franc", - "BMD": "Bermudian Dollar", - "BND": "Brunei Dollar", - "BOB": "Bolivian Boliviano", - "BRL": "Brazilian Real", - "BSD": "Bahamian Dollar", - "BTN": "Bhutanese Ngultrum", - "BWP": "Botswana Pula", - "BYN": "Belarusian Ruble", - "BYR": "Belarusian Ruble", - "BZD": "Belize Dollar", - "CAD": "Canadian Dollar", - "CDF": "Congolese Franc", - "CHF": "Swiss Franc", - "CLF": "Unidad de Fomento", - "CLP": "Chilean Peso", - "CNH": "Chinese Renminbi Yuan Offshore", - "CNY": "Chinese Renminbi Yuan", - "COP": "Colombian Peso", - "CRC": "Costa Rican Colón", - "CUC": "Cuban Convertible Peso", - "CVE": "Cape Verdean Escudo", - "CZK": "Czech Koruna", - "DJF": "Djiboutian Franc", - "DKK": "Danish Krone", - "DOP": "Dominican Peso", - "DZD": "Algerian Dinar", - "EGP": "Egyptian Pound", - "ERN": "Eritrean Nakfa", - "ETB": "Ethiopian Birr", - "EUR": "Euro", - "FJD": "Fijian Dollar", - "FKP": "Falkland Pound", - "GBP": "British Pound", - "GEL": "Georgian Lari", - "GGP": "Guernsey Pound", - "GHS": "Ghanaian Cedi", - "GIP": "Gibraltar Pound", - "GMD": "Gambian Dalasi", - "GNF": "Guinean Franc", - "GTQ": "Guatemalan Quetzal", - "GYD": "Guyanese Dollar", - "HKD": "Hong Kong Dollar", - "HNL": "Honduran Lempira", - "HRK": "Croatian Kuna", - "HTG": "Haitian Gourde", - "HUF": "Hungarian Forint", - "IDR": "Indonesian Rupiah", - "ILS": "Israeli New Sheqel", - "IMP": "Isle of Man Pound", - "INR": "Indian Rupee", - "IQD": "Iraqi Dinar", - "ISK": "Icelandic Króna", - "JEP": "Jersey Pound", - "JMD": "Jamaican Dollar", - "JOD": "Jordanian Dinar", - "JPY": "Japanese Yen", - "KES": "Kenyan Shilling", - "KGS": "Kyrgyzstani Som", - "KHR": "Cambodian Riel", - "KMF": "Comorian Franc", - "KRW": "South Korean Won", - "KWD": "Kuwaiti Dinar", - "KYD": "Cayman Islands Dollar", - "KZT": "Kazakhstani Tenge", - "LAK": "Lao Kip", - "LBP": "Lebanese Pound", - "LKR": "Sri Lankan Rupee", - "LRD": "Liberian Dollar", - "LSL": "Lesotho Loti", - "LYD": "Libyan Dinar", - "MAD": "Moroccan Dirham", - "MDL": "Moldovan Leu", - "MGA": "Malagasy Ariary", - "MKD": "Macedonian Denar", - "MMK": "Myanmar Kyat", - "MNT": "Mongolian Tögrög", - "MOP": "Macanese Pataca", - "MRO": "Mauritanian Ouguiya", - "MUR": "Mauritian Rupee", - "MVR": "Maldivian Rufiyaa", - "MWK": "Malawian Kwacha", - "MXN": "Mexican Peso", - "MYR": "Malaysian Ringgit", - "MZN": "Mozambican Metical", - "NAD": "Namibian Dollar", - "NGN": "Nigerian Naira", - "NIO": "Nicaraguan Córdoba", - "NOK": "Norwegian Krone", - "NPR": "Nepalese Rupee", - "NZD": "New Zealand Dollar", - "OMR": "Omani Rial", - "PAB": "Panamanian Balboa", - "PEN": "Peruvian Sol", - "PGK": "Papua New Guinean Kina", - "PHP": "Philippine Peso", - "PKR": "Pakistani Rupee", - "PLN": "Polish Złoty", - "PYG": "Paraguayan Guaraní", - "QAR": "Qatari Riyal", - "RON": "Romanian Leu", - "RSD": "Serbian Dinar", - "RUB": "Russian Ruble", - "RWF": "Rwandan Franc", - "SAR": "Saudi Riyal", - "SBD": "Solomon Islands Dollar", - "SCR": "Seychellois Rupee", - "SEK": "Swedish Krona", - "SGD": "Singapore Dollar", - "SHP": "Saint Helenian Pound", - "SLL": "Sierra Leonean Leone", - "SOS": "Somali Shilling", - "SRD": "Surinamese Dollar", - "SSP": "South Sudanese Pound", - "STD": "São Tomé and Príncipe Dobra", - "SVC": "Salvadoran Colón", - "SZL": "Swazi Lilangeni", - "THB": "Thai Baht", - "TJS": "Tajikistani Somoni", - "TMT": "Turkmenistani Manat", - "TND": "Tunisian Dinar", - "TOP": "Tongan Paʻanga", - "TRY": "Turkish Lira", - "TTD": "Trinidad and Tobago Dollar", - "TWD": "New Taiwan Dollar", - "TZS": "Tanzanian Shilling", - "UAH": "Ukrainian Hryvnia", - "UGX": "Ugandan Shilling", - "USD": "US Dollar", - "UYU": "Uruguayan Peso", - "UZS": "Uzbekistan Som", - "VEF": "Venezuelan Bolívar", - "VES": "Venezuelan Bolívar Soberano", - "VND": "Vietnamese Đồng", - "VUV": "Vanuatu Vatu", - "WST": "Samoan Tala", - "XAF": "Central African Cfa Franc", - "XAG": "Silver (Troy Ounce)", - "XAU": "Gold (Troy Ounce)", - "XCD": "East Caribbean Dollar", - "XDR": "Special Drawing Rights", - "XOF": "West African Cfa Franc", - "XPD": "Palladium", - "XPF": "Cfp Franc", - "XPT": "Platinum", - "YER": "Yemeni Rial", - "ZAR": "South African Rand", - "ZMW": "Zambian Kwacha", - "ZWL": "Zimbabwean Dollar" -} \ No newline at end of file diff --git a/lnbits/extensions/bleskomat/helpers.py b/lnbits/extensions/bleskomat/helpers.py deleted file mode 100644 index a3857b773..000000000 --- a/lnbits/extensions/bleskomat/helpers.py +++ /dev/null @@ -1,153 +0,0 @@ -import base64 -import hashlib -import hmac -from http import HTTPStatus -from binascii import unhexlify -from typing import Dict -from quart import url_for -import urllib - - -def generate_bleskomat_lnurl_hash(secret: str): - m = hashlib.sha256() - m.update(f"{secret}".encode()) - return m.hexdigest() - - -def generate_bleskomat_lnurl_signature( - payload: str, api_key_secret: str, api_key_encoding: str = "hex" -): - if api_key_encoding == "hex": - key = unhexlify(api_key_secret) - elif api_key_encoding == "base64": - key = base64.b64decode(api_key_secret) - else: - key = bytes(f"{api_key_secret}") - return hmac.new(key=key, msg=payload.encode(), digestmod=hashlib.sha256).hexdigest() - - -def generate_bleskomat_lnurl_secret(api_key_id: str, signature: str): - # The secret is not randomly generated by the server. - # Instead it is the hash of the API key ID and signature concatenated together. - m = hashlib.sha256() - m.update(f"{api_key_id}-{signature}".encode()) - return m.hexdigest() - - -def get_callback_url(): - return url_for("bleskomat.api_bleskomat_lnurl", _external=True) - - -def is_supported_lnurl_subprotocol(tag: str) -> bool: - return tag == "withdrawRequest" - - -class LnurlHttpError(Exception): - def __init__( - self, - message: str = "", - http_status: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR, - ): - self.message = message - self.http_status = http_status - super().__init__(self.message) - - -class LnurlValidationError(Exception): - pass - - -def prepare_lnurl_params(tag: str, query: Dict[str, str]): - params = {} - if not is_supported_lnurl_subprotocol(tag): - raise LnurlValidationError(f'Unsupported subprotocol: "{tag}"') - if tag == "withdrawRequest": - params["minWithdrawable"] = float(query["minWithdrawable"]) - params["maxWithdrawable"] = float(query["maxWithdrawable"]) - params["defaultDescription"] = query["defaultDescription"] - if not params["minWithdrawable"] > 0: - raise LnurlValidationError('"minWithdrawable" must be greater than zero') - if not params["maxWithdrawable"] >= params["minWithdrawable"]: - raise LnurlValidationError( - '"maxWithdrawable" must be greater than or equal to "minWithdrawable"' - ) - return params - - -encode_uri_component_safe_chars = ( - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.!~*'()" -) - - -def query_to_signing_payload(query: Dict[str, str]) -> str: - # Sort the query by key, then stringify it to create the payload. - sorted_keys = sorted(query.keys(), key=str.lower) - payload = [] - for key in sorted_keys: - if not key == "signature": - encoded_key = urllib.parse.quote(key, safe=encode_uri_component_safe_chars) - encoded_value = urllib.parse.quote( - query[key], safe=encode_uri_component_safe_chars - ) - payload.append(f"{encoded_key}={encoded_value}") - return "&".join(payload) - - -unshorten_rules = { - "query": {"n": "nonce", "s": "signature", "t": "tag"}, - "tags": { - "c": "channelRequest", - "l": "login", - "p": "payRequest", - "w": "withdrawRequest", - }, - "params": { - "channelRequest": {"pl": "localAmt", "pp": "pushAmt"}, - "login": {}, - "payRequest": {"pn": "minSendable", "px": "maxSendable", "pm": "metadata"}, - "withdrawRequest": { - "pn": "minWithdrawable", - "px": "maxWithdrawable", - "pd": "defaultDescription", - }, - }, -} - - -def unshorten_lnurl_query(query: Dict[str, str]) -> Dict[str, str]: - new_query = {} - rules = unshorten_rules - if "tag" in query: - tag = query["tag"] - elif "t" in query: - tag = query["t"] - else: - raise LnurlValidationError('Missing required query parameter: "tag"') - # Unshorten tag: - if tag in rules["tags"]: - long_tag = rules["tags"][tag] - new_query["tag"] = long_tag - tag = long_tag - if not tag in rules["params"]: - raise LnurlValidationError(f'Unknown tag: "{tag}"') - for key in query: - if key in rules["params"][tag]: - short_param_key = key - long_param_key = rules["params"][tag][short_param_key] - if short_param_key in query: - new_query[long_param_key] = query[short_param_key] - else: - new_query[long_param_key] = query[long_param_key] - elif key in rules["query"]: - # Unshorten general keys: - short_key = key - long_key = rules["query"][short_key] - if not long_key in new_query: - if short_key in query: - new_query[long_key] = query[short_key] - else: - new_query[long_key] = query[long_key] - else: - # Keep unknown key/value pairs unchanged: - new_query[key] = query[key] - return new_query diff --git a/lnbits/extensions/bleskomat/lnurl_api.py b/lnbits/extensions/bleskomat/lnurl_api.py deleted file mode 100644 index 086562d1c..000000000 --- a/lnbits/extensions/bleskomat/lnurl_api.py +++ /dev/null @@ -1,134 +0,0 @@ -import json -import math -from quart import jsonify, request -from http import HTTPStatus -import traceback - -from . import bleskomat_ext -from .crud import ( - create_bleskomat_lnurl, - get_bleskomat_by_api_key_id, - get_bleskomat_lnurl, -) - -from .exchange_rates import ( - fetch_fiat_exchange_rate, -) - -from .helpers import ( - generate_bleskomat_lnurl_signature, - generate_bleskomat_lnurl_secret, - LnurlHttpError, - LnurlValidationError, - prepare_lnurl_params, - query_to_signing_payload, - unshorten_lnurl_query, -) - - -# Handles signed URL from Bleskomat ATMs and "action" callback of auto-generated LNURLs. -@bleskomat_ext.route("/u", methods=["GET"]) -async def api_bleskomat_lnurl(): - try: - query = request.args.to_dict() - - # Unshorten query if "s" is used instead of "signature". - if "s" in query: - query = unshorten_lnurl_query(query) - - if "signature" in query: - - # Signature provided. - # Use signature to verify that the URL was generated by an authorized device. - # Later validate parameters, auto-generate LNURL, reply with LNURL response object. - signature = query["signature"] - - # The API key ID, nonce, and tag should be present in the query string. - for field in ["id", "nonce", "tag"]: - if not field in query: - raise LnurlHttpError( - f'Failed API key signature check: Missing "{field}"', - HTTPStatus.BAD_REQUEST, - ) - - # URL signing scheme is described here: - # https://github.com/chill117/lnurl-node#how-to-implement-url-signing-scheme - payload = query_to_signing_payload(query) - api_key_id = query["id"] - bleskomat = await get_bleskomat_by_api_key_id(api_key_id) - if not bleskomat: - raise LnurlHttpError("Unknown API key", HTTPStatus.BAD_REQUEST) - api_key_secret = bleskomat.api_key_secret - api_key_encoding = bleskomat.api_key_encoding - expected_signature = generate_bleskomat_lnurl_signature( - payload, api_key_secret, api_key_encoding - ) - if signature != expected_signature: - raise LnurlHttpError("Invalid API key signature", HTTPStatus.FORBIDDEN) - - # Signature is valid. - # In the case of signed URLs, the secret is deterministic based on the API key ID and signature. - secret = generate_bleskomat_lnurl_secret(api_key_id, signature) - lnurl = await get_bleskomat_lnurl(secret) - if not lnurl: - try: - tag = query["tag"] - params = prepare_lnurl_params(tag, query) - if "f" in query: - rate = await fetch_fiat_exchange_rate( - currency=query["f"], - provider=bleskomat.exchange_rate_provider, - ) - # Convert fee (%) to decimal: - fee = float(bleskomat.fee) / 100 - if tag == "withdrawRequest": - for key in ["minWithdrawable", "maxWithdrawable"]: - amount_sats = int( - math.floor((params[key] / rate) * 1e8) - ) - fee_sats = int(math.floor(amount_sats * fee)) - amount_sats_less_fee = amount_sats - fee_sats - # Convert to msats: - params[key] = int(amount_sats_less_fee * 1e3) - except LnurlValidationError as e: - raise LnurlHttpError(e.message, HTTPStatus.BAD_REQUEST) - # Create a new LNURL using the query parameters provided in the signed URL. - params = json.JSONEncoder().encode(params) - lnurl = await create_bleskomat_lnurl( - bleskomat=bleskomat, secret=secret, tag=tag, params=params, uses=1 - ) - - # Reply with LNURL response object. - return jsonify(lnurl.get_info_response_object(secret)), HTTPStatus.OK - - # No signature provided. - # Treat as "action" callback. - - if not "k1" in query: - raise LnurlHttpError("Missing secret", HTTPStatus.BAD_REQUEST) - - secret = query["k1"] - lnurl = await get_bleskomat_lnurl(secret) - if not lnurl: - raise LnurlHttpError("Invalid secret", HTTPStatus.BAD_REQUEST) - - if not lnurl.has_uses_remaining(): - raise LnurlHttpError( - "Maximum number of uses already reached", HTTPStatus.BAD_REQUEST - ) - - try: - await lnurl.execute_action(query) - except LnurlValidationError as e: - raise LnurlHttpError(str(e), HTTPStatus.BAD_REQUEST) - - except LnurlHttpError as e: - return jsonify({"status": "ERROR", "reason": str(e)}), e.http_status - except Exception: - traceback.print_exc() - return ( - jsonify({"status": "ERROR", "reason": "Unexpected error"}), - HTTPStatus.INTERNAL_SERVER_ERROR, - ) - - return jsonify({"status": "OK"}), HTTPStatus.OK diff --git a/lnbits/extensions/bleskomat/migrations.py b/lnbits/extensions/bleskomat/migrations.py deleted file mode 100644 index 84e886e56..000000000 --- a/lnbits/extensions/bleskomat/migrations.py +++ /dev/null @@ -1,37 +0,0 @@ -async def m001_initial(db): - - await db.execute( - """ - CREATE TABLE bleskomat.bleskomats ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - api_key_id TEXT NOT NULL, - api_key_secret TEXT NOT NULL, - api_key_encoding TEXT NOT NULL, - name TEXT NOT NULL, - fiat_currency TEXT NOT NULL, - exchange_rate_provider TEXT NOT NULL, - fee TEXT NOT NULL, - UNIQUE(api_key_id) - ); - """ - ) - - await db.execute( - """ - CREATE TABLE bleskomat.bleskomat_lnurls ( - id TEXT PRIMARY KEY, - bleskomat TEXT NOT NULL, - wallet TEXT NOT NULL, - hash TEXT NOT NULL, - tag TEXT NOT NULL, - params TEXT NOT NULL, - api_key_id TEXT NOT NULL, - initial_uses INTEGER DEFAULT 1, - remaining_uses INTEGER DEFAULT 0, - created_time INTEGER, - updated_time INTEGER, - UNIQUE(hash) - ); - """ - ) diff --git a/lnbits/extensions/bleskomat/models.py b/lnbits/extensions/bleskomat/models.py deleted file mode 100644 index 9b5e43223..000000000 --- a/lnbits/extensions/bleskomat/models.py +++ /dev/null @@ -1,112 +0,0 @@ -import json -import time -from typing import NamedTuple, Dict -from lnbits import bolt11 -from lnbits.core.services import pay_invoice -from . import db -from .helpers import get_callback_url, LnurlValidationError -from sqlite3 import Row -from pydantic import BaseModel - - -class Bleskomat(BaseModel): - id: str - wallet: str - api_key_id: str - api_key_secret: str - api_key_encoding: str - name: str - fiat_currency: str - exchange_rate_provider: str - fee: str - - -class BleskomatLnurl(BaseModel): - id: str - bleskomat: str - wallet: str - hash: str - tag: str - params: str - api_key_id: str - initial_uses: int - remaining_uses: int - created_time: int - updated_time: int - - def has_uses_remaining(self) -> bool: - # When initial uses is 0 then the LNURL has unlimited uses. - return self.initial_uses == 0 or self.remaining_uses > 0 - - def get_info_response_object(self, secret: str) -> Dict[str, str]: - tag = self.tag - params = json.loads(self.params) - response = {"tag": tag} - if tag == "withdrawRequest": - for key in ["minWithdrawable", "maxWithdrawable", "defaultDescription"]: - response[key] = params[key] - response["callback"] = get_callback_url() - response["k1"] = secret - return response - - def validate_action(self, query: Dict[str, str]) -> None: - tag = self.tag - params = json.loads(self.params) - # Perform tag-specific checks. - if tag == "withdrawRequest": - for field in ["pr"]: - if not field in query: - raise LnurlValidationError(f'Missing required parameter: "{field}"') - # Check the bolt11 invoice(s) provided. - pr = query["pr"] - if "," in pr: - raise LnurlValidationError("Multiple payment requests not supported") - try: - invoice = bolt11.decode(pr) - except ValueError: - raise LnurlValidationError( - 'Invalid parameter ("pr"): Lightning payment request expected' - ) - if invoice.amount_msat < params["minWithdrawable"]: - raise LnurlValidationError( - 'Amount in invoice must be greater than or equal to "minWithdrawable"' - ) - if invoice.amount_msat > params["maxWithdrawable"]: - raise LnurlValidationError( - 'Amount in invoice must be less than or equal to "maxWithdrawable"' - ) - else: - raise LnurlValidationError(f'Unknown subprotocol: "{tag}"') - - async def execute_action(self, query: Dict[str, str]): - self.validate_action(query) - used = False - async with db.connect() as conn: - if self.initial_uses > 0: - used = await self.use(conn) - if not used: - raise LnurlValidationError("Maximum number of uses already reached") - tag = self.tag - if tag == "withdrawRequest": - try: - payment_hash = await pay_invoice( - wallet_id=self.wallet, - payment_request=query["pr"], - ) - except Exception: - raise LnurlValidationError("Failed to pay invoice") - if not payment_hash: - raise LnurlValidationError("Failed to pay invoice") - - async def use(self, conn) -> bool: - now = int(time.time()) - result = await conn.execute( - """ - UPDATE bleskomat.bleskomat_lnurls - SET remaining_uses = remaining_uses - 1, updated_time = ? - WHERE id = ? - AND remaining_uses > 0 - """, - (now, self.id), - ) - return result.rowcount > 0 diff --git a/lnbits/extensions/bleskomat/static/js/index.js b/lnbits/extensions/bleskomat/static/js/index.js deleted file mode 100644 index fd166ff39..000000000 --- a/lnbits/extensions/bleskomat/static/js/index.js +++ /dev/null @@ -1,216 +0,0 @@ -/* global Vue, VueQrcode, _, Quasar, LOCALE, windowMixin, LNbits */ - -Vue.component(VueQrcode.name, VueQrcode) - -var mapBleskomat = function (obj) { - obj._data = _.clone(obj) - return obj -} - -var defaultValues = { - name: 'My Bleskomat', - fiat_currency: 'EUR', - exchange_rate_provider: 'coinbase', - fee: '0.00' -} - -new Vue({ - el: '#vue', - mixins: [windowMixin], - data: function () { - return { - checker: null, - bleskomats: [], - bleskomatsTable: { - columns: [ - { - name: 'api_key_id', - align: 'left', - label: 'API Key ID', - field: 'api_key_id' - }, - { - name: 'name', - align: 'left', - label: 'Name', - field: 'name' - }, - { - name: 'fiat_currency', - align: 'left', - label: 'Fiat Currency', - field: 'fiat_currency' - }, - { - name: 'exchange_rate_provider', - align: 'left', - label: 'Exchange Rate Provider', - field: 'exchange_rate_provider' - }, - { - name: 'fee', - align: 'left', - label: 'Fee (%)', - field: 'fee' - } - ], - pagination: { - rowsPerPage: 10 - } - }, - formDialog: { - show: false, - fiatCurrencies: _.keys(window.bleskomat_vars.fiat_currencies), - exchangeRateProviders: _.keys( - window.bleskomat_vars.exchange_rate_providers - ), - data: _.clone(defaultValues) - } - } - }, - computed: { - sortedBleskomats: function () { - return this.bleskomats.sort(function (a, b) { - // Sort by API Key ID alphabetically. - var apiKeyId_A = a.api_key_id.toLowerCase() - var apiKeyId_B = b.api_key_id.toLowerCase() - return apiKeyId_A < apiKeyId_B ? -1 : apiKeyId_A > apiKeyId_B ? 1 : 0 - }) - } - }, - methods: { - getBleskomats: function () { - var self = this - LNbits.api - .request( - 'GET', - '/bleskomat/api/v1/bleskomats?all_wallets', - this.g.user.wallets[0].adminkey - ) - .then(function (response) { - self.bleskomats = response.data.map(function (obj) { - return mapBleskomat(obj) - }) - }) - .catch(function (error) { - clearInterval(self.checker) - LNbits.utils.notifyApiError(error) - }) - }, - closeFormDialog: function () { - this.formDialog.data = _.clone(defaultValues) - }, - exportConfigFile: function (bleskomatId) { - var bleskomat = _.findWhere(this.bleskomats, {id: bleskomatId}) - var fieldToKey = { - api_key_id: 'apiKey.id', - api_key_secret: 'apiKey.key', - api_key_encoding: 'apiKey.encoding', - fiat_currency: 'fiatCurrency' - } - var lines = _.chain(bleskomat) - .map(function (value, field) { - var key = fieldToKey[field] || null - return key ? [key, value].join('=') : null - }) - .compact() - .value() - lines.push('callbackUrl=' + window.bleskomat_vars.callback_url) - lines.push('shorten=true') - var content = lines.join('\n') - var status = Quasar.utils.exportFile( - 'bleskomat.conf', - content, - 'text/plain' - ) - if (status !== true) { - Quasar.plugins.Notify.create({ - message: 'Browser denied file download...', - color: 'negative', - icon: null - }) - } - }, - openUpdateDialog: function (bleskomatId) { - var bleskomat = _.findWhere(this.bleskomats, {id: bleskomatId}) - this.formDialog.data = _.clone(bleskomat._data) - this.formDialog.show = true - }, - sendFormData: function () { - var wallet = _.findWhere(this.g.user.wallets, { - id: this.formDialog.data.wallet - }) - var data = _.omit(this.formDialog.data, 'wallet') - if (data.id) { - this.updateBleskomat(wallet, data) - } else { - this.createBleskomat(wallet, data) - } - }, - updateBleskomat: function (wallet, data) { - var self = this - LNbits.api - .request( - 'PUT', - '/bleskomat/api/v1/bleskomat/' + data.id, - wallet.adminkey, - _.pick(data, 'name', 'fiat_currency', 'exchange_rate_provider', 'fee') - ) - .then(function (response) { - self.bleskomats = _.reject(self.bleskomats, function (obj) { - return obj.id === data.id - }) - self.bleskomats.push(mapBleskomat(response.data)) - self.formDialog.show = false - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) - }) - }, - createBleskomat: function (wallet, data) { - var self = this - LNbits.api - .request('POST', '/bleskomat/api/v1/bleskomat', wallet.adminkey, data) - .then(function (response) { - self.bleskomats.push(mapBleskomat(response.data)) - self.formDialog.show = false - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) - }) - }, - deleteBleskomat: function (bleskomatId) { - var self = this - var bleskomat = _.findWhere(this.bleskomats, {id: bleskomatId}) - LNbits.utils - .confirmDialog( - 'Are you sure you want to delete "' + bleskomat.name + '"?' - ) - .onOk(function () { - LNbits.api - .request( - 'DELETE', - '/bleskomat/api/v1/bleskomat/' + bleskomatId, - _.findWhere(self.g.user.wallets, {id: bleskomat.wallet}).adminkey - ) - .then(function (response) { - self.bleskomats = _.reject(self.bleskomats, function (obj) { - return obj.id === bleskomatId - }) - }) - .catch(function (error) { - LNbits.utils.notifyApiError(error) - }) - }) - } - }, - created: function () { - if (this.g.user.wallets.length) { - var getBleskomats = this.getBleskomats - getBleskomats() - this.checker = setInterval(function () { - getBleskomats() - }, 20000) - } - } -}) diff --git a/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html b/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html deleted file mode 100644 index 210d534c2..000000000 --- a/lnbits/extensions/bleskomat/templates/bleskomat/_api_docs.html +++ /dev/null @@ -1,65 +0,0 @@ - - - -

- This extension allows you to connect a Bleskomat ATM to an lnbits - wallet. It will work with both the - open-source DIY Bleskomat ATM project - as well as the - commercial Bleskomat ATM. -

-
Connect Your Bleskomat ATM
-
-
    -
  1. Click the "Add Bleskomat" button on this page to begin.
  2. -
  3. - Choose a wallet. This will be the wallet that is used to pay - satoshis to your ATM customers. -
  4. -
  5. - Choose the fiat currency. This should match the fiat currency that - your ATM accepts. -
  6. -
  7. - Pick an exchange rate provider. This is the API that will be used to - query the fiat to satoshi exchange rate at the time your customer - attempts to withdraw their funds. -
  8. -
  9. Set your ATM's fee percentage.
  10. -
  11. Click the "Done" button.
  12. -
  13. - Find the new Bleskomat in the list and then click the export icon to - download a new configuration file for your ATM. -
  14. -
  15. - Copy the configuration file ("bleskomat.conf") to your ATM's SD - card. -
  16. -
  17. - Restart Your Bleskomat ATM. It should automatically reload the - configurations from the SD card. -
  18. -
-
-
How does it work?
-

- Since the Bleskomat ATMs are designed to be offline, a cryptographic - signing scheme is used to verify that the URL was generated by an - authorized device. When one of your customers inserts fiat money into - the device, a signed URL (lnurl-withdraw) is created and displayed as a - QR code. Your customer scans the QR code with their lnurl-supporting - mobile app, their mobile app communicates with the web API of lnbits to - verify the signature, the fiat currency amount is converted to sats, the - customer accepts the withdrawal, and finally lnbits will pay the - customer from your lnbits wallet. -

-
-
-
diff --git a/lnbits/extensions/bleskomat/templates/bleskomat/index.html b/lnbits/extensions/bleskomat/templates/bleskomat/index.html deleted file mode 100644 index 0cc512378..000000000 --- a/lnbits/extensions/bleskomat/templates/bleskomat/index.html +++ /dev/null @@ -1,180 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block scripts %} {{ window_vars(user) }} - - -{% endblock %} {% block page %} -
-
- - - Add Bleskomat - - - - - -
-
-
Bleskomats
-
-
- - {% raw %} - - - {% endraw %} - -
-
-
- -
- - -
- {{SITE_TITLE}} Bleskomat extension -
-
- - - {% include "bleskomat/_api_docs.html" %} - -
-
- - - - - - - - - - - - -
- Update Bleskomat - Add Bleskomat - Cancel -
-
-
-
-
-{% endblock %} diff --git a/lnbits/extensions/bleskomat/views.py b/lnbits/extensions/bleskomat/views.py deleted file mode 100644 index 4e57e9c2c..000000000 --- a/lnbits/extensions/bleskomat/views.py +++ /dev/null @@ -1,24 +0,0 @@ -from quart import g, render_template - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import bleskomat_ext - -from .exchange_rates import exchange_rate_providers_serializable, fiat_currencies -from .helpers import get_callback_url -from fastapi.templating import Jinja2Templates -from fastapi import Request - -templates = Jinja2Templates(directory="templates") - -@bleskomat_ext.get("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - bleskomat_vars = { - "callback_url": get_callback_url(), - "exchange_rate_providers": exchange_rate_providers_serializable, - "fiat_currencies": fiat_currencies, - } - return await templates.TemplateResponse("bleskomat/index.html", {"request": request, "user":g.user, "bleskomat_vars":bleskomat_vars}) - diff --git a/lnbits/extensions/bleskomat/views_api.py b/lnbits/extensions/bleskomat/views_api.py deleted file mode 100644 index a2aea00fd..000000000 --- a/lnbits/extensions/bleskomat/views_api.py +++ /dev/null @@ -1,109 +0,0 @@ -from typing import Union -from fastapi.param_functions import Query -from pydantic import BaseModel -from quart import g, jsonify, request -from http import HTTPStatus - -from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from . import bleskomat_ext -from .crud import ( - create_bleskomat, - get_bleskomat, - get_bleskomats, - update_bleskomat, - delete_bleskomat, -) - -from .exchange_rates import ( - exchange_rate_providers, - fetch_fiat_exchange_rate, - fiat_currencies, -) - - -@bleskomat_ext.get("/api/v1/bleskomats") -@api_check_wallet_key("admin") -async def api_bleskomats(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - return ( - [bleskomat._asdict() for bleskomat in await get_bleskomats(wallet_ids)], - HTTPStatus.OK, - ) - - -@bleskomat_ext.get("/api/v1/bleskomat/{bleskomat_id}") -@api_check_wallet_key("admin") -async def api_bleskomat_retrieve(bleskomat_id): - bleskomat = await get_bleskomat(bleskomat_id) - - if not bleskomat or bleskomat.wallet != g.wallet.id: - return ( - jsonify({"message": "Bleskomat configuration not found."}), - HTTPStatus.NOT_FOUND, - ) - - return jsonify(bleskomat._asdict()), HTTPStatus.OK - - -class CreateData(BaseModel): - name: str - fiat_currency: str = "EUR" # TODO: fix this - exchange_rate_provider: str = "bitfinex" - fee: Union[str, int, float] = Query(...) - -@bleskomat_ext.post("/api/v1/bleskomat") -@bleskomat_ext.put("/api/v1/bleskomat/{bleskomat_id}") -@api_check_wallet_key("admin") -async def api_bleskomat_create_or_update(data: CreateData, bleskomat_id=None): - try: - fiat_currency = data.fiat_currency - exchange_rate_provider = data.exchange_rate_provider - await fetch_fiat_exchange_rate( - currency=fiat_currency, provider=exchange_rate_provider - ) - except Exception as e: - print(e) - return ( - { - "message": f'Failed to fetch BTC/{fiat_currency} currency pair from "{exchange_rate_provider}"' - }, - HTTPStatus.INTERNAL_SERVER_ERROR, - ) - - if bleskomat_id: - bleskomat = await get_bleskomat(bleskomat_id) - if not bleskomat or bleskomat.wallet != g.wallet.id: - return ( - jsonify({"message": "Bleskomat configuration not found."}), - HTTPStatus.NOT_FOUND, - ) - bleskomat = await update_bleskomat(bleskomat_id, **data) - else: - bleskomat = await create_bleskomat(wallet_id=g.wallet.id, **data) - - return ( - bleskomat._asdict(), - HTTPStatus.OK if bleskomat_id else HTTPStatus.CREATED, - ) - - -@bleskomat_ext.delete("/api/v1/bleskomat/{bleskomat_id}") -@api_check_wallet_key("admin") -async def api_bleskomat_delete(bleskomat_id): - bleskomat = await get_bleskomat(bleskomat_id) - - if not bleskomat or bleskomat.wallet != g.wallet.id: - return ( - jsonify({"message": "Bleskomat configuration not found."}), - HTTPStatus.NOT_FOUND, - ) - - await delete_bleskomat(bleskomat_id) - - return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/captcha/README.md b/lnbits/extensions/captcha/README.md deleted file mode 100644 index 277294592..000000000 --- a/lnbits/extensions/captcha/README.md +++ /dev/null @@ -1,11 +0,0 @@ -

Example Extension

-

*tagline*

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

If your extension has API endpoints, include useful ones here

- -curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY" diff --git a/lnbits/extensions/captcha/__init__.py b/lnbits/extensions/captcha/__init__.py deleted file mode 100644 index f25dccce2..000000000 --- a/lnbits/extensions/captcha/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_captcha") - -captcha_ext: Blueprint = Blueprint( - "captcha", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/captcha/config.json b/lnbits/extensions/captcha/config.json deleted file mode 100644 index 4ef7c43fb..000000000 --- a/lnbits/extensions/captcha/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Captcha", - "short_description": "Create captcha to stop spam", - "icon": "block", - "contributors": ["pseudozach"] -} diff --git a/lnbits/extensions/captcha/crud.py b/lnbits/extensions/captcha/crud.py deleted file mode 100644 index 43a0374e1..000000000 --- a/lnbits/extensions/captcha/crud.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import List, Optional, Union - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import Captcha - - -async def create_captcha( - *, - wallet_id: str, - url: str, - memo: str, - description: Optional[str] = None, - amount: int = 0, - remembers: bool = True, -) -> Captcha: - captcha_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO captcha.captchas (id, wallet, url, memo, description, amount, remembers) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, - (captcha_id, wallet_id, url, memo, description, amount, int(remembers)), - ) - - captcha = await get_captcha(captcha_id) - assert captcha, "Newly created captcha couldn't be retrieved" - return captcha - - -async def get_captcha(captcha_id: str) -> Optional[Captcha]: - row = await db.fetchone( - "SELECT * FROM captcha.captchas WHERE id = ?", (captcha_id,) - ) - - return Captcha.from_row(row) if row else None - - -async def get_captchas(wallet_ids: Union[str, List[str]]) -> List[Captcha]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM captcha.captchas WHERE wallet IN ({q})", (*wallet_ids,) - ) - - return [Captcha.from_row(row) for row in rows] - - -async def delete_captcha(captcha_id: str) -> None: - await db.execute("DELETE FROM captcha.captchas WHERE id = ?", (captcha_id,)) diff --git a/lnbits/extensions/captcha/migrations.py b/lnbits/extensions/captcha/migrations.py deleted file mode 100644 index 744fc5067..000000000 --- a/lnbits/extensions/captcha/migrations.py +++ /dev/null @@ -1,63 +0,0 @@ -async def m001_initial(db): - """ - Initial captchas table. - """ - await db.execute( - """ - CREATE TABLE captcha.captchas ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - secret TEXT NOT NULL, - url TEXT NOT NULL, - memo TEXT NOT NULL, - amount INTEGER NOT NULL, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) - - -async def m002_redux(db): - """ - Creates an improved captchas table and migrates the existing data. - """ - await db.execute("ALTER TABLE captcha.captchas RENAME TO captchas_old") - await db.execute( - """ - CREATE TABLE captcha.captchas ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - url TEXT NOT NULL, - memo TEXT NOT NULL, - description TEXT NULL, - amount INTEGER DEFAULT 0, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """, - remembers INTEGER DEFAULT 0, - extras TEXT NULL - ); - """ - ) - - for row in [ - list(row) for row in await db.fetchall("SELECT * FROM captcha.captchas_old") - ]: - await db.execute( - """ - INSERT INTO captcha.captchas ( - id, - wallet, - url, - memo, - amount, - time - ) - VALUES (?, ?, ?, ?, ?, ?) - """, - (row[0], row[1], row[3], row[4], row[5], row[6]), - ) - - await db.execute("DROP TABLE captcha.captchas_old") diff --git a/lnbits/extensions/captcha/models.py b/lnbits/extensions/captcha/models.py deleted file mode 100644 index 2b98a91e4..000000000 --- a/lnbits/extensions/captcha/models.py +++ /dev/null @@ -1,24 +0,0 @@ -import json - -from sqlite3 import Row -from pydantic import BaseModel -from typing import Optional - - -class Captcha(BaseModel): - id: str - wallet: str - url: str - memo: str - description: str - amount: int - time: int - remembers: bool - extras: Optional[dict] - - @classmethod - def from_row(cls, row: Row) -> "Captcha": - data = dict(row) - data["remembers"] = bool(data["remembers"]) - data["extras"] = json.loads(data["extras"]) if data["extras"] else None - return cls(**data) diff --git a/lnbits/extensions/captcha/static/js/captcha.js b/lnbits/extensions/captcha/static/js/captcha.js deleted file mode 100644 index 1da24f572..000000000 --- a/lnbits/extensions/captcha/static/js/captcha.js +++ /dev/null @@ -1,82 +0,0 @@ -var ciframeLoaded = !1, - captchaStyleAdded = !1 - -function ccreateIframeElement(t = {}) { - const e = document.createElement('iframe') - // e.style.marginLeft = "25px", - ;(e.style.border = 'none'), - (e.style.width = '100%'), - (e.style.height = '100%'), - (e.scrolling = 'no'), - (e.id = 'captcha-iframe') - t.dest, t.amount, t.currency, t.label, t.opReturn - var captchaid = document - .getElementById('captchascript') - .getAttribute('data-captchaid') - var lnbhostsrc = document.getElementById('captchascript').getAttribute('src') - var lnbhost = lnbhostsrc.split('/captcha/static/js/captcha.js')[0] - return (e.src = lnbhost + '/captcha/' + captchaid), e -} -document.addEventListener('DOMContentLoaded', function () { - if (captchaStyleAdded) console.log('Captcha already added!') - else { - console.log('Adding captcha'), (captchaStyleAdded = !0) - var t = document.createElement('style') - t.innerHTML = - "\t/*Button*/\t\t.button-captcha-filled\t\t\t{\t\t\tdisplay: flex;\t\t\talign-items: center;\t\t\tjustify-content: center;\t\t\twidth: 120px;\t\t\tmin-width: 30px;\t\t\theight: 40px;\t\t\tline-height: 2.5;\t\t\ttext-align: center;\t\t\tcursor: pointer;\t\t\t/* Rectangle 2: */\t\t\tbackground: #FF7979;\t\t\tbox-shadow: 0 2px 4px 0 rgba(0,0,0,0.20);\t\t\tborder-radius: 20px;\t\t\t/* Sign up: */\t\t\tfont-family: 'Avenir-Heavy', Futura, Helvetica, Arial;\t\t\tfont-size: 16px;\t\t\tcolor: #FFFFFF;\t\t}\t\t.button-captcha-filled:hover\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #FF7979;\t\t\tbox-shadow: 0 0 4px 0 rgba(0,0,0,0.20);\t\t}\t\t.button-captcha-filled:active\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #FF7979;\t\t\t/*Move it down a little bit*/\t\t\tposition: relative;\t\t\ttop: 1px;\t\t}\t\t.button-captcha-filled-dark\t\t\t{\t\t\tdisplay: flex;\t\t\talign-items: center;\t\t\tjustify-content: center;\t\t\twidth: 120px;\t\t\tmin-width: 30px;\t\t\theight: 40px;\t\t\tline-height: 2.5;\t\t\ttext-align: center;\t\t\tcursor: pointer;\t\t\t/* Rectangle 2: */\t\t\tbackground: #161C38;\t\t\tbox-shadow: 0 0px 4px 0 rgba(0,0,0,0.20);\t\t\tborder-radius: 20px;\t\t\t/* Sign up: */\t\t\tfont-family: 'Avenir-Heavy', Futura, Helvetica, Arial;\t\t\tfont-size: 16px;\t\t\tcolor: #FFFFFF;\t\t}\t\t.button-captcha-filled-dark:hover\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #161C38;\t\t\tbox-shadow: 0 0px 4px 0 rgba(0,0,0,0.20);\t\t}\t\t.button-captcha-filled-dark:active\t\t{\t\t\tbackground:#FFFFFF;\t\t\tcolor: #161C38;\t\t\t/*Move it down a little bit*/\t\t\tposition: relative;\t\t\ttop: 1px;\t\t}\t\t.modal-captcha-container {\t\t position: fixed;\t\t z-index: 1000;\t\t text-align: left;/*Si no añado esto, a veces hereda el text-align:center del body, y entonces el popup queda movido a la derecha, por center + margin left que aplico*/\t\t left: 0;\t\t top: 0;\t\t width: 100%;\t\t height: 100%;\t\t background-color: rgba(0, 0, 0, 0.5);\t\t opacity: 0;\t\t visibility: hidden;\t\t transform: scale(1.1);\t\t transition: visibility 0s linear 0.25s, opacity 0.25s 0s, transform 0.25s;\t\t}\t\t.modal-captcha-content {\t\t position: absolute;\t\t top: 50%;\t\t left: 50%;\t\t transform: translate(-50%, -50%);\t\t background-color: white;\t\t width: 100%;\t\t height: 100%;\t\t border-radius: 0.5rem;\t\t /*Rounded shadowed borders*/\t\t\tbox-shadow: 2px 2px 4px 0 rgba(0,0,0,0.15);\t\t\tborder-radius: 5px;\t\t}\t\t.close-button-captcha {\t\t float: right;\t\t width: 1.5rem;\t\t line-height: 1.5rem;\t\t text-align: center;\t\t cursor: pointer;\t\t margin-right:20px;\t\t margin-top:10px;\t\t border-radius: 0.25rem;\t\t background-color: lightgray;\t\t}\t\t.close-button-captcha:hover {\t\t background-color: darkgray;\t\t}\t\t.show-modal-captcha {\t\t opacity: 1;\t\t visibility: visible;\t\t transform: scale(1.0);\t\t transition: visibility 0s linear 0s, opacity 0.25s 0s, transform 0.25s;\t\t}\t\t/* Mobile */\t\t@media screen and (min-device-width: 160px) and ( max-width: 1077px ) /*No tendria ni por que poner un min-device, porq abarca todo lo humano...*/\t\t{\t\t}" - var e = document.querySelector('script') - e.parentNode.insertBefore(t, e) - var i = document.getElementById('captchacheckbox'), - n = i.dataset, - o = 'true' === n.dark - var a = document.createElement('div') - ;(a.className += ' modal-captcha-container'), - (a.innerHTML = - '\t\t\t'), - document.getElementsByTagName('body')[0].appendChild(a) - var r = document.getElementsByClassName('modal-captcha-content').item(0) - document - .getElementsByClassName('close-button-captcha') - .item(0) - .addEventListener('click', d), - window.addEventListener('click', function (t) { - t.target === a && d() - }), - i.addEventListener('change', function () { - if (this.checked) { - // console.log("checkbox checked"); - if (0 == ciframeLoaded) { - // console.log("n: ", n); - var t = ccreateIframeElement(n) - r.appendChild(t), (ciframeLoaded = !0) - } - d() - } - }) - } - - function d() { - a.classList.toggle('show-modal-captcha') - } -}) - -function receiveMessage(event) { - if (event.data.includes('paymenthash')) { - // console.log("paymenthash received: ", event.data); - document.getElementById('captchapayhash').value = event.data.split('_')[1] - } - if (event.data.includes('removetheiframe')) { - if (event.data.includes('nok')) { - //invoice was NOT paid - // console.log("receiveMessage not paid") - document.getElementById('captchacheckbox').checked = false - } - ciframeLoaded = !1 - var element = document.getElementById('captcha-iframe') - document - .getElementsByClassName('modal-captcha-container')[0] - .classList.toggle('show-modal-captcha') - element.parentNode.removeChild(element) - } -} -window.addEventListener('message', receiveMessage, false) diff --git a/lnbits/extensions/captcha/templates/captcha/_api_docs.html b/lnbits/extensions/captcha/templates/captcha/_api_docs.html deleted file mode 100644 index dfe2f32f8..000000000 --- a/lnbits/extensions/captcha/templates/captcha/_api_docs.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - GET /captcha/api/v1/captchas -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<captcha_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}captcha/api/v1/captchas -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - POST /captcha/api/v1/captchas -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
- {"amount": <integer>, "description": <string>, "memo": - <string>, "remembers": <boolean>, "url": - <string>} -
- Returns 201 CREATED (application/json) -
- {"amount": <integer>, "description": <string>, "id": - <string>, "memo": <string>, "remembers": <boolean>, - "time": <int>, "url": <string>, "wallet": - <string>} -
Curl example
- curl -X POST {{ request.url_root }}captcha/api/v1/captchas -d - '{"url": <string>, "memo": <string>, "description": - <string>, "amount": <integer>, "remembers": - <boolean>}' -H "Content-type: application/json" -H "X-Api-Key: - {{ g.user.wallets[0].adminkey }}" - -
-
-
- - - - POST - /captcha/api/v1/captchas/<captcha_id>/invoice -
Body (application/json)
- {"amount": <integer>} -
- Returns 201 CREATED (application/json) -
- {"payment_hash": <string>, "payment_request": - <string>} -
Curl example
- curl -X POST {{ request.url_root - }}captcha/api/v1/captchas/<captcha_id>/invoice -d '{"amount": - <integer>}' -H "Content-type: application/json" - -
-
-
- - - - POST - /captcha/api/v1/captchas/<captcha_id>/check_invoice -
Body (application/json)
- {"payment_hash": <string>} -
- Returns 200 OK (application/json) -
- {"paid": false}
- {"paid": true, "url": <string>, "remembers": - <boolean>} -
Curl example
- curl -X POST {{ request.url_root - }}captcha/api/v1/captchas/<captcha_id>/check_invoice -d - '{"payment_hash": <string>}' -H "Content-type: application/json" - -
-
-
- - - - DELETE - /captcha/api/v1/captchas/<captcha_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.url_root - }}captcha/api/v1/captchas/<captcha_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" - -
-
-
-
diff --git a/lnbits/extensions/captcha/templates/captcha/display.html b/lnbits/extensions/captcha/templates/captcha/display.html deleted file mode 100644 index a96cae058..000000000 --- a/lnbits/extensions/captcha/templates/captcha/display.html +++ /dev/null @@ -1,178 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
{{ captcha.memo }}
- {% if captcha.description %} -

{{ captcha.description }}

- {% endif %} -
- - - - - -
- - - - - -
- Copy invoice - Cancel -
-
-
-
- -

- Captcha accepted. You are probably human.
- -

- -
-
-
-
-
-{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/captcha/templates/captcha/index.html b/lnbits/extensions/captcha/templates/captcha/index.html deleted file mode 100644 index 45318f080..000000000 --- a/lnbits/extensions/captcha/templates/captcha/index.html +++ /dev/null @@ -1,427 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New captcha - - - - - -
-
-
Captchas
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
- -
- - -
- {{SITE_TITLE}} captcha extension -
-
- - - {% include "captcha/_api_docs.html" %} - -
-
- - - - - - - - - - - - - - - - - Remember payments - A succesful payment will be registered in the browser's - storage, so the user doesn't need to pay again to prove they are - human. - - - -
- Create captcha - Cancel -
-
-
-
- - - - {% raw %} - - - - {{ qrCodeDialog.data.snippet }} - -

- Copy the snippet above and paste into your website/form. The checkbox - can be in checked state only after user pays. -

-
-

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

- {% endraw %} -
- Copy Snippet - - Close -
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/captcha/views.py b/lnbits/extensions/captcha/views.py deleted file mode 100644 index eb2bce0d4..000000000 --- a/lnbits/extensions/captcha/views.py +++ /dev/null @@ -1,24 +0,0 @@ -from quart import g, abort, render_template -from http import HTTPStatus - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import captcha_ext -from .crud import get_captcha -from fastapi.templating import Jinja2Templates -from fastapi import Request -templates = Jinja2Templates(directory="templates") - -@captcha_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(): - return await render_template("captcha/index.html", user=g.user) - - -@captcha_ext.route("/{captcha_id}") -async def display(request: Request, captcha_id): - captcha = await get_captcha(captcha_id) or abort( - HTTPStatus.NOT_FOUND, "captcha does not exist." - ) - return await templates.TemplateResponse("captcha/display.html", {"request": request, "captcha": captcha}) diff --git a/lnbits/extensions/captcha/views_api.py b/lnbits/extensions/captcha/views_api.py deleted file mode 100644 index 39d5da693..000000000 --- a/lnbits/extensions/captcha/views_api.py +++ /dev/null @@ -1,121 +0,0 @@ -from quart import g, jsonify, request -from http import HTTPStatus - -from lnbits.core.crud import get_user, get_wallet -from lnbits.core.services import create_invoice, check_invoice_status -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from . import captcha_ext -from .crud import create_captcha, get_captcha, get_captchas, delete_captcha - - -@captcha_ext.get("/api/v1/captchas") -@api_check_wallet_key("invoice") -async def api_captchas(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - return ( - jsonify([captcha._asdict() for captcha in await get_captchas(wallet_ids)]), - HTTPStatus.OK, - ) - - -@captcha_ext.post("/api/v1/captchas") -@api_check_wallet_key("invoice") -@api_validate_post_request( - schema={ - "url": {"type": "string", "empty": False, "required": True}, - "memo": {"type": "string", "empty": False, "required": True}, - "description": { - "type": "string", - "empty": True, - "nullable": True, - "required": False, - }, - "amount": {"type": "integer", "min": 0, "required": True}, - "remembers": {"type": "boolean", "required": True}, - } -) -async def api_captcha_create(): - captcha = await create_captcha(wallet_id=g.wallet.id, **g.data) - return jsonify(captcha._asdict()), HTTPStatus.CREATED - - -@captcha_ext.delete("/api/v1/captchas/{captcha_id}") -@api_check_wallet_key("invoice") -async def api_captcha_delete(captcha_id): - captcha = await get_captcha(captcha_id) - - if not captcha: - return jsonify({"message": "captcha does not exist."}), HTTPStatus.NOT_FOUND - - if captcha.wallet != g.wallet.id: - return jsonify({"message": "Not your captcha."}), HTTPStatus.FORBIDDEN - - await delete_captcha(captcha_id) - - return "", HTTPStatus.NO_CONTENT - - -@captcha_ext.post("/api/v1/captchas/{captcha_id}/invoice") -@api_validate_post_request( - schema={"amount": {"type": "integer", "min": 1, "required": True}} -) -async def api_captcha_create_invoice(captcha_id): - captcha = await get_captcha(captcha_id) - - if g.data["amount"] < captcha.amount: - return ( - jsonify({"message": f"Minimum amount is {captcha.amount} sat."}), - HTTPStatus.BAD_REQUEST, - ) - - try: - amount = ( - g.data["amount"] if g.data["amount"] > captcha.amount else captcha.amount - ) - payment_hash, payment_request = await create_invoice( - wallet_id=captcha.wallet, - amount=amount, - memo=f"{captcha.memo}", - extra={"tag": "captcha"}, - ) - except Exception as e: - return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - - return ( - jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), - HTTPStatus.CREATED, - ) - - -@captcha_ext.post("/api/v1/captchas/{captcha_id}/check_invoice") -@api_validate_post_request( - schema={"payment_hash": {"type": "string", "empty": False, "required": True}} -) -async def api_paywal_check_invoice(captcha_id): - captcha = await get_captcha(captcha_id) - - if not captcha: - return {"message": "captcha does not exist."}, HTTPStatus.NOT_FOUND - - try: - status = await check_invoice_status(captcha.wallet, g.data["payment_hash"]) - is_paid = not status.pending - except Exception: - return {"paid": False}, HTTPStatus.OK - - if is_paid: - wallet = await get_wallet(captcha.wallet) - payment = await wallet.get_payment(g.data["payment_hash"]) - await payment.set_pending(False) - - return ( - {"paid": True, "url": captcha.url, "remembers": captcha.remembers}, - HTTPStatus.OK, - ) - - return {"paid": False}, HTTPStatus.OK diff --git a/lnbits/extensions/copilot/README.md b/lnbits/extensions/copilot/README.md deleted file mode 100644 index 323aeddc0..000000000 --- a/lnbits/extensions/copilot/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# StreamerCopilot - -Tool to help streamers accept sats for tips diff --git a/lnbits/extensions/copilot/__init__.py b/lnbits/extensions/copilot/__init__.py deleted file mode 100644 index b255282b2..000000000 --- a/lnbits/extensions/copilot/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_copilot") - -copilot_ext: Blueprint = Blueprint( - "copilot", __name__, static_folder="static", template_folder="templates" -) - -from .views_api import * # noqa -from .views import * # noqa -from .lnurl import * # noqa -from .tasks import register_listeners - -from lnbits.tasks import record_async - -copilot_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/copilot/config.json b/lnbits/extensions/copilot/config.json deleted file mode 100644 index e17389e7b..000000000 --- a/lnbits/extensions/copilot/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "StreamerCopilot", - "short_description": "Video tips/animations/webhooks", - "icon": "face", - "contributors": [ - "arcbtc" - ] -} diff --git a/lnbits/extensions/copilot/crud.py b/lnbits/extensions/copilot/crud.py deleted file mode 100644 index d083675e2..000000000 --- a/lnbits/extensions/copilot/crud.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import List, Optional, Union - -# from lnbits.db import open_ext_db -from . import db -from .models import Copilots - -from lnbits.helpers import urlsafe_short_hash - -from quart import jsonify - - -###############COPILOTS########################## - - -async def create_copilot( - title: str, - user: str, - lnurl_toggle: Optional[int] = 0, - wallet: Optional[str] = None, - animation1: Optional[str] = None, - animation2: Optional[str] = None, - animation3: Optional[str] = None, - animation1threshold: Optional[int] = None, - animation2threshold: Optional[int] = None, - animation3threshold: Optional[int] = None, - animation1webhook: Optional[str] = None, - animation2webhook: Optional[str] = None, - animation3webhook: Optional[str] = None, - lnurl_title: Optional[str] = None, - show_message: Optional[int] = 0, - show_ack: Optional[int] = 0, - show_price: Optional[str] = None, - amount_made: Optional[int] = None, -) -> Copilots: - copilot_id = urlsafe_short_hash() - - await db.execute( - """ - INSERT INTO copilot.copilots ( - id, - "user", - lnurl_toggle, - wallet, - title, - animation1, - animation2, - animation3, - animation1threshold, - animation2threshold, - animation3threshold, - animation1webhook, - animation2webhook, - animation3webhook, - lnurl_title, - show_message, - show_ack, - show_price, - amount_made - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - copilot_id, - user, - int(lnurl_toggle), - wallet, - title, - animation1, - animation2, - animation3, - animation1threshold, - animation2threshold, - animation3threshold, - animation1webhook, - animation2webhook, - animation3webhook, - lnurl_title, - int(show_message), - int(show_ack), - show_price, - 0, - ), - ) - return await get_copilot(copilot_id) - - -async def update_copilot(copilot_id: str, **kwargs) -> Optional[Copilots]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE copilot.copilots SET {q} WHERE id = ?", (*kwargs.values(), copilot_id) - ) - row = await db.fetchone("SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,)) - return Copilots.from_row(row) if row else None - - -async def get_copilot(copilot_id: str) -> Copilots: - row = await db.fetchone("SELECT * FROM copilot.copilots WHERE id = ?", (copilot_id,)) - return Copilots.from_row(row) if row else None - - -async def get_copilots(user: str) -> List[Copilots]: - rows = await db.fetchall("""SELECT * FROM copilot.copilots WHERE "user" = ?""", (user,)) - return [Copilots.from_row(row) for row in rows] - - -async def delete_copilot(copilot_id: str) -> None: - await db.execute("DELETE FROM copilot.copilots WHERE id = ?", (copilot_id,)) diff --git a/lnbits/extensions/copilot/lnurl.py b/lnbits/extensions/copilot/lnurl.py deleted file mode 100644 index 0a10e29bc..000000000 --- a/lnbits/extensions/copilot/lnurl.py +++ /dev/null @@ -1,86 +0,0 @@ -import json -import hashlib -import math -from quart import jsonify, url_for, request -from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore -from lnurl.types import LnurlPayMetadata -from lnbits.core.services import create_invoice - -from . import copilot_ext -from .crud import get_copilot - - -@copilot_ext.route("/lnurl/", methods=["GET"]) -async def lnurl_response(cp_id): - cp = await get_copilot(cp_id) - if not cp: - return jsonify({"status": "ERROR", "reason": "Copilot not found."}) - - resp = LnurlPayResponse( - callback=url_for("copilot.lnurl_callback", cp_id=cp_id, _external=True), - min_sendable=10000, - max_sendable=50000000, - metadata=LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]])), - ) - - params = resp.dict() - if cp.show_message: - params["commentAllowed"] = 300 - - return jsonify(params) - - -@copilot_ext.route("/lnurl/cb/", methods=["GET"]) -async def lnurl_callback(cp_id): - cp = await get_copilot(cp_id) - if not cp: - return jsonify({"status": "ERROR", "reason": "Copilot not found."}) - - amount_received = int(request.args.get("amount")) - - if amount_received < 10000: - return ( - jsonify( - LnurlErrorResponse( - reason=f"Amount {round(amount_received / 1000)} is smaller than minimum 10 sats." - ).dict() - ), - ) - elif amount_received / 1000 > 10000000: - return ( - jsonify( - LnurlErrorResponse( - reason=f"Amount {round(amount_received / 1000)} is greater than maximum 50000." - ).dict() - ), - ) - comment = "" - if request.args.get("comment"): - comment = request.args.get("comment") - if len(comment or "") > 300: - return jsonify( - LnurlErrorResponse( - reason=f"Got a comment with {len(comment)} characters, but can only accept 300" - ).dict() - ) - if len(comment) < 1: - comment = "none" - - payment_hash, payment_request = await create_invoice( - wallet_id=cp.wallet, - amount=int(amount_received / 1000), - memo=cp.lnurl_title, - description_hash=hashlib.sha256( - ( - LnurlPayMetadata(json.dumps([["text/plain", str(cp.lnurl_title)]])) - ).encode("utf-8") - ).digest(), - extra={"tag": "copilot", "copilot": cp.id, "comment": comment}, - ) - resp = LnurlPayActionResponse( - pr=payment_request, - success_action=None, - disposable=False, - routes=[], - ) - return jsonify(resp.dict()) diff --git a/lnbits/extensions/copilot/migrations.py b/lnbits/extensions/copilot/migrations.py deleted file mode 100644 index c1fbfc0dc..000000000 --- a/lnbits/extensions/copilot/migrations.py +++ /dev/null @@ -1,76 +0,0 @@ -async def m001_initial(db): - """ - Initial copilot table. - """ - - await db.execute( - f""" - CREATE TABLE copilot.copilots ( - id TEXT NOT NULL PRIMARY KEY, - "user" TEXT, - title TEXT, - lnurl_toggle INTEGER, - wallet TEXT, - animation1 TEXT, - animation2 TEXT, - animation3 TEXT, - animation1threshold INTEGER, - animation2threshold INTEGER, - animation3threshold INTEGER, - animation1webhook TEXT, - animation2webhook TEXT, - animation3webhook TEXT, - lnurl_title TEXT, - show_message INTEGER, - show_ack INTEGER, - show_price INTEGER, - amount_made INTEGER, - fullscreen_cam INTEGER, - iframe_url TEXT, - timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} - ); - """ - ) - -async def m002_fix_data_types(db): - """ - Fix data types. - """ - - if(db.type != "SQLITE"): - await db.execute("ALTER TABLE copilot.copilots ALTER COLUMN show_price TYPE TEXT;") - - # If needed, migration for SQLite (RENAME not working properly) - # - # await db.execute( - # f""" - # CREATE TABLE copilot.new_copilots ( - # id TEXT NOT NULL PRIMARY KEY, - # "user" TEXT, - # title TEXT, - # lnurl_toggle INTEGER, - # wallet TEXT, - # animation1 TEXT, - # animation2 TEXT, - # animation3 TEXT, - # animation1threshold INTEGER, - # animation2threshold INTEGER, - # animation3threshold INTEGER, - # animation1webhook TEXT, - # animation2webhook TEXT, - # animation3webhook TEXT, - # lnurl_title TEXT, - # show_message INTEGER, - # show_ack INTEGER, - # show_price TEXT, - # amount_made INTEGER, - # fullscreen_cam INTEGER, - # iframe_url TEXT, - # timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} - # ); - # """ - # ) - # - # await db.execute("INSERT INTO copilot.new_copilots SELECT * FROM copilot.copilots;") - # await db.execute("DROP TABLE IF EXISTS copilot.copilots;") - # await db.execute("ALTER TABLE copilot.new_copilots RENAME TO copilot.copilots;") diff --git a/lnbits/extensions/copilot/models.py b/lnbits/extensions/copilot/models.py deleted file mode 100644 index 7eabaf9b4..000000000 --- a/lnbits/extensions/copilot/models.py +++ /dev/null @@ -1,42 +0,0 @@ -from sqlite3 import Row -from typing import NamedTuple -import time -from quart import url_for -from lnurl import Lnurl, encode as lnurl_encode # type: ignore -from lnurl.types import LnurlPayMetadata # type: ignore -from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore -from sqlite3 import Row -from pydantic import BaseModel - -class Copilots(BaseModel): - id: str - user: str - title: str - lnurl_toggle: int - wallet: str - animation1: str - animation2: str - animation3: str - animation1threshold: int - animation2threshold: int - animation3threshold: int - animation1webhook: str - animation2webhook: str - animation3webhook: str - lnurl_title: str - show_message: int - show_ack: int - show_price: int - amount_made: int - timestamp: int - fullscreen_cam: int - iframe_url: str - - @classmethod - def from_row(cls, row: Row) -> "Copilots": - return cls(**dict(row)) - - @property - def lnurl(self) -> Lnurl: - url = url_for("copilot.lnurl_response", cp_id=self.id, _external=True) - return lnurl_encode(url) diff --git a/lnbits/extensions/copilot/static/bitcoin.gif b/lnbits/extensions/copilot/static/bitcoin.gif deleted file mode 100644 index ef8c2ecd5..000000000 Binary files a/lnbits/extensions/copilot/static/bitcoin.gif and /dev/null differ diff --git a/lnbits/extensions/copilot/static/confetti.gif b/lnbits/extensions/copilot/static/confetti.gif deleted file mode 100644 index a3fec9712..000000000 Binary files a/lnbits/extensions/copilot/static/confetti.gif and /dev/null differ diff --git a/lnbits/extensions/copilot/static/face.gif b/lnbits/extensions/copilot/static/face.gif deleted file mode 100644 index 3e70d779c..000000000 Binary files a/lnbits/extensions/copilot/static/face.gif and /dev/null differ diff --git a/lnbits/extensions/copilot/static/lnurl.png b/lnbits/extensions/copilot/static/lnurl.png deleted file mode 100644 index ad2c97155..000000000 Binary files a/lnbits/extensions/copilot/static/lnurl.png and /dev/null differ diff --git a/lnbits/extensions/copilot/static/martijn.gif b/lnbits/extensions/copilot/static/martijn.gif deleted file mode 100644 index e410677dc..000000000 Binary files a/lnbits/extensions/copilot/static/martijn.gif and /dev/null differ diff --git a/lnbits/extensions/copilot/static/rick.gif b/lnbits/extensions/copilot/static/rick.gif deleted file mode 100644 index c36c7e197..000000000 Binary files a/lnbits/extensions/copilot/static/rick.gif and /dev/null differ diff --git a/lnbits/extensions/copilot/static/rocket.gif b/lnbits/extensions/copilot/static/rocket.gif deleted file mode 100644 index 6f19597d0..000000000 Binary files a/lnbits/extensions/copilot/static/rocket.gif and /dev/null differ diff --git a/lnbits/extensions/copilot/tasks.py b/lnbits/extensions/copilot/tasks.py deleted file mode 100644 index ff291e9ac..000000000 --- a/lnbits/extensions/copilot/tasks.py +++ /dev/null @@ -1,88 +0,0 @@ -import trio # type: ignore -import json -import httpx -from quart import g, jsonify, url_for, websocket -from http import HTTPStatus - -from lnbits.core import db as core_db -from lnbits.core.models import Payment -from lnbits.tasks import register_invoice_listener - -from .crud import get_copilot -from .views import updater -import shortuuid - - -async def register_listeners(): - invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) - register_invoice_listener(invoice_paid_chan_send) - await wait_for_paid_invoices(invoice_paid_chan_recv) - - -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: - await on_invoice_paid(payment) - - -async def on_invoice_paid(payment: Payment) -> None: - webhook = None - data = None - if "copilot" != payment.extra.get("tag"): - # not an copilot invoice - return - - if payment.extra.get("wh_status"): - # this webhook has already been sent - return - - copilot = await get_copilot(payment.extra.get("copilot", -1)) - - if not copilot: - return ( - jsonify({"message": "Copilot link link does not exist."}), - HTTPStatus.NOT_FOUND, - ) - if copilot.animation1threshold: - if int(payment.amount / 1000) >= copilot.animation1threshold: - data = copilot.animation1 - webhook = copilot.animation1webhook - if copilot.animation2threshold: - if int(payment.amount / 1000) >= copilot.animation2threshold: - data = copilot.animation2 - webhook = copilot.animation1webhook - if copilot.animation3threshold: - if int(payment.amount / 1000) >= copilot.animation3threshold: - data = copilot.animation3 - webhook = copilot.animation1webhook - if webhook: - async with httpx.AsyncClient() as client: - try: - r = await client.post( - webhook, - json={ - "payment_hash": payment.payment_hash, - "payment_request": payment.bolt11, - "amount": payment.amount, - "comment": payment.extra.get("comment"), - }, - timeout=40, - ) - await mark_webhook_sent(payment, r.status_code) - except (httpx.ConnectError, httpx.RequestError): - await mark_webhook_sent(payment, -1) - if payment.extra.get("comment"): - await updater(copilot.id, data, payment.extra.get("comment")) - else: - await updater(copilot.id, data, "none") - - -async def mark_webhook_sent(payment: Payment, status: int) -> None: - payment.extra["wh_status"] = status - - await core_db.execute( - """ - UPDATE apipayments SET extra = ? - WHERE hash = ? - """, - (json.dumps(payment.extra), payment.payment_hash), - ) diff --git a/lnbits/extensions/copilot/templates/copilot/_api_docs.html b/lnbits/extensions/copilot/templates/copilot/_api_docs.html deleted file mode 100644 index d6289be9c..000000000 --- a/lnbits/extensions/copilot/templates/copilot/_api_docs.html +++ /dev/null @@ -1,172 +0,0 @@ - - -

- StreamerCopilot: get tips via static QR (lnurl-pay) and show an - animation
- - Created by, Ben Arc -

-
- - - - - POST /copilot/api/v1/copilot -
Headers
- {"X-Api-Key": <admin_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<copilot_object>, ...] -
Curl example
- curl -X POST {{ request.url_root }}api/v1/copilot -d '{"title": - <string>, "animation": <string>, - "show_message":<string>, "amount": <integer>, - "lnurl_title": <string>}' -H "Content-type: application/json" - -H "X-Api-Key: {{g.user.wallets[0].adminkey }}" - -
-
-
- - - - PUT - /copilot/api/v1/copilot/<copilot_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<copilot_object>, ...] -
Curl example
- curl -X POST {{ request.url_root - }}api/v1/copilot/<copilot_id> -d '{"title": <string>, - "animation": <string>, "show_message":<string>, - "amount": <integer>, "lnurl_title": <string>}' -H - "Content-type: application/json" -H "X-Api-Key: - {{g.user.wallets[0].adminkey }}" - -
-
-
- - - - - GET - /copilot/api/v1/copilot/<copilot_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<copilot_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/copilot/<copilot_id> - -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - GET /copilot/api/v1/copilots -
Headers
- {"X-Api-Key": <invoice_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<copilot_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/copilots -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - DELETE - /copilot/api/v1/copilot/<copilot_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.url_root - }}api/v1/copilot/<copilot_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" - -
-
-
- - - - GET - /api/v1/copilot/ws/<copilot_id>/<comment>/<data> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 200
- -
Curl example
- curl -X GET {{ request.url_root }}/api/v1/copilot/ws/<string, - copilot_id>/<string, comment>/<string, gif name> -H - "X-Api-Key: {{ g.user.wallets[0].adminkey }}" - -
-
-
-
-
diff --git a/lnbits/extensions/copilot/templates/copilot/compose.html b/lnbits/extensions/copilot/templates/copilot/compose.html deleted file mode 100644 index 33bffda32..000000000 --- a/lnbits/extensions/copilot/templates/copilot/compose.html +++ /dev/null @@ -1,289 +0,0 @@ -{% extends "public.html" %} {% block page %} - - - - -
-
- -
- {% raw %}{{ copilot.lnurl_title }}{% endraw %} -
-
-
- -

- {% raw %}{{ price }}{% endraw %} -

-

- Powered by LNbits/StreamerCopilot -

-
-{% endblock %} {% block scripts %} - - - -{% endblock %} diff --git a/lnbits/extensions/copilot/templates/copilot/index.html b/lnbits/extensions/copilot/templates/copilot/index.html deleted file mode 100644 index 12d7058a8..000000000 --- a/lnbits/extensions/copilot/templates/copilot/index.html +++ /dev/null @@ -1,658 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - {% raw %} - New copilot instance - - - - - - -
-
-
Copilots
-
- -
- - - - Export to CSV -
-
- - - - - {% endraw %} - -
-
-
- -
- - -
- {{SITE_TITLE}} StreamCopilot Extension -
-
- - - {% include "copilot/_api_docs.html" %} - -
-
- - - - -
- -
- -
- - - - - - - -
-
- -
- -
- - -
-
- - -
-
-
-
-
- - - - -
-
- -
- -
- - -
-
- - -
-
-
-
-
- - - - -
-
- -
- -
- - -
-
- - -
-
-
-
-
- - - -
- -
- -
-
-
- -
-
-
- Update Copilot - Create Copilot - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - - - -{% endblock %} diff --git a/lnbits/extensions/copilot/templates/copilot/panel.html b/lnbits/extensions/copilot/templates/copilot/panel.html deleted file mode 100644 index 904ab104b..000000000 --- a/lnbits/extensions/copilot/templates/copilot/panel.html +++ /dev/null @@ -1,157 +0,0 @@ -{% extends "public.html" %} {% block page %} -
- -
-
-
- -
-
-
-
- Title: {% raw %} {{ copilot.title }} {% endraw %} -
-
- -
-
-
- - -
-
-
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
- -
-
-
-
-
-
- -{% endblock %} {% block scripts %} - - -{% endblock %} diff --git a/lnbits/extensions/copilot/views.py b/lnbits/extensions/copilot/views.py deleted file mode 100644 index 7f5e5d955..000000000 --- a/lnbits/extensions/copilot/views.py +++ /dev/null @@ -1,62 +0,0 @@ -from quart import g, abort, render_template, jsonify, websocket -from http import HTTPStatus -import httpx -from collections import defaultdict -from lnbits.decorators import check_user_exists, validate_uuids -from . import copilot_ext -from .crud import get_copilot -from quart import g, abort, render_template, jsonify, websocket -from functools import wraps -import trio -import shortuuid -from . import copilot_ext -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@copilot_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("copilot/index.html", {"request": request, "user":g.user}) - -@copilot_ext.route("/cp/") -async def compose(request: Request): - return await templates.TemplateResponse("copilot/compose.html", {"request": request}) - -@copilot_ext.route("/pn/") -async def panel(request: Request): - return await templates.TemplateResponse("copilot/panel.html", {"request": request}) - - -##################WEBSOCKET ROUTES######################## - -# socket_relay is a list where the control panel or -# lnurl endpoints can leave a message for the compose window - -connected_websockets = defaultdict(set) - - -@copilot_ext.websocket("/ws/{id}/") -async def wss(id): - copilot = await get_copilot(id) - if not copilot: - return "", HTTPStatus.FORBIDDEN - global connected_websockets - send_channel, receive_channel = trio.open_memory_channel(0) - connected_websockets[id].add(send_channel) - try: - while True: - data = await receive_channel.receive() - await websocket.send(data) - finally: - connected_websockets[id].remove(send_channel) - - -async def updater(copilot_id, data, comment): - copilot = await get_copilot(copilot_id) - if not copilot: - return - for queue in connected_websockets[copilot_id]: - await queue.send(f"{data + '-' + comment}") diff --git a/lnbits/extensions/copilot/views_api.py b/lnbits/extensions/copilot/views_api.py deleted file mode 100644 index 774820384..000000000 --- a/lnbits/extensions/copilot/views_api.py +++ /dev/null @@ -1,104 +0,0 @@ -import hashlib -from quart import g, jsonify, url_for, websocket -from http import HTTPStatus -import httpx - -from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from .views import updater - -from . import copilot_ext - -from lnbits.extensions.copilot import copilot_ext -from .crud import ( - create_copilot, - update_copilot, - get_copilot, - get_copilots, - delete_copilot, -) - -#######################COPILOT########################## - -class CreateData(BaseModel): - title: str - lnurl_toggle: Optional[int] - wallet: Optional[str] - animation1: Optional[str] - animation2: Optional[str] - animation3: Optional[str] - animation1threshold: Optional[int] - animation2threshold: Optional[int] - animation2threshold: Optional[int] - animation1webhook: Optional[str] - animation2webhook: Optional[str] - animation3webhook: Optional[str] - lnurl_title: Optional[str] - show_message: Optional[int] - show_ack: Optional[int] - show_price: Optional[str] - -@copilot_ext.post("/api/v1/copilot") -@copilot_ext.put("/api/v1/copilot/{copilot_id}") -@api_check_wallet_key("admin") -async def api_copilot_create_or_update(data: CreateData,copilot_id=None): - if not copilot_id: - copilot = await create_copilot(user=g.wallet.user, **data) - return jsonify(copilot._asdict()), HTTPStatus.CREATED - else: - copilot = await update_copilot(copilot_id=copilot_id, **data) - return jsonify(copilot._asdict()), HTTPStatus.OK - - -@copilot_ext.get("/api/v1/copilot") -@api_check_wallet_key("invoice") -async def api_copilots_retrieve(): - try: - return ( - [{**copilot._asdict()} for copilot in await get_copilots(g.wallet.user)], - HTTPStatus.OK, - ) - except: - return "" - - -@copilot_ext.get("/api/v1/copilot/{copilot_id}") -@api_check_wallet_key("invoice") -async def api_copilot_retrieve(copilot_id): - copilot = await get_copilot(copilot_id) - if not copilot: - return {"message": "copilot does not exist"}, HTTPStatus.NOT_FOUND - if not copilot.lnurl_toggle: - return ( - {**copilot._asdict()}, - HTTPStatus.OK, - ) - return ( - {**copilot._asdict(), **{"lnurl": copilot.lnurl}}, - HTTPStatus.OK, - ) - - -@copilot_ext.delete("/api/v1/copilot/{copilot_id}") -@api_check_wallet_key("admin") -async def api_copilot_delete(copilot_id): - copilot = await get_copilot(copilot_id) - - if not copilot: - return {"message": "Wallet link does not exist."}, HTTPStatus.NOT_FOUND - - await delete_copilot(copilot_id) - - return "", HTTPStatus.NO_CONTENT - - -@copilot_ext.get("/api/v1/copilot/ws/{copilot_id}/{comment}/{data}") -async def api_copilot_ws_relay(copilot_id, comment, data): - copilot = await get_copilot(copilot_id) - if not copilot: - return {"message": "copilot does not exist"}, HTTPStatus.NOT_FOUND - try: - await updater(copilot_id, data, comment) - except: - return "", HTTPStatus.FORBIDDEN - return "", HTTPStatus.OK diff --git a/lnbits/extensions/diagonalley/README.md b/lnbits/extensions/diagonalley/README.md deleted file mode 100644 index 6ba653e79..000000000 --- a/lnbits/extensions/diagonalley/README.md +++ /dev/null @@ -1,10 +0,0 @@ -

Diagon Alley

-

A movable market stand

-Make a list of products to sell, point the list to an indexer (or many), stack sats. -Diagon Alley is a movable market stand, for anon transactions. You then give permission for an indexer to list those products. Delivery addresses are sent through the Lightning Network. - - - -

API endpoints

- -curl -X GET http://YOUR-TOR-ADDRESS diff --git a/lnbits/extensions/diagonalley/__init__.py b/lnbits/extensions/diagonalley/__init__.py deleted file mode 100644 index ac907f5c7..000000000 --- a/lnbits/extensions/diagonalley/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from quart import Blueprint - - -diagonalley_ext: Blueprint = Blueprint( - "diagonalley", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/diagonalley/config.json.example b/lnbits/extensions/diagonalley/config.json.example deleted file mode 100644 index 057d0f234..000000000 --- a/lnbits/extensions/diagonalley/config.json.example +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Diagon Alley", - "short_description": "Movable anonymous market stand", - "icon": "add_shopping_cart", - "contributors": ["benarc"] -} diff --git a/lnbits/extensions/diagonalley/crud.py b/lnbits/extensions/diagonalley/crud.py deleted file mode 100644 index 971cd449d..000000000 --- a/lnbits/extensions/diagonalley/crud.py +++ /dev/null @@ -1,308 +0,0 @@ -from base64 import urlsafe_b64encode -from uuid import uuid4 -from typing import List, Optional, Union -import httpx -from lnbits.db import open_ext_db -from lnbits.settings import WALLET -from .models import Products, Orders, Indexers -import re - -regex = re.compile( - r"^(?:http|ftp)s?://" # http:// or https:// - r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|" - r"localhost|" - r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" - r"(?::\d+)?" - r"(?:/?|[/?]\S+)$", - re.IGNORECASE, -) - -###Products - - -def create_diagonalleys_product( - *, - wallet_id: str, - product: str, - categories: str, - description: str, - image: str, - price: int, - quantity: int, -) -> Products: - with open_ext_db("diagonalley") as db: - product_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") - db.execute( - """ - INSERT INTO diagonalley.products (id, wallet, product, categories, description, image, price, quantity) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - product_id, - wallet_id, - product, - categories, - description, - image, - price, - quantity, - ), - ) - - return get_diagonalleys_product(product_id) - - -def update_diagonalleys_product(product_id: str, **kwargs) -> Optional[Indexers]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - - with open_ext_db("diagonalley") as db: - db.execute( - f"UPDATE diagonalley.products SET {q} WHERE id = ?", - (*kwargs.values(), product_id), - ) - row = db.fetchone( - "SELECT * FROM diagonalley.products WHERE id = ?", (product_id,) - ) - - return get_diagonalleys_indexer(product_id) - - -def get_diagonalleys_product(product_id: str) -> Optional[Products]: - with open_ext_db("diagonalley") as db: - row = db.fetchone( - "SELECT * FROM diagonalley.products WHERE id = ?", (product_id,) - ) - - return Products(**row) if row else None - - -def get_diagonalleys_products(wallet_ids: Union[str, List[str]]) -> List[Products]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - with open_ext_db("diagonalley") as db: - q = ",".join(["?"] * len(wallet_ids)) - rows = db.fetchall( - f"SELECT * FROM diagonalley.products WHERE wallet IN ({q})", (*wallet_ids,) - ) - - return [Products(**row) for row in rows] - - -def delete_diagonalleys_product(product_id: str) -> None: - with open_ext_db("diagonalley") as db: - db.execute("DELETE FROM diagonalley.products WHERE id = ?", (product_id,)) - - -###Indexers - - -def create_diagonalleys_indexer( - wallet_id: str, - shopname: str, - indexeraddress: str, - shippingzone1: str, - shippingzone2: str, - zone1cost: int, - zone2cost: int, - email: str, -) -> Indexers: - with open_ext_db("diagonalley") as db: - indexer_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") - db.execute( - """ - INSERT INTO diagonalley.indexers (id, wallet, shopname, indexeraddress, online, rating, shippingzone1, shippingzone2, zone1cost, zone2cost, email) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - indexer_id, - wallet_id, - shopname, - indexeraddress, - False, - 0, - shippingzone1, - shippingzone2, - zone1cost, - zone2cost, - email, - ), - ) - return get_diagonalleys_indexer(indexer_id) - - -def update_diagonalleys_indexer(indexer_id: str, **kwargs) -> Optional[Indexers]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - - with open_ext_db("diagonalley") as db: - db.execute( - f"UPDATE diagonalley.indexers SET {q} WHERE id = ?", - (*kwargs.values(), indexer_id), - ) - row = db.fetchone( - "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,) - ) - - return get_diagonalleys_indexer(indexer_id) - - -def get_diagonalleys_indexer(indexer_id: str) -> Optional[Indexers]: - with open_ext_db("diagonalley") as db: - roww = db.fetchone( - "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,) - ) - try: - x = httpx.get(roww["indexeraddress"] + "/" + roww["ratingkey"]) - if x.status_code == 200: - print(x) - print("poo") - with open_ext_db("diagonalley") as db: - db.execute( - "UPDATE diagonalley.indexers SET online = ? WHERE id = ?", - ( - True, - indexer_id, - ), - ) - else: - with open_ext_db("diagonalley") as db: - db.execute( - "UPDATE diagonalley.indexers SET online = ? WHERE id = ?", - ( - False, - indexer_id, - ), - ) - except: - print("An exception occurred") - with open_ext_db("diagonalley") as db: - row = db.fetchone( - "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,) - ) - return Indexers(**row) if row else None - - -def get_diagonalleys_indexers(wallet_ids: Union[str, List[str]]) -> List[Indexers]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - with open_ext_db("diagonalley") as db: - q = ",".join(["?"] * len(wallet_ids)) - rows = db.fetchall( - f"SELECT * FROM diagonalley.indexers WHERE wallet IN ({q})", (*wallet_ids,) - ) - - for r in rows: - try: - x = httpx.get(r["indexeraddress"] + "/" + r["ratingkey"]) - if x.status_code == 200: - with open_ext_db("diagonalley") as db: - db.execute( - "UPDATE diagonalley.indexers SET online = ? WHERE id = ?", - ( - True, - r["id"], - ), - ) - else: - with open_ext_db("diagonalley") as db: - db.execute( - "UPDATE diagonalley.indexers SET online = ? WHERE id = ?", - ( - False, - r["id"], - ), - ) - except: - print("An exception occurred") - with open_ext_db("diagonalley") as db: - q = ",".join(["?"] * len(wallet_ids)) - rows = db.fetchall( - f"SELECT * FROM diagonalley.indexers WHERE wallet IN ({q})", (*wallet_ids,) - ) - return [Indexers(**row) for row in rows] - - -def delete_diagonalleys_indexer(indexer_id: str) -> None: - with open_ext_db("diagonalley") as db: - db.execute("DELETE FROM diagonalley.indexers WHERE id = ?", (indexer_id,)) - - -###Orders - - -def create_diagonalleys_order( - *, - productid: str, - wallet: str, - product: str, - quantity: int, - shippingzone: str, - address: str, - email: str, - invoiceid: str, - paid: bool, - shipped: bool, -) -> Indexers: - with open_ext_db("diagonalley") as db: - order_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") - db.execute( - """ - INSERT INTO diagonalley.orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - order_id, - productid, - wallet, - product, - quantity, - shippingzone, - address, - email, - invoiceid, - False, - False, - ), - ) - - return get_diagonalleys_order(order_id) - - -def get_diagonalleys_order(order_id: str) -> Optional[Orders]: - with open_ext_db("diagonalley") as db: - row = db.fetchone("SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,)) - - return Orders(**row) if row else None - - -def get_diagonalleys_orders(wallet_ids: Union[str, List[str]]) -> List[Orders]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - with open_ext_db("diagonalley") as db: - q = ",".join(["?"] * len(wallet_ids)) - rows = db.fetchall( - f"SELECT * FROM diagonalley.orders WHERE wallet IN ({q})", (*wallet_ids,) - ) - for r in rows: - PAID = (await WALLET.get_invoice_status(r["invoiceid"])).paid - if PAID: - with open_ext_db("diagonalley") as db: - db.execute( - "UPDATE diagonalley.orders SET paid = ? WHERE id = ?", - ( - True, - r["id"], - ), - ) - rows = db.fetchall( - f"SELECT * FROM diagonalley.orders WHERE wallet IN ({q})", - (*wallet_ids,), - ) - return [Orders(**row) for row in rows] - - -def delete_diagonalleys_order(order_id: str) -> None: - with open_ext_db("diagonalley") as db: - db.execute("DELETE FROM diagonalley.orders WHERE id = ?", (order_id,)) diff --git a/lnbits/extensions/diagonalley/migrations.py b/lnbits/extensions/diagonalley/migrations.py deleted file mode 100644 index 9f2b787f9..000000000 --- a/lnbits/extensions/diagonalley/migrations.py +++ /dev/null @@ -1,60 +0,0 @@ -async def m001_initial(db): - """ - Initial products table. - """ - await db.execute( - """ - CREATE TABLE diagonalley.products ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - product TEXT NOT NULL, - categories TEXT NOT NULL, - description TEXT NOT NULL, - image TEXT NOT NULL, - price INTEGER NOT NULL, - quantity INTEGER NOT NULL - ); - """ - ) - - """ - Initial indexers table. - """ - await db.execute( - """ - CREATE TABLE diagonalley.indexers ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - shopname TEXT NOT NULL, - indexeraddress TEXT NOT NULL, - online BOOLEAN NOT NULL, - rating INTEGER NOT NULL, - shippingzone1 TEXT NOT NULL, - shippingzone2 TEXT NOT NULL, - zone1cost INTEGER NOT NULL, - zone2cost INTEGER NOT NULL, - email TEXT NOT NULL - ); - """ - ) - - """ - Initial orders table. - """ - await db.execute( - """ - CREATE TABLE diagonalley.orders ( - id TEXT PRIMARY KEY, - productid TEXT NOT NULL, - wallet TEXT NOT NULL, - product TEXT NOT NULL, - quantity INTEGER NOT NULL, - shippingzone INTEGER NOT NULL, - address TEXT NOT NULL, - email TEXT NOT NULL, - invoiceid TEXT NOT NULL, - paid BOOLEAN NOT NULL, - shipped BOOLEAN NOT NULL - ); - """ - ) diff --git a/lnbits/extensions/diagonalley/models.py b/lnbits/extensions/diagonalley/models.py deleted file mode 100644 index ab1c592de..000000000 --- a/lnbits/extensions/diagonalley/models.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import NamedTuple -from sqlite3 import Row -from pydantic import BaseModel - -class Indexers(BaseModel): - id: str - wallet: str - shopname: str - indexeraddress: str - online: bool - rating: str - shippingzone1: str - shippingzone2: str - zone1cost: int - zone2cost: int - email: str - - -class Products(BaseModel): - id: str - wallet: str - product: str - categories: str - description: str - image: str - price: int - quantity: int - - -class Orders(BaseModel): - id: str - productid: str - wallet: str - product: str - quantity: int - shippingzone: int - address: str - email: str - invoiceid: str - paid: bool - shipped: bool diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html b/lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html deleted file mode 100644 index 585e8d7c8..000000000 --- a/lnbits/extensions/diagonalley/templates/diagonalley/_api_docs.html +++ /dev/null @@ -1,122 +0,0 @@ - - - -
- Diagon Alley: Decentralised Market-Stalls -
-

- Make a list of products to sell, point your list of products at a public - indexer. Buyers browse your products on the indexer, and pay you - directly. Ratings are managed by the indexer. Your stall can be listed - in multiple indexers, even over TOR, if you wish to be anonymous.
- More information on the - Diagon Alley Protocol
- - Created by, Ben Arc -

-
-
-
- - - - - GET - /api/v1/diagonalley/stall/products/<indexer_id> -
Body (application/json)
-
- Returns 201 CREATED (application/json) -
- Product JSON list -
Curl example
- curl -X GET {{ request.url_root - }}diagonalley/api/v1/diagonalley/stall/products/<indexer_id> -
-
-
- - - - POST - /api/v1/diagonalley/stall/order/<indexer_id> -
Body (application/json)
- {"id": <string>, "address": <string>, "shippingzone": - <integer>, "email": <string>, "quantity": - <integer>} -
- Returns 201 CREATED (application/json) -
- {"checking_id": <string>,"payment_request": - <string>} -
Curl example
- curl -X POST {{ request.url_root - }}diagonalley/api/v1/diagonalley/stall/order/<indexer_id> -d - '{"id": <product_id&>, "email": <customer_email>, - "address": <customer_address>, "quantity": 2, "shippingzone": - 1}' -H "Content-type: application/json" - -
-
-
- - - - GET - /diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id> -
Headers
-
- Returns 200 OK (application/json) -
- {"shipped": <boolean>} -
Curl example
- curl -X GET {{ request.url_root - }}diagonalley/api/v1/diagonalley/stall/checkshipped/<checking_id> - -H "Content-type: application/json" -
-
-
-
diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/index.html b/lnbits/extensions/diagonalley/templates/diagonalley/index.html deleted file mode 100644 index c041239f6..000000000 --- a/lnbits/extensions/diagonalley/templates/diagonalley/index.html +++ /dev/null @@ -1,906 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New Product - New Indexer - - Frontend shop your stall will list its products in - - - - - - -
-
-
Products
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - - -
-
-
Indexers
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - - -
-
-
Orders
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
- -
- - -
LNbits Diagon Alley Extension
-
- - - {% include "diagonalley/_api_docs.html" %} - -
-
- - - - - - - - - - - - - -
- Update Product - - Create Product - - Cancel -
-
-
-
- - - - - - - - - - - - - - - - -
- Update Indexer - - Create Indexer - - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/diagonalley/templates/diagonalley/stall.html b/lnbits/extensions/diagonalley/templates/diagonalley/stall.html deleted file mode 100644 index a45d254de..000000000 --- a/lnbits/extensions/diagonalley/templates/diagonalley/stall.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/lnbits/extensions/diagonalley/views.py b/lnbits/extensions/diagonalley/views.py deleted file mode 100644 index df264a666..000000000 --- a/lnbits/extensions/diagonalley/views.py +++ /dev/null @@ -1,14 +0,0 @@ -from quart import g, render_template - -from lnbits.decorators import check_user_exists, validate_uuids -from lnbits.extensions.diagonalley import diagonalley_ext -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@diagonalley_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("diagonalley/index.html", {"request": request, "user": g.user}) \ No newline at end of file diff --git a/lnbits/extensions/diagonalley/views_api.py b/lnbits/extensions/diagonalley/views_api.py deleted file mode 100644 index 4656fcce7..000000000 --- a/lnbits/extensions/diagonalley/views_api.py +++ /dev/null @@ -1,350 +0,0 @@ -from quart import g, jsonify, request -from http import HTTPStatus - -from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from lnbits.extensions.diagonalley import diagonalley_ext -from .crud import ( - create_diagonalleys_product, - get_diagonalleys_product, - get_diagonalleys_products, - delete_diagonalleys_product, - create_diagonalleys_indexer, - update_diagonalleys_indexer, - get_diagonalleys_indexer, - get_diagonalleys_indexers, - delete_diagonalleys_indexer, - create_diagonalleys_order, - get_diagonalleys_order, - get_diagonalleys_orders, - update_diagonalleys_product, -) -from lnbits.core.services import create_invoice -from base64 import urlsafe_b64encode -from uuid import uuid4 -from lnbits.db import open_ext_db - -### Products - - -@diagonalley_ext.get("/api/v1/diagonalley/products") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalley_products(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = get_user(g.wallet.user).wallet_ids - - return ( - jsonify( - [product._asdict() for product in get_diagonalleys_products(wallet_ids)] - ), - HTTPStatus.OK, - ) - -class CreateData(BaseModel): - product: str - categories: str - description: str - image: str - price: int = Query(ge=0) - quantity: int = Query(ge=0) - -@diagonalley_ext.post("/api/v1/diagonalley/products") -@diagonalley_ext.put("/api/v1/diagonalley/products{product_id}") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalley_product_create(product_id=None): - - if product_id: - product = get_diagonalleys_indexer(product_id) - - if not product: - return ( - jsonify({"message": "Withdraw product does not exist."}), - HTTPStatus.NOT_FOUND, - ) - - if product.wallet != g.wallet.id: - return ( - jsonify({"message": "Not your withdraw product."}), - HTTPStatus.FORBIDDEN, - ) - - product = update_diagonalleys_product(product_id, **g.data) - else: - product = create_diagonalleys_product(wallet_id=g.wallet.id, **g.data) - - return ( - jsonify(product._asdict()), - HTTPStatus.OK if product_id else HTTPStatus.CREATED, - ) - - -@diagonalley_ext.delete("/api/v1/diagonalley/products/{product_id}") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalley_products_delete(product_id): - product = get_diagonalleys_product(product_id) - - if not product: - return jsonify({"message": "Product does not exist."}), HTTPStatus.NOT_FOUND - - if product.wallet != g.wallet.id: - return jsonify({"message": "Not your Diagon Alley."}), HTTPStatus.FORBIDDEN - - delete_diagonalleys_product(product_id) - - return "", HTTPStatus.NO_CONTENT - - -###Indexers - - -@diagonalley_ext.get("/api/v1/diagonalley/indexers") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalley_indexers(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = get_user(g.wallet.user).wallet_ids - - return ( - jsonify( - [indexer._asdict() for indexer in get_diagonalleys_indexers(wallet_ids)] - ), - HTTPStatus.OK, - ) - -class CreateData(BaseModel): - shopname: str - indexeraddress: str - shippingzone1: str - shippingzone2: str - email: str - zone1cost: int = Query(ge=0) - zone2cost: int = Query(ge=0) - -@diagonalley_ext.post("/api/v1/diagonalley/indexers") -@diagonalley_ext.put("/api/v1/diagonalley/indexers{indexer_id}") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalley_indexer_create(data: CreateData, indexer_id=None): - - if indexer_id: - indexer = get_diagonalleys_indexer(indexer_id) - - if not indexer: - return ( - jsonify({"message": "Withdraw indexer does not exist."}), - HTTPStatus.NOT_FOUND, - ) - - if indexer.wallet != g.wallet.id: - return ( - jsonify({"message": "Not your withdraw indexer."}), - HTTPStatus.FORBIDDEN, - ) - - indexer = update_diagonalleys_indexer(indexer_id, **data) - else: - indexer = create_diagonalleys_indexer(wallet_id=g.wallet.id, **data) - - return ( - indexer._asdict(), - HTTPStatus.OK if indexer_id else HTTPStatus.CREATED, - ) - - -@diagonalley_ext.delete("/api/v1/diagonalley/indexers/{indexer_id}") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalley_indexer_delete(indexer_id): - indexer = get_diagonalleys_indexer(indexer_id) - - if not indexer: - return {"message": "Indexer does not exist."}, HTTPStatus.NOT_FOUND - - if indexer.wallet != g.wallet.id: - return {"message": "Not your Indexer."}, HTTPStatus.FORBIDDEN - - delete_diagonalleys_indexer(indexer_id) - - return "", HTTPStatus.NO_CONTENT - - -###Orders - - -@diagonalley_ext.get("/api/v1/diagonalley/orders") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalley_orders(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = get_user(g.wallet.user).wallet_ids - - return ( - [order._asdict() for order in get_diagonalleys_orders(wallet_ids)], - HTTPStatus.OK, - ) - -class CreateData(BaseModel): - id: str - address: str - email: str - quantity: int - shippingzone: int - -@diagonalley_ext.post("/api/v1/diagonalley/orders") -@api_check_wallet_key(key_type="invoice") - -async def api_diagonalley_order_create(data: CreateData): - order = create_diagonalleys_order(wallet_id=g.wallet.id, **data) - return order._asdict(), HTTPStatus.CREATED - - -@diagonalley_ext.delete("/api/v1/diagonalley/orders/{order_id}") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalley_order_delete(order_id): - order = get_diagonalleys_order(order_id) - - if not order: - return {"message": "Indexer does not exist."}, HTTPStatus.NOT_FOUND - - if order.wallet != g.wallet.id: - return {"message": "Not your Indexer."}, HTTPStatus.FORBIDDEN - - delete_diagonalleys_indexer(order_id) - - return "", HTTPStatus.NO_CONTENT - - -@diagonalley_ext.get("/api/v1/diagonalley/orders/paid/{order_id}") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalleys_order_paid(order_id): - with open_ext_db("diagonalley") as db: - db.execute( - "UPDATE diagonalley.orders SET paid = ? WHERE id = ?", - ( - True, - order_id, - ), - ) - return "", HTTPStatus.OK - - -@diagonalley_ext.get("/api/v1/diagonalley/orders/shipped/{order_id}") -@api_check_wallet_key(key_type="invoice") -async def api_diagonalleys_order_shipped(order_id): - with open_ext_db("diagonalley") as db: - db.execute( - "UPDATE diagonalley.orders SET shipped = ? WHERE id = ?", - ( - True, - order_id, - ), - ) - order = db.fetchone( - "SELECT * FROM diagonalley.orders WHERE id = ?", (order_id,) - ) - - return ( - jsonify( - [order._asdict() for order in get_diagonalleys_orders(order["wallet"])] - ), - HTTPStatus.OK, - ) - - -###List products based on indexer id - - -@diagonalley_ext.get( - "/api/v1/diagonalley/stall/products/{indexer_id}" -) -async def api_diagonalleys_stall_products(indexer_id): - with open_ext_db("diagonalley") as db: - rows = db.fetchone( - "SELECT * FROM diagonalley.indexers WHERE id = ?", (indexer_id,) - ) - print(rows[1]) - if not rows: - return {"message": "Indexer does not exist."}, HTTPStatus.NOT_FOUND - - products = db.fetchone( - "SELECT * FROM diagonalley.products WHERE wallet = ?", (rows[1],) - ) - if not products: - return {"message": "No products"}, HTTPStatus.NOT_FOUND - - return ( - [products._asdict() for products in get_diagonalleys_products(rows[1])], - HTTPStatus.OK, - ) - - -###Check a product has been shipped - - -@diagonalley_ext.get( - "/api/v1/diagonalley/stall/checkshipped/{checking_id}" -) -async def api_diagonalleys_stall_checkshipped(checking_id): - with open_ext_db("diagonalley") as db: - rows = db.fetchone( - "SELECT * FROM diagonalley.orders WHERE invoiceid = ?", (checking_id,) - ) - - return {"shipped": rows["shipped"]}, HTTPStatus.OK - - -###Place order - - -@diagonalley_ext.post("/api/v1/diagonalley/stall/order/{indexer_id}") -@api_validate_post_request( - schema={ - "id": {"type": "string", "empty": False, "required": True}, - "email": {"type": "string", "empty": False, "required": True}, - "address": {"type": "string", "empty": False, "required": True}, - "quantity": {"type": "integer", "empty": False, "required": True}, - "shippingzone": {"type": "integer", "empty": False, "required": True}, - } -) -async def api_diagonalley_stall_order(indexer_id): - product = get_diagonalleys_product(g.data["id"]) - shipping = get_diagonalleys_indexer(indexer_id) - - if g.data["shippingzone"] == 1: - shippingcost = shipping.zone1cost - else: - shippingcost = shipping.zone2cost - - checking_id, payment_request = create_invoice( - wallet_id=product.wallet, - amount=shippingcost + (g.data["quantity"] * product.price), - memo=g.data["id"], - ) - selling_id = urlsafe_b64encode(uuid4().bytes_le).decode("utf-8") - with open_ext_db("diagonalley") as db: - db.execute( - """ - INSERT INTO diagonalley.orders (id, productid, wallet, product, quantity, shippingzone, address, email, invoiceid, paid, shipped) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - selling_id, - g.data["id"], - product.wallet, - product.product, - g.data["quantity"], - g.data["shippingzone"], - g.data["address"], - g.data["email"], - checking_id, - False, - False, - ), - ) - return ( - {"checking_id": checking_id, "payment_request": payment_request}, - HTTPStatus.OK, - ) diff --git a/lnbits/extensions/events/README.md b/lnbits/extensions/events/README.md deleted file mode 100644 index 11b62fecb..000000000 --- a/lnbits/extensions/events/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Events - -## Sell tickets for events and use the built-in scanner for registering attendants - -Events alows you to make tickets for an event. Each ticket is in the form of a uniqque QR code. After registering, and paying for ticket, the user gets a QR code to present at registration/entrance. - -Events includes a shareable ticket scanner, which can be used to register attendees. - -## Usage - -1. Create an event\ - ![create event](https://i.imgur.com/dadK1dp.jpg) -2. Fill out the event information: - - - event name - - wallet (normally there's only one) - - event information - - closing date for event registration - - begin and end date of the event - - ![event info](https://imgur.com/KAv68Yr.jpg) - -3. Share the event registration link\ - ![event ticket](https://imgur.com/AQWUOBY.jpg) - - - ticket example\ - ![ticket example](https://i.imgur.com/trAVSLd.jpg) - - - QR code ticket, presented after invoice paid, to present at registration\ - ![event ticket](https://i.imgur.com/M0ROM82.jpg) - -4. Use the built-in ticket scanner to validate registered, and paid, attendees\ - ![ticket scanner](https://i.imgur.com/zrm9202.jpg) diff --git a/lnbits/extensions/events/__init__.py b/lnbits/extensions/events/__init__.py deleted file mode 100644 index b8f4deb55..000000000 --- a/lnbits/extensions/events/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_events") - - -events_ext: Blueprint = Blueprint( - "events", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/events/config.json b/lnbits/extensions/events/config.json deleted file mode 100644 index 6bc144ab0..000000000 --- a/lnbits/extensions/events/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Events", - "short_description": "Sell and register event tickets", - "icon": "local_activity", - "contributors": ["benarc"] -} diff --git a/lnbits/extensions/events/crud.py b/lnbits/extensions/events/crud.py deleted file mode 100644 index dece8e6d6..000000000 --- a/lnbits/extensions/events/crud.py +++ /dev/null @@ -1,168 +0,0 @@ -from typing import List, Optional, Union - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import Tickets, Events - - -# TICKETS - - -async def create_ticket( - payment_hash: str, wallet: str, event: str, name: str, email: str -) -> Tickets: - await db.execute( - """ - INSERT INTO events.ticket (id, wallet, event, name, email, registered, paid) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, - (payment_hash, wallet, event, name, email, False, False), - ) - - ticket = await get_ticket(payment_hash) - assert ticket, "Newly created ticket couldn't be retrieved" - return ticket - - -async def set_ticket_paid(payment_hash: str) -> Tickets: - row = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (payment_hash,)) - if row[6] != True: - await db.execute( - """ - UPDATE events.ticket - SET paid = true - WHERE id = ? - """, - (payment_hash,), - ) - - eventdata = await get_event(row[2]) - assert eventdata, "Couldn't get event from ticket being paid" - - sold = eventdata.sold + 1 - amount_tickets = eventdata.amount_tickets - 1 - await db.execute( - """ - UPDATE events.events - SET sold = ?, amount_tickets = ? - WHERE id = ? - """, - (sold, amount_tickets, row[2]), - ) - - ticket = await get_ticket(payment_hash) - assert ticket, "Newly updated ticket couldn't be retrieved" - return ticket - - -async def get_ticket(payment_hash: str) -> Optional[Tickets]: - row = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (payment_hash,)) - return Tickets(**row) if row else None - - -async def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM events.ticket WHERE wallet IN ({q})", (*wallet_ids,) - ) - return [Tickets(**row) for row in rows] - - -async def delete_ticket(payment_hash: str) -> None: - await db.execute("DELETE FROM events.ticket WHERE id = ?", (payment_hash,)) - - -# EVENTS - - -async def create_event( - *, - wallet: str, - name: str, - info: str, - closing_date: str, - event_start_date: str, - event_end_date: str, - amount_tickets: int, - price_per_ticket: int, -) -> Events: - event_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO events.events (id, wallet, name, info, closing_date, event_start_date, event_end_date, amount_tickets, price_per_ticket, sold) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - event_id, - wallet, - name, - info, - closing_date, - event_start_date, - event_end_date, - amount_tickets, - price_per_ticket, - 0, - ), - ) - - event = await get_event(event_id) - assert event, "Newly created event couldn't be retrieved" - return event - - -async def update_event(event_id: str, **kwargs) -> Events: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE events.events SET {q} WHERE id = ?", (*kwargs.values(), event_id) - ) - event = await get_event(event_id) - assert event, "Newly updated event couldn't be retrieved" - return event - - -async def get_event(event_id: str) -> Optional[Events]: - row = await db.fetchone("SELECT * FROM events.events WHERE id = ?", (event_id,)) - return Events(**row) if row else None - - -async def get_events(wallet_ids: Union[str, List[str]]) -> List[Events]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM events.events WHERE wallet IN ({q})", (*wallet_ids,) - ) - - return [Events(**row) for row in rows] - - -async def delete_event(event_id: str) -> None: - await db.execute("DELETE FROM events.events WHERE id = ?", (event_id,)) - - -# EVENTTICKETS - - -async def get_event_tickets(event_id: str, wallet_id: str) -> List[Tickets]: - rows = await db.fetchall( - "SELECT * FROM events.ticket WHERE wallet = ? AND event = ?", - (wallet_id, event_id), - ) - return [Tickets(**row) for row in rows] - - -async def reg_ticket(ticket_id: str) -> List[Tickets]: - await db.execute( - "UPDATE events.ticket SET registered = ? WHERE id = ?", (True, ticket_id) - ) - ticket = await db.fetchone("SELECT * FROM events.ticket WHERE id = ?", (ticket_id,)) - rows = await db.fetchall( - "SELECT * FROM events.ticket WHERE event = ?", (ticket[1],) - ) - return [Tickets(**row) for row in rows] diff --git a/lnbits/extensions/events/migrations.py b/lnbits/extensions/events/migrations.py deleted file mode 100644 index d8f3d94e8..000000000 --- a/lnbits/extensions/events/migrations.py +++ /dev/null @@ -1,91 +0,0 @@ -async def m001_initial(db): - - await db.execute( - """ - CREATE TABLE events.events ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - name TEXT NOT NULL, - info TEXT NOT NULL, - closing_date TEXT NOT NULL, - event_start_date TEXT NOT NULL, - event_end_date TEXT NOT NULL, - amount_tickets INTEGER NOT NULL, - price_per_ticket INTEGER NOT NULL, - sold INTEGER NOT NULL, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) - - await db.execute( - """ - CREATE TABLE events.tickets ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - event TEXT NOT NULL, - name TEXT NOT NULL, - email TEXT NOT NULL, - registered BOOLEAN NOT NULL, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) - - -async def m002_changed(db): - - await db.execute( - """ - CREATE TABLE events.ticket ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - event TEXT NOT NULL, - name TEXT NOT NULL, - email TEXT NOT NULL, - registered BOOLEAN NOT NULL, - paid BOOLEAN NOT NULL, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) - - for row in [list(row) for row in await db.fetchall("SELECT * FROM events.tickets")]: - usescsv = "" - - for i in range(row[5]): - if row[7]: - usescsv += "," + str(i + 1) - else: - usescsv += "," + str(1) - usescsv = usescsv[1:] - await db.execute( - """ - INSERT INTO events.ticket ( - id, - wallet, - event, - name, - email, - registered, - paid - ) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, - ( - row[0], - row[1], - row[2], - row[3], - row[4], - row[5], - True, - ), - ) - await db.execute("DROP TABLE events.tickets") diff --git a/lnbits/extensions/events/models.py b/lnbits/extensions/events/models.py deleted file mode 100644 index 59b8071dd..000000000 --- a/lnbits/extensions/events/models.py +++ /dev/null @@ -1,28 +0,0 @@ -from sqlite3 import Row -from pydantic import BaseModel -#from typing import NamedTuple - - -class Events(BaseModel): - id: str - wallet: str - name: str - info: str - closing_date: str - event_start_date: str - event_end_date: str - amount_tickets: int - price_per_ticket: int - sold: int - time: int - - -class Tickets(BaseModel): - id: str - wallet: str - event: str - name: str - email: str - registered: bool - paid: bool - time: int \ No newline at end of file diff --git a/lnbits/extensions/events/templates/events/_api_docs.html b/lnbits/extensions/events/templates/events/_api_docs.html deleted file mode 100644 index a5c821747..000000000 --- a/lnbits/extensions/events/templates/events/_api_docs.html +++ /dev/null @@ -1,23 +0,0 @@ - - - -
- Events: Sell and register ticket waves for an event -
-

- Events alows you to make a wave of tickets for an event, each ticket is - in the form of a unqiue QRcode, which the user presents at registration. - Events comes with a shareable ticket scanner, which can be used to - register attendees.
- - Created by, Ben Arc - -

-
-
-
diff --git a/lnbits/extensions/events/templates/events/display.html b/lnbits/extensions/events/templates/events/display.html deleted file mode 100644 index 4c1f557f1..000000000 --- a/lnbits/extensions/events/templates/events/display.html +++ /dev/null @@ -1,207 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -

{{ event_name }}

-
-
{{ event_info }}
-
- - - - -
- Submit - Cancel -
-
-
-
- - -
- Link to your ticket! -

-

You'll be redirected in a few moments...

-
-
-
- - - - - - -
- Copy invoice - Close -
-
-
-
- -{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/events/templates/events/error.html b/lnbits/extensions/events/templates/events/error.html deleted file mode 100644 index f231177b4..000000000 --- a/lnbits/extensions/events/templates/events/error.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
-

{{ event_name }} error

-
- - -
{{ event_error }}
-
-
-
-
-
- - {% endblock %} {% block scripts %} - - - - {% endblock %} -
diff --git a/lnbits/extensions/events/templates/events/index.html b/lnbits/extensions/events/templates/events/index.html deleted file mode 100644 index 1ad3d885f..000000000 --- a/lnbits/extensions/events/templates/events/index.html +++ /dev/null @@ -1,538 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New Event - - - - - -
-
-
Events
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - - -
-
-
Tickets
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
-
- - -
- {{SITE_TITLE}} Events extension -
-
- - - {% include "events/_api_docs.html" %} - -
-
- - - - -
-
- -
-
- - -
-
- - -
-
Ticket closing date
-
- -
-
- -
-
Event begins
-
- -
-
- -
-
Event ends
-
- -
-
- -
-
- -
-
- -
-
- -
- Update Event - Create Event - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/events/templates/events/register.html b/lnbits/extensions/events/templates/events/register.html deleted file mode 100644 index 4dff9afbb..000000000 --- a/lnbits/extensions/events/templates/events/register.html +++ /dev/null @@ -1,173 +0,0 @@ -{% extends "public.html" %} {% block page %} - -
-
- - -
-

{{ event_name }} Registration

-
- -
- - Scan ticket -
-
-
- - - - - {% raw %} - - - {% endraw %} - - - -
- - - -
- -
-
- Cancel -
-
-
-
-{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/events/templates/events/ticket.html b/lnbits/extensions/events/templates/events/ticket.html deleted file mode 100644 index a53f834f9..000000000 --- a/lnbits/extensions/events/templates/events/ticket.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
-

{{ ticket_name }} Ticket

-
-
- Bookmark, print or screenshot this page,
- and present it for registration! -
-
- - -
- - Print -
-
-
-
-
-{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/events/views.py b/lnbits/extensions/events/views.py deleted file mode 100644 index f3d8cc295..000000000 --- a/lnbits/extensions/events/views.py +++ /dev/null @@ -1,83 +0,0 @@ -from quart import g, abort, render_template -from datetime import date, datetime -from http import HTTPStatus - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import events_ext -from .crud import get_ticket, get_event -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@events_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return templates.TemplateResponse("events/index.html", {"user":g.user}) - -@events_ext.route("/") -async def display(request: Request, event_id): - event = await get_event(event_id) - if not event: - abort(HTTPStatus.NOT_FOUND, "Event does not exist.") - - if event.amount_tickets < 1: - return await templates.TemplateResponse( - "events/error.html", - {"request":request, - "event_name":event.name, - "event_error":"Sorry, tickets are sold out :("} - ) - datetime_object = datetime.strptime(event.closing_date, "%Y-%m-%d").date() - if date.today() > datetime_object: - return await templates.TemplateResponse( - "events/error.html", - {"request":request, - "event_name":event.name, - "event_error":"Sorry, ticket closing date has passed :("} - ) - - return await templates.TemplateResponse( - "events/display.html", - {"request":request, - "event_id":event_id, - "event_name":event.name, - "event_info":event.info, - "event_price":event.price_per_ticket} - ) - - -@events_ext.route("/ticket/") -async def ticket(request: Request, ticket_id): - ticket = await get_ticket(ticket_id) - if not ticket: - abort(HTTPStatus.NOT_FOUND, "Ticket does not exist.") - - event = await get_event(ticket.event) - if not event: - abort(HTTPStatus.NOT_FOUND, "Event does not exist.") - - return await templates.TemplateResponse( - "events/ticket.html", - {"request":request, - "ticket_id":ticket_id, - "ticket_name":event.name, - "ticket_info":event.info} - ) - - -@events_ext.route("/register/") -async def register(request: Request, event_id): - event = await get_event(event_id) - if not event: - abort(HTTPStatus.NOT_FOUND, "Event does not exist.") - - return await templates.TemplateResponse( - "events/register.html", - {"request":request, - "event_id":event_id, - "event_name":event.name, - "wallet_id":event.wallet} - ) diff --git a/lnbits/extensions/events/views_api.py b/lnbits/extensions/events/views_api.py deleted file mode 100644 index 1ea516a9d..000000000 --- a/lnbits/extensions/events/views_api.py +++ /dev/null @@ -1,191 +0,0 @@ -from quart import g, jsonify, request -from http import HTTPStatus - -from lnbits.core.crud import get_user, get_wallet -from lnbits.core.services import create_invoice, check_invoice_status -from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from fastapi.encoders import jsonable_encoder -from fastapi import Query -from pydantic import BaseModel - -from . import events_ext -from .crud import ( - create_ticket, - set_ticket_paid, - get_ticket, - get_tickets, - delete_ticket, - create_event, - update_event, - get_event, - get_events, - delete_event, - get_event_tickets, - reg_ticket, -) - - -# Events - - - -@events_ext.get("/api/v1/events") -@api_check_wallet_key("invoice") -async def api_events(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - return ( - [event._asdict() for event in await get_events(wallet_ids)], - HTTPStatus.OK, - ) - -class CreateData(BaseModel): - wallet: str = Query(...) - name: str = Query(...) - info: str = Query(...) - closing_date: str = Query(...) - event_start_date: str = Query(...) - event_end_date: str = Query(...) - amount_tickets: int = Query(..., ge=0) - price_per_ticket: int = Query(..., ge=0) - -@events_ext.post("/api/v1/events") -@events_ext.put("/api/v1/events/{event_id}") -@api_check_wallet_key("invoice") -async def api_event_create(data: CreateData, event_id=None): - if event_id: - event = await get_event(event_id) - if not event: - return {"message": "Form does not exist."}, HTTPStatus.NOT_FOUND - - if event.wallet != g.wallet.id: - return {"message": "Not your event."}, HTTPStatus.FORBIDDEN - - event = await update_event(event_id, **data) - else: - event = await create_event(**data) - - return event._asdict(), HTTPStatus.CREATED - - -@events_ext.delete("/api/v1/events/{event_id}") -@api_check_wallet_key("invoice") -async def api_form_delete(event_id): - event = await get_event(event_id) - if not event: - return {"message": "Event does not exist."}, HTTPStatus.NOT_FOUND - - if event.wallet != g.wallet.id: - return {"message": "Not your event."}, HTTPStatus.FORBIDDEN - - await delete_event(event_id) - return "", HTTPStatus.NO_CONTENT - - -#########Tickets########## - - -@events_ext.get("/api/v1/tickets") -@api_check_wallet_key("invoice") -async def api_tickets(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - return ( - [ticket._asdict() for ticket in await get_tickets(wallet_ids)], - HTTPStatus.OK, - ) - -class CreateTicketData(BaseModel): - name: str = Query(...) - email: str - -@events_ext.post("/api/v1/tickets/{event_id}/{sats}") -async def api_ticket_make_ticket(data: CreateTicketData, event_id, sats): - event = await get_event(event_id) - if not event: - return {"message": "Event does not exist."}, HTTPStatus.NOT_FOUND - try: - payment_hash, payment_request = await create_invoice( - wallet_id=event.wallet, - amount=int(sats), - memo=f"{event_id}", - extra={"tag": "events"}, - ) - except Exception as e: - return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR - - ticket = await create_ticket( - payment_hash=payment_hash, wallet=event.wallet, event=event_id, **data - ) - - if not ticket: - return {"message": "Event could not be fetched."}, HTTPStatus.NOT_FOUND - - return {"payment_hash": payment_hash, "payment_request": payment_request}, HTTPStatus.OK - - -@events_ext.get("/api/v1/tickets/{payment_hash}") -async def api_ticket_send_ticket(payment_hash): - ticket = await get_ticket(payment_hash) - - try: - status = await check_invoice_status(ticket.wallet, payment_hash) - is_paid = not status.pending - except Exception: - return {"message": "Not paid."}, HTTPStatus.NOT_FOUND - - if is_paid: - wallet = await get_wallet(ticket.wallet) - payment = await wallet.get_payment(payment_hash) - await payment.set_pending(False) - ticket = await set_ticket_paid(payment_hash=payment_hash) - - return {"paid": True, "ticket_id": ticket.id}, HTTPStatus.OK - - return {"paid": False}, HTTPStatus.OK - - -@events_ext.delete("/api/v1/tickets/{ticket_id}") -@api_check_wallet_key("invoice") -async def api_ticket_delete(ticket_id): - ticket = await get_ticket(ticket_id) - - if not ticket: - return {"message": "Ticket does not exist."}, HTTPStatus.NOT_FOUND - - if ticket.wallet != g.wallet.id: - return {"message": "Not your ticket."}, HTTPStatus.FORBIDDEN - - await delete_ticket(ticket_id) - return "", HTTPStatus.NO_CONTENT - - -# Event Tickets - - -@events_ext.get("/api/v1/eventtickets/{wallet_id}/{event_id]") -async def api_event_tickets(wallet_id, event_id): - return ([ticket._asdict() for ticket in await get_event_tickets(wallet_id=wallet_id, event_id=event_id)], - HTTPStatus.OK, - ) - - -@events_ext.get("/api/v1/register/ticket/{ticket_id}") -async def api_event_register_ticket(ticket_id): - ticket = await get_ticket(ticket_id) - if not ticket: - return {"message": "Ticket does not exist."}, HTTPStatus.FORBIDDEN - - if not ticket.paid: - return {"message": "Ticket not paid for."}, HTTPStatus.FORBIDDEN - - if ticket.registered == True: - return {"message": "Ticket already registered"}, HTTPStatus.FORBIDDEN - - return [ticket._asdict() for ticket in await reg_ticket(ticket_id)], HTTPStatus.OK diff --git a/lnbits/extensions/example/README.md b/lnbits/extensions/example/README.md deleted file mode 100644 index 277294592..000000000 --- a/lnbits/extensions/example/README.md +++ /dev/null @@ -1,11 +0,0 @@ -

Example Extension

-

*tagline*

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

If your extension has API endpoints, include useful ones here

- -curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY" diff --git a/lnbits/extensions/example/__init__.py b/lnbits/extensions/example/__init__.py deleted file mode 100644 index e16e0372f..000000000 --- a/lnbits/extensions/example/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_example") - -example_ext: Blueprint = Blueprint( - "example", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/example/config.json b/lnbits/extensions/example/config.json deleted file mode 100644 index 55389373b..000000000 --- a/lnbits/extensions/example/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Build your own!", - "short_description": "Join us, make an extension", - "icon": "info", - "contributors": ["github_username"] -} diff --git a/lnbits/extensions/example/migrations.py b/lnbits/extensions/example/migrations.py deleted file mode 100644 index 99d7c362d..000000000 --- a/lnbits/extensions/example/migrations.py +++ /dev/null @@ -1,10 +0,0 @@ -# async def m001_initial(db): -# await db.execute( -# f""" -# CREATE TABLE example.example ( -# id TEXT PRIMARY KEY, -# wallet TEXT NOT NULL, -# time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} -# ); -# """ -# ) diff --git a/lnbits/extensions/example/models.py b/lnbits/extensions/example/models.py deleted file mode 100644 index be5232339..000000000 --- a/lnbits/extensions/example/models.py +++ /dev/null @@ -1,11 +0,0 @@ -# from sqlite3 import Row -# from typing import NamedTuple - - -# class Example(NamedTuple): -# id: str -# wallet: str -# -# @classmethod -# def from_row(cls, row: Row) -> "Example": -# return cls(**dict(row)) diff --git a/lnbits/extensions/example/templates/example/index.html b/lnbits/extensions/example/templates/example/index.html deleted file mode 100644 index d732ef376..000000000 --- a/lnbits/extensions/example/templates/example/index.html +++ /dev/null @@ -1,59 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} - - -
- Frameworks used by {{SITE_TITLE}} -
- - - {% raw %} - - - {{ tool.name }} - {{ tool.language }} - - {% endraw %} - - - -

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

- {% raw %}{{ g }}{% endraw %} -
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/example/views.py b/lnbits/extensions/example/views.py deleted file mode 100644 index 6884b4974..000000000 --- a/lnbits/extensions/example/views.py +++ /dev/null @@ -1,16 +0,0 @@ -from quart import g, render_template - -from lnbits.decorators import check_user_exists, validate_uuids -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates -from . import example_ext -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@example_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("example/index.html", {"request": request, "user":g.user}) diff --git a/lnbits/extensions/example/views_api.py b/lnbits/extensions/example/views_api.py deleted file mode 100644 index e59c1072a..000000000 --- a/lnbits/extensions/example/views_api.py +++ /dev/null @@ -1,40 +0,0 @@ -# views_api.py is for you API endpoints that could be hit by another service - -# add your dependencies here - -# import json -# import httpx -# (use httpx just like requests, except instead of response.ok there's only the -# response.is_error that is its inverse) - -from quart import jsonify -from http import HTTPStatus - -from . import example_ext - - -# add your endpoints here - - -@example_ext.route("/api/v1/tools", methods=["GET"]) -async def api_example(): - """Try to add descriptions for others.""" - tools = [ - { - "name": "Quart", - "url": "https://pgjones.gitlab.io/quart/", - "language": "Python", - }, - { - "name": "Vue.js", - "url": "https://vuejs.org/", - "language": "JavaScript", - }, - { - "name": "Quasar Framework", - "url": "https://quasar.dev/", - "language": "JavaScript", - }, - ] - - return jsonify(tools), HTTPStatus.OK diff --git a/lnbits/extensions/hivemind/README.md b/lnbits/extensions/hivemind/README.md deleted file mode 100644 index 1e9667ec9..000000000 --- a/lnbits/extensions/hivemind/README.md +++ /dev/null @@ -1,3 +0,0 @@ -

Hivemind

- -Placeholder for a future Bitcoin Hivemind extension. diff --git a/lnbits/extensions/hivemind/__init__.py b/lnbits/extensions/hivemind/__init__.py deleted file mode 100644 index cc2420d83..000000000 --- a/lnbits/extensions/hivemind/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_hivemind") - -hivemind_ext: Blueprint = Blueprint( - "hivemind", __name__, static_folder="static", template_folder="templates" -) - - -from .views import * # noqa diff --git a/lnbits/extensions/hivemind/config.json b/lnbits/extensions/hivemind/config.json deleted file mode 100644 index a5469b15f..000000000 --- a/lnbits/extensions/hivemind/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Hivemind", - "short_description": "Make cheap talk expensive!", - "icon": "batch_prediction", - "contributors": ["fiatjaf"] -} diff --git a/lnbits/extensions/hivemind/migrations.py b/lnbits/extensions/hivemind/migrations.py deleted file mode 100644 index 775a94548..000000000 --- a/lnbits/extensions/hivemind/migrations.py +++ /dev/null @@ -1,10 +0,0 @@ -# async def m001_initial(db): -# await db.execute( -# f""" -# CREATE TABLE hivemind.hivemind ( -# id TEXT PRIMARY KEY, -# wallet TEXT NOT NULL, -# time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} -# ); -# """ -# ) diff --git a/lnbits/extensions/hivemind/models.py b/lnbits/extensions/hivemind/models.py deleted file mode 100644 index be5232339..000000000 --- a/lnbits/extensions/hivemind/models.py +++ /dev/null @@ -1,11 +0,0 @@ -# from sqlite3 import Row -# from typing import NamedTuple - - -# class Example(NamedTuple): -# id: str -# wallet: str -# -# @classmethod -# def from_row(cls, row: Row) -> "Example": -# return cls(**dict(row)) diff --git a/lnbits/extensions/hivemind/templates/hivemind/index.html b/lnbits/extensions/hivemind/templates/hivemind/index.html deleted file mode 100644 index 40a320f0b..000000000 --- a/lnbits/extensions/hivemind/templates/hivemind/index.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} - - -
- This extension is just a placeholder for now. -
-

- Hivemind is a Bitcoin sidechain - project for a peer-to-peer oracle protocol that absorbs accurate data into - a blockchain so that Bitcoin users can speculate in prediction markets. -

-

- These markets have the potential to revolutionize the emergence of - diffusion of knowledge in society and fix all sorts of problems in the - world. -

-

- This extension will become fully operative when the - BIP300 soft-fork gets activated and - Bitcoin Hivemind is launched. -

-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/hivemind/views.py b/lnbits/extensions/hivemind/views.py deleted file mode 100644 index 9a76c335e..000000000 --- a/lnbits/extensions/hivemind/views.py +++ /dev/null @@ -1,15 +0,0 @@ -from quart import g, render_template - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import hivemind_ext -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@hivemind_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("hivemind/index.html", {"request": request, "user":g.user}) diff --git a/lnbits/extensions/jukebox/README.md b/lnbits/extensions/jukebox/README.md deleted file mode 100644 index c761db448..000000000 --- a/lnbits/extensions/jukebox/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# Jukebox - -## An actual Jukebox where users pay sats to play their favourite music from your playlists - -**Note:** To use this extension you need a Premium Spotify subscription. - -## Usage - -1. Click on "ADD SPOTIFY JUKEBOX"\ - ![add jukebox](https://i.imgur.com/NdVoKXd.png) -2. Follow the steps required on the form\ - - - give your jukebox a name - - select a wallet to receive payment - - define the price a user must pay to select a song\ - ![pick wallet price](https://i.imgur.com/4bJ8mb9.png) - - follow the steps to get your Spotify App and get the client ID and secret key\ - ![spotify keys](https://i.imgur.com/w2EzFtB.png) - - paste the codes in the form\ - ![api keys](https://i.imgur.com/6b9xauo.png) - - copy the _Redirect URL_ presented on the form\ - ![redirect url](https://i.imgur.com/GMzl0lG.png) - - on Spotify click the "EDIT SETTINGS" button and paste the copied link in the _Redirect URI's_ prompt - ![spotify app setting](https://i.imgur.com/vb0x4Tl.png) - - back on LNBits, click "AUTORIZE ACCESS" and "Agree" on the page that will open - - choose on which device the LNBits Jukebox extensions will stream to, you may have to be logged in in order to select the device (browser, smartphone app, etc...) - - and select what playlist will be available for users to choose songs (you need to have already playlist on Spotify)\ - ![select playlists](https://i.imgur.com/g4dbtED.png) - -3. After Jukebox is created, click the icon to open the dialog with the shareable QR, open the Jukebox page, etc...\ - ![shareable jukebox](https://i.imgur.com/EAh9PI0.png) -4. The users will see the Jukebox page and choose a song from the selected playlist\ - ![select song](https://i.imgur.com/YYjeQAs.png) -5. After selecting a song they'd like to hear next a dialog will show presenting the music\ - ![play for sats](https://i.imgur.com/eEHl3o8.png) -6. After payment, the song will automatically start playing on the device selected or enter the queue if some other music is already playing diff --git a/lnbits/extensions/jukebox/__init__.py b/lnbits/extensions/jukebox/__init__.py deleted file mode 100644 index 076ae4d9d..000000000 --- a/lnbits/extensions/jukebox/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from quart import Blueprint - -from lnbits.db import Database - -db = Database("ext_jukebox") - -jukebox_ext: Blueprint = Blueprint( - "jukebox", __name__, static_folder="static", template_folder="templates" -) - -from .views_api import * # noqa -from .views import * # noqa -from .tasks import register_listeners - -from lnbits.tasks import record_async - -jukebox_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/jukebox/config.json b/lnbits/extensions/jukebox/config.json deleted file mode 100644 index 91134bc28..000000000 --- a/lnbits/extensions/jukebox/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "SpotifyJukebox", - "short_description": "Spotify jukebox middleware", - "icon": "radio", - "contributors": ["benarc"] -} diff --git a/lnbits/extensions/jukebox/crud.py b/lnbits/extensions/jukebox/crud.py deleted file mode 100644 index 4e3ba2f15..000000000 --- a/lnbits/extensions/jukebox/crud.py +++ /dev/null @@ -1,122 +0,0 @@ -from typing import List, Optional - -from . import db -from .models import Jukebox, JukeboxPayment -from lnbits.helpers import urlsafe_short_hash - - -async def create_jukebox( - inkey: str, - user: str, - wallet: str, - title: str, - price: int, - sp_user: str, - sp_secret: str, - sp_access_token: Optional[str] = "", - sp_refresh_token: Optional[str] = "", - sp_device: Optional[str] = "", - sp_playlists: Optional[str] = "", -) -> Jukebox: - juke_id = urlsafe_short_hash() - result = await db.execute( - """ - INSERT INTO jukebox.jukebox (id, user, title, wallet, sp_user, sp_secret, sp_access_token, sp_refresh_token, sp_device, sp_playlists, price, profit) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - juke_id, - user, - title, - wallet, - sp_user, - sp_secret, - sp_access_token, - sp_refresh_token, - sp_device, - sp_playlists, - int(price), - 0, - ), - ) - jukebox = await get_jukebox(juke_id) - assert jukebox, "Newly created Jukebox couldn't be retrieved" - return jukebox - - -async def update_jukebox(juke_id: str, **kwargs) -> Optional[Jukebox]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE jukebox.jukebox SET {q} WHERE id = ?", (*kwargs.values(), juke_id) - ) - row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) - return Jukebox(**row) if row else None - - -async def get_jukebox(juke_id: str) -> Optional[Jukebox]: - row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE id = ?", (juke_id,)) - return Jukebox(**row) if row else None - - -async def get_jukebox_by_user(user: str) -> Optional[Jukebox]: - row = await db.fetchone("SELECT * FROM jukebox.jukebox WHERE sp_user = ?", (user,)) - return Jukebox(**row) if row else None - - -async def get_jukeboxs(user: str) -> List[Jukebox]: - rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) - for row in rows: - if row.sp_playlists == "": - await delete_jukebox(row.id) - rows = await db.fetchall("SELECT * FROM jukebox.jukebox WHERE user = ?", (user,)) - return [Jukebox.from_row(row) for row in rows] - - -async def delete_jukebox(juke_id: str): - await db.execute( - """ - DELETE FROM jukebox.jukebox WHERE id = ? - """, - (juke_id), - ) - - -#####################################PAYMENTS - - -async def create_jukebox_payment( - song_id: str, payment_hash: str, juke_id: str -) -> JukeboxPayment: - result = await db.execute( - """ - INSERT INTO jukebox.jukebox_payment (payment_hash, juke_id, song_id, paid) - VALUES (?, ?, ?, ?) - """, - ( - payment_hash, - juke_id, - song_id, - False, - ), - ) - jukebox_payment = await get_jukebox_payment(payment_hash) - assert jukebox_payment, "Newly created Jukebox Payment couldn't be retrieved" - return jukebox_payment - - -async def update_jukebox_payment( - payment_hash: str, **kwargs -) -> Optional[JukeboxPayment]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE jukebox.jukebox_payment SET {q} WHERE payment_hash = ?", - (*kwargs.values(), payment_hash), - ) - return await get_jukebox_payment(payment_hash) - - -async def get_jukebox_payment(payment_hash: str) -> Optional[JukeboxPayment]: - row = await db.fetchone( - "SELECT * FROM jukebox.jukebox_payment WHERE payment_hash = ?", (payment_hash,) - ) - return JukeboxPayment(**row) if row else None diff --git a/lnbits/extensions/jukebox/migrations.py b/lnbits/extensions/jukebox/migrations.py deleted file mode 100644 index a0a3bd285..000000000 --- a/lnbits/extensions/jukebox/migrations.py +++ /dev/null @@ -1,39 +0,0 @@ -async def m001_initial(db): - """ - Initial jukebox table. - """ - await db.execute( - """ - CREATE TABLE jukebox.jukebox ( - id TEXT PRIMARY KEY, - "user" TEXT, - title TEXT, - wallet TEXT, - inkey TEXT, - sp_user TEXT NOT NULL, - sp_secret TEXT NOT NULL, - sp_access_token TEXT, - sp_refresh_token TEXT, - sp_device TEXT, - sp_playlists TEXT, - price INTEGER, - profit INTEGER - ); - """ - ) - - -async def m002_initial(db): - """ - Initial jukebox_payment table. - """ - await db.execute( - """ - CREATE TABLE jukebox.jukebox_payment ( - payment_hash TEXT PRIMARY KEY, - juke_id TEXT, - song_id TEXT, - paid BOOL - ); - """ - ) diff --git a/lnbits/extensions/jukebox/models.py b/lnbits/extensions/jukebox/models.py deleted file mode 100644 index f09f76555..000000000 --- a/lnbits/extensions/jukebox/models.py +++ /dev/null @@ -1,33 +0,0 @@ -from sqlite3 import Row -from pydantic import BaseModel - - -class Jukebox(BaseModel): - 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 - - @classmethod - def from_row(cls, row: Row) -> "Jukebox": - return cls(**dict(row)) - - -class JukeboxPayment(BaseModel): - payment_hash: str - juke_id: str - song_id: str - paid: bool - - @classmethod - def from_row(cls, row: Row) -> "JukeboxPayment": - return cls(**dict(row)) diff --git a/lnbits/extensions/jukebox/static/js/index.js b/lnbits/extensions/jukebox/static/js/index.js deleted file mode 100644 index fc382d711..000000000 --- a/lnbits/extensions/jukebox/static/js/index.js +++ /dev/null @@ -1,420 +0,0 @@ -/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ - -Vue.component(VueQrcode.name, VueQrcode) - -var mapJukebox = obj => { - obj._data = _.clone(obj) - obj.sp_id = obj.id - obj.device = obj.sp_device.split('-')[0] - playlists = obj.sp_playlists.split(',') - var i - playlistsar = [] - for (i = 0; i < playlists.length; i++) { - playlistsar.push(playlists[i].split('-')[0]) - } - obj.playlist = playlistsar.join() - return obj -} - -new Vue({ - el: '#vue', - mixins: [windowMixin], - data() { - return { - JukeboxTable: { - columns: [ - { - name: 'title', - align: 'left', - label: 'Title', - field: 'title' - }, - { - name: 'device', - align: 'left', - label: 'Device', - field: 'device' - }, - { - name: 'playlist', - align: 'left', - label: 'Playlist', - field: 'playlist' - }, - { - name: 'price', - align: 'left', - label: 'Price', - field: 'price' - } - ], - pagination: { - rowsPerPage: 10 - } - }, - isPwd: true, - tokenFetched: true, - devices: [], - filter: '', - jukebox: {}, - playlists: [], - JukeboxLinks: [], - step: 1, - locationcbPath: '', - locationcb: '', - jukeboxDialog: { - show: false, - data: {} - }, - spotifyDialog: false, - qrCodeDialog: { - show: false, - data: null - } - } - }, - computed: {}, - methods: { - openQrCodeDialog: function (linkId) { - var link = _.findWhere(this.JukeboxLinks, {id: linkId}) - - this.qrCodeDialog.data = _.clone(link) - console.log(this.qrCodeDialog.data) - this.qrCodeDialog.data.url = - window.location.protocol + '//' + window.location.host - this.qrCodeDialog.show = true - }, - getJukeboxes() { - self = this - LNbits.api - .request( - 'GET', - '/jukebox/api/v1/jukebox', - self.g.user.wallets[0].adminkey - ) - .then(function (response) { - self.JukeboxLinks = response.data.map(mapJukebox) - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - deleteJukebox(juke_id) { - self = this - LNbits.utils - .confirmDialog('Are you sure you want to delete this Jukebox?') - .onOk(function () { - LNbits.api - .request( - 'DELETE', - '/jukebox/api/v1/jukebox/' + juke_id, - self.g.user.wallets[0].adminkey - ) - .then(function (response) { - self.JukeboxLinks = _.reject(self.JukeboxLinks, function (obj) { - return obj.id === juke_id - }) - }) - - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }) - }, - updateJukebox: function (linkId) { - self = this - var link = _.findWhere(self.JukeboxLinks, {id: linkId}) - self.jukeboxDialog.data = _.clone(link._data) - console.log(this.jukeboxDialog.data.sp_access_token) - - self.refreshDevices() - self.refreshPlaylists() - - self.step = 4 - self.jukeboxDialog.data.sp_device = [] - self.jukeboxDialog.data.sp_playlists = [] - self.jukeboxDialog.data.sp_id = self.jukeboxDialog.data.id - self.jukeboxDialog.data.price = String(self.jukeboxDialog.data.price) - self.jukeboxDialog.show = true - }, - closeFormDialog() { - this.jukeboxDialog.data = {} - this.jukeboxDialog.show = false - this.step = 1 - }, - submitSpotifyKeys() { - self = this - self.jukeboxDialog.data.user = self.g.user.id - - LNbits.api - .request( - 'POST', - '/jukebox/api/v1/jukebox/', - self.g.user.wallets[0].adminkey, - self.jukeboxDialog.data - ) - .then(response => { - if (response.data) { - self.jukeboxDialog.data.sp_id = response.data.id - self.step = 3 - } - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - authAccess() { - self = this - self.requestAuthorization() - self.getSpotifyTokens() - self.$q.notify({ - spinner: true, - message: 'Processing', - timeout: 10000 - }) - }, - getSpotifyTokens() { - self = this - var counter = 0 - var timerId = setInterval(function () { - counter++ - if (!self.jukeboxDialog.data.sp_user) { - clearInterval(timerId) - } - LNbits.api - .request( - 'GET', - '/jukebox/api/v1/jukebox/' + self.jukeboxDialog.data.sp_id, - self.g.user.wallets[0].adminkey - ) - .then(response => { - if (response.data.sp_access_token) { - self.fetchAccessToken(response.data.sp_access_token) - if (self.jukeboxDialog.data.sp_access_token) { - self.refreshPlaylists() - self.refreshDevices() - console.log('this.devices') - console.log(self.devices) - console.log('this.devices') - setTimeout(function () { - if (self.devices.length < 1 || self.playlists.length < 1) { - self.$q.notify({ - spinner: true, - color: 'red', - message: - 'Error! Make sure Spotify is open on the device you wish to use, has playlists, and is playing something', - timeout: 10000 - }) - LNbits.api - .request( - 'DELETE', - '/jukebox/api/v1/jukebox/' + response.data.id, - self.g.user.wallets[0].adminkey - ) - .then(function (response) { - self.getJukeboxes() - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - clearInterval(timerId) - self.closeFormDialog() - } else { - self.step = 4 - clearInterval(timerId) - } - }, 2000) - } - } - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, 3000) - }, - requestAuthorization() { - self = this - var url = 'https://accounts.spotify.com/authorize' - url += '?client_id=' + self.jukeboxDialog.data.sp_user - url += '&response_type=code' - url += - '&redirect_uri=' + - encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id) - url += '&show_dialog=true' - url += - '&scope=user-read-private user-read-email user-modify-playback-state user-read-playback-position user-library-read streaming user-read-playback-state user-read-recently-played playlist-read-private' - - window.open(url) - }, - openNewDialog() { - this.jukeboxDialog.show = true - this.jukeboxDialog.data = {} - }, - createJukebox() { - self = this - self.jukeboxDialog.data.sp_playlists = self.jukeboxDialog.data.sp_playlists.join() - self.updateDB() - self.jukeboxDialog.show = false - self.getJukeboxes() - }, - updateDB() { - self = this - console.log(self.jukeboxDialog.data) - LNbits.api - .request( - 'PUT', - '/jukebox/api/v1/jukebox/' + this.jukeboxDialog.data.sp_id, - self.g.user.wallets[0].adminkey, - self.jukeboxDialog.data - ) - .then(function (response) { - console.log(response.data) - if ( - self.jukeboxDialog.data.sp_playlists && - self.jukeboxDialog.data.sp_devices - ) { - self.getJukeboxes() - // self.JukeboxLinks.push(mapJukebox(response.data)) - } - }) - }, - playlistApi(method, url, body) { - self = this - let xhr = new XMLHttpRequest() - xhr.open(method, url, true) - xhr.setRequestHeader('Content-Type', 'application/json') - xhr.setRequestHeader( - 'Authorization', - 'Bearer ' + this.jukeboxDialog.data.sp_access_token - ) - xhr.send(body) - xhr.onload = function () { - if (xhr.status == 401) { - self.refreshAccessToken() - self.playlistApi( - 'GET', - 'https://api.spotify.com/v1/me/playlists', - null - ) - } - let responseObj = JSON.parse(xhr.response) - self.jukeboxDialog.data.playlists = null - self.playlists = [] - self.jukeboxDialog.data.playlists = [] - var i - for (i = 0; i < responseObj.items.length; i++) { - self.playlists.push( - responseObj.items[i].name + '-' + responseObj.items[i].id - ) - } - console.log(self.playlists) - } - }, - refreshPlaylists() { - self = this - self.playlistApi('GET', 'https://api.spotify.com/v1/me/playlists', null) - }, - deviceApi(method, url, body) { - self = this - let xhr = new XMLHttpRequest() - xhr.open(method, url, true) - xhr.setRequestHeader('Content-Type', 'application/json') - xhr.setRequestHeader( - 'Authorization', - 'Bearer ' + this.jukeboxDialog.data.sp_access_token - ) - xhr.send(body) - xhr.onload = function () { - if (xhr.status == 401) { - self.refreshAccessToken() - self.deviceApi( - 'GET', - 'https://api.spotify.com/v1/me/player/devices', - null - ) - } - let responseObj = JSON.parse(xhr.response) - self.jukeboxDialog.data.devices = [] - - self.devices = [] - var i - for (i = 0; i < responseObj.devices.length; i++) { - self.devices.push( - responseObj.devices[i].name + '-' + responseObj.devices[i].id - ) - } - } - }, - refreshDevices() { - self = this - self.deviceApi( - 'GET', - 'https://api.spotify.com/v1/me/player/devices', - null - ) - }, - fetchAccessToken(code) { - self = this - let body = 'grant_type=authorization_code' - body += '&code=' + code - body += - '&redirect_uri=' + - encodeURI(self.locationcbPath + self.jukeboxDialog.data.sp_id) - - self.callAuthorizationApi(body) - }, - refreshAccessToken() { - self = this - let body = 'grant_type=refresh_token' - body += '&refresh_token=' + self.jukeboxDialog.data.sp_refresh_token - body += '&client_id=' + self.jukeboxDialog.data.sp_user - self.callAuthorizationApi(body) - }, - callAuthorizationApi(body) { - self = this - console.log( - btoa( - self.jukeboxDialog.data.sp_user + - ':' + - self.jukeboxDialog.data.sp_secret - ) - ) - let xhr = new XMLHttpRequest() - xhr.open('POST', 'https://accounts.spotify.com/api/token', true) - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') - xhr.setRequestHeader( - 'Authorization', - 'Basic ' + - btoa( - self.jukeboxDialog.data.sp_user + - ':' + - self.jukeboxDialog.data.sp_secret - ) - ) - xhr.send(body) - xhr.onload = function () { - let responseObj = JSON.parse(xhr.response) - if (responseObj.access_token) { - self.jukeboxDialog.data.sp_access_token = responseObj.access_token - self.jukeboxDialog.data.sp_refresh_token = responseObj.refresh_token - self.updateDB() - } - } - } - }, - created() { - console.log(this.g.user.wallets[0]) - var getJukeboxes = this.getJukeboxes - getJukeboxes() - this.selectedWallet = this.g.user.wallets[0] - this.locationcbPath = String( - [ - window.location.protocol, - '//', - window.location.host, - '/jukebox/api/v1/jukebox/spotify/cb/' - ].join('') - ) - this.locationcb = this.locationcbPath - } -}) diff --git a/lnbits/extensions/jukebox/static/js/jukebox.js b/lnbits/extensions/jukebox/static/js/jukebox.js deleted file mode 100644 index ddbb27646..000000000 --- a/lnbits/extensions/jukebox/static/js/jukebox.js +++ /dev/null @@ -1,14 +0,0 @@ -/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ - -Vue.component(VueQrcode.name, VueQrcode) - -new Vue({ - el: '#vue', - mixins: [windowMixin], - data() { - return {} - }, - computed: {}, - methods: {}, - created() {} -}) diff --git a/lnbits/extensions/jukebox/static/spotapi.gif b/lnbits/extensions/jukebox/static/spotapi.gif deleted file mode 100644 index 023efc9a9..000000000 Binary files a/lnbits/extensions/jukebox/static/spotapi.gif and /dev/null differ diff --git a/lnbits/extensions/jukebox/static/spotapi1.gif b/lnbits/extensions/jukebox/static/spotapi1.gif deleted file mode 100644 index 478032c56..000000000 Binary files a/lnbits/extensions/jukebox/static/spotapi1.gif and /dev/null differ diff --git a/lnbits/extensions/jukebox/tasks.py b/lnbits/extensions/jukebox/tasks.py deleted file mode 100644 index 65fca93dc..000000000 --- a/lnbits/extensions/jukebox/tasks.py +++ /dev/null @@ -1,28 +0,0 @@ -import json -import trio # type: ignore - -from lnbits.core.models import Payment -from lnbits.core.crud import create_payment -from lnbits.core import db as core_db -from lnbits.tasks import register_invoice_listener, internal_invoice_paid -from lnbits.helpers import urlsafe_short_hash - -from .crud import get_jukebox, update_jukebox_payment - - -async def register_listeners(): - invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) - register_invoice_listener(invoice_paid_chan_send) - await wait_for_paid_invoices(invoice_paid_chan_recv) - - -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: - await on_invoice_paid(payment) - - -async def on_invoice_paid(payment: Payment) -> None: - if "jukebox" != payment.extra.get("tag"): - # not a jukebox invoice - return - await update_jukebox_payment(payment.payment_hash, paid=True) diff --git a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html b/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html deleted file mode 100644 index f5a913130..000000000 --- a/lnbits/extensions/jukebox/templates/jukebox/_api_docs.html +++ /dev/null @@ -1,125 +0,0 @@ - - To use this extension you need a Spotify client ID and client secret. You get - these by creating an app in the Spotify developers dashboard - here - -

Select the playlists you want people to be able to pay for, share - the frontend page, profit :)

- Made by, - benarc. - Inspired by, - pirosb3. -
- - - - - - GET /jukebox/api/v1/jukebox -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<jukebox_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/jukebox -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" - -
-
-
- - - - GET - /jukebox/api/v1/jukebox/<juke_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- <jukebox_object> -
Curl example
- curl -X GET {{ request.url_root }}api/v1/jukebox/<juke_id> -H - "X-Api-Key: {{ g.user.wallets[0].adminkey }}" - -
-
-
- - - - POST/PUT - /jukebox/api/v1/jukebox/ -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- <jukbox_object> -
Curl example
- curl -X POST {{ request.url_root }}api/v1/jukebox/ -d '{"user": - <string, user_id>, "title": <string>, - "wallet":<string>, "sp_user": <string, - spotify_user_account>, "sp_secret": <string, - spotify_user_secret>, "sp_access_token": <string, - not_required>, "sp_refresh_token": <string, not_required>, - "sp_device": <string, spotify_user_secret>, "sp_playlists": - <string, not_required>, "price": <integer, not_required>}' - -H "Content-type: application/json" -H "X-Api-Key: - {{g.user.wallets[0].adminkey }}" - -
-
-
- - - - DELETE - /jukebox/api/v1/jukebox/<juke_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- <jukebox_object> -
Curl example
- curl -X DELETE {{ request.url_root }}api/v1/jukebox/<juke_id> - -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" - -
-
-
diff --git a/lnbits/extensions/jukebox/templates/jukebox/error.html b/lnbits/extensions/jukebox/templates/jukebox/error.html deleted file mode 100644 index f6f7fd584..000000000 --- a/lnbits/extensions/jukebox/templates/jukebox/error.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
-

Jukebox error

-
- - -
- Ask the host to turn on the device and launch spotify -
-
-
-
-
-
- - {% endblock %} {% block scripts %} - - - - {% endblock %} -
diff --git a/lnbits/extensions/jukebox/templates/jukebox/index.html b/lnbits/extensions/jukebox/templates/jukebox/index.html deleted file mode 100644 index 9b4efbd5c..000000000 --- a/lnbits/extensions/jukebox/templates/jukebox/index.html +++ /dev/null @@ -1,368 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - Add Spotify Jukebox - - {% raw %} - - - - - - - {% endraw %} - - -
- -
- - -
- {{SITE_TITLE}} jukebox extension -
-
- - - {% include "jukebox/_api_docs.html" %} - -
-
- - - - - - - - - -
-
- Continue - Continue -
-
- Cancel -
-
- -
-
- - - - To use this extension you need a Spotify client ID and client secret. - You get these by creating an app in the Spotify developers dashboard - here. - - - - - - - -
-
- Submit keys - Submit keys -
-
- Cancel -
-
- -
-
- - - - In the app go to edit-settings, set the redirect URI to this link -
- {% raw %}{{ locationcb }}{{ jukeboxDialog.data.sp_id }}{% endraw - %} Click to copy URL - -
- Settings can be found - here. - -
-
- Authorise access - Authorise access -
-
- Cancel -
-
- -
-
- - - - -
-
- Create Jukebox - Create Jukebox -
-
- Cancel -
-
-
-
-
-
- - - -
-
Shareable Jukebox QR
-
- - - -
- - Copy jukebox link - Open jukebox - Close -
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - - -{% endblock %} diff --git a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html b/lnbits/extensions/jukebox/templates/jukebox/jukebox.html deleted file mode 100644 index cb3ab49d8..000000000 --- a/lnbits/extensions/jukebox/templates/jukebox/jukebox.html +++ /dev/null @@ -1,277 +0,0 @@ -{% extends "public.html" %} {% block page %} {% raw %} -
-
- - -

Currently playing

-
-
- -
-
- {{ currentPlay.name }}
- {{ currentPlay.artist }} -
-
-
-
- - - -

Pick a song

- - -
- - - - - - -
-
- - - - -
-
- -
-
- {{ receive.name }}
- {{ receive.artist }} -
-
-
-
-
- Play for {% endraw %}{{ price }}{% raw %} sats - -
-
-
- - - - - -
- Copy invoice -
-
-
-
-{% endraw %} {% endblock %} {% block scripts %} - - - -{% endblock %} diff --git a/lnbits/extensions/jukebox/views.py b/lnbits/extensions/jukebox/views.py deleted file mode 100644 index c38b24b37..000000000 --- a/lnbits/extensions/jukebox/views.py +++ /dev/null @@ -1,45 +0,0 @@ -import time -from datetime import datetime -from quart import g, render_template, request, jsonify, websocket -from http import HTTPStatus -import trio -from lnbits.decorators import check_user_exists, validate_uuids -from lnbits.core.models import Payment - -import json -from . import jukebox_ext -from .crud import get_jukebox -from .views_api import api_get_jukebox_device_check -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@jukebox_ext.get("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("jukebox/index.html", {"request": request,"user":g.user}) - - -@jukebox_ext.get("/") -async def connect_to_jukebox(request: Request, juke_id): - jukebox = await get_jukebox(juke_id) - if not jukebox: - return "error" - deviceCheck = await api_get_jukebox_device_check(juke_id) - devices = json.loads(deviceCheck[0].text) - deviceConnected = False - for device in devices["devices"]: - if device["id"] == jukebox.sp_device.split("-")[1]: - deviceConnected = True - if deviceConnected: - return await templates.TemplateResponse( - "jukebox/jukebox.html", - playlists=jukebox.sp_playlists.split(","), - juke_id=juke_id, - price=jukebox.price, - inkey=jukebox.inkey, - ) - else: - return await templates.TemplateResponse("jukebox/error.html",{"request": request}) diff --git a/lnbits/extensions/jukebox/views_api.py b/lnbits/extensions/jukebox/views_api.py deleted file mode 100644 index 4c5dc40af..000000000 --- a/lnbits/extensions/jukebox/views_api.py +++ /dev/null @@ -1,490 +0,0 @@ -from quart import g, jsonify, request -from http import HTTPStatus -import base64 -from lnbits.core.crud import get_wallet -from lnbits.core.services import create_invoice, check_invoice_status -import json -from typing import Optional -from pydantic import BaseModel - - -from lnbits.decorators import api_check_wallet_key, api_validate_post_request -import httpx -from . import jukebox_ext -from .crud import ( - create_jukebox, - update_jukebox, - get_jukebox, - get_jukeboxs, - delete_jukebox, - create_jukebox_payment, - get_jukebox_payment, - update_jukebox_payment, -) -from lnbits.core.services import create_invoice, check_invoice_status -from fastapi.encoders import jsonable_encoder - - -@jukebox_ext.get("/api/v1/jukebox") -@api_check_wallet_key("admin") -async def api_get_jukeboxs(): - try: - return ( - [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)], - HTTPStatus.OK, - ) - except: - return "", HTTPStatus.NO_CONTENT - - -##################SPOTIFY AUTH##################### - - -@jukebox_ext.get("/api/v1/jukebox/spotify/cb/{juke_id}") -async def api_check_credentials_callbac(juke_id): - sp_code = "" - sp_access_token = "" - sp_refresh_token = "" - try: - jukebox = await get_jukebox(juke_id) - except: - return ( - {"error": "No Jukebox"}, - HTTPStatus.FORBIDDEN, - ) - if request.args.get("code"): - sp_code = request.args.get("code") - jukebox = await update_jukebox( - juke_id=juke_id, sp_secret=jukebox.sp_secret, sp_access_token=sp_code - ) - if request.args.get("access_token"): - sp_access_token = request.args.get("access_token") - sp_refresh_token = request.args.get("refresh_token") - jukebox = await update_jukebox( - juke_id=juke_id, - sp_secret=jukebox.sp_secret, - sp_access_token=sp_access_token, - sp_refresh_token=sp_refresh_token, - ) - return "

Success!

You can close this window

" - - -@jukebox_ext.get("/api/v1/jukebox/{juke_id}") -@api_check_wallet_key("admin") -async def api_check_credentials_check(juke_id): - jukebox = await get_jukebox(juke_id) - return jsonify(jukebox._asdict()), HTTPStatus.CREATED - - -class CreateData(BaseModel): - user: str = None - title: str = None - wallet: str = None - sp_user: str = None - sp_secret: str = None - sp_access_token: Optional[str] = None - sp_refresh_token: Optional[str] = None - sp_device: Optional[str] = None - sp_playlists: Optional[str] = None - price: Optional[str] = None - -@jukebox_ext.post("/api/v1/jukebox/") -@jukebox_ext.put("/api/v1/jukebox/{juke_id}") -@api_check_wallet_key("admin") -async def api_create_update_jukebox(data: CreateData, juke_id=None): - if juke_id: - jukebox = await update_jukebox(juke_id=juke_id, inkey=g.wallet.inkey, **data) - else: - jukebox = await create_jukebox(inkey=g.wallet.inkey, **data) - - return jukebox._asdict(), HTTPStatus.CREATED - - -@jukebox_ext.delete("/api/v1/jukebox/{juke_id}") -@api_check_wallet_key("admin") -async def api_delete_item(juke_id): - await delete_jukebox(juke_id) - try: - return ( - [{**jukebox._asdict()} for jukebox in await get_jukeboxs(g.wallet.user)] - , - HTTPStatus.OK, - ) - except: - return "", HTTPStatus.NO_CONTENT - - -################JUKEBOX ENDPOINTS################## - -######GET ACCESS TOKEN###### - - -@jukebox_ext.get( - "/api/v1/jukebox/jb/playlist/{juke_id}/{sp_playlist}" -) -async def api_get_jukebox_song(juke_id, sp_playlist, retry=False): - try: - jukebox = await get_jukebox(juke_id) - except: - return ( - jsonable_encoder({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, - ) - tracks = [] - async with httpx.AsyncClient() as client: - try: - r = await client.get( - "https://api.spotify.com/v1/playlists/" + sp_playlist + "/tracks", - timeout=40, - headers={"Authorization": "Bearer " + jukebox.sp_access_token}, - ) - if "items" not in r.json(): - if r.status_code == 401: - token = await api_get_token(juke_id) - if token == False: - return False - elif retry: - return ( - jsonable_encoder({"error": "Failed to get auth"}), - HTTPStatus.FORBIDDEN, - ) - else: - return await api_get_jukebox_song( - juke_id, sp_playlist, retry=True - ) - return r, HTTPStatus.OK - for item in r.json()["items"]: - tracks.append( - { - "id": item["track"]["id"], - "name": item["track"]["name"], - "album": item["track"]["album"]["name"], - "artist": item["track"]["artists"][0]["name"], - "image": item["track"]["album"]["images"][0]["url"], - } - ) - except AssertionError: - something = None - return [track for track in tracks] - - -async def api_get_token(juke_id): - try: - jukebox = await get_jukebox(juke_id) - except: - return ( - jsonable_encoder({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, - ) - - async with httpx.AsyncClient() as client: - try: - r = await client.post( - "https://accounts.spotify.com/api/token", - timeout=40, - params={ - "grant_type": "refresh_token", - "refresh_token": jukebox.sp_refresh_token, - "client_id": jukebox.sp_user, - }, - headers={ - "Content-Type": "application/x-www-form-urlencoded", - "Authorization": "Basic " - + base64.b64encode( - str(jukebox.sp_user + ":" + jukebox.sp_secret).encode("ascii") - ).decode("ascii"), - "Content-Type": "application/x-www-form-urlencoded", - }, - ) - if "access_token" not in r.json(): - return False - else: - await update_jukebox( - juke_id=juke_id, sp_access_token=r.json()["access_token"] - ) - except AssertionError: - something = None - return True - - -######CHECK DEVICE - - -@jukebox_ext.get("/api/v1/jukebox/jb/{juke_id}") -async def api_get_jukebox_device_check(juke_id, retry=False): - try: - jukebox = await get_jukebox(juke_id) - except: - return ( - {"error": "No Jukebox"}, - HTTPStatus.FORBIDDEN, - ) - async with httpx.AsyncClient() as client: - rDevice = await client.get( - "https://api.spotify.com/v1/me/player/devices", - timeout=40, - headers={"Authorization": "Bearer " + jukebox.sp_access_token}, - ) - - if rDevice.status_code == 204 or rDevice.status_code == 200: - return ( - rDevice, - HTTPStatus.OK, - ) - elif rDevice.status_code == 401 or rDevice.status_code == 403: - token = await api_get_token(juke_id) - if token == False: - return ( - jsonable_encoder({"error": "No device connected"}), - HTTPStatus.FORBIDDEN, - ) - elif retry: - return ( - jsonable_encoder({"error": "Failed to get auth"}), - HTTPStatus.FORBIDDEN, - ) - else: - return api_get_jukebox_device_check(juke_id, retry=True) - else: - return ( - jsonable_encoder({"error": "No device connected"}), - HTTPStatus.FORBIDDEN, - ) - - -######GET INVOICE STUFF - - -@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: - return ( - {"error": "No Jukebox"}, - HTTPStatus.FORBIDDEN, - ) - try: - deviceCheck = await api_get_jukebox_device_check(juke_id) - devices = json.loads(deviceCheck[0].text) - deviceConnected = False - for device in devices["devices"]: - if device["id"] == jukebox.sp_device.split("-")[1]: - deviceConnected = True - if not deviceConnected: - return ( - {"error": "No device connected"}, - HTTPStatus.NOT_FOUND, - ) - except: - return ( - {"error": "No device connected"}, - HTTPStatus.NOT_FOUND, - ) - - invoice = await create_invoice( - wallet_id=jukebox.wallet, - amount=jukebox.price, - memo=jukebox.title, - extra={"tag": "jukebox"}, - ) - - jukebox_payment = await create_jukebox_payment(song_id, invoice[0], juke_id) - - return invoice, jukebox_payment - - -@jukebox_ext.get( - "/api/v1/jukebox/jb/checkinvoice/{pay_hash}/{juke_id}" -) -async def api_get_jukebox_invoice_check(pay_hash, juke_id): - try: - jukebox = await get_jukebox(juke_id) - except: - return ( - {"error": "No Jukebox"}, - HTTPStatus.FORBIDDEN, - ) - try: - status = await check_invoice_status(jukebox.wallet, pay_hash) - is_paid = not status.pending - except Exception as exc: - return {"paid": False}, HTTPStatus.OK - if is_paid: - wallet = await get_wallet(jukebox.wallet) - payment = await wallet.get_payment(pay_hash) - await payment.set_pending(False) - await update_jukebox_payment(pay_hash, paid=True) - return {"paid": True}, HTTPStatus.OK - return {"paid": False}, HTTPStatus.OK - - -@jukebox_ext.get( - "/api/v1/jukebox/jb/invoicep/{song_id}/{juke_id}/{pay_hash}" -) -async def api_get_jukebox_invoice_paid(song_id, juke_id, pay_hash, retry=False): - try: - jukebox = await get_jukebox(juke_id) - except: - return ( - jsonify({"error": "No Jukebox"}), - HTTPStatus.FORBIDDEN, - ) - await api_get_jukebox_invoice_check(pay_hash, juke_id) - jukebox_payment = await get_jukebox_payment(pay_hash) - if jukebox_payment.paid: - async with httpx.AsyncClient() as client: - r = await client.get( - "https://api.spotify.com/v1/me/player/currently-playing?market=ES", - timeout=40, - headers={"Authorization": "Bearer " + jukebox.sp_access_token}, - ) - rDevice = await client.get( - "https://api.spotify.com/v1/me/player", - timeout=40, - headers={"Authorization": "Bearer " + jukebox.sp_access_token}, - ) - isPlaying = False - if rDevice.status_code == 200: - isPlaying = rDevice.json()["is_playing"] - - if r.status_code == 204 or isPlaying == False: - async with httpx.AsyncClient() as client: - uri = ["spotify:track:" + song_id] - r = await client.put( - "https://api.spotify.com/v1/me/player/play?device_id=" - + jukebox.sp_device.split("-")[1], - json={"uris": uri}, - timeout=40, - headers={"Authorization": "Bearer " + jukebox.sp_access_token}, - ) - if r.status_code == 204: - return jukebox_payment, HTTPStatus.OK - elif r.status_code == 401 or r.status_code == 403: - token = await api_get_token(juke_id) - if token == False: - return ( - {"error": "Invoice not paid"}, - HTTPStatus.FORBIDDEN, - ) - elif retry: - return ( - {"error": "Failed to get auth"}, - HTTPStatus.FORBIDDEN, - ) - else: - return api_get_jukebox_invoice_paid( - song_id, juke_id, pay_hash, retry=True - ) - else: - return ( - {"error": "Invoice not paid"}, - HTTPStatus.FORBIDDEN, - ) - elif r.status_code == 200: - async with httpx.AsyncClient() as client: - r = await client.post( - "https://api.spotify.com/v1/me/player/queue?uri=spotify%3Atrack%3A" - + song_id - + "&device_id=" - + jukebox.sp_device.split("-")[1], - timeout=40, - headers={"Authorization": "Bearer " + jukebox.sp_access_token}, - ) - if r.status_code == 204: - return jukebox_payment, HTTPStatus.OK - - elif r.status_code == 401 or r.status_code == 403: - token = await api_get_token(juke_id) - if token == False: - return ( - {"error": "Invoice not paid"}, - HTTPStatus.OK, - ) - elif retry: - return ( - {"error": "Failed to get auth"}, - HTTPStatus.FORBIDDEN, - ) - else: - return await api_get_jukebox_invoice_paid( - song_id, juke_id, pay_hash - ) - else: - return ( - {"error": "Invoice not paid"}, - HTTPStatus.OK, - ) - elif r.status_code == 401 or r.status_code == 403: - token = await api_get_token(juke_id) - if token == False: - return ( - {"error": "Invoice not paid"}, - HTTPStatus.OK, - ) - elif retry: - return ( - {"error": "Failed to get auth"}, - HTTPStatus.FORBIDDEN, - ) - else: - return await api_get_jukebox_invoice_paid( - song_id, juke_id, pay_hash - ) - return {"error": "Invoice not paid"}, HTTPStatus.OK - - -############################GET TRACKS - - -@jukebox_ext.get("/api/v1/jukebox/jb/currently/{juke_id}") -async def api_get_jukebox_currently(juke_id, retry=False): - try: - jukebox = await get_jukebox(juke_id) - except: - return ( - {"error": "No Jukebox"}, - HTTPStatus.FORBIDDEN, - ) - async with httpx.AsyncClient() as client: - try: - r = await client.get( - "https://api.spotify.com/v1/me/player/currently-playing?market=ES", - timeout=40, - headers={"Authorization": "Bearer " + jukebox.sp_access_token}, - ) - if r.status_code == 204: - return {"error": "Nothing"}, HTTPStatus.OK - elif r.status_code == 200: - try: - response = r.json() - - track = { - "id": response["item"]["id"], - "name": response["item"]["name"], - "album": response["item"]["album"]["name"], - "artist": response["item"]["artists"][0]["name"], - "image": response["item"]["album"]["images"][0]["url"], - } - return track, HTTPStatus.OK - except: - return "Something went wrong", HTTPStatus.NOT_FOUND - - elif r.status_code == 401: - token = await api_get_token(juke_id) - if token == False: - return ( - {"error": "Invoice not paid"}, - HTTPStatus.FORBIDDEN, - ) - elif retry: - return ( - {"error": "Failed to get auth"}, - HTTPStatus.FORBIDDEN, - ) - else: - return await api_get_jukebox_currently(juke_id, retry=True) - else: - return "Something went wrong", HTTPStatus.NOT_FOUND - except AssertionError: - return "Something went wrong", HTTPStatus.NOT_FOUND diff --git a/lnbits/extensions/livestream/README.md b/lnbits/extensions/livestream/README.md deleted file mode 100644 index 4e88e7bc7..000000000 --- a/lnbits/extensions/livestream/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# DJ Livestream - -## Help DJ's and music producers conduct music livestreams - -LNBits Livestream extension produces a static QR code that can be shown on screen while livestreaming a DJ set for example. If someone listening to the livestream likes a song and want to support the DJ and/or the producer he can scan the QR code with a LNURL-pay capable wallet. - -When scanned, the QR code sends up information about the song playing at the moment (name and the producer of that song). Also, if the user likes the song and would like to support the producer, he can send a tip and a message for that specific track. If the user sends an amount over a specific threshold they will be given a link to download it (optional). - -The revenue will be sent to a wallet created specifically for that producer, with optional revenue splitting between the DJ and the producer. - -[**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets) - -[![video tutorial livestream](http://img.youtube.com/vi/zDrSWShKz7k/0.jpg)](https://youtu.be/zDrSWShKz7k 'video tutorial offline shop') - -## Usage - -1. Start by adding a track\ - ![add new track](https://i.imgur.com/Cu0eGrW.jpg) - - set the producer, or choose an existing one - - set the track name - - define a minimum price where a user can download the track - - set the download URL, where user will be redirected if he tips the livestream and the tip is equal or above the set price\ - ![track settings](https://i.imgur.com/HTJYwcW.jpg) -2. Adjust the percentage of the pay you want to take from the user's tips. 10%, the default, means that the DJ will keep 10% of all the tips sent by users. The other 90% will go to an auto generated producer wallet\ - ![adjust percentage](https://i.imgur.com/9weHKAB.jpg) -3. For every different producer added, when adding tracks, a wallet is generated for them\ - ![producer wallet](https://i.imgur.com/YFIZ7Tm.jpg) -4. On the bottom of the LNBits DJ Livestream extension you'll find the static QR code ([LNURL-pay](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/lnurlp/README.md)) you can add to the livestream or if you're a street performer you can print it and have it displayed -5. After all tracks and producers are added, you can start "playing" songs\ - ![play tracks](https://i.imgur.com/7ytiBkq.jpg) -6. You'll see the current track playing and a green icon indicating active track also\ - ![active track](https://i.imgur.com/W1vBz54.jpg) -7. When a user scans the QR code, and sends a tip, you'll receive 10%, in the example case, in your wallet and the producer's wallet will get the rest. For example someone tips 100 sats, you'll get 10 sats and the producer will get 90 sats - - producer's wallet receiving 18 sats from 20 sats tips\ - ![producer wallet](https://i.imgur.com/OM9LawA.jpg) - -## Use cases - -You can print the QR code and display it on a live gig, a street performance, etc... OR you can use the QR as an overlay in an online stream of you playing music, doing a DJ set, making a podcast. - -You can use the extension's API to trigger updates for the current track, update fees, add tracks... - -## Sponsored by - -[![](https://cdn.shopify.com/s/files/1/0826/9235/files/cryptograffiti_logo_clear_background.png?v=1504730421)](https://cryptograffiti.com/) diff --git a/lnbits/extensions/livestream/__init__.py b/lnbits/extensions/livestream/__init__.py deleted file mode 100644 index d8f61fe0a..000000000 --- a/lnbits/extensions/livestream/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -from quart import Blueprint - -from lnbits.db import Database - -db = Database("ext_livestream") - -livestream_ext: Blueprint = Blueprint( - "livestream", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa -from .lnurl import * # noqa -from .tasks import register_listeners - -from lnbits.tasks import record_async - -livestream_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/livestream/config.json b/lnbits/extensions/livestream/config.json deleted file mode 100644 index 12ba6b797..000000000 --- a/lnbits/extensions/livestream/config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "DJ Livestream", - "short_description": "Sell tracks and split revenue (lnurl-pay)", - "icon": "speaker", - "contributors": [ - "fiatjaf", - "cryptograffiti" - ], - "hidden": false -} diff --git a/lnbits/extensions/livestream/crud.py b/lnbits/extensions/livestream/crud.py deleted file mode 100644 index 47854dbdc..000000000 --- a/lnbits/extensions/livestream/crud.py +++ /dev/null @@ -1,199 +0,0 @@ -from typing import List, Optional - -from lnbits.core.crud import create_account, create_wallet -from lnbits.db import SQLITE -from . import db -from .models import Livestream, Track, Producer - - -async def create_livestream(*, wallet_id: str) -> int: - returning = "" if db.type == SQLITE else "RETURNING ID" - method = db.execute if db.type == SQLITE else db.fetchone - - result = await (method)( - f""" - INSERT INTO livestream.livestreams (wallet) - VALUES (?) - {returning} - """, - (wallet_id,), - ) - - if db.type == SQLITE: - return result._result_proxy.lastrowid - else: - return result[0] - - -async def get_livestream(ls_id: int) -> Optional[Livestream]: - row = await db.fetchone( - "SELECT * FROM livestream.livestreams WHERE id = ?", (ls_id,) - ) - return Livestream(**dict(row)) if row else None - - -async def get_livestream_by_track(track_id: int) -> Optional[Livestream]: - row = await db.fetchone( - """ - SELECT livestreams.* FROM livestream.livestreams - INNER JOIN tracks ON tracks.livestream = livestreams.id - WHERE tracks.id = ? - """, - (track_id,), - ) - return Livestream(**dict(row)) if row else None - - -async def get_or_create_livestream_by_wallet(wallet: str) -> Optional[Livestream]: - row = await db.fetchone( - "SELECT * FROM livestream.livestreams WHERE wallet = ?", (wallet,) - ) - - if not row: - # create on the fly - ls_id = await create_livestream(wallet_id=wallet) - return await get_livestream(ls_id) - - return Livestream(**dict(row)) if row else None - - -async def update_current_track(ls_id: int, track_id: Optional[int]): - await db.execute( - "UPDATE livestream.livestreams SET current_track = ? WHERE id = ?", - (track_id, ls_id), - ) - - -async def update_livestream_fee(ls_id: int, fee_pct: int): - await db.execute( - "UPDATE livestream.livestreams SET fee_pct = ? WHERE id = ?", - (fee_pct, ls_id), - ) - - -async def add_track( - livestream: int, - name: str, - download_url: Optional[str], - price_msat: int, - producer: Optional[int], -) -> int: - result = await db.execute( - """ - INSERT INTO livestream.tracks (livestream, name, download_url, price_msat, producer) - VALUES (?, ?, ?, ?, ?) - """, - (livestream, name, download_url, price_msat, producer), - ) - return result._result_proxy.lastrowid - - -async def update_track( - livestream: int, - track_id: int, - name: str, - download_url: Optional[str], - price_msat: int, - producer: int, -) -> int: - result = await db.execute( - """ - UPDATE livestream.tracks SET - name = ?, - download_url = ?, - price_msat = ?, - producer = ? - WHERE livestream = ? AND id = ? - """, - (name, download_url, price_msat, producer, livestream, track_id), - ) - return result._result_proxy.lastrowid - - -async def get_track(track_id: Optional[int]) -> Optional[Track]: - if not track_id: - return None - - row = await db.fetchone( - """ - SELECT id, download_url, price_msat, name, producer - FROM livestream.tracks WHERE id = ? - """, - (track_id,), - ) - return Track(**dict(row)) if row else None - - -async def get_tracks(livestream: int) -> List[Track]: - rows = await db.fetchall( - """ - SELECT id, download_url, price_msat, name, producer - FROM livestream.tracks WHERE livestream = ? - """, - (livestream,), - ) - return [Track(**dict(row)) for row in rows] - - -async def delete_track_from_livestream(livestream: int, track_id: int): - await db.execute( - """ - DELETE FROM livestream.tracks WHERE livestream = ? AND id = ? - """, - (livestream, track_id), - ) - - -async def add_producer(livestream: int, name: str) -> int: - name = name.strip() - - existing = await db.fetchall( - """ - SELECT id FROM livestream.producers - WHERE livestream = ? AND lower(name) = ? - """, - (livestream, name.lower()), - ) - if existing: - return existing[0].id - - user = await create_account() - wallet = await create_wallet(user_id=user.id, wallet_name="livestream: " + name) - - returning = "" if db.type == SQLITE else "RETURNING ID" - method = db.execute if db.type == SQLITE else db.fetchone - - result = await method( - f""" - INSERT INTO livestream.producers (livestream, name, "user", wallet) - VALUES (?, ?, ?, ?) - {returning} - """, - (livestream, name, user.id, wallet.id), - ) - if db.type == SQLITE: - return result._result_proxy.lastrowid - else: - return result[0] - - -async def get_producer(producer_id: int) -> Optional[Producer]: - row = await db.fetchone( - """ - SELECT id, "user", wallet, name - FROM livestream.producers WHERE id = ? - """, - (producer_id,), - ) - return Producer(**dict(row)) if row else None - - -async def get_producers(livestream: int) -> List[Producer]: - rows = await db.fetchall( - """ - SELECT id, "user", wallet, name - FROM livestream.producers WHERE livestream = ? - """, - (livestream,), - ) - return [Producer(**dict(row)) for row in rows] diff --git a/lnbits/extensions/livestream/lnurl.py b/lnbits/extensions/livestream/lnurl.py deleted file mode 100644 index 3b9e7e316..000000000 --- a/lnbits/extensions/livestream/lnurl.py +++ /dev/null @@ -1,114 +0,0 @@ -import hashlib -import math -from quart import jsonify, url_for, request -from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore - -from lnbits.core.services import create_invoice - -from . import livestream_ext -from .crud import get_livestream, get_livestream_by_track, get_track - - -@livestream_ext.route("/lnurl/", methods=["GET"]) -async def lnurl_livestream(ls_id): - ls = await get_livestream(ls_id) - if not ls: - return jsonify({"status": "ERROR", "reason": "Livestream not found."}) - - track = await get_track(ls.current_track) - if not track: - return jsonify({"status": "ERROR", "reason": "This livestream is offline."}) - - resp = LnurlPayResponse( - callback=url_for( - "livestream.lnurl_callback", track_id=track.id, _external=True - ), - min_sendable=track.min_sendable, - max_sendable=track.max_sendable, - metadata=await track.lnurlpay_metadata(), - ) - - params = resp.dict() - params["commentAllowed"] = 300 - - return jsonify(params) - - -@livestream_ext.route("/lnurl/t/", methods=["GET"]) -async def lnurl_track(track_id): - track = await get_track(track_id) - if not track: - return jsonify({"status": "ERROR", "reason": "Track not found."}) - - resp = LnurlPayResponse( - callback=url_for( - "livestream.lnurl_callback", track_id=track.id, _external=True - ), - min_sendable=track.min_sendable, - max_sendable=track.max_sendable, - metadata=await track.lnurlpay_metadata(), - ) - - params = resp.dict() - params["commentAllowed"] = 300 - - return jsonify(params) - - -@livestream_ext.route("/lnurl/cb/", methods=["GET"]) -async def lnurl_callback(track_id): - track = await get_track(track_id) - if not track: - return jsonify({"status": "ERROR", "reason": "Couldn't find track."}) - - amount_received = int(request.args.get("amount") or 0) - - if amount_received < track.min_sendable: - return ( - jsonify( - LnurlErrorResponse( - reason=f"Amount {round(amount_received / 1000)} is smaller than minimum {math.floor(track.min_sendable)}." - ).dict() - ), - ) - elif track.max_sendable < amount_received: - return ( - jsonify( - LnurlErrorResponse( - reason=f"Amount {round(amount_received / 1000)} is greater than maximum {math.floor(track.max_sendable)}." - ).dict() - ), - ) - - comment = request.args.get("comment") - if len(comment or "") > 300: - return jsonify( - LnurlErrorResponse( - reason=f"Got a comment with {len(comment)} characters, but can only accept 300" - ).dict() - ) - - ls = await get_livestream_by_track(track_id) - - payment_hash, payment_request = await create_invoice( - wallet_id=ls.wallet, - amount=int(amount_received / 1000), - memo=await track.fullname(), - description_hash=hashlib.sha256( - (await track.lnurlpay_metadata()).encode("utf-8") - ).digest(), - extra={"tag": "livestream", "track": track.id, "comment": comment}, - ) - - if amount_received < track.price_msat: - success_action = None - else: - success_action = track.success_action(payment_hash) - - resp = LnurlPayActionResponse( - pr=payment_request, - success_action=success_action, - routes=[], - ) - - return jsonify(resp.dict()) diff --git a/lnbits/extensions/livestream/migrations.py b/lnbits/extensions/livestream/migrations.py deleted file mode 100644 index fb664ab16..000000000 --- a/lnbits/extensions/livestream/migrations.py +++ /dev/null @@ -1,39 +0,0 @@ -async def m001_initial(db): - """ - Initial livestream tables. - """ - await db.execute( - f""" - CREATE TABLE livestream.livestreams ( - id {db.serial_primary_key}, - wallet TEXT NOT NULL, - fee_pct INTEGER NOT NULL DEFAULT 10, - current_track INTEGER - ); - """ - ) - - await db.execute( - f""" - CREATE TABLE livestream.producers ( - livestream INTEGER NOT NULL REFERENCES {db.references_schema}livestreams (id), - id {db.serial_primary_key}, - "user" TEXT NOT NULL, - wallet TEXT NOT NULL, - name TEXT NOT NULL - ); - """ - ) - - await db.execute( - f""" - CREATE TABLE livestream.tracks ( - livestream INTEGER NOT NULL REFERENCES {db.references_schema}livestreams (id), - id {db.serial_primary_key}, - download_url TEXT, - price_msat INTEGER NOT NULL DEFAULT 0, - name TEXT, - producer INTEGER REFERENCES {db.references_schema}producers (id) NOT NULL - ); - """ - ) diff --git a/lnbits/extensions/livestream/models.py b/lnbits/extensions/livestream/models.py deleted file mode 100644 index 7df1f961a..000000000 --- a/lnbits/extensions/livestream/models.py +++ /dev/null @@ -1,82 +0,0 @@ -import json -from quart import url_for -from typing import Optional -from lnurl import Lnurl, encode as lnurl_encode # type: ignore -from lnurl.types import LnurlPayMetadata # type: ignore -from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore -from sqlite3 import Row -from pydantic import BaseModel - -class Livestream(BaseModel): - id: int - wallet: str - fee_pct: int - current_track: Optional[int] - - @property - def lnurl(self) -> Lnurl: - url = url_for("livestream.lnurl_livestream", ls_id=self.id, _external=True) - return lnurl_encode(url) - - -class Track(BaseModel): - id: int - download_url: str - price_msat: int - name: str - producer: int - - @property - def min_sendable(self) -> int: - return min(100_000, self.price_msat or 100_000) - - @property - def max_sendable(self) -> int: - return max(50_000_000, self.price_msat * 5) - - @property - def lnurl(self) -> Lnurl: - url = url_for("livestream.lnurl_track", track_id=self.id, _external=True) - return lnurl_encode(url) - - async def fullname(self) -> str: - from .crud import get_producer - - producer = await get_producer(self.producer) - if producer: - producer_name = producer.name - else: - producer_name = "unknown author" - - return f"'{self.name}', from {producer_name}." - - async def lnurlpay_metadata(self) -> LnurlPayMetadata: - description = ( - await self.fullname() - ) + " Like this track? Send some sats in appreciation." - - if self.download_url: - description += f" Send {round(self.price_msat/1000)} sats or more and you can download it." - - return LnurlPayMetadata(json.dumps([["text/plain", description]])) - - def success_action(self, payment_hash: str) -> Optional[LnurlPaySuccessAction]: - if not self.download_url: - return None - - return UrlAction( - url=url_for( - "livestream.track_redirect_download", - track_id=self.id, - p=payment_hash, - _external=True, - ), - description=f"Download the track {self.name}!", - ) - - -class Producer(BaseModel): - id: int - user: str - wallet: str - name: str diff --git a/lnbits/extensions/livestream/static/js/index.js b/lnbits/extensions/livestream/static/js/index.js deleted file mode 100644 index c49befce2..000000000 --- a/lnbits/extensions/livestream/static/js/index.js +++ /dev/null @@ -1,216 +0,0 @@ -/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ - -Vue.component(VueQrcode.name, VueQrcode) - -new Vue({ - el: '#vue', - mixins: [windowMixin], - data() { - return { - cancelListener: () => {}, - selectedWallet: null, - nextCurrentTrack: null, - livestream: { - tracks: [], - producers: [] - }, - trackDialog: { - show: false, - data: {} - } - } - }, - computed: { - sortedTracks() { - return this.livestream.tracks.sort((a, b) => a.name - b.name) - }, - tracksMap() { - return Object.fromEntries( - this.livestream.tracks.map(track => [track.id, track]) - ) - }, - producersMap() { - return Object.fromEntries( - this.livestream.producers.map(prod => [prod.id, prod]) - ) - } - }, - methods: { - getTrackLabel(trackId) { - if (!trackId) return - let track = this.tracksMap[trackId] - return `${track.name}, ${this.producersMap[track.producer].name}` - }, - disabledAddTrackButton() { - return ( - !this.trackDialog.data.name || - this.trackDialog.data.name.length === 0 || - !this.trackDialog.data.producer || - this.trackDialog.data.producer.length === 0 - ) - }, - changedWallet(wallet) { - this.selectedWallet = wallet - this.loadLivestream() - this.startPaymentNotifier() - }, - loadLivestream() { - LNbits.api - .request( - 'GET', - '/livestream/api/v1/livestream', - this.selectedWallet.inkey - ) - .then(response => { - this.livestream = response.data - this.nextCurrentTrack = this.livestream.current_track - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - startPaymentNotifier() { - this.cancelListener() - - this.cancelListener = LNbits.events.onInvoicePaid( - this.selectedWallet, - payment => { - let satoshiAmount = Math.round(payment.amount / 1000) - let trackName = ( - this.tracksMap[payment.extra.track] || {name: '[unknown]'} - ).name - - this.$q.notify({ - message: `Someone paid ${satoshiAmount} sat for the track ${trackName}.`, - caption: payment.extra.comment - ? `"${payment.extra.comment}"` - : undefined, - color: 'secondary', - html: true, - timeout: 0, - actions: [{label: 'Dismiss', color: 'white', handler: () => {}}] - }) - } - ) - }, - addTrack() { - let {id, name, producer, price_sat, download_url} = this.trackDialog.data - - const [method, path] = id - ? ['PUT', `/livestream/api/v1/livestream/tracks/${id}`] - : ['POST', '/livestream/api/v1/livestream/tracks'] - - LNbits.api - .request(method, path, this.selectedWallet.inkey, { - download_url: - download_url && download_url.length > 0 ? download_url : undefined, - name, - price_msat: price_sat * 1000 || 0, - producer_name: typeof producer === 'string' ? producer : undefined, - producer_id: typeof producer === 'object' ? producer.id : undefined - }) - .then(response => { - this.$q.notify({ - message: `Track '${this.trackDialog.data.name}' added.`, - timeout: 700 - }) - this.loadLivestream() - this.trackDialog.show = false - this.trackDialog.data = {} - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - openAddTrackDialog() { - this.trackDialog.show = true - this.trackDialog.data = {} - }, - openUpdateDialog(itemId) { - this.trackDialog.show = true - let item = this.livestream.tracks.find(item => item.id === itemId) - this.trackDialog.data = { - ...item, - producer: this.livestream.producers.find( - prod => prod.id === item.producer - ), - price_sat: Math.round(item.price_msat / 1000) - } - }, - deleteTrack(trackId) { - LNbits.utils - .confirmDialog('Are you sure you want to delete this track?') - .onOk(() => { - LNbits.api - .request( - 'DELETE', - '/livestream/api/v1/livestream/tracks/' + trackId, - this.selectedWallet.inkey - ) - .then(response => { - this.$q.notify({ - message: `Track deleted`, - timeout: 700 - }) - this.livestream.tracks.splice( - this.livestream.tracks.findIndex(track => track.id === trackId), - 1 - ) - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }) - }, - updateCurrentTrack(track) { - console.log(this.nextCurrentTrack, this.livestream) - if (this.livestream.current_track === track) { - // if clicking the same, stop it - track = 0 - } - - LNbits.api - .request( - 'PUT', - '/livestream/api/v1/livestream/track/' + track, - this.selectedWallet.inkey - ) - .then(() => { - this.livestream.current_track = track - this.nextCurrentTrack = track - this.$q.notify({ - message: `Current track updated.`, - timeout: 700 - }) - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - updateFeePct() { - LNbits.api - .request( - 'PUT', - '/livestream/api/v1/livestream/fee/' + this.livestream.fee_pct, - this.selectedWallet.inkey - ) - .then(() => { - this.$q.notify({ - message: `Percentage updated.`, - timeout: 700 - }) - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - }, - producerAdded(added, cb) { - cb(added) - } - }, - created() { - this.selectedWallet = this.g.user.wallets[0] - this.loadLivestream() - this.startPaymentNotifier() - } -}) diff --git a/lnbits/extensions/livestream/tasks.py b/lnbits/extensions/livestream/tasks.py deleted file mode 100644 index 52f86d155..000000000 --- a/lnbits/extensions/livestream/tasks.py +++ /dev/null @@ -1,89 +0,0 @@ -import json -import trio - -from lnbits.core.models import Payment -from lnbits.core.crud import create_payment -from lnbits.core import db as core_db -from lnbits.tasks import register_invoice_listener, internal_invoice_paid -from lnbits.helpers import urlsafe_short_hash - -from .crud import get_track, get_producer, get_livestream_by_track - - -async def register_listeners(): - invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) - register_invoice_listener(invoice_paid_chan_send) - await wait_for_paid_invoices(invoice_paid_chan_recv) - - -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: - await on_invoice_paid(payment) - - -async def on_invoice_paid(payment: Payment) -> None: - if "livestream" != payment.extra.get("tag"): - # not a livestream invoice - return - - track = await get_track(payment.extra.get("track", -1)) - if not track: - print("this should never happen", payment) - return - - if payment.extra.get("shared_with"): - print("payment was shared already", payment) - return - - producer = await get_producer(track.producer) - assert producer, f"track {track.id} is not associated with a producer" - - ls = await get_livestream_by_track(track.id) - assert ls, f"track {track.id} is not associated with a livestream" - - # now we make a special kind of internal transfer - amount = int(payment.amount * (100 - ls.fee_pct) / 100) - - # mark the original payment with two extra keys, "shared_with" and "received" - # (this prevents us from doing this process again and it's informative) - # and reduce it by the amount we're going to send to the producer - await core_db.execute( - """ - UPDATE apipayments - SET extra = ?, amount = ? - WHERE hash = ? - AND checking_id NOT LIKE 'internal_%' - """, - ( - json.dumps( - dict( - **payment.extra, - shared_with=[producer.name, producer.id], - received=payment.amount, - ) - ), - payment.amount - amount, - payment.payment_hash, - ), - ) - - # perform an internal transfer using the same payment_hash to the producer wallet - internal_checking_id = f"internal_{urlsafe_short_hash()}" - await create_payment( - wallet_id=producer.wallet, - checking_id=internal_checking_id, - payment_request="", - payment_hash=payment.payment_hash, - amount=amount, - memo=f"Revenue from '{track.name}'.", - pending=False, - ) - - # manually send this for now - await internal_invoice_paid.send(internal_checking_id) - - # so the flow is the following: - # - we receive, say, 1000 satoshis - # - if the fee_pct is, say, 30%, the amount we will send is 700 - # - we change the amount of receiving payment on the database from 1000 to 300 - # - we create a new payment on the producer's wallet with amount 700 diff --git a/lnbits/extensions/livestream/templates/livestream/_api_docs.html b/lnbits/extensions/livestream/templates/livestream/_api_docs.html deleted file mode 100644 index fd92f0f31..000000000 --- a/lnbits/extensions/livestream/templates/livestream/_api_docs.html +++ /dev/null @@ -1,146 +0,0 @@ - - - -

Add tracks, profit.

-
-
-
- - - - - - GET - /livestream/api/v1/livestream -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<livestream_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - PUT - /livestream/api/v1/livestream/track/<track_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 201 CREATED (application/json) -
-
Curl example
- curl -X PUT {{ request.url_root - }}api/v1/livestream/track/<track_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - PUT - /livestream/api/v1/livestream/fee/<fee_pct> -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 201 CREATED (application/json) -
-
Curl example
- curl -X PUT {{ request.url_root - }}api/v1/livestream/fee/<fee_pct> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - - POST - /livestream/api/v1/livestream/tracks -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
- {"name": <string>, "download_url": <string>, - "price_msat": <integer>, "producer_id": <integer>, - "producer_name": <string>} -
- Returns 201 CREATED (application/json) -
-
Curl example
- curl -X POST {{ request.url_root }}api/v1/livestream/tracks -d - '{"name": <string>, "download_url": <string>, - "price_msat": <integer>, "producer_id": <integer>, - "producer_name": <string>}' -H "Content-type: application/json" - -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" - -
-
-
- - - - DELETE - /livestream/api/v1/livestream/tracks/<track_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.url_root - }}api/v1/livestream/tracks/<track_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
-
diff --git a/lnbits/extensions/livestream/templates/livestream/index.html b/lnbits/extensions/livestream/templates/livestream/index.html deleted file mode 100644 index a93bab71e..000000000 --- a/lnbits/extensions/livestream/templates/livestream/index.html +++ /dev/null @@ -1,322 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - -
-
- -
-
- {% raw %} - - {{ nextCurrentTrack && nextCurrentTrack === - livestream.current_track ? 'Stop' : 'Set' }} current track - - {% endraw %} -
-
-
- -
-
- -
-
- Set percent rate -
-
-
-
- - - -
-
-
Tracks
-
-
- Add new track -
-
- - {% raw %} - - - {% endraw %} - -
-
- - - -
-
-
Producers
-
-
- - {% raw %} - - - {% endraw %} - -
-
- - - - - - - - - - - - - - - Copy LNURL-pay code - - -
- -
- - -
- {{SITE_TITLE}} Livestream extension -
-
- - - {% include "livestream/_api_docs.html" %} - -
-
- - - - -

- Standalone QR Code for this track -

- - - - - - - Copy LNURL-pay code -
- - - - - - - -
-
- - Update track - Add track - -
-
- Cancel -
-
-
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/livestream/views.py b/lnbits/extensions/livestream/views.py deleted file mode 100644 index 5628c00c4..000000000 --- a/lnbits/extensions/livestream/views.py +++ /dev/null @@ -1,41 +0,0 @@ -from quart import g, render_template, request, redirect -from http import HTTPStatus - -from lnbits.decorators import check_user_exists, validate_uuids -from lnbits.core.models import Payment -from lnbits.core.crud import get_wallet_payment - -from . import livestream_ext -from .crud import get_track, get_livestream_by_track -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@livestream_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("livestream/index.html", {"request": request,"user":g.user}) - - -@livestream_ext.route("/track/") -async def track_redirect_download(track_id): - payment_hash = request.args.get("p") - track = await get_track(track_id) - ls = await get_livestream_by_track(track_id) - payment: Payment = await get_wallet_payment(ls.wallet, payment_hash) - - if not payment: - return ( - f"Couldn't find the payment {payment_hash} or track {track.id}.", - HTTPStatus.NOT_FOUND, - ) - - if payment.pending: - return ( - f"Payment {payment_hash} wasn't received yet. Please try again in a minute.", - HTTPStatus.PAYMENT_REQUIRED, - ) - - return redirect(track.download_url) diff --git a/lnbits/extensions/livestream/views_api.py b/lnbits/extensions/livestream/views_api.py deleted file mode 100644 index 222b1e53b..000000000 --- a/lnbits/extensions/livestream/views_api.py +++ /dev/null @@ -1,144 +0,0 @@ -from quart import g, jsonify -from http import HTTPStatus -from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore - -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from fastapi import FastAPI, Query -from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse -from pydantic import BaseModel - -from . import livestream_ext -from .crud import ( - get_or_create_livestream_by_wallet, - add_track, - get_tracks, - update_track, - add_producer, - get_producers, - update_current_track, - update_livestream_fee, - delete_track_from_livestream, -) - - -@livestream_ext.get("/api/v1/livestream") -@api_check_wallet_key("invoice") -async def api_livestream_from_wallet(): - ls = await get_or_create_livestream_by_wallet(g.wallet.id) - tracks = await get_tracks(ls.id) - producers = await get_producers(ls.id) - - try: - return - { - **ls._asdict(), - **{ - "lnurl": ls.lnurl, - "tracks": [ - dict(lnurl=track.lnurl, **track._asdict()) - for track in tracks - ], - "producers": [producer._asdict() for producer in producers], - }, - } - , - HTTPStatus.OK - - except LnurlInvalidUrl: - return - { - "message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor." - } - , - HTTPStatus.UPGRADE_REQUIRED - - -@livestream_ext.put("/api/v1/livestream/track/{track_id}") -@api_check_wallet_key("invoice") -async def api_update_track(track_id): - try: - id = int(track_id) - except ValueError: - id = 0 - if id <= 0: - id = None - - ls = await get_or_create_livestream_by_wallet(g.wallet.id) - await update_current_track(ls.id, id) - return "", HTTPStatus.NO_CONTENT - - -@livestream_ext.put("/api/v1/livestream/fee/{fee_pct}") -@api_check_wallet_key("invoice") -async def api_update_fee(fee_pct): - ls = await get_or_create_livestream_by_wallet(g.wallet.id) - await update_livestream_fee(ls.id, int(fee_pct)) - return "", HTTPStatus.NO_CONTENT - - -class CreateData(BaseModel): - name: str - download_url: str = Query(None) - price_msat: int = Query(None, ge=0) - producer_id: int #missing the exclude thing - producer_name: str #missing the exclude thing - -@livestream_ext.post("/api/v1/livestream/tracks") -@livestream_ext.put("/api/v1/livestream/tracks/{id}") -@api_check_wallet_key("invoice") -# @api_validate_post_request( -# schema={ -# "name": {"type": "string", "empty": False, "required": True}, -# "download_url": {"type": "string", "empty": False, "required": False}, -# "price_msat": {"type": "number", "min": 0, "required": False}, -# "producer_id": { -# "type": "number", -# "required": True, -# "excludes": "producer_name", -# }, -# "producer_name": { -# "type": "string", -# "required": True, -# "excludes": "producer_id", -# }, -# } -# ) -async def api_add_track(data: CreateData, id=None): - ls = await get_or_create_livestream_by_wallet(g.wallet.id) - - if "producer_id" in data: - p_id = data["producer_id"] - elif "producer_name" in data: - p_id = await add_producer(ls.id, data["producer_name"]) - else: - raise TypeError("need either producer_id or producer_name arguments") - - if id: - await update_track( - ls.id, - id, - data["name"], - data.get("download_url"), - data.get("price_msat", 0), - p_id, - ) - return "", HTTPStatus.OK - else: - await add_track( - ls.id, - data["name"], - data.get("download_url"), - data.get("price_msat", 0), - p_id, - ) - return "", HTTPStatus.CREATED - - -@livestream_ext.delete("/api/v1/livestream/tracks/{track_id}") -@api_check_wallet_key("invoice") -async def api_delete_track(track_id): - ls = await get_or_create_livestream_by_wallet(g.wallet.id) - await delete_track_from_livestream(ls.id, track_id) - return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/lndhub/README.md b/lnbits/extensions/lndhub/README.md deleted file mode 100644 index f567d5492..000000000 --- a/lnbits/extensions/lndhub/README.md +++ /dev/null @@ -1,6 +0,0 @@ -

lndhub Extension

-

*connect to your lnbits wallet from BlueWallet or Zeus*

- -Lndhub has nothing to do with lnd, it is just the name of the HTTP/JSON protocol https://bluewallet.io/ uses to talk to their Lightning custodian server at https://lndhub.io/. - -Despite not having been planned to this, Lndhub because somewhat a standard for custodian wallet communication when https://t.me/lntxbot and https://zeusln.app/ implemented the same interface. And with this extension LNBits joins the same club. diff --git a/lnbits/extensions/lndhub/__init__.py b/lnbits/extensions/lndhub/__init__.py deleted file mode 100644 index 7610b0a3e..000000000 --- a/lnbits/extensions/lndhub/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_lndhub") - -lndhub_ext: Blueprint = Blueprint( - "lndhub", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/lndhub/config.json b/lnbits/extensions/lndhub/config.json deleted file mode 100644 index 6285ff80d..000000000 --- a/lnbits/extensions/lndhub/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "LndHub", - "short_description": "Access lnbits from BlueWallet or Zeus", - "icon": "navigation", - "contributors": ["fiatjaf"] -} diff --git a/lnbits/extensions/lndhub/decorators.py b/lnbits/extensions/lndhub/decorators.py deleted file mode 100644 index c9c3bb71e..000000000 --- a/lnbits/extensions/lndhub/decorators.py +++ /dev/null @@ -1,29 +0,0 @@ -from base64 import b64decode -from quart import jsonify, g, request -from functools import wraps - -from lnbits.core.crud import get_wallet_for_key - - -def check_wallet(requires_admin=False): - def wrap(view): - @wraps(view) - async def wrapped_view(**kwargs): - token = request.headers["Authorization"].split("Bearer ")[1] - key_type, key = b64decode(token).decode("utf-8").split(":") - - if requires_admin and key_type != "admin": - return jsonify( - {"error": True, "code": 2, "message": "insufficient permissions"} - ) - - g.wallet = await get_wallet_for_key(key, key_type) - if not g.wallet: - return jsonify( - {"error": True, "code": 2, "message": "insufficient permissions"} - ) - return await view(**kwargs) - - return wrapped_view - - return wrap diff --git a/lnbits/extensions/lndhub/migrations.py b/lnbits/extensions/lndhub/migrations.py deleted file mode 100644 index d6ea5fdea..000000000 --- a/lnbits/extensions/lndhub/migrations.py +++ /dev/null @@ -1,2 +0,0 @@ -async def migrate(): - pass diff --git a/lnbits/extensions/lndhub/templates/lndhub/_instructions.html b/lnbits/extensions/lndhub/templates/lndhub/_instructions.html deleted file mode 100644 index 4db79aba8..000000000 --- a/lnbits/extensions/lndhub/templates/lndhub/_instructions.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - To access an LNbits wallet from a mobile phone, -
    -
  1. - Install either Zeus or - BlueWallet; -
  2. -
  3. - Go to Add a wallet / Import wallet on BlueWallet or - Settings / Add a new node on Zeus. -
  4. -
  5. Select the desired wallet on this page;
  6. -
  7. Scan one of the two QR codes from the mobile wallet.
  8. -
-
    -
  • - Invoice URLs mean the mobile wallet will only have the - authorization to read your payments and invoices and generate new - invoices. -
  • -
  • - Admin URLs mean the mobile wallet will be able to pay - invoices.. -
  • -
-
-
-
diff --git a/lnbits/extensions/lndhub/templates/lndhub/_lndhub.html b/lnbits/extensions/lndhub/templates/lndhub/_lndhub.html deleted file mode 100644 index a15cab8fa..000000000 --- a/lnbits/extensions/lndhub/templates/lndhub/_lndhub.html +++ /dev/null @@ -1,19 +0,0 @@ - - - -

- LndHub is a protocol invented by - BlueWallet that allows mobile - wallets to query payments and balances, generate invoices and make - payments from accounts that exist on a server. The protocol is a - collection of HTTP endpoints exposed through the internet. -

-

- For a wallet that supports it, reading a QR code that contains the URL - along with secret access credentials should enable access. Currently it - is supported by Zeus and - BlueWallet. -

-
-
-
diff --git a/lnbits/extensions/lndhub/templates/lndhub/index.html b/lnbits/extensions/lndhub/templates/lndhub/index.html deleted file mode 100644 index ad0a3b046..000000000 --- a/lnbits/extensions/lndhub/templates/lndhub/index.html +++ /dev/null @@ -1,94 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} {% raw %} -
-
-
- - - -
- Copy LndHub {{type}} URL -
-
-
-
- - - - - - - -
- - {% endraw %} - -
- - -
- {{SITE_TITLE}} LndHub extension -
-
- - - - {% include "lndhub/_instructions.html" %} - - {% include "lndhub/_lndhub.html" %} - - -
-
-
- -{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/lndhub/utils.py b/lnbits/extensions/lndhub/utils.py deleted file mode 100644 index 3db6317a7..000000000 --- a/lnbits/extensions/lndhub/utils.py +++ /dev/null @@ -1,21 +0,0 @@ -from binascii import unhexlify - -from lnbits.bolt11 import Invoice - - -def to_buffer(payment_hash: str): - return {"type": "Buffer", "data": [b for b in unhexlify(payment_hash)]} - - -def decoded_as_lndhub(invoice: Invoice): - return { - "destination": invoice.payee, - "payment_hash": invoice.payment_hash, - "num_satoshis": invoice.amount_msat / 1000, - "timestamp": str(invoice.date), - "expiry": str(invoice.expiry), - "description": invoice.description, - "fallback_addr": "", - "cltv_expiry": invoice.min_final_cltv_expiry, - "route_hints": "", - } diff --git a/lnbits/extensions/lndhub/views.py b/lnbits/extensions/lndhub/views.py deleted file mode 100644 index dbe3f0de6..000000000 --- a/lnbits/extensions/lndhub/views.py +++ /dev/null @@ -1,14 +0,0 @@ -from quart import render_template, g - -from lnbits.decorators import check_user_exists, validate_uuids -from . import lndhub_ext -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@lndhub_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def lndhub_index(request: Request): - return await templates.TemplateResponse("lndhub/index.html", {"request": request,"user":g.user}) diff --git a/lnbits/extensions/lndhub/views_api.py b/lnbits/extensions/lndhub/views_api.py deleted file mode 100644 index 057c8d08a..000000000 --- a/lnbits/extensions/lndhub/views_api.py +++ /dev/null @@ -1,245 +0,0 @@ -import time -from base64 import urlsafe_b64encode -from quart import jsonify, g, request - -from fastapi import FastAPI, Query -from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse -from pydantic import BaseModel - -from lnbits.core.services import pay_invoice, create_invoice -from lnbits.core.crud import get_payments, delete_expired_invoices -from lnbits.decorators import api_validate_post_request -from lnbits.settings import WALLET -from lnbits import bolt11 - -from . import lndhub_ext -from .decorators import check_wallet -from .utils import to_buffer, decoded_as_lndhub - - -@lndhub_ext.get("/ext/getinfo") -async def lndhub_getinfo(): - return {"error": True, "code": 1, "message": "bad auth"} - - -@lndhub_ext.post("/ext/auth") -# @api_validate_post_request( -# schema={ -# "login": {"type": "string", "required": True, "excludes": "refresh_token"}, -# "password": {"type": "string", "required": True, "excludes": "refresh_token"}, -# "refresh_token": { -# "type": "string", -# "required": True, -# "excludes": ["login", "password"], -# }, -# } -# ) -async def lndhub_auth(login: str, password: str, refresh_token: str): #missing the "excludes" thing - token = ( - refresh_token - if refresh_token - else urlsafe_b64encode( - (login + ":" + password).encode("utf-8") - ).decode("ascii") - ) - return {"refresh_token": token, "access_token": token} - - -@lndhub_ext.post("/ext/addinvoice") -@check_wallet() -# @api_validate_post_request( -# schema={ -# "amt": {"type": "string", "required": True}, -# "memo": {"type": "string", "required": True}, -# "preimage": {"type": "string", "required": False}, -# } -# ) -async def lndhub_addinvoice(amt: str, memo: str, preimage: str = ""): - try: - _, pr = await create_invoice( - wallet_id=g.wallet.id, - amount=int(amt), - memo=memo, - extra={"tag": "lndhub"}, - ) - except Exception as e: - return - { - "error": True, - "code": 7, - "message": "Failed to create invoice: " + str(e), - } - - - invoice = bolt11.decode(pr) - return - { - "pay_req": pr, - "payment_request": pr, - "add_index": "500", - "r_hash": to_buffer(invoice.payment_hash), - "hash": invoice.payment_hash, - } - - - -@lndhub_ext.post("/ext/payinvoice") -@check_wallet(requires_admin=True) -# @api_validate_post_request(schema={"invoice": {"type": "string", "required": True}}) -async def lndhub_payinvoice(invoice: str): - try: - await pay_invoice( - wallet_id=g.wallet.id, - payment_request=invoice, - extra={"tag": "lndhub"}, - ) - except Exception as e: - return - { - "error": True, - "code": 10, - "message": "Payment failed: " + str(e), - } - - - invoice: bolt11.Invoice = bolt11.decode(invoice) - return - { - "payment_error": "", - "payment_preimage": "0" * 64, - "route": {}, - "payment_hash": invoice.payment_hash, - "decoded": decoded_as_lndhub(invoice), - "fee_msat": 0, - "type": "paid_invoice", - "fee": 0, - "value": invoice.amount_msat / 1000, - "timestamp": int(time.time()), - "memo": invoice.description, - } - - - -@lndhub_ext.get("/ext/balance") -@check_wallet() -async def lndhub_balance(): - return {"BTC": {"AvailableBalance": g.wallet.balance}} - - -@lndhub_ext.get("/ext/gettxs") -@check_wallet() -async def lndhub_gettxs(): - for payment in await get_payments( - wallet_id=g.wallet.id, - complete=False, - pending=True, - outgoing=True, - incoming=False, - exclude_uncheckable=True, - ): - await payment.set_pending( - (await WALLET.get_payment_status(payment.checking_id)).pending - ) - - limit = int(request.args.get("limit", 200)) - return - [ - { - "payment_preimage": payment.preimage, - "payment_hash": payment.payment_hash, - "fee_msat": payment.fee * 1000, - "type": "paid_invoice", - "fee": payment.fee, - "value": int(payment.amount / 1000), - "timestamp": payment.time, - "memo": payment.memo - if not payment.pending - else "Payment in transition", - } - for payment in reversed( - ( - await get_payments( - wallet_id=g.wallet.id, - pending=True, - complete=True, - outgoing=True, - incoming=False, - ) - )[:limit] - ) - ] - - - -@lndhub_ext.get("/ext/getuserinvoices") -@check_wallet() -async def lndhub_getuserinvoices(): - await delete_expired_invoices() - for invoice in await get_payments( - wallet_id=g.wallet.id, - complete=False, - pending=True, - outgoing=False, - incoming=True, - exclude_uncheckable=True, - ): - await invoice.set_pending( - (await WALLET.get_invoice_status(invoice.checking_id)).pending - ) - - limit = int(request.args.get("limit", 200)) - return - [ - { - "r_hash": to_buffer(invoice.payment_hash), - "payment_request": invoice.bolt11, - "add_index": "500", - "description": invoice.memo, - "payment_hash": invoice.payment_hash, - "ispaid": not invoice.pending, - "amt": int(invoice.amount / 1000), - "expire_time": int(time.time() + 1800), - "timestamp": invoice.time, - "type": "user_invoice", - } - for invoice in reversed( - ( - await get_payments( - wallet_id=g.wallet.id, - pending=True, - complete=True, - incoming=True, - outgoing=False, - ) - )[:limit] - ) - ] - - - -@lndhub_ext.get("/ext/getbtc") -@check_wallet() -async def lndhub_getbtc(): - "load an address for incoming onchain btc" - return [] - - -@lndhub_ext.get("/ext/getpending") -@check_wallet() -async def lndhub_getpending(): - "pending onchain transactions" - return [] - - -@lndhub_ext.get("/ext/decodeinvoice") -async def lndhub_decodeinvoice(): - invoice = request.args.get("invoice") - inv = bolt11.decode(invoice) - return decoded_as_lndhub(inv) - - -@lndhub_ext.get("/ext/checkrouteinvoice") -async def lndhub_checkrouteinvoice(): - "not implemented on canonical lndhub" - pass diff --git a/lnbits/extensions/ngrok/README.md b/lnbits/extensions/ngrok/README.md deleted file mode 100644 index 666f95bcd..000000000 --- a/lnbits/extensions/ngrok/README.md +++ /dev/null @@ -1,20 +0,0 @@ -

Ngrok

-

Serve lnbits over https for free using ngrok

- - - -

How it works

- -When enabled, ngrok creates a tunnel to ngrok.io with https support and tells you the https web address where you can access your lnbits instance. If you are not the first user to enable it, it doesn't create a new one, it just tells you the existing one. Useful for creating/managing/using lnurls, which must be served either via https or via tor. Note that if you restart your device, your device will generate a new url. If anyone is using your old one for wallets, lnurls, etc., whatever they are doing will stop working. - -

Installation

- -Check the Extensions page on your instance of lnbits. If you have copy of lnbits with ngrok as one of the built in extensions, click Enable -- that's the only thing you need to do to install it. - -If your copy of lnbits does not have ngrok as one of the built in extensions, stop lnbits, create go into your lnbits folder, and run this command: ./venv/bin/pip install pyngrok. Then go into the lnbits subdirectory and the extensions subdirectory within that. (So lnbits > lnbits > extensions.) Create a new subdirectory in there called freetunnel, download this repository as a zip file, and unzip it in the freetunnel directory. If your unzipper creates a new "freetunnel" subdirectory, take everything out of there and put it in the freetunnel directory you created. Then go back to the top level lnbits directory and run these commands: - -``` -./venv/bin/quart assets -./venv/bin/quart migrate -./venv/bin/hypercorn -k trio --bind 0.0.0.0:5000 'lnbits.app:create_app()' -``` diff --git a/lnbits/extensions/ngrok/__init__.py b/lnbits/extensions/ngrok/__init__.py deleted file mode 100644 index 4933aa7fc..000000000 --- a/lnbits/extensions/ngrok/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_ngrok") - -ngrok_ext: Blueprint = Blueprint("ngrok", __name__, template_folder="templates") - -from .views import * # noqa diff --git a/lnbits/extensions/ngrok/config.json.example b/lnbits/extensions/ngrok/config.json.example deleted file mode 100644 index 58e9ff8e2..000000000 --- a/lnbits/extensions/ngrok/config.json.example +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Ngrok", - "short_description": "Serve lnbits over https for free using ngrok", - "icon": "trip_origin", - "contributors": ["supertestnet"] -} diff --git a/lnbits/extensions/ngrok/migrations.py b/lnbits/extensions/ngrok/migrations.py deleted file mode 100644 index f9b8b37dc..000000000 --- a/lnbits/extensions/ngrok/migrations.py +++ /dev/null @@ -1,11 +0,0 @@ -# async def m001_initial(db): - -# await db.execute( -# """ -# CREATE TABLE example.example ( -# id TEXT PRIMARY KEY, -# wallet TEXT NOT NULL, -# time TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """ -# ); -# """ -# ) diff --git a/lnbits/extensions/ngrok/templates/ngrok/index.html b/lnbits/extensions/ngrok/templates/ngrok/index.html deleted file mode 100644 index 3af4fa44f..000000000 --- a/lnbits/extensions/ngrok/templates/ngrok/index.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} - -
-
- - -
- Access this lnbits instance at the following url -
- -

{{ ngrok }}

-
-
-
- -
- - -
Ngrok extension
-
- - - -

- Note that if you restart your device, your device will generate a - new url. If anyone is using your old one for wallets, lnurls, - etc., whatever they are doing will stop working. -

- Created by - Supertestnet. -
-
-
-
-
-
- -{% endblock %}{% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/ngrok/views.py b/lnbits/extensions/ngrok/views.py deleted file mode 100644 index a71300214..000000000 --- a/lnbits/extensions/ngrok/views.py +++ /dev/null @@ -1,33 +0,0 @@ -from quart import g, render_template - -from lnbits.decorators import check_user_exists, validate_uuids - -from pyngrok import conf, ngrok -from . import ngrok_ext -from os import getenv -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -def log_event_callback(log): - string = str(log) - string2 = string[string.find('url="https') : string.find('url="https') + 40] - if string2: - string3 = string2 - string4 = string3[4:] - global string5 - string5 = string4.replace('"', "") - - -conf.get_default().log_event_callback = log_event_callback - -port = getenv("PORT") -ngrok_tunnel = ngrok.connect(port) - - -@ngrok_ext.get("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("ngrok/index.html", {"request": request, "ngrok":string5, "user":g.user}) diff --git a/lnbits/extensions/paywall/README.md b/lnbits/extensions/paywall/README.md deleted file mode 100644 index 738485e28..000000000 --- a/lnbits/extensions/paywall/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Paywall - -## Hide content behind a paywall, a user has to pay some amount to access your hidden content - -A Paywall is a way of restricting to content via a purchase or paid subscription. For example to read a determined blog post, or to continue reading further, to access a downloads area, etc... - -## Usage - -1. Create a paywall by clicking "NEW PAYWALL"\ - ![create new paywall](https://i.imgur.com/q0ZIekC.png) -2. Fill the options for your PAYWALL - - select the wallet - - set the link that will be unlocked after a successful payment - - give your paywall a _Title_ - - an optional small description - - and set an amount a user must pay to access the hidden content. Note this is the minimum amount, a user can over pay if they wish - - if _Remember payments_ is checked, a returning paying user won't have to pay again for the same content.\ - ![paywall config](https://i.imgur.com/CBW48F6.png) -3. You can then use your paywall link to secure your awesome content\ - ![paywall link](https://i.imgur.com/hDQmCDf.png) -4. When a user wants to access your hidden content, he can use the minimum amount or increase and click the "_Check icon_" to generate an invoice, user will then be redirected to the content page\ - ![user paywall view](https://i.imgur.com/3pLywkZ.png) diff --git a/lnbits/extensions/paywall/__init__.py b/lnbits/extensions/paywall/__init__.py deleted file mode 100644 index cf9570a15..000000000 --- a/lnbits/extensions/paywall/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_paywall") - -paywall_ext: Blueprint = Blueprint( - "paywall", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/paywall/config.json b/lnbits/extensions/paywall/config.json deleted file mode 100644 index d08ce7bad..000000000 --- a/lnbits/extensions/paywall/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Paywall", - "short_description": "Create paywalls for content", - "icon": "policy", - "contributors": ["eillarra"] -} diff --git a/lnbits/extensions/paywall/crud.py b/lnbits/extensions/paywall/crud.py deleted file mode 100644 index c13aba439..000000000 --- a/lnbits/extensions/paywall/crud.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import List, Optional, Union - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import Paywall - - -async def create_paywall( - *, - wallet_id: str, - url: str, - memo: str, - description: Optional[str] = None, - amount: int = 0, - remembers: bool = True, -) -> Paywall: - paywall_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO paywall.paywalls (id, wallet, url, memo, description, amount, remembers) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, - (paywall_id, wallet_id, url, memo, description, amount, int(remembers)), - ) - - paywall = await get_paywall(paywall_id) - assert paywall, "Newly created paywall couldn't be retrieved" - return paywall - - -async def get_paywall(paywall_id: str) -> Optional[Paywall]: - row = await db.fetchone( - "SELECT * FROM paywall.paywalls WHERE id = ?", (paywall_id,) - ) - - return Paywall.from_row(row) if row else None - - -async def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM paywall.paywalls WHERE wallet IN ({q})", (*wallet_ids,) - ) - - return [Paywall.from_row(row) for row in rows] - - -async def delete_paywall(paywall_id: str) -> None: - await db.execute("DELETE FROM paywall.paywalls WHERE id = ?", (paywall_id,)) diff --git a/lnbits/extensions/paywall/migrations.py b/lnbits/extensions/paywall/migrations.py deleted file mode 100644 index 8afe58b18..000000000 --- a/lnbits/extensions/paywall/migrations.py +++ /dev/null @@ -1,66 +0,0 @@ -from sqlalchemy.exc import OperationalError # type: ignore - - -async def m001_initial(db): - """ - Initial paywalls table. - """ - await db.execute( - """ - CREATE TABLE paywall.paywalls ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - secret TEXT NOT NULL, - url TEXT NOT NULL, - memo TEXT NOT NULL, - amount INTEGER NOT NULL, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) - - -async def m002_redux(db): - """ - Creates an improved paywalls table and migrates the existing data. - """ - await db.execute("ALTER TABLE paywall.paywalls RENAME TO paywalls_old") - await db.execute( - """ - CREATE TABLE paywall.paywalls ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - url TEXT NOT NULL, - memo TEXT NOT NULL, - description TEXT NULL, - amount INTEGER DEFAULT 0, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """, - remembers INTEGER DEFAULT 0, - extras TEXT NULL - ); - """ - ) - - for row in [ - list(row) for row in await db.fetchall("SELECT * FROM paywall.paywalls_old") - ]: - await db.execute( - """ - INSERT INTO paywall.paywalls ( - id, - wallet, - url, - memo, - amount, - time - ) - VALUES (?, ?, ?, ?, ?, ?) - """, - (row[0], row[1], row[3], row[4], row[5], row[6]), - ) - - await db.execute("DROP TABLE paywall.paywalls_old") diff --git a/lnbits/extensions/paywall/models.py b/lnbits/extensions/paywall/models.py deleted file mode 100644 index 7b64db4c1..000000000 --- a/lnbits/extensions/paywall/models.py +++ /dev/null @@ -1,24 +0,0 @@ -import json - -from sqlite3 import Row -from pydantic import BaseModel -from typing import Optional - - -class Paywall(BaseModel): - id: str - wallet: str - url: str - memo: str - description: str - amount: int - time: int - remembers: bool - extras: Optional[dict] - - @classmethod - def from_row(cls, row: Row) -> "Paywall": - data = dict(row) - data["remembers"] = bool(data["remembers"]) - data["extras"] = json.loads(data["extras"]) if data["extras"] else None - return cls(**data) diff --git a/lnbits/extensions/paywall/templates/paywall/_api_docs.html b/lnbits/extensions/paywall/templates/paywall/_api_docs.html deleted file mode 100644 index 1157fa467..000000000 --- a/lnbits/extensions/paywall/templates/paywall/_api_docs.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - GET /paywall/api/v1/paywalls -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<paywall_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/paywalls -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - POST /paywall/api/v1/paywalls -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
- {"amount": <integer>, "description": <string>, "memo": - <string>, "remembers": <boolean>, "url": - <string>} -
- Returns 201 CREATED (application/json) -
- {"amount": <integer>, "description": <string>, "id": - <string>, "memo": <string>, "remembers": <boolean>, - "time": <int>, "url": <string>, "wallet": - <string>} -
Curl example
- curl -X POST {{ request.url_root }}api/v1/paywalls -d '{"url": - <string>, "memo": <string>, "description": <string>, - "amount": <integer>, "remembers": <boolean>}' -H - "Content-type: application/json" -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" - -
-
-
- - - - POST - /paywall/api/v1/paywalls/<paywall_id>/invoice -
Body (application/json)
- {"amount": <integer>} -
- Returns 201 CREATED (application/json) -
- {"payment_hash": <string>, "payment_request": - <string>} -
Curl example
- curl -X POST {{ request.url_root - }}api/v1/paywalls/<paywall_id>/invoice -d '{"amount": - <integer>}' -H "Content-type: application/json" - -
-
-
- - - - POST - /paywall/api/v1/paywalls/<paywall_id>/check_invoice -
Body (application/json)
- {"payment_hash": <string>} -
- Returns 200 OK (application/json) -
- {"paid": false}
- {"paid": true, "url": <string>, "remembers": - <boolean>} -
Curl example
- curl -X POST {{ request.url_root - }}api/v1/paywalls/<paywall_id>/check_invoice -d - '{"payment_hash": <string>}' -H "Content-type: application/json" - -
-
-
- - - - DELETE - /paywall/api/v1/paywalls/<paywall_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.url_root - }}api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" - -
-
-
-
diff --git a/lnbits/extensions/paywall/templates/paywall/display.html b/lnbits/extensions/paywall/templates/paywall/display.html deleted file mode 100644 index 7bc7d9b88..000000000 --- a/lnbits/extensions/paywall/templates/paywall/display.html +++ /dev/null @@ -1,162 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
{{ paywall.memo }}
- {% if paywall.description %} -

{{ paywall.description }}

- {% endif %} -
- - - - - -
- - - - - -
- Copy invoice - Cancel -
-
-
-
- -

- You can access the URL behind this paywall:
- {% raw %}{{ redirectUrl }}{% endraw %} -

-
- Open URL -
-
-
-
-
-
-{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/paywall/templates/paywall/index.html b/lnbits/extensions/paywall/templates/paywall/index.html deleted file mode 100644 index 8be3b2fab..000000000 --- a/lnbits/extensions/paywall/templates/paywall/index.html +++ /dev/null @@ -1,312 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New paywall - - - - - -
-
-
Paywalls
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
- -
- - -
- {{SITE_TITLE}} paywall extension -
-
- - - {% include "paywall/_api_docs.html" %} - -
-
- - - - - - - - - - - - - - - - - Remember payments - A succesful payment will be registered in the browser's - storage, so the user doesn't need to pay again to access the - URL. - - - -
- Create paywall - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/paywall/views.py b/lnbits/extensions/paywall/views.py deleted file mode 100644 index f2dee6af0..000000000 --- a/lnbits/extensions/paywall/views.py +++ /dev/null @@ -1,25 +0,0 @@ -from quart import g, abort, render_template -from http import HTTPStatus - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import paywall_ext -from .crud import get_paywall -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@paywall_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("paywall/index.html", {"request": request,"user":g.user}) - - -@paywall_ext.route("/") -async def display(request: Request, paywall_id): - paywall = await get_paywall(paywall_id) or abort( - HTTPStatus.NOT_FOUND, "Paywall does not exist." - ) - return await templates.TemplateResponse("paywall/display.html", {"request": request,"paywall":paywall}) diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py deleted file mode 100644 index 0d4b181fa..000000000 --- a/lnbits/extensions/paywall/views_api.py +++ /dev/null @@ -1,110 +0,0 @@ -from quart import g, jsonify, request -from http import HTTPStatus - -from lnbits.core.crud import get_user, get_wallet -from lnbits.core.services import create_invoice, check_invoice_status -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from . import paywall_ext -from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall -from typing import Optional -from pydantic import BaseModel -from fastapi import FastAPI, Query -from fastapi.encoders import jsonable_encoder - -@paywall_ext.get("/api/v1/paywalls") -@api_check_wallet_key("invoice") -async def api_paywalls(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - return ( - jsonable_encoder([paywall._asdict() for paywall in await get_paywalls(wallet_ids)]), - HTTPStatus.OK, - ) - -class CreateData(BaseModel): - url: Optional[str] = Query(...) - memo: Optional[str] = Query(...) - description: str - amount: int - remembers: bool - -@paywall_ext.post("/api/v1/paywalls") -@api_check_wallet_key("invoice") -async def api_paywall_create(data: CreateData): - paywall = await create_paywall(wallet_id=g.wallet.id, **data) - return paywall, HTTPStatus.CREATED - - -@paywall_ext.delete("/api/v1/paywalls/{paywall_id}") -@api_check_wallet_key("invoice") -async def api_paywall_delete(paywall_id): - paywall = await get_paywall(paywall_id) - - if not paywall: - return jsonable_encoder({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND - - if paywall.wallet != g.wallet.id: - return jsonable_encoder({"message": "Not your paywall."}), HTTPStatus.FORBIDDEN - - await delete_paywall(paywall_id) - - return "", HTTPStatus.NO_CONTENT - - -@paywall_ext.post("/api/v1/paywalls/{paywall_id}/invoice") -async def api_paywall_create_invoice(amount: int = Query(..., ge=1), paywall_id = None): - paywall = await get_paywall(paywall_id) - - if amount < paywall.amount: - return ( - jsonable_encoder({"message": f"Minimum amount is {paywall.amount} sat."}), - HTTPStatus.BAD_REQUEST, - ) - - try: - amount = ( - amount if amount > paywall.amount else paywall.amount - ) - payment_hash, payment_request = await create_invoice( - wallet_id=paywall.wallet, - amount=amount, - memo=f"{paywall.memo}", - extra={"tag": "paywall"}, - ) - except Exception as e: - return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR - - return ( - {"payment_hash": payment_hash, "payment_request": payment_request}, - HTTPStatus.CREATED, - ) - - -@paywall_ext.post("/api/v1/paywalls/{paywall_id}/check_invoice") -async def api_paywal_check_invoice(payment_hash: str = Query(...), paywall_id = None): - paywall = await get_paywall(paywall_id) - - if not paywall: - return {"message": "Paywall does not exist."}, HTTPStatus.NOT_FOUND - - try: - status = await check_invoice_status(paywall.wallet, payment_hash) - is_paid = not status.pending - except Exception: - return {"paid": False}, HTTPStatus.OK - - if is_paid: - wallet = await get_wallet(paywall.wallet) - payment = await wallet.get_payment(payment_hash) - await payment.set_pending(False) - - return ( - {"paid": True, "url": paywall.url, "remembers": paywall.remembers}, - HTTPStatus.OK, - ) - - return {"paid": False}, HTTPStatus.OK diff --git a/lnbits/extensions/satspay/README.md b/lnbits/extensions/satspay/README.md deleted file mode 100644 index d52547aea..000000000 --- a/lnbits/extensions/satspay/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# SatsPay Server - -## Create onchain and LN charges. Includes webhooks! - -Easilly create invoices that support Lightning Network and on-chain BTC payment. - -1. Create a "NEW CHARGE"\ - ![new charge](https://i.imgur.com/fUl6p74.png) -2. Fill out the invoice fields - - set a descprition for the payment - - the amount in sats - - the time, in minutes, the invoice is valid for, after this period the invoice can't be payed - - set a webhook that will get the transaction details after a successful payment - - set to where the user should redirect after payment - - set the text for the button that will show after payment (not setting this, will display "NONE" in the button) - - select if you want onchain payment, LN payment or both - - depending on what you select you'll have to choose the respective wallets where to receive your 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 - - they can choose to pay on LN\ - ![offchain payment](https://i.imgur.com/4191SMV.png) - - or pay on chain\ - ![onchain payment](https://i.imgur.com/wzLRR5N.png) -5. You can check the state of your charges in LNBits\ - ![invoice state](https://i.imgur.com/JnBd22p.png) diff --git a/lnbits/extensions/satspay/__init__.py b/lnbits/extensions/satspay/__init__.py deleted file mode 100644 index 4bdaa2b63..000000000 --- a/lnbits/extensions/satspay/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_satspay") - - -satspay_ext: Blueprint = Blueprint( - "satspay", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/satspay/config.json b/lnbits/extensions/satspay/config.json deleted file mode 100644 index b8cd185ab..000000000 --- a/lnbits/extensions/satspay/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "SatsPayServer", - "short_description": "Create onchain and LN charges", - "icon": "payment", - "contributors": [ - "arcbtc" - ] -} diff --git a/lnbits/extensions/satspay/crud.py b/lnbits/extensions/satspay/crud.py deleted file mode 100644 index 56cabdbe3..000000000 --- a/lnbits/extensions/satspay/crud.py +++ /dev/null @@ -1,130 +0,0 @@ -from typing import List, Optional, Union - -# from lnbits.db import open_ext_db -from . import db -from .models import Charges - -from lnbits.helpers import urlsafe_short_hash - -from quart import jsonify -import httpx -from lnbits.core.services import create_invoice, check_invoice_status -from ..watchonly.crud import get_watch_wallet, get_fresh_address, get_mempool - - -###############CHARGES########################## - - -async def create_charge( - user: str, - description: str = None, - onchainwallet: Optional[str] = None, - lnbitswallet: Optional[str] = None, - webhook: Optional[str] = None, - completelink: Optional[str] = None, - completelinktext: Optional[str] = "Back to Merchant", - time: Optional[int] = None, - amount: Optional[int] = None, -) -> Charges: - charge_id = urlsafe_short_hash() - if onchainwallet: - wallet = await get_watch_wallet(onchainwallet) - onchain = await get_fresh_address(onchainwallet) - onchainaddress = onchain.address - else: - onchainaddress = None - if lnbitswallet: - payment_hash, payment_request = await create_invoice( - wallet_id=lnbitswallet, amount=amount, memo=charge_id - ) - else: - payment_hash = None - payment_request = None - await db.execute( - """ - INSERT INTO satspay.charges ( - id, - "user", - description, - onchainwallet, - onchainaddress, - lnbitswallet, - payment_request, - payment_hash, - webhook, - completelink, - completelinktext, - time, - amount, - balance - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - charge_id, - user, - description, - onchainwallet, - onchainaddress, - lnbitswallet, - payment_request, - payment_hash, - webhook, - completelink, - completelinktext, - time, - amount, - 0, - ), - ) - return await get_charge(charge_id) - - -async def update_charge(charge_id: str, **kwargs) -> Optional[Charges]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE satspay.charges SET {q} WHERE id = ?", (*kwargs.values(), charge_id) - ) - row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,)) - return Charges.from_row(row) if row else None - - -async def get_charge(charge_id: str) -> Charges: - row = await db.fetchone("SELECT * FROM satspay.charges WHERE id = ?", (charge_id,)) - return Charges.from_row(row) if row else None - - -async def get_charges(user: str) -> List[Charges]: - rows = await db.fetchall( - """SELECT * FROM satspay.charges WHERE "user" = ?""", (user,) - ) - return [Charges.from_row(row) for row in rows] - - -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]: - charge = await get_charge(charge_id) - if not charge.paid: - if charge.onchainaddress: - mempool = await get_mempool(charge.user) - try: - async with httpx.AsyncClient() as client: - r = await client.get( - 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 - if charge.lnbitswallet: - invoice_status = await check_invoice_status( - charge.lnbitswallet, 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 diff --git a/lnbits/extensions/satspay/migrations.py b/lnbits/extensions/satspay/migrations.py deleted file mode 100644 index 87446c800..000000000 --- a/lnbits/extensions/satspay/migrations.py +++ /dev/null @@ -1,28 +0,0 @@ -async def m001_initial(db): - """ - Initial wallet table. - """ - - await db.execute( - """ - CREATE TABLE satspay.charges ( - id TEXT NOT NULL PRIMARY KEY, - "user" TEXT, - description TEXT, - onchainwallet TEXT, - onchainaddress TEXT, - lnbitswallet TEXT, - payment_request TEXT, - payment_hash TEXT, - webhook TEXT, - completelink TEXT, - completelinktext TEXT, - time INTEGER, - amount INTEGER, - balance INTEGER DEFAULT 0, - timestamp TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) diff --git a/lnbits/extensions/satspay/models.py b/lnbits/extensions/satspay/models.py deleted file mode 100644 index 3bac7a2bb..000000000 --- a/lnbits/extensions/satspay/models.py +++ /dev/null @@ -1,39 +0,0 @@ -from sqlite3 import Row -from pydantic import BaseModel -import time - - -class Charges(BaseModel): - id: str - user: str - description: str - onchainwallet: str - onchainaddress: str - lnbitswallet: str - payment_request: str - payment_hash: str - webhook: str - completelink: str - completelinktext: str - time: int - amount: int - balance: int - timestamp: int - - @classmethod - def from_row(cls, row: Row) -> "Charges": - return cls(**dict(row)) - - @property - def time_elapsed(self): - if (self.timestamp + (self.time * 60)) >= time.time(): - return False - else: - return True - - @property - def paid(self): - if self.balance >= self.amount: - return True - else: - return False diff --git a/lnbits/extensions/satspay/templates/satspay/_api_docs.html b/lnbits/extensions/satspay/templates/satspay/_api_docs.html deleted file mode 100644 index 526af7f37..000000000 --- a/lnbits/extensions/satspay/templates/satspay/_api_docs.html +++ /dev/null @@ -1,171 +0,0 @@ - - -

- SatsPayServer, create Onchain/LN charges.
WARNING: If using with the - WatchOnly extension, we highly reccomend using a fresh extended public Key - specifically for SatsPayServer!
- - Created by, Ben Arc -

-
- - - - - POST /satspay/api/v1/charge -
Headers
- {"X-Api-Key": <admin_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<charge_object>, ...] -
Curl example
- curl -X POST {{ request.url_root }}api/v1/charge -d - '{"onchainwallet": <string, watchonly_wallet_id>, - "description": <string>, "webhook":<string>, "time": - <integer>, "amount": <integer>, "lnbitswallet": - <string, lnbits_wallet_id>}' -H "Content-type: - application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}" - -
-
-
- - - - PUT - /satspay/api/v1/charge/<charge_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<charge_object>, ...] -
Curl example
- curl -X POST {{ request.url_root }}api/v1/charge/<charge_id> - -d '{"onchainwallet": <string, watchonly_wallet_id>, - "description": <string>, "webhook":<string>, "time": - <integer>, "amount": <integer>, "lnbitswallet": - <string, lnbits_wallet_id>}' -H "Content-type: - application/json" -H "X-Api-Key: {{g.user.wallets[0].adminkey }}" - -
-
-
- - - - - GET - /satspay/api/v1/charge/<charge_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<charge_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/charge/<charge_id> - -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - GET /satspay/api/v1/charges -
Headers
- {"X-Api-Key": <invoice_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<charge_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/charges -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - DELETE - /satspay/api/v1/charge/<charge_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.url_root - }}api/v1/charge/<charge_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" - -
-
-
- - - - GET - /satspay/api/v1/charges/balance/<charge_id> -
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<charge_object>, ...] -
Curl example
- curl -X GET {{ request.url_root - }}api/v1/charges/balance/<charge_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
-
-
diff --git a/lnbits/extensions/satspay/templates/satspay/display.html b/lnbits/extensions/satspay/templates/satspay/display.html deleted file mode 100644 index b3386074e..000000000 --- a/lnbits/extensions/satspay/templates/satspay/display.html +++ /dev/null @@ -1,318 +0,0 @@ -{% extends "public.html" %} {% block page %} -
- -
-
-
{{ charge.description }}
-
-
-
-
Time elapsed
-
-
-
Charge paid
-
-
- - - - Awaiting payment... - - {% raw %} {{ newTimeLeft }} {% endraw %} - - - -
-
-
-
- Charge ID: {{ charge.id }} -
- {% raw %} Total to pay: {{ charge_amount }}sats
- Amount paid: {{ charge_balance }}

- Amount due: {{ charge_amount - charge_balance }}sats {% endraw %} -
-
- -
-
-
- - - bitcoin onchain payment method not available - - - - pay with lightning - -
-
- - - bitcoin lightning payment method not available - - - - pay onchain - -
-
- -
-
- - -
-
- -
-
- - -
-
-
- Pay this
- lightning-network invoice
-
- - - - - -
- Copy invoice -
-
-
-
-
- - -
-
- -
-
- - -
-
-
- Send {{ charge.amount }}sats
- to this onchain address
-
- - - - - -
- Copy address -
-
-
-
-
-
-
- -{% endblock %} {% block scripts %} - - - -{% endblock %} diff --git a/lnbits/extensions/satspay/templates/satspay/index.html b/lnbits/extensions/satspay/templates/satspay/index.html deleted file mode 100644 index f3566c7c4..000000000 --- a/lnbits/extensions/satspay/templates/satspay/index.html +++ /dev/null @@ -1,555 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - {% raw %} - New charge - - - - - - -
-
-
Charges
-
- -
- - - - Export to CSV -
-
- - - - - {% endraw %} - -
-
-
- -
- - -
- {{SITE_TITLE}} satspay Extension -
-
- - - {% include "satspay/_api_docs.html" %} - -
-
- - - - - - - - - - - - - - - - - - -
-
-
- -
-
- - - Watch-Only extension MUST be activated and have a wallet - - -
-
-
-
- -
-
-
- -
- -
- - - -
- Create Charge - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - - - -{% endblock %} diff --git a/lnbits/extensions/satspay/views.py b/lnbits/extensions/satspay/views.py deleted file mode 100644 index 20468502e..000000000 --- a/lnbits/extensions/satspay/views.py +++ /dev/null @@ -1,26 +0,0 @@ -from quart import g, abort, render_template, jsonify -from http import HTTPStatus - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import satspay_ext -from .crud import get_charge - -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@satspay_ext.route("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("satspay/index.html", {"request":request, "user":g.user}) - - -@satspay_ext.route("/") -async def display(request: Request, charge_id): - charge = await get_charge(charge_id) or abort( - HTTPStatus.NOT_FOUND, "Charge link does not exist." - ) - return await templates.TemplateResponse("satspay/display.html", {"request":request, "charge":charge}) diff --git a/lnbits/extensions/satspay/views_api.py b/lnbits/extensions/satspay/views_api.py deleted file mode 100644 index 89c0f594c..000000000 --- a/lnbits/extensions/satspay/views_api.py +++ /dev/null @@ -1,170 +0,0 @@ -import hashlib -from quart import g, jsonify, url_for -from http import HTTPStatus -import httpx - -from fastapi import FastAPI, Query -from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse -from pydantic import BaseModel - -from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from lnbits.extensions.satspay import satspay_ext -from .crud import ( - create_charge, - update_charge, - get_charge, - get_charges, - delete_charge, - check_address_balance, -) - -#############################CHARGES########################## - -class CreateData(BaseModel): - onchainwallet: str - lnbitswallet: str - description: str = Query(...) - webhook: str - completelink: str - completelinktext: str - time: int = Query(..., ge=1) - amount: int = Query(..., ge=1) - -@satspay_ext.post("/api/v1/charge") -@satspay_ext.put("/api/v1/charge/{charge_id}") -@api_check_wallet_key("admin") -# @api_validate_post_request( -# schema={ -# "onchainwallet": {"type": "string"}, -# "lnbitswallet": {"type": "string"}, -# "description": {"type": "string", "empty": False, "required": True}, -# "webhook": {"type": "string"}, -# "completelink": {"type": "string"}, -# "completelinktext": {"type": "string"}, -# "time": {"type": "integer", "min": 1, "required": True}, -# "amount": {"type": "integer", "min": 1, "required": True}, -# } -# ) -async def api_charge_create_or_update(data: CreateData, charge_id=None): - if not charge_id: - charge = await create_charge(user=g.wallet.user, **data) - return charge._asdict(), HTTPStatus.CREATED - else: - charge = await update_charge(charge_id=charge_id, **data) - return charge._asdict(), HTTPStatus.OK - - -@satspay_ext.get("/api/v1/charges") -@api_check_wallet_key("invoice") -async def api_charges_retrieve(): - try: - return ( - - [ - { - **charge._asdict(), - **{"time_elapsed": charge.time_elapsed}, - **{"paid": charge.paid}, - } - for charge in await get_charges(g.wallet.user) - ] - , - HTTPStatus.OK, - ) - except: - return "" - - -@satspay_ext.get("/api/v1/charge/{charge_id}") -@api_check_wallet_key("invoice") -async def api_charge_retrieve(charge_id: str): - charge = await get_charge(charge_id) - - if not charge: - return {"message": "charge does not exist"}, HTTPStatus.NOT_FOUND - - return ( - - { - **charge._asdict(), - **{"time_elapsed": charge.time_elapsed}, - **{"paid": charge.paid}, - } - , - HTTPStatus.OK, - ) - - -@satspay_ext.delete("/api/v1/charge/{charge_id}") -@api_check_wallet_key("invoice") -async def api_charge_delete(charge_id: str): - charge = await get_charge(charge_id) - - if not charge: - return {"message": "Wallet link does not exist."}, HTTPStatus.NOT_FOUND - - await delete_charge(charge_id) - - return "", HTTPStatus.NO_CONTENT - - -#############################BALANCE########################## - - -@satspay_ext.get("/api/v1/charges/balance/{charge_id}") -async def api_charges_balance(charge_id: str): - - charge = await check_address_balance(charge_id) - - if not charge: - return {"message": "charge does not exist"}, HTTPStatus.NOT_FOUND - if charge.paid and charge.webhook: - async with httpx.AsyncClient() as client: - try: - r = await client.post( - charge.webhook, - json={ - "id": charge.id, - "description": charge.description, - "onchainaddress": charge.onchainaddress, - "payment_request": charge.payment_request, - "payment_hash": charge.payment_hash, - "time": charge.time, - "amount": charge.amount, - "balance": charge.balance, - "paid": charge.paid, - "timestamp": charge.timestamp, - "completelink": charge.completelink, - }, - timeout=40, - ) - except AssertionError: - charge.webhook = None - return charge._asdict(), HTTPStatus.OK - - -#############################MEMPOOL########################## - - -@satspay_ext.put("/api/v1/mempool") -@api_check_wallet_key("invoice") -# @api_validate_post_request( -# schema={ -# "endpoint": {"type": "string", "empty": False, "required": True}, -# } -# ) -async def api_update_mempool(endpoint: str = Query(...)): - mempool = await update_mempool(user=g.wallet.user, endpoint) - return mempool._asdict(), HTTPStatus.OK - - -@satspay_ext.get("/api/v1/mempool") -@api_check_wallet_key("invoice") -async def api_get_mempool(): - mempool = await get_mempool(g.wallet.user) - if not mempool: - mempool = await create_mempool(user=g.wallet.user) - return mempool._asdict(), HTTPStatus.OK diff --git a/lnbits/extensions/splitpayments/README.md b/lnbits/extensions/splitpayments/README.md deleted file mode 100644 index 04576a573..000000000 --- a/lnbits/extensions/splitpayments/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Split Payments - -## Have payments split between multiple wallets - -LNBits Split Payments extension allows for distributing payments across multiple wallets. Set it and forget it. It will keep splitting your payments across wallets forever. - -## Usage - -1. After enabling the extension, choose the source wallet that will receive and distribute the Payments - -![choose wallet](https://i.imgur.com/nPQudqL.png) - -2. Add the wallet or wallets info to split payments to - -![split wallets](https://i.imgur.com/5hCNWpg.png) - get the wallet id, or an invoice key from a different wallet. It can be a completely different user as long as it's under the same LNbits instance/domain. You can get the wallet information on the API Info section on every wallet page\ - ![wallet info](https://i.imgur.com/betqflC.png) - set a wallet _Alias_ for your own identification\ - -- set how much, in percentage, this wallet will receive from every payment sent to the source wallets - -3. When done, click "SAVE TARGETS" to make the splits effective - -4. You can have several wallets to split to, as long as the sum of the percentages is under or equal to 100% - -5. When the source wallet receives a payment, the extension will automatically split the corresponding values to every wallet\ - - on receiving a 20 sats payment\ - ![get 20 sats payment](https://i.imgur.com/BKp0xvy.png) - - source wallet gets 18 sats\ - ![source wallet](https://i.imgur.com/GCxDZ5s.png) - - Ben's wallet (the wallet from the example) instantly, and feeless, gets the corresponding 10%, or 2 sats\ - ![ben wallet](https://i.imgur.com/MfsccNa.png) - -## Sponsored by - -[![](https://cdn.shopify.com/s/files/1/0826/9235/files/cryptograffiti_logo_clear_background.png?v=1504730421)](https://cryptograffiti.com/) diff --git a/lnbits/extensions/splitpayments/__init__.py b/lnbits/extensions/splitpayments/__init__.py deleted file mode 100644 index 2cd2d7c64..000000000 --- a/lnbits/extensions/splitpayments/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from quart import Blueprint - -from lnbits.db import Database - -db = Database("ext_splitpayments") - -splitpayments_ext: Blueprint = Blueprint( - "splitpayments", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa -from .tasks import register_listeners - -from lnbits.tasks import record_async - -splitpayments_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/splitpayments/config.json b/lnbits/extensions/splitpayments/config.json deleted file mode 100644 index 5d084c700..000000000 --- a/lnbits/extensions/splitpayments/config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "SplitPayments", - "short_description": "Split incoming payments across wallets", - "icon": "call_split", - "contributors": [ - "fiatjaf", - "cryptograffiti" - ] -} diff --git a/lnbits/extensions/splitpayments/crud.py b/lnbits/extensions/splitpayments/crud.py deleted file mode 100644 index ef10add48..000000000 --- a/lnbits/extensions/splitpayments/crud.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import List - -from . import db -from .models import Target - - -async def get_targets(source_wallet: str) -> List[Target]: - rows = await db.fetchall( - "SELECT * FROM splitpayments.targets WHERE source = ?", (source_wallet,) - ) - return [Target(**dict(row)) for row in rows] - - -async def set_targets(source_wallet: str, targets: List[Target]): - async with db.connect() as conn: - await conn.execute( - "DELETE FROM splitpayments.targets WHERE source = ?", (source_wallet,) - ) - for target in targets: - await conn.execute( - """ - INSERT INTO splitpayments.targets - (source, wallet, percent, alias) - VALUES (?, ?, ?, ?) - """, - (source_wallet, target.wallet, target.percent, target.alias), - ) diff --git a/lnbits/extensions/splitpayments/migrations.py b/lnbits/extensions/splitpayments/migrations.py deleted file mode 100644 index 735afc6c3..000000000 --- a/lnbits/extensions/splitpayments/migrations.py +++ /dev/null @@ -1,16 +0,0 @@ -async def m001_initial(db): - """ - Initial split payment table. - """ - await db.execute( - """ - CREATE TABLE splitpayments.targets ( - wallet TEXT NOT NULL, - source TEXT NOT NULL, - percent INTEGER NOT NULL CHECK (percent >= 0 AND percent <= 100), - alias TEXT, - - UNIQUE (source, wallet) - ); - """ - ) diff --git a/lnbits/extensions/splitpayments/models.py b/lnbits/extensions/splitpayments/models.py deleted file mode 100644 index e17000594..000000000 --- a/lnbits/extensions/splitpayments/models.py +++ /dev/null @@ -1,9 +0,0 @@ - -from pydantic import BaseModel - - -class Target(BaseModel): - wallet: str - source: str - percent: int - alias: str diff --git a/lnbits/extensions/splitpayments/static/js/index.js b/lnbits/extensions/splitpayments/static/js/index.js deleted file mode 100644 index d9750bef1..000000000 --- a/lnbits/extensions/splitpayments/static/js/index.js +++ /dev/null @@ -1,143 +0,0 @@ -/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ - -Vue.component(VueQrcode.name, VueQrcode) - -function hashTargets(targets) { - return targets - .filter(isTargetComplete) - .map(({wallet, percent, alias}) => `${wallet}${percent}${alias}`) - .join('') -} - -function isTargetComplete(target) { - return target.wallet && target.wallet.trim() !== '' && target.percent > 0 -} - -new Vue({ - el: '#vue', - mixins: [windowMixin], - data() { - return { - selectedWallet: null, - currentHash: '', // a string that must match if the edit data is unchanged - targets: [] - } - }, - computed: { - isDirty() { - return hashTargets(this.targets) !== this.currentHash - } - }, - methods: { - clearTargets() { - this.targets = [{}] - this.$q.notify({ - message: - 'Cleared the form, but not saved. You must click to save manually.', - timeout: 500 - }) - }, - getTargets() { - LNbits.api - .request( - 'GET', - '/splitpayments/api/v1/targets', - this.selectedWallet.adminkey - ) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - .then(response => { - this.currentHash = hashTargets(response.data) - this.targets = response.data.concat({}) - }) - }, - changedWallet(wallet) { - this.selectedWallet = wallet - this.getTargets() - }, - targetChanged(isPercent, index) { - // fix percent min and max range - if (isPercent) { - if (this.targets[index].percent > 100) this.targets[index].percent = 100 - if (this.targets[index].percent < 0) this.targets[index].percent = 0 - } - - // remove empty lines (except last) - if (this.targets.length >= 2) { - for (let i = this.targets.length - 2; i >= 0; i--) { - let target = this.targets[i] - if ( - (!target.wallet || target.wallet.trim() === '') && - (!target.alias || target.alias.trim() === '') && - !target.percent - ) { - this.targets.splice(i, 1) - } - } - } - - // add a line at the end if the last one is filled - let last = this.targets[this.targets.length - 1] - if (last.wallet && last.wallet.trim() !== '' && last.percent > 0) { - this.targets.push({}) - } - - // sum of all percents - let currentTotal = this.targets.reduce( - (acc, target) => acc + (target.percent || 0), - 0 - ) - - // remove last (unfilled) line if the percent is already 100 - if (currentTotal >= 100) { - let last = this.targets[this.targets.length - 1] - if ( - (!last.wallet || last.wallet.trim() === '') && - (!last.alias || last.alias.trim() === '') && - !last.percent - ) { - this.targets = this.targets.slice(0, -1) - } - } - - // adjust percents of other lines (not this one) - if (currentTotal > 100 && isPercent) { - let diff = (currentTotal - 100) / (100 - this.targets[index].percent) - this.targets.forEach((target, t) => { - if (t !== index) target.percent -= Math.round(diff * target.percent) - }) - } - - // overwrite so changes appear - this.targets = this.targets - }, - saveTargets() { - LNbits.api - .request( - 'PUT', - '/splitpayments/api/v1/targets', - this.selectedWallet.adminkey, - { - targets: this.targets - .filter(isTargetComplete) - .map(({wallet, percent, alias}) => ({wallet, percent, alias})) - } - ) - .then(response => { - this.$q.notify({ - message: 'Split payments targets set.', - timeout: 700 - }) - this.getTargets() - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) - } - }, - created() { - this.selectedWallet = this.g.user.wallets[0] - this.getTargets() - } -}) diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py deleted file mode 100644 index 50057d9ff..000000000 --- a/lnbits/extensions/splitpayments/tasks.py +++ /dev/null @@ -1,77 +0,0 @@ -import json -import trio - -from lnbits.core.models import Payment -from lnbits.core.crud import create_payment -from lnbits.core import db as core_db -from lnbits.tasks import register_invoice_listener, internal_invoice_paid -from lnbits.helpers import urlsafe_short_hash - -from .crud import get_targets - - -async def register_listeners(): - invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) - register_invoice_listener(invoice_paid_chan_send) - await wait_for_paid_invoices(invoice_paid_chan_recv) - - -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: - await on_invoice_paid(payment) - - -async def on_invoice_paid(payment: Payment) -> None: - if "splitpayments" == payment.extra.get("tag") or payment.extra.get("splitted"): - # already splitted, ignore - return - - # now we make some special internal transfers (from no one to the receiver) - targets = await get_targets(payment.wallet_id) - transfers = [ - (target.wallet, int(target.percent * payment.amount / 100)) - for target in targets - ] - transfers = [(wallet, amount) for wallet, amount in transfers if amount > 0] - amount_left = payment.amount - sum([amount for _, amount in transfers]) - - if amount_left < 0: - print("splitpayments failure: amount_left is negative.", payment.payment_hash) - return - - if not targets: - return - - # mark the original payment with one extra key, "splitted" - # (this prevents us from doing this process again and it's informative) - # and reduce it by the amount we're going to send to the producer - await core_db.execute( - """ - UPDATE apipayments - SET extra = ?, amount = ? - WHERE hash = ? - AND checking_id NOT LIKE 'internal_%' - """, - ( - json.dumps(dict(**payment.extra, splitted=True)), - amount_left, - payment.payment_hash, - ), - ) - - # perform the internal transfer using the same payment_hash - for wallet, amount in transfers: - internal_checking_id = f"internal_{urlsafe_short_hash()}" - await create_payment( - wallet_id=wallet, - checking_id=internal_checking_id, - payment_request="", - payment_hash=payment.payment_hash, - amount=amount, - memo=payment.memo, - pending=False, - extra={"tag": "splitpayments"}, - ) - - # manually send this for now - await internal_invoice_paid.send(internal_checking_id) diff --git a/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html b/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html deleted file mode 100644 index e92fac96f..000000000 --- a/lnbits/extensions/splitpayments/templates/splitpayments/_api_docs.html +++ /dev/null @@ -1,90 +0,0 @@ - - - -

- Add some wallets to the list of "Target Wallets", each with an - associated percent. After saving, every time any payment - arrives at the "Source Wallet" that payment will be split with the - target wallets according to their percent. -

-

This is valid for every payment, doesn't matter how it was created.

-

Target wallets can be any wallet from this same LNbits instance.

-

- To remove a wallet from the targets list, just erase its fields and - save. To remove all, click "Clear" then save. -

-
-
-
- - - - - - GET - /splitpayments/api/v1/targets -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [{"wallet": <wallet id>, "alias": <chosen name for this - wallet>, "percent": <number between 1 and 100>}, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/livestream -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - PUT - /splitpayments/api/v1/targets -
Headers
- {"X-Api-Key": <admin_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
-
Curl example
- curl -X PUT {{ request.url_root }}api/v1/splitpayments/targets -H - "X-Api-Key: {{ g.user.wallets[0].adminkey }}" -H 'Content-Type: - application/json' -d '{"targets": [{"wallet": <wallet id or invoice - key>, "alias": <name to identify this>, "percent": <number - between 1 and 100>}, ...]}' - -
-
-
-
diff --git a/lnbits/extensions/splitpayments/templates/splitpayments/index.html b/lnbits/extensions/splitpayments/templates/splitpayments/index.html deleted file mode 100644 index 1aae4e334..000000000 --- a/lnbits/extensions/splitpayments/templates/splitpayments/index.html +++ /dev/null @@ -1,100 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - - - - - - - - - -
-
Target Wallets
-
- - -
- - - -
- - - - - Clear - - - - - - Save Targets - - - -
-
-
-
- -
- - -
- {{SITE_TITLE}} SplitPayments extension -
-
- - - {% include "splitpayments/_api_docs.html" %} - -
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/splitpayments/views.py b/lnbits/extensions/splitpayments/views.py deleted file mode 100644 index 70d4ecb23..000000000 --- a/lnbits/extensions/splitpayments/views.py +++ /dev/null @@ -1,15 +0,0 @@ -from quart import g, render_template - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import splitpayments_ext -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@splitpayments_ext.get("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("splitpayments/index.html", {"request":request, "user":g.user}) diff --git a/lnbits/extensions/splitpayments/views_api.py b/lnbits/extensions/splitpayments/views_api.py deleted file mode 100644 index 76e2b729a..000000000 --- a/lnbits/extensions/splitpayments/views_api.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import List, Optional -from pydantic import BaseModel -from quart import g, jsonify -from http import HTTPStatus - -from lnbits.decorators import api_check_wallet_key, api_validate_post_request -from lnbits.core.crud import get_wallet, get_wallet_for_key - -from . import splitpayments_ext -from .crud import get_targets, set_targets -from .models import Target - - -@splitpayments_ext.get("/api/v1/targets") -@api_check_wallet_key("admin") -async def api_targets_get(): - targets = await get_targets(g.wallet.id) - return [target._asdict() for target in targets] or [] - -class SchemaData(BaseModel): - wallet: str - alias: str - percent: int - -@splitpayments_ext.put("/api/v1/targets") -@api_check_wallet_key("admin") -async def api_targets_set(targets: Optional[List[SchemaData]] - = None): - targets = [] - - for entry in targets: - wallet = await get_wallet(entry["wallet"]) - if not wallet: - wallet = await get_wallet_for_key(entry["wallet"], "invoice") - if not wallet: - return ( - {"message": f"Invalid wallet '{entry['wallet']}'."}, - HTTPStatus.BAD_REQUEST, - ) - - if wallet.id == g.wallet.id: - return ( - {"message": "Can't split to itself."}, - HTTPStatus.BAD_REQUEST, - ) - - if entry["percent"] < 0: - return ( - {"message": f"Invalid percent '{entry['percent']}'."}, - HTTPStatus.BAD_REQUEST, - ) - - targets.append( - Target(wallet.id, g.wallet.id, entry["percent"], entry["alias"] or "") - ) - - percent_sum = sum([target.percent for target in targets]) - if percent_sum > 100: - return {"message": "Splitting over 100%."}, HTTPStatus.BAD_REQUEST - - await set_targets(g.wallet.id, targets) - return "", HTTPStatus.OK diff --git a/lnbits/extensions/streamalerts/README.md b/lnbits/extensions/streamalerts/README.md deleted file mode 100644 index 726ffe767..000000000 --- a/lnbits/extensions/streamalerts/README.md +++ /dev/null @@ -1,39 +0,0 @@ -

Stream Alerts

-

Integrate Bitcoin Donations into your livestream alerts

-The StreamAlerts extension allows you to integrate Bitcoin Lightning (and on-chain) paymnents in to your existing Streamlabs alerts! - -![image](https://user-images.githubusercontent.com/28876473/127759038-aceb2503-6cff-4061-8b81-c769438ebcaa.png) - -

How to set it up

- -At the moment, the only service that has an open API to work with is Streamlabs, so this setup requires linking your Twitch/YouTube/Facebook account to Streamlabs. - -1. Log into [Streamlabs](https://streamlabs.com/login?r=https://streamlabs.com/dashboard). -1. Navigate to the API settings page to register an App: -![image](https://user-images.githubusercontent.com/28876473/127759145-710d53b6-3c19-4815-812a-9a6279d1b8bb.png) -![image](https://user-images.githubusercontent.com/28876473/127759182-da8a27cb-bb59-48fa-868e-c8892080ae98.png) -![image](https://user-images.githubusercontent.com/28876473/127759201-7c28e9f1-6286-42be-a38e-1c377a86976b.png) -1. Fill out the form with anything it will accept as valid. Most fields can be gibberish, as the application is not supposed to ever move past the "testing" stage and is for your personal use only. -In the "Whitelist Users" field, input the username of a Twitch account you control. While this feature is *technically* limited to Twitch, you can use the alerts overlay for donations on YouTube and Facebook as well. -For now, simply set the "Redirect URI" to `http://localhost`, you will change this soon. -Then, hit create: -![image](https://user-images.githubusercontent.com/28876473/127759264-ae91539a-5694-4096-a478-80eb02b7b594.png) -1. In LNbits, enable the Stream Alerts extension and optionally the SatsPayServer (to observe donations directly) and Watch Only (to accept on-chain donations) extenions: -![image](https://user-images.githubusercontent.com/28876473/127759486-0e3420c2-c498-4bf9-932e-0abfa17bd478.png) -1. Create a "NEW SERVICE" using the button. Fill in all the information (you get your Client ID and Secret from the Streamlabs App page): -![image](https://user-images.githubusercontent.com/28876473/127759512-8e8b4e90-2a64-422a-bf0a-5508d0630bed.png) -![image](https://user-images.githubusercontent.com/28876473/127759526-7f2a4980-39ea-4e58-8af0-c9fb381e5524.png) -1. Right-click and copy the "Redirect URI for Streamlabs" link (you might have to refresh the page for the text to turn into a link) and input it into the "Redirect URI" field for your Streamelements App, and hit "Save Settings": -![image](https://user-images.githubusercontent.com/28876473/127759570-52d34c07-6857-467b-bcb3-54e10679aedb.png) -![image](https://user-images.githubusercontent.com/28876473/127759604-b3c8270b-bd02-44df-a525-9d85af337d14.png) -1. You can now authenticate your app on LNbits by clicking on this button and following the instructions. Make sure to log in with the Twitch account you entered in the "Whitelist Users" field: -![image](https://user-images.githubusercontent.com/28876473/127759642-a3787a6a-3cab-4c44-a2d4-ab45fbbe3fab.png) -![image](https://user-images.githubusercontent.com/28876473/127759681-7289e7f6-0ff1-4988-944f-484040f6b9c7.png) -If everything worked correctly, you should now be redirected back to LNbits. When scrolling all the way right, you should now see that the service has been authenticated: -![image](https://user-images.githubusercontent.com/28876473/127759715-7e839261-d505-4e07-a0e4-f347f114149f.png) -You can now share the link to your donations page, which you can get here: -![image](https://user-images.githubusercontent.com/28876473/127759730-8dd11e61-0186-4935-b1ed-b66d35b05043.png) -![image](https://user-images.githubusercontent.com/28876473/127759747-67d3033f-6ef1-4033-b9b1-51b87189ff8b.png) -Of course, this has to be available publicly on the internet (or, depending on your viewers' technical ability, over Tor). -When your viewers donate to you, these donations will now show up in your Streamlabs donation feed, as well as your alerts overlay (if it is configured to include donations). -

CONGRATS! Let the sats flow!

diff --git a/lnbits/extensions/streamalerts/__init__.py b/lnbits/extensions/streamalerts/__init__.py deleted file mode 100644 index 72f0ae7c9..000000000 --- a/lnbits/extensions/streamalerts/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_streamalerts") - -streamalerts_ext: Blueprint = Blueprint( - "streamalerts", __name__, static_folder="static", template_folder="templates" -) - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/streamalerts/config.json b/lnbits/extensions/streamalerts/config.json deleted file mode 100644 index 2fbcc55e2..000000000 --- a/lnbits/extensions/streamalerts/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Stream Alerts", - "short_description": "Integrate Bitcoin donations into your stream alerts!", - "icon": "notifications_active", - "contributors": ["Fittiboy"] -} diff --git a/lnbits/extensions/streamalerts/crud.py b/lnbits/extensions/streamalerts/crud.py deleted file mode 100644 index cbd9d3b04..000000000 --- a/lnbits/extensions/streamalerts/crud.py +++ /dev/null @@ -1,297 +0,0 @@ -from . import db -from .models import Donation, Service - -from ..satspay.crud import delete_charge # type: ignore - -import httpx - -from http import HTTPStatus -from quart import jsonify - -from typing import Optional - -from lnbits.db import SQLITE -from lnbits.helpers import urlsafe_short_hash -from lnbits.core.crud import get_wallet - - -async def get_service_redirect_uri(request, service_id): - """Return the service's redirect URI, to be given to the third party API""" - uri_base = request.scheme + "://" - uri_base += request.headers["Host"] + "/streamalerts/api/v1" - redirect_uri = uri_base + f"/authenticate/{service_id}" - return redirect_uri - - -async def get_charge_details(service_id): - """Return the default details for a satspay charge - - These might be different depending for services implemented in the future. - """ - details = { - "time": 1440, - } - service = await get_service(service_id) - wallet_id = service.wallet - wallet = await get_wallet(wallet_id) - user = wallet.user - details["user"] = user - details["lnbitswallet"] = wallet_id - details["onchainwallet"] = service.onchain - return details - - -async def create_donation( - id: str, - wallet: str, - cur_code: str, - sats: int, - amount: float, - service: int, - name: str = "Anonymous", - message: str = "", - posted: bool = False, -) -> Donation: - """Create a new Donation""" - await db.execute( - """ - INSERT INTO streamalerts.Donations ( - id, - wallet, - name, - message, - cur_code, - sats, - amount, - service, - posted - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - (id, wallet, name, message, cur_code, sats, amount, service, posted), - ) - - donation = await get_donation(id) - assert donation, "Newly created donation couldn't be retrieved" - return donation - - -async def post_donation(donation_id: str) -> tuple: - """Post donations to their respective third party APIs - - If the donation has already been posted, it will not be posted again. - """ - donation = await get_donation(donation_id) - if not donation: - return (jsonify({"message": "Donation not found!"}), HTTPStatus.BAD_REQUEST) - if donation.posted: - return ( - jsonify({"message": "Donation has already been posted!"}), - HTTPStatus.BAD_REQUEST, - ) - service = await get_service(donation.service) - assert service, "Couldn't fetch service to donate to" - - if service.servicename == "Streamlabs": - url = "https://streamlabs.com/api/v1.0/donations" - data = { - "name": donation.name[:25], - "message": donation.message[:255], - "identifier": "LNbits", - "amount": donation.amount, - "currency": donation.cur_code.upper(), - "access_token": service.token, - } - async with httpx.AsyncClient() as client: - response = await client.post(url, data=data) - print(response.json()) - status = [s for s in list(HTTPStatus) if s == response.status_code][0] - elif service.servicename == "StreamElements": - return ( - jsonify({"message": "StreamElements not yet supported!"}), - HTTPStatus.BAD_REQUEST, - ) - else: - return (jsonify({"message": "Unsopported servicename"}), HTTPStatus.BAD_REQUEST) - await db.execute( - "UPDATE streamalerts.Donations SET posted = 1 WHERE id = ?", (donation_id,) - ) - return (jsonify(response.json()), status) - - -async def create_service( - twitchuser: str, - client_id: str, - client_secret: str, - wallet: str, - servicename: str, - state: str = None, - onchain: str = None, -) -> Service: - """Create a new Service""" - - returning = "" if db.type == SQLITE else "RETURNING ID" - method = db.execute if db.type == SQLITE else db.fetchone - - result = await (method)( - f""" - INSERT INTO streamalerts.Services ( - twitchuser, - client_id, - client_secret, - wallet, - servicename, - authenticated, - state, - onchain - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - {returning} - """, - ( - twitchuser, - client_id, - client_secret, - wallet, - servicename, - False, - urlsafe_short_hash(), - onchain, - ), - ) - if db.type == SQLITE: - service_id = result._result_proxy.lastrowid - else: - service_id = result[0] - - service = await get_service(service_id) - assert service - return service - - -async def get_service(service_id: int, by_state: str = None) -> Optional[Service]: - """Return a service either by ID or, available, by state - - Each Service's donation page is reached through its "state" hash - instead of the ID, preventing accidental payments to the wrong - streamer via typos like 2 -> 3. - """ - if by_state: - row = await db.fetchone( - "SELECT * FROM streamalerts.Services WHERE state = ?", (by_state,) - ) - else: - row = await db.fetchone( - "SELECT * FROM streamalerts.Services WHERE id = ?", (service_id,) - ) - return Service.from_row(row) if row else None - - -async def get_services(wallet_id: str) -> Optional[list]: - """Return all services belonging assigned to the wallet_id""" - rows = await db.fetchall( - "SELECT * FROM streamalerts.Services WHERE wallet = ?", (wallet_id,) - ) - return [Service.from_row(row) for row in rows] if rows else None - - -async def authenticate_service(service_id, code, redirect_uri): - """Use authentication code from third party API to retreive access token""" - # The API token is passed in the querystring as 'code' - service = await get_service(service_id) - wallet = await get_wallet(service.wallet) - user = wallet.user - url = "https://streamlabs.com/api/v1.0/token" - data = { - "grant_type": "authorization_code", - "code": code, - "client_id": service.client_id, - "client_secret": service.client_secret, - "redirect_uri": redirect_uri, - } - print(data) - async with httpx.AsyncClient() as client: - response = (await client.post(url, data=data)).json() - print(response) - token = response["access_token"] - success = await service_add_token(service_id, token) - return f"/streamalerts/?usr={user}", success - - -async def service_add_token(service_id, token): - """Add access token to its corresponding Service - - This also sets authenticated = 1 to make sure the token - is not overwritten. - Tokens for Streamlabs never need to be refreshed. - """ - if (await get_service(service_id)).authenticated: - return False - await db.execute( - "UPDATE streamalerts.Services SET authenticated = 1, token = ? where id = ?", - ( - token, - service_id, - ), - ) - return True - - -async def delete_service(service_id: int) -> None: - """Delete a Service and all corresponding Donations""" - await db.execute("DELETE FROM streamalerts.Services WHERE id = ?", (service_id,)) - rows = await db.fetchall( - "SELECT * FROM streamalerts.Donations WHERE service = ?", (service_id,) - ) - for row in rows: - await delete_donation(row["id"]) - - -async def get_donation(donation_id: str) -> Optional[Donation]: - """Return a Donation""" - row = await db.fetchone( - "SELECT * FROM streamalerts.Donations WHERE id = ?", (donation_id,) - ) - return Donation.from_row(row) if row else None - - -async def get_donations(wallet_id: str) -> Optional[list]: - """Return all streamalerts.Donations assigned to wallet_id""" - rows = await db.fetchall( - "SELECT * FROM streamalerts.Donations WHERE wallet = ?", (wallet_id,) - ) - return [Donation.from_row(row) for row in rows] if rows else None - - -async def delete_donation(donation_id: str) -> None: - """Delete a Donation and its corresponding statspay charge""" - await db.execute("DELETE FROM streamalerts.Donations WHERE id = ?", (donation_id,)) - await delete_charge(donation_id) - - -async def update_donation(donation_id: str, **kwargs) -> Donation: - """Update a Donation""" - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE streamalerts.Donations SET {q} WHERE id = ?", - (*kwargs.values(), donation_id), - ) - row = await db.fetchone( - "SELECT * FROM streamalerts.Donations WHERE id = ?", (donation_id,) - ) - assert row, "Newly updated donation couldn't be retrieved" - return Donation(**row) - - -async def update_service(service_id: str, **kwargs) -> Service: - """Update a service""" - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE streamalerts.Services SET {q} WHERE id = ?", - (*kwargs.values(), service_id), - ) - row = await db.fetchone( - "SELECT * FROM streamalerts.Services WHERE id = ?", (service_id,) - ) - assert row, "Newly updated service couldn't be retrieved" - return Service(**row) diff --git a/lnbits/extensions/streamalerts/migrations.py b/lnbits/extensions/streamalerts/migrations.py deleted file mode 100644 index 1b0cea375..000000000 --- a/lnbits/extensions/streamalerts/migrations.py +++ /dev/null @@ -1,35 +0,0 @@ -async def m001_initial(db): - - await db.execute( - f""" - CREATE TABLE IF NOT EXISTS streamalerts.Services ( - id {db.serial_primary_key}, - state TEXT NOT NULL, - twitchuser TEXT NOT NULL, - client_id TEXT NOT NULL, - client_secret TEXT NOT NULL, - wallet TEXT NOT NULL, - onchain TEXT, - servicename TEXT NOT NULL, - authenticated BOOLEAN NOT NULL, - token TEXT - ); - """ - ) - - await db.execute( - f""" - CREATE TABLE IF NOT EXISTS streamalerts.Donations ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - name TEXT NOT NULL, - message TEXT NOT NULL, - cur_code TEXT NOT NULL, - sats INT NOT NULL, - amount FLOAT NOT NULL, - service INTEGER NOT NULL, - posted BOOLEAN NOT NULL, - FOREIGN KEY(service) REFERENCES {db.references_schema}Services(id) - ); - """ - ) diff --git a/lnbits/extensions/streamalerts/models.py b/lnbits/extensions/streamalerts/models.py deleted file mode 100644 index 81e47386e..000000000 --- a/lnbits/extensions/streamalerts/models.py +++ /dev/null @@ -1,45 +0,0 @@ -from sqlite3 import Row -from pydantic import BaseModel -from typing import Optional - - -class Donation(BaseModel): - """A Donation simply contains all the necessary information about a - user's donation to a streamer - """ - - id: str # This ID always corresponds to a satspay charge ID - wallet: str - name: str # Name of the donor - message: str # Donation message - cur_code: str # Three letter currency code accepted by Streamlabs - sats: int - amount: float # The donation amount after fiat conversion - service: int # The ID of the corresponding Service - posted: bool # Whether the donation has already been posted to a Service - - @classmethod - def from_row(cls, row: Row) -> "Donation": - return cls(**dict(row)) - - -class Service(BaseModel): - """A Service represents an integration with a third-party API - - Currently, Streamlabs is the only supported Service. - """ - - id: int - state: str # A random hash used during authentication - twitchuser: str # The Twitch streamer's username - client_id: str # Third party service Client ID - client_secret: str # Secret corresponding to the Client ID - wallet: str - onchain: str - servicename: str # Currently, this will just always be "Streamlabs" - authenticated: bool # Whether a token (see below) has been acquired yet - token: Optional[int] # The token with which to authenticate requests - - @classmethod - def from_row(cls, row: Row) -> "Service": - return cls(**dict(row)) diff --git a/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html b/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html deleted file mode 100644 index 33b52f150..000000000 --- a/lnbits/extensions/streamalerts/templates/streamalerts/_api_docs.html +++ /dev/null @@ -1,18 +0,0 @@ - - -

- Stream Alerts: Integrate Bitcoin into your stream alerts! -

-

- Accept Bitcoin donations on Twitch, and integrate them into your alerts. - Present your viewers with a simple donation page, and add those donations - to Streamlabs to play alerts on your stream!
- For detailed setup instructions, check out - this guide!
- - Created by, Fitti -

-
-
diff --git a/lnbits/extensions/streamalerts/templates/streamalerts/display.html b/lnbits/extensions/streamalerts/templates/streamalerts/display.html deleted file mode 100644 index a10e64d88..000000000 --- a/lnbits/extensions/streamalerts/templates/streamalerts/display.html +++ /dev/null @@ -1,97 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -
Donate Bitcoin to {{ twitchuser }}
-
- - - - -
- Submit -
-
-
-
-
-
- -{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/streamalerts/templates/streamalerts/index.html b/lnbits/extensions/streamalerts/templates/streamalerts/index.html deleted file mode 100644 index 46d1bb313..000000000 --- a/lnbits/extensions/streamalerts/templates/streamalerts/index.html +++ /dev/null @@ -1,502 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New Service - - - - - -
-
-
Services
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - - -
-
-
Donations
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
-
- - -
- {{SITE_TITLE}} Stream Alerts extension -
-
- - - {% include "streamalerts/_api_docs.html" %} - -
-
- - - - - - -
-
-
- -
-
- - - Watch-Only extension MUST be activated and have a wallet - - -
-
-
-
- -
- - - - -
- Update Service - - Create Service - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/streamalerts/views.py b/lnbits/extensions/streamalerts/views.py deleted file mode 100644 index 15a2c399c..000000000 --- a/lnbits/extensions/streamalerts/views.py +++ /dev/null @@ -1,32 +0,0 @@ -from quart import g, abort, render_template - -from lnbits.decorators import check_user_exists, validate_uuids -from http import HTTPStatus - -from . import streamalerts_ext -from .crud import get_service -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@streamalerts_ext.get("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - """Return the extension's settings page""" - return await templates.TemplateResponse("streamalerts/index.html", {"request":request, "user":g.user}) - - -@streamalerts_ext.get("/") -async def donation(request: Request, state): - """Return the donation form for the Service corresponding to state""" - service = await get_service(0, by_state=state) - if not service: - abort(HTTPStatus.NOT_FOUND, "Service does not exist.") - return await templates.TemplateResponse( - "streamalerts/display.html", - {"request":request, - "twitchuser":service.twitchuser, - "service":service.id} - ) diff --git a/lnbits/extensions/streamalerts/views_api.py b/lnbits/extensions/streamalerts/views_api.py deleted file mode 100644 index 992e51882..000000000 --- a/lnbits/extensions/streamalerts/views_api.py +++ /dev/null @@ -1,262 +0,0 @@ -from typing import Optional -from pydantic.main import BaseModel -from quart import g, redirect, request, jsonify -from http import HTTPStatus - -from lnbits.decorators import api_validate_post_request, api_check_wallet_key -from lnbits.core.crud import get_user -from lnbits.utils.exchange_rates import btc_price - -from . import streamalerts_ext -from .crud import ( - get_charge_details, - get_service_redirect_uri, - create_donation, - post_donation, - get_donation, - get_donations, - delete_donation, - create_service, - get_service, - get_services, - authenticate_service, - update_donation, - update_service, - delete_service, -) -from ..satspay.crud import create_charge, get_charge - -class CreateServicesData(BaseModel): - twitchuser: str - client_id: str - client_secret: str - wallet: str - servicename: str - onchain: Optional[str] - -@streamalerts_ext.post("/api/v1/services") -@api_check_wallet_key("invoice") -async def api_create_service(data: CreateServicesData): - """Create a service, which holds data about how/where to post donations""" - try: - service = await create_service(**data) - except Exception as e: - return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR - - return service._asdict(), HTTPStatus.CREATED - - -@streamalerts_ext.get("/api/v1/getaccess/{service_id}") -async def api_get_access(service_id): - """Redirect to Streamlabs' Approve/Decline page for API access for Service - with service_id - """ - service = await get_service(service_id) - if service: - redirect_uri = await get_service_redirect_uri(request, service_id) - params = { - "response_type": "code", - "client_id": service.client_id, - "redirect_uri": redirect_uri, - "scope": "donations.create", - "state": service.state, - } - endpoint_url = "https://streamlabs.com/api/v1.0/authorize/?" - querystring = "&".join([f"{key}={value}" for key, value in params.items()]) - redirect_url = endpoint_url + querystring - return redirect(redirect_url) - else: - return ({"message": "Service does not exist!"}, HTTPStatus.BAD_REQUEST) - - -@streamalerts_ext.get("/api/v1/authenticate/{service_id}") -async def api_authenticate_service(Code: str, State: str, service_id): - """Endpoint visited via redirect during third party API authentication - - If successful, an API access token will be added to the service, and - the user will be redirected to index.html. - """ - code = Code - state = State - service = await get_service(service_id) - if service.state != state: - return ({"message": "State doesn't match!"}, HTTPStatus.BAD_Request) - redirect_uri = request.scheme + "://" + request.headers["Host"] - redirect_uri += f"/streamalerts/api/v1/authenticate/{service_id}" - url, success = await authenticate_service(service_id, code, redirect_uri) - if success: - return redirect(url) - else: - return ({"message": "Service already authenticated!"}, - HTTPStatus.BAD_REQUEST, - ) - -class CreateDonationsData(BaseModel): - name: str - sats: int - service: int - message: str - -@streamalerts_ext.post("/api/v1/donations") -async def api_create_donation(data:CreateDonationsData): - """Take data from donation form and return satspay charge""" - # Currency is hardcoded while frotnend is limited - cur_code = "USD" - sats = data.sats - message = data.message - # Fiat amount is calculated here while frontend is limited - price = await btc_price(cur_code) - amount = sats * (10 ** (-8)) * price - webhook_base = request.scheme + "://" + request.headers["Host"] - service_id = data.service - service = await get_service(service_id) - charge_details = await get_charge_details(service.id) - name = data.name - if not name: - name = "Anonymous" - description = f"{sats} sats donation from {name} to {service.twitchuser}" - charge = await create_charge( - amount=sats, - completelink=f"https://twitch.tv/{service.twitchuser}", - completelinktext="Back to Stream!", - webhook=webhook_base + "/streamalerts/api/v1/postdonation", - description=description, - **charge_details, - ) - await create_donation( - id=charge.id, - wallet=service.wallet, - message=message, - name=name, - cur_code=cur_code, - sats=data.sats, - amount=amount, - service=data.service, - ) - return ({"redirect_url": f"/satspay/{charge.id}"}), HTTPStatus.OK - - -@streamalerts_ext.post("/api/v1/postdonation") - -async def api_post_donation(id: str): - """Post a paid donation to Stremalabs/StreamElements. - - This endpoint acts as a webhook for the SatsPayServer extension.""" - data = await request.get_json(force=True) - donation_id = id - charge = await get_charge(donation_id) - if charge and charge.paid: - return await post_donation(donation_id) - else: - return ({"message": "Not a paid charge!"}, HTTPStatus.BAD_REQUEST) - - -@streamalerts_ext.get("/api/v1/services") -@api_check_wallet_key("invoice") -async def api_get_services(): - """Return list of all services assigned to wallet with given invoice key""" - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - services = [] - for wallet_id in wallet_ids: - new_services = await get_services(wallet_id) - services += new_services if new_services else [] - return ( - [service._asdict() for service in services] if services else [], - HTTPStatus.OK, - ) - - -@streamalerts_ext.get("/api/v1/donations") -@api_check_wallet_key("invoice") -async def api_get_donations(): - """Return list of all donations assigned to wallet with given invoice - key - """ - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - donations = [] - for wallet_id in wallet_ids: - new_donations = await get_donations(wallet_id) - donations += new_donations if new_donations else [] - return ( - [donation._asdict() for donation in donations] if donations else [], - HTTPStatus.OK, - ) - - -@streamalerts_ext.put("/api/v1/donations/{donation_id}") -@api_check_wallet_key("invoice") -async def api_update_donation(donation_id=None): - """Update a donation with the data given in the request""" - if donation_id: - donation = await get_donation(donation_id) - - if not donation: - return ( - {"message": "Donation does not exist."}, - HTTPStatus.NOT_FOUND, - ) - - if donation.wallet != g.wallet.id: - return ({"message": "Not your donation."}, HTTPStatus.FORBIDDEN) - - donation = await update_donation(donation_id, **g.data) - else: - return ( - {"message": "No donation ID specified"}, - HTTPStatus.BAD_REQUEST, - ) - return donation._asdict(), HTTPStatus.CREATED - - -@streamalerts_ext.put("/api/v1/services/{service_id}") -@api_check_wallet_key("invoice") -async def api_update_service(service_id=None): - """Update a service with the data given in the request""" - if service_id: - service = await get_service(service_id) - - if not service: - return ({"message": "Service does not exist."}, - HTTPStatus.NOT_FOUND, - ) - - if service.wallet != g.wallet.id: - return ({"message": "Not your service."}), HTTPStatus.FORBIDDEN - - service = await update_service(service_id, **g.data) - else: - return ({"message": "No service ID specified"}), HTTPStatus.BAD_REQUEST - return service._asdict(), HTTPStatus.CREATED - - -@streamalerts_ext.delete("/api/v1/donations/{donation_id}") -@api_check_wallet_key("invoice") -async def api_delete_donation(donation_id): - """Delete the donation with the given donation_id""" - donation = await get_donation(donation_id) - if not donation: - return ({"message": "No donation with this ID!"}, HTTPStatus.NOT_FOUND) - if donation.wallet != g.wallet.id: - return ({"message": "Not authorized to delete this donation!"}, - HTTPStatus.FORBIDDEN, - ) - await delete_donation(donation_id) - - return "", HTTPStatus.NO_CONTENT - - -@streamalerts_ext.delete("/api/v1/services/{service_id}") -@api_check_wallet_key("invoice") -async def api_delete_service(service_id): - """Delete the service with the given service_id""" - service = await get_service(service_id) - if not service: - return ({"message": "No service with this ID!"}, HTTPStatus.NOT_FOUND) - if service.wallet != g.wallet.id: - return ( - {"message": "Not authorized to delete this service!"}, - HTTPStatus.FORBIDDEN, - ) - await delete_service(service_id) - - return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/subdomains/README.md b/lnbits/extensions/subdomains/README.md deleted file mode 100644 index 729f40f41..000000000 --- a/lnbits/extensions/subdomains/README.md +++ /dev/null @@ -1,54 +0,0 @@ -

Subdomains Extension

- -So the goal of the extension is to allow the owner of a domain to sell subdomains to anyone who is willing to pay some money for it. - -[![video tutorial livestream](http://img.youtube.com/vi/O1X0fy3uNpw/0.jpg)](https://youtu.be/O1X0fy3uNpw 'video tutorial subdomains') - -## Requirements - -- Free Cloudflare account -- Cloudflare as a DNS server provider -- Cloudflare TOKEN and Cloudflare zone-ID where the domain is parked - -## Usage - -1. Register at Cloudflare and setup your domain with them. (Just follow instructions they provide...) -2. Change DNS server at your domain registrar to point to Cloudflare's -3. Get Cloudflare zone-ID for your domain - -4. Get Cloudflare API TOKEN - - -5. Open the LNBits subdomains extension and register your domain -6. Click on the button in the table to open the public form that was generated for your domain - - - Extension also supports webhooks so you can get notified when someone buys a new subdomain\ - - -## API Endpoints - -- **Domains** - - GET /api/v1/domains - - POST /api/v1/domains - - PUT /api/v1/domains/ - - DELETE /api/v1/domains/ -- **Subdomains** - - GET /api/v1/subdomains - - POST /api/v1/subdomains/ - - GET /api/v1/subdomains/ - - DELETE /api/v1/subdomains/ - -### Cloudflare - -- Cloudflare offers programmatic subdomain registration... (create new A record) -- you can keep your existing domain's registrar, you just have to transfer dns records to the cloudflare (free service) -- more information: - - https://api.cloudflare.com/#getting-started-requests - - API endpoints needed for our project: - - https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records - - https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record - - https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record - - https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record -- api can be used by providing authorization token OR authorization key - - check API Tokens and API Keys : https://api.cloudflare.com/#getting-started-requests -- Cloudflare API postman collection: https://support.cloudflare.com/hc/en-us/articles/115002323852-Using-Cloudflare-API-with-Postman-Collections diff --git a/lnbits/extensions/subdomains/__init__.py b/lnbits/extensions/subdomains/__init__.py deleted file mode 100644 index 5013230c9..000000000 --- a/lnbits/extensions/subdomains/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_subdomains") - -subdomains_ext: Blueprint = Blueprint( - "subdomains", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa - -from .tasks import register_listeners -from lnbits.tasks import record_async - -subdomains_ext.record(record_async(register_listeners)) diff --git a/lnbits/extensions/subdomains/cloudflare.py b/lnbits/extensions/subdomains/cloudflare.py deleted file mode 100644 index 9a0fc4cfb..000000000 --- a/lnbits/extensions/subdomains/cloudflare.py +++ /dev/null @@ -1,60 +0,0 @@ -from lnbits.extensions.subdomains.models import Domains -import httpx, json - - -async def cloudflare_create_subdomain( - domain: Domains, subdomain: str, record_type: str, ip: str -): - # Call to cloudflare sort of a dry-run - if success delete the domain and wait for payment - ### SEND REQUEST TO CLOUDFLARE - url = ( - "https://api.cloudflare.com/client/v4/zones/" - + domain.cf_zone_id - + "/dns_records" - ) - header = { - "Authorization": "Bearer " + domain.cf_token, - "Content-Type": "application/json", - } - aRecord = subdomain + "." + domain.domain - cf_response = "" - async with httpx.AsyncClient() as client: - try: - r = await client.post( - url, - headers=header, - json={ - "type": record_type, - "name": aRecord, - "content": ip, - "ttl": 0, - "proxed": False, - }, - timeout=40, - ) - cf_response = json.loads(r.text) - except AssertionError: - cf_response = "Error occured" - return cf_response - - -async def cloudflare_deletesubdomain(domain: Domains, domain_id: str): - url = ( - "https://api.cloudflare.com/client/v4/zones/" - + domain.cf_zone_id - + "/dns_records" - ) - header = { - "Authorization": "Bearer " + domain.cf_token, - "Content-Type": "application/json", - } - async with httpx.AsyncClient() as client: - try: - r = await client.delete( - url + "/" + domain_id, - headers=header, - timeout=40, - ) - cf_response = r.text - except AssertionError: - cf_response = "Error occured" diff --git a/lnbits/extensions/subdomains/config.json b/lnbits/extensions/subdomains/config.json deleted file mode 100644 index 6bf9480cd..000000000 --- a/lnbits/extensions/subdomains/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "Subdomains", - "short_description": "Sell subdomains of your domain", - "icon": "domain", - "contributors": ["grmkris"] -} diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py deleted file mode 100644 index 08cb19eb4..000000000 --- a/lnbits/extensions/subdomains/crud.py +++ /dev/null @@ -1,182 +0,0 @@ -from typing import List, Optional, Union - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import Domains, Subdomains - - -async def create_subdomain( - payment_hash: str, - wallet: str, - domain: str, - subdomain: str, - email: str, - ip: str, - sats: int, - duration: int, - record_type: str, -) -> Subdomains: - await db.execute( - """ - INSERT INTO subdomains.subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid, record_type) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - payment_hash, - domain, - email, - subdomain, - ip, - wallet, - sats, - duration, - False, - record_type, - ), - ) - - new_subdomain = await get_subdomain(payment_hash) - assert new_subdomain, "Newly created subdomain couldn't be retrieved" - return new_subdomain - - -async def set_subdomain_paid(payment_hash: str) -> Subdomains: - row = await db.fetchone( - "SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?", - (payment_hash,), - ) - if row[8] == False: - await db.execute( - """ - UPDATE subdomains.subdomain - SET paid = true - WHERE id = ? - """, - (payment_hash,), - ) - - domaindata = await get_domain(row[1]) - assert domaindata, "Couldn't get domain from paid subdomain" - - amount = domaindata.amountmade + row[8] - await db.execute( - """ - UPDATE subdomains.domain - SET amountmade = ? - WHERE id = ? - """, - (amount, row[1]), - ) - - new_subdomain = await get_subdomain(payment_hash) - assert new_subdomain, "Newly paid subdomain couldn't be retrieved" - return new_subdomain - - -async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]: - row = await db.fetchone( - "SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?", - (subdomain_id,), - ) - return Subdomains(**row) if row else None - - -async def get_subdomainBySubdomain(subdomain: str) -> Optional[Subdomains]: - row = await db.fetchone( - "SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.subdomain = ?", - (subdomain,), - ) - print(row) - return Subdomains(**row) if row else None - - -async def get_subdomains(wallet_ids: Union[str, List[str]]) -> List[Subdomains]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT s.*, d.domain as domain_name FROM subdomains.subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.wallet IN ({q})", - (*wallet_ids,), - ) - - return [Subdomains(**row) for row in rows] - - -async def delete_subdomain(subdomain_id: str) -> None: - await db.execute("DELETE FROM subdomains.subdomain WHERE id = ?", (subdomain_id,)) - - -# Domains - - -async def create_domain( - *, - wallet: str, - domain: str, - cf_token: str, - cf_zone_id: str, - webhook: Optional[str] = None, - description: str, - cost: int, - allowed_record_types: str, -) -> Domains: - domain_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO subdomains.domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade, allowed_record_types) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - domain_id, - wallet, - domain, - webhook, - cf_token, - cf_zone_id, - description, - cost, - 0, - allowed_record_types, - ), - ) - - new_domain = await get_domain(domain_id) - assert new_domain, "Newly created domain couldn't be retrieved" - return new_domain - - -async def update_domain(domain_id: str, **kwargs) -> Domains: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - await db.execute( - f"UPDATE subdomains.domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id) - ) - row = await db.fetchone( - "SELECT * FROM subdomains.domain WHERE id = ?", (domain_id,) - ) - assert row, "Newly updated domain couldn't be retrieved" - return Domains(**row) - - -async def get_domain(domain_id: str) -> Optional[Domains]: - row = await db.fetchone( - "SELECT * FROM subdomains.domain WHERE id = ?", (domain_id,) - ) - return Domains(**row) if row else None - - -async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domains]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM subdomains.domain WHERE wallet IN ({q})", (*wallet_ids,) - ) - - return [Domains(**row) for row in rows] - - -async def delete_domain(domain_id: str) -> None: - await db.execute("DELETE FROM subdomains.domain WHERE id = ?", (domain_id,)) diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py deleted file mode 100644 index 292d1f180..000000000 --- a/lnbits/extensions/subdomains/migrations.py +++ /dev/null @@ -1,41 +0,0 @@ -async def m001_initial(db): - - await db.execute( - """ - CREATE TABLE subdomains.domain ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - domain TEXT NOT NULL, - webhook TEXT, - cf_token TEXT NOT NULL, - cf_zone_id TEXT NOT NULL, - description TEXT NOT NULL, - cost INTEGER NOT NULL, - amountmade INTEGER NOT NULL, - allowed_record_types TEXT NOT NULL, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) - - await db.execute( - """ - CREATE TABLE subdomains.subdomain ( - id TEXT PRIMARY KEY, - domain TEXT NOT NULL, - email TEXT NOT NULL, - subdomain TEXT NOT NULL, - ip TEXT NOT NULL, - wallet TEXT NOT NULL, - sats INTEGER NOT NULL, - duration INTEGER NOT NULL, - paid BOOLEAN NOT NULL, - record_type TEXT NOT NULL, - time TIMESTAMP NOT NULL DEFAULT """ - + db.timestamp_now - + """ - ); - """ - ) diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py deleted file mode 100644 index 0f2648d25..000000000 --- a/lnbits/extensions/subdomains/models.py +++ /dev/null @@ -1,30 +0,0 @@ - -from pydantic import BaseModel - -class Domains(BaseModel): - id: str - wallet: str - domain: str - cf_token: str - cf_zone_id: str - webhook: str - description: str - cost: int - amountmade: int - time: int - allowed_record_types: str - - -class Subdomains(BaseModel): - id: str - wallet: str - domain: str - domain_name: str - subdomain: str - email: str - ip: str - sats: int - duration: int - paid: bool - time: int - record_type: str diff --git a/lnbits/extensions/subdomains/tasks.py b/lnbits/extensions/subdomains/tasks.py deleted file mode 100644 index b15703fb6..000000000 --- a/lnbits/extensions/subdomains/tasks.py +++ /dev/null @@ -1,61 +0,0 @@ -from http import HTTPStatus -from quart.json import jsonify -import trio -import httpx - -from .crud import get_domain, set_subdomain_paid -from lnbits.core.crud import get_user, get_wallet -from lnbits.core import db as core_db -from lnbits.core.models import Payment -from lnbits.tasks import register_invoice_listener -from .cloudflare import cloudflare_create_subdomain - - -async def register_listeners(): - invoice_paid_chan_send, invoice_paid_chan_recv = trio.open_memory_channel(2) - register_invoice_listener(invoice_paid_chan_send) - await wait_for_paid_invoices(invoice_paid_chan_recv) - - -async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): - async for payment in invoice_paid_chan: - await on_invoice_paid(payment) - - -async def on_invoice_paid(payment: Payment) -> None: - if "lnsubdomain" != payment.extra.get("tag"): - # not an lnurlp invoice - return - - await payment.set_pending(False) - subdomain = await set_subdomain_paid(payment_hash=payment.payment_hash) - domain = await get_domain(subdomain.domain) - - ### Create subdomain - cf_response = cloudflare_create_subdomain( - domain=domain, - subdomain=subdomain.subdomain, - record_type=subdomain.record_type, - ip=subdomain.ip, - ) - - ### Use webhook to notify about cloudflare registration - if domain.webhook: - async with httpx.AsyncClient() as client: - try: - r = await client.post( - domain.webhook, - json={ - "domain": subdomain.domain_name, - "subdomain": subdomain.subdomain, - "record_type": subdomain.record_type, - "email": subdomain.email, - "ip": subdomain.ip, - "cost:": str(subdomain.sats) + " sats", - "duration": str(subdomain.duration) + " days", - "cf_response": cf_response, - }, - timeout=40, - ) - except AssertionError: - webhook = None diff --git a/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html b/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html deleted file mode 100644 index b839c641d..000000000 --- a/lnbits/extensions/subdomains/templates/subdomains/_api_docs.html +++ /dev/null @@ -1,26 +0,0 @@ - - - -
- lnSubdomains: Get paid sats to sell your subdomains -
-

- Charge people for using your subdomain name...
- - More details -
- - Created by, Kris -

-
-
-
diff --git a/lnbits/extensions/subdomains/templates/subdomains/display.html b/lnbits/extensions/subdomains/templates/subdomains/display.html deleted file mode 100644 index e52ac73cf..000000000 --- a/lnbits/extensions/subdomains/templates/subdomains/display.html +++ /dev/null @@ -1,221 +0,0 @@ -{% extends "public.html" %} {% block page %} -
-
- - -

{{ domain_domain }}

-
-
{{ domain_desc }}
-
- - - - - - - - - -

- Cost per day: {{ domain_cost }} sats
- {% raw %} Total cost: {{amountSats}} sats {% endraw %} -

-
- Submit - Cancel -
-
-
-
-
- - - - - - -
- Copy invoice - Close -
-
-
-
- -{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html deleted file mode 100644 index 26b5d7a68..000000000 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ /dev/null @@ -1,550 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} - -
-
- - - New Domain - - - - - -
-
-
Domains
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - - -
-
-
Subdomains
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
-
- - -
- {{SITE_TITLE}} Subdomain extension -
-
- - - {% include "subdomains/_api_docs.html" %} - -
-
- - - - - - - - - - - - - - - - -
- Update Form - Create Domain - Cancel -
-
-
-
-
- -{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/subdomains/util.py b/lnbits/extensions/subdomains/util.py deleted file mode 100644 index c7d663073..000000000 --- a/lnbits/extensions/subdomains/util.py +++ /dev/null @@ -1,36 +0,0 @@ -from lnbits.extensions.subdomains.models import Subdomains - -# Python3 program to validate -# domain name -# using regular expression -import re -import socket - -# Function to validate domain name. -def isValidDomain(str): - # Regex to check valid - # domain name. - regex = "^((?!-)[A-Za-z0-9-]{1,63}(?") -async def display(request: Request, domain_id): - domain = await get_domain(domain_id) - if not domain: - abort(HTTPStatus.NOT_FOUND, "Domain does not exist.") - allowed_records = ( - domain.allowed_record_types.replace('"', "").replace(" ", "").split(",") - ) - print(allowed_records) - return await templates.TemplateResponse( - "subdomains/display.html", - {"request":request, - "domain_id":domain.id, - "domain_domain":domain.domain, - "domain_desc":domain.description, - "domain_cost":domain.cost, - "domain_allowed_record_types":allowed_records} - ) diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py deleted file mode 100644 index 720002380..000000000 --- a/lnbits/extensions/subdomains/views_api.py +++ /dev/null @@ -1,215 +0,0 @@ -from typing import Optional -from fastapi.param_functions import Query -from pydantic.main import BaseModel -from quart import g, jsonify, request -from http import HTTPStatus - -from lnbits.core.crud import get_user -from lnbits.core.services import create_invoice, check_invoice_status -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from . import subdomains_ext -from .crud import ( - create_subdomain, - get_subdomain, - get_subdomains, - delete_subdomain, - create_domain, - update_domain, - get_domain, - get_domains, - delete_domain, - get_subdomainBySubdomain, -) -from .cloudflare import cloudflare_create_subdomain, cloudflare_deletesubdomain - - -# domainS - - -@subdomains_ext.get("/api/v1/domains") -@api_check_wallet_key("invoice") -async def api_domains(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - return ( - [domain._asdict() for domain in await get_domains(wallet_ids)], - HTTPStatus.OK, - ) - -class CreateDomainsData(BaseModel): - wallet: str - domain: str - cf_token: str - cf_zone_id: str - webhook: Optional[str] - description: str - cost: int - allowed_record_types: str - -@subdomains_ext.post("/api/v1/domains") -@subdomains_ext.put("/api/v1/domains/{domain_id}") -@api_check_wallet_key("invoice") -async def api_domain_create(data: CreateDomainsData, domain_id=None): - if domain_id: - domain = await get_domain(domain_id) - - if not domain: - return {"message": "domain does not exist."}, HTTPStatus.NOT_FOUND - - if domain.wallet != g.wallet.id: - return {"message": "Not your domain."}, HTTPStatus.FORBIDDEN - - domain = await update_domain(domain_id, **data) - else: - domain = await create_domain(**data) - return domain._asdict(), HTTPStatus.CREATED - - -@subdomains_ext.delete("/api/v1/domains/{domain_id}") -@api_check_wallet_key("invoice") -async def api_domain_delete(domain_id): - domain = await get_domain(domain_id) - - if not domain: - return {"message": "domain does not exist."}, HTTPStatus.NOT_FOUND - - if domain.wallet != g.wallet.id: - return {"message": "Not your domain."}, HTTPStatus.FORBIDDEN - - await delete_domain(domain_id) - - return "", HTTPStatus.NO_CONTENT - - -#########subdomains########## - - -@subdomains_ext.get("/api/v1/subdomains") -@api_check_wallet_key("invoice") -async def api_subdomains(): - wallet_ids = [g.wallet.id] - - if "all_wallets" in request.args: - wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - return ( - [domain._asdict() for domain in await get_subdomains(wallet_ids)], - HTTPStatus.OK, - ) - -class CreateDomainsData(BaseModel): - domain: str - subdomain: str - email: str - ip: str - sats: int = Query(0, ge=0) - duration: int - record_type: str - -@subdomains_ext.post("/api/v1/subdomains/{domain_id}") - -async def api_subdomain_make_subdomain(data: CreateDomainsData, domain_id): - domain = await get_domain(domain_id) - - # If the request is coming for the non-existant domain - if not domain: - return {"message": "LNsubdomain does not exist."}, HTTPStatus.NOT_FOUND - - ## If record_type is not one of the allowed ones reject the request - if data.record_type not in domain.allowed_record_types: - return ({"message": data.record_type + "Not a valid record"}, - HTTPStatus.BAD_REQUEST, - ) - - ## If domain already exist in our database reject it - if await get_subdomainBySubdomain(data.subdomain) is not None: - return ( - { - "message": data.subdomain - + "." - + domain.domain - + " domain already taken" - }, - HTTPStatus.BAD_REQUEST, - ) - - ## Dry run cloudflare... (create and if create is sucessful delete it) - cf_response = await cloudflare_create_subdomain( - domain=domain, - subdomain=data.subdomain, - record_type=data.record_type, - ip=data.ip, - ) - if cf_response["success"] == True: - cloudflare_deletesubdomain(domain=domain, domain_id=cf_response["result"]["id"]) - else: - return ( - { - "message": "Problem with cloudflare: " - + cf_response["errors"][0]["message"] - }, - HTTPStatus.BAD_REQUEST, - ) - - ## ALL OK - create an invoice and return it to the user - sats = data.sats - - try: - payment_hash, payment_request = await create_invoice( - wallet_id=domain.wallet, - amount=sats, - memo=f"subdomain {data.subdomain}.{domain.domain} for {sats} sats for {data.duration} days", - extra={"tag": "lnsubdomain"}, - ) - except Exception as e: - return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR - - subdomain = await create_subdomain( - payment_hash=payment_hash, wallet=domain.wallet, **data - ) - - if not subdomain: - return ( - {"message": "LNsubdomain could not be fetched."}, - HTTPStatus.NOT_FOUND, - ) - - return ( - {"payment_hash": payment_hash, "payment_request": payment_request}, - HTTPStatus.OK, - ) - - -@subdomains_ext.get("/api/v1/subdomains/{payment_hash}") -async def api_subdomain_send_subdomain(payment_hash): - subdomain = await get_subdomain(payment_hash) - try: - status = await check_invoice_status(subdomain.wallet, payment_hash) - is_paid = not status.pending - except Exception: - return {"paid": False}, HTTPStatus.OK - - if is_paid: - return {"paid": True}, HTTPStatus.OK - - return {"paid": False}, HTTPStatus.OK - - -@subdomains_ext.delete("/api/v1/subdomains/{subdomain_id}") -@api_check_wallet_key("invoice") -async def api_subdomain_delete(subdomain_id): - subdomain = await get_subdomain(subdomain_id) - - if not subdomain: - return {"message": "Paywall does not exist."}, HTTPStatus.NOT_FOUND - - if subdomain.wallet != g.wallet.id: - return {"message": "Not your subdomain."}, HTTPStatus.FORBIDDEN - - await delete_subdomain(subdomain_id) - - return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/tpos/README.md b/lnbits/extensions/tpos/README.md deleted file mode 100644 index 04e049e37..000000000 --- a/lnbits/extensions/tpos/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# TPoS - -## A Shareable PoS (Point of Sale) that doesn't need to be installed and can run in the browser! - -An easy, fast and secure way to accept Bitcoin, over Lightning Network, at your business. The PoS is isolated from the wallet, so it's safe for any employee to use. You can create as many TPOS's as you need, for example one for each employee, or one for each branch of your business. - -### Usage - -1. Enable extension -2. Create a TPOS\ - ![create](https://imgur.com/8jNj8Zq.jpg) -3. Open TPOS on the browser\ - ![open](https://imgur.com/LZuoWzb.jpg) -4. Present invoice QR to costumer\ - ![pay](https://imgur.com/tOwxn77.jpg) diff --git a/lnbits/extensions/tpos/__init__.py b/lnbits/extensions/tpos/__init__.py deleted file mode 100644 index daa3022e5..000000000 --- a/lnbits/extensions/tpos/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_tpos") - -tpos_ext: Blueprint = Blueprint( - "tpos", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/tpos/config.json b/lnbits/extensions/tpos/config.json deleted file mode 100644 index c5789afb7..000000000 --- a/lnbits/extensions/tpos/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "TPoS", - "short_description": "A shareable PoS terminal!", - "icon": "dialpad", - "contributors": ["talvasconcelos", "arcbtc"] -} diff --git a/lnbits/extensions/tpos/crud.py b/lnbits/extensions/tpos/crud.py deleted file mode 100644 index 99dab6627..000000000 --- a/lnbits/extensions/tpos/crud.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import List, Optional, Union - -from lnbits.helpers import urlsafe_short_hash - -from . import db -from .models import TPoS - - -async def create_tpos(*, wallet_id: str, name: str, currency: str) -> TPoS: - tpos_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO tpos.tposs (id, wallet, name, currency) - VALUES (?, ?, ?, ?) - """, - (tpos_id, wallet_id, name, currency), - ) - - tpos = await get_tpos(tpos_id) - assert tpos, "Newly created tpos couldn't be retrieved" - return tpos - - -async def get_tpos(tpos_id: str) -> Optional[TPoS]: - row = await db.fetchone("SELECT * FROM tpos.tposs WHERE id = ?", (tpos_id,)) - return TPoS.from_row(row) if row else None - - -async def get_tposs(wallet_ids: Union[str, List[str]]) -> List[TPoS]: - if isinstance(wallet_ids, str): - wallet_ids = [wallet_ids] - - q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall( - f"SELECT * FROM tpos.tposs WHERE wallet IN ({q})", (*wallet_ids,) - ) - - return [TPoS.from_row(row) for row in rows] - - -async def delete_tpos(tpos_id: str) -> None: - await db.execute("DELETE FROM tpos.tposs WHERE id = ?", (tpos_id,)) diff --git a/lnbits/extensions/tpos/migrations.py b/lnbits/extensions/tpos/migrations.py deleted file mode 100644 index 7a7fff0d5..000000000 --- a/lnbits/extensions/tpos/migrations.py +++ /dev/null @@ -1,14 +0,0 @@ -async def m001_initial(db): - """ - Initial tposs table. - """ - await db.execute( - """ - CREATE TABLE tpos.tposs ( - id TEXT PRIMARY KEY, - wallet TEXT NOT NULL, - name TEXT NOT NULL, - currency TEXT NOT NULL - ); - """ - ) diff --git a/lnbits/extensions/tpos/models.py b/lnbits/extensions/tpos/models.py deleted file mode 100644 index 3400924af..000000000 --- a/lnbits/extensions/tpos/models.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlite3 import Row -from pydantic import BaseModel -#from typing import NamedTuple - - -class TPoS(BaseModel): - id: str - wallet: str - name: str - currency: str - - @classmethod - def from_row(cls, row: Row) -> "TPoS": - return cls(**dict(row)) diff --git a/lnbits/extensions/tpos/templates/tpos/_api_docs.html b/lnbits/extensions/tpos/templates/tpos/_api_docs.html deleted file mode 100644 index 6ceab7284..000000000 --- a/lnbits/extensions/tpos/templates/tpos/_api_docs.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - GET /tpos/api/v1/tposs -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 200 OK (application/json) -
- [<tpos_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/tposs -H "X-Api-Key: - <invoice_key>" - -
-
-
- - - - POST /tpos/api/v1/tposs -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
- {"name": <string>, "currency": <string*ie USD*>} -
- Returns 201 CREATED (application/json) -
- {"currency": <string>, "id": <string>, "name": - <string>, "wallet": <string>} -
Curl example
- curl -X POST {{ request.url_root }}api/v1/tposs -d '{"name": - <string>, "currency": <string>}' -H "Content-type: - application/json" -H "X-Api-Key: <admin_key>" - -
-
-
- - - - - DELETE - /tpos/api/v1/tposs/<tpos_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.url_root }}api/v1/tposs/<tpos_id> -H - "X-Api-Key: <admin_key>" - -
-
-
-
diff --git a/lnbits/extensions/tpos/templates/tpos/_tpos.html b/lnbits/extensions/tpos/templates/tpos/_tpos.html deleted file mode 100644 index 54ddcd0f9..000000000 --- a/lnbits/extensions/tpos/templates/tpos/_tpos.html +++ /dev/null @@ -1,18 +0,0 @@ - - - -

- Thiago's Point of Sale is a secure, mobile-ready, instant and shareable - point of sale terminal (PoS) for merchants. The PoS is linked to your - LNbits wallet but completely air-gapped so users can ONLY create - invoices. To share the TPoS hit the hash on the terminal. -

- Created by - Tiago Vasconcelos. -
-
-
diff --git a/lnbits/extensions/tpos/templates/tpos/index.html b/lnbits/extensions/tpos/templates/tpos/index.html deleted file mode 100644 index f3b55b37d..000000000 --- a/lnbits/extensions/tpos/templates/tpos/index.html +++ /dev/null @@ -1,423 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New TPoS - - - - - -
-
-
TPoS
-
-
- Export to CSV -
-
- - {% raw %} - - - - {% endraw %} - -
-
-
- -
- - -
{{SITE_TITLE}} TPoS extension
-
- - - - {% include "tpos/_api_docs.html" %} - - {% include "tpos/_tpos.html" %} - - -
-
- - - - - - - -
- Create TPoS - Cancel -
-
-
-
-
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/tpos/templates/tpos/tpos.html b/lnbits/extensions/tpos/templates/tpos/tpos.html deleted file mode 100644 index 1727e6e99..000000000 --- a/lnbits/extensions/tpos/templates/tpos/tpos.html +++ /dev/null @@ -1,264 +0,0 @@ -{% extends "public.html" %} {% block toolbar_title %}{{ tpos.name }}{% endblock -%} {% block footer %}{% endblock %} {% block page_container %} - - - -
-
-

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

-
- {% raw %}{{ fsat }}{% endraw %} sat -
-
-
-
- -
-
-
- 1 - 2 - 3 - C - 4 - 5 - 6 - 7 - 8 - 9 - OK - DEL - 0 - # -
-
-
-
- - - - - -
-

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

-
- {% raw %}{{ fsat }}{% endraw %} sat -
-
-
- Close -
-
-
- - - - - -
-

- {{ tpos.name }}
{{ request.url }} -

-
-
- Copy URL - Close -
-
-
-
-
-{% endblock %} {% block styles %} - -{% endblock %} {% block scripts %} - -{% endblock %} diff --git a/lnbits/extensions/tpos/views.py b/lnbits/extensions/tpos/views.py deleted file mode 100644 index 278546bb0..000000000 --- a/lnbits/extensions/tpos/views.py +++ /dev/null @@ -1,26 +0,0 @@ -from quart import g, abort, render_template -from http import HTTPStatus - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import tpos_ext -from .crud import get_tpos -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@tpos_ext.get("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("tpos/index.html", {"request":request,"user":g.user}) - - -@tpos_ext.get("/{tpos_id}") -async def tpos(request: Request, tpos_id): - tpos = await get_tpos(tpos_id) - if not tpos: - abort(HTTPStatus.NOT_FOUND, "TPoS does not exist.") - - return await templates.TemplateResponse("tpos/tpos.html", {"request":request,"tpos":tpos}) diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py deleted file mode 100644 index 3b95491d5..000000000 --- a/lnbits/extensions/tpos/views_api.py +++ /dev/null @@ -1,107 +0,0 @@ -from quart import g, jsonify, request -from http import HTTPStatus - -from fastapi import FastAPI, Query -from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse -from pydantic import BaseModel - -from lnbits.core.crud import get_user, get_wallet -from lnbits.core.services import create_invoice, check_invoice_status -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from . import tpos_ext -from .crud import create_tpos, get_tpos, get_tposs, delete_tpos -from .models import TPoS - - -@tpos_ext.get("/api/v1/tposs") -@api_check_wallet_key("invoice") -async def api_tposs(all_wallets: bool = Query(None)): - wallet_ids = [g.wallet.id] - if all_wallets: - wallet_ids = wallet_ids = (await get_user(g.wallet.user)).wallet_ids(await get_user(g.wallet.user)).wallet_ids - # if "all_wallets" in request.args: - # wallet_ids = (await get_user(g.wallet.user)).wallet_ids - - return [tpos._asdict() for tpos in await get_tposs(wallet_ids)], HTTPStatus.OK - - -class CreateData(BaseModel): - name: str - currency: str - -@tpos_ext.post("/api/v1/tposs") -@api_check_wallet_key("invoice") -# @api_validate_post_request( -# schema={ -# "name": {"type": "string", "empty": False, "required": True}, -# "currency": {"type": "string", "empty": False, "required": True}, -# } -# ) -async def api_tpos_create(data: CreateData): - tpos = await create_tpos(wallet_id=g.wallet.id, **data) - return tpos._asdict(), HTTPStatus.CREATED - - -@tpos_ext.delete("/api/v1/tposs/{tpos_id}") -@api_check_wallet_key("admin") -async def api_tpos_delete(tpos_id: str): - tpos = await get_tpos(tpos_id) - - if not tpos: - return {"message": "TPoS does not exist."}, HTTPStatus.NOT_FOUND - - if tpos.wallet != g.wallet.id: - return {"message": "Not your TPoS."}, HTTPStatus.FORBIDDEN - - await delete_tpos(tpos_id) - - return "", HTTPStatus.NO_CONTENT - - -@tpos_ext.post("/api/v1/tposs/{tpos_id}/invoices/") -# @api_validate_post_request( -# schema={"amount": {"type": "integer", "min": 1, "required": True}} -# ) -async def api_tpos_create_invoice(amount: int = Query(..., ge=1), tpos_id: str = None): - tpos = await get_tpos(tpos_id) - - if not tpos: - return {"message": "TPoS does not exist."}, HTTPStatus.NOT_FOUND - - try: - payment_hash, payment_request = await create_invoice( - wallet_id=tpos.wallet, - amount=amount, - memo=f"{tpos.name}", - extra={"tag": "tpos"}, - ) - except Exception as e: - return {"message": str(e)}, HTTPStatus.INTERNAL_SERVER_ERROR - - return {"payment_hash": payment_hash, "payment_request": payment_request}, HTTPStatus.CREATED - - -@tpos_ext.get("/api/v1/tposs/{tpos_id}/invoices/{payment_hash}") -async def api_tpos_check_invoice(tpos_id: str, payment_hash: str): - tpos = await get_tpos(tpos_id) - - if not tpos: - return {"message": "TPoS does not exist."}, HTTPStatus.NOT_FOUND - - try: - status = await check_invoice_status(tpos.wallet, payment_hash) - is_paid = not status.pending - except Exception as exc: - print(exc) - return {"paid": False}, HTTPStatus.OK - - if is_paid: - wallet = await get_wallet(tpos.wallet) - payment = await wallet.get_payment(payment_hash) - await payment.set_pending(False) - - return {"paid": True}, HTTPStatus.OK - - return {"paid": False}, HTTPStatus.OK diff --git a/lnbits/extensions/usermanager/README.md b/lnbits/extensions/usermanager/README.md deleted file mode 100644 index b6f306275..000000000 --- a/lnbits/extensions/usermanager/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# User Manager - -## Make and manage users/wallets - -To help developers use LNbits to manage their users, the User Manager extension allows the creation and management of users and wallets. - -For example, a games developer may be developing a game that needs each user to have their own wallet, LNbits can be included in the developers stack as the user and wallet manager. Or someone wanting to manage their family's wallets (wife, children, parents, etc...) or you want to host a community Lightning Network node and want to manage wallets for the users. - -## Usage - -1. Click the button "NEW USER" to create a new user\ - ![new user](https://i.imgur.com/4yZyfJE.png) -2. Fill the user information\ - - username - - the generated wallet name, user can create other wallets later on - - email - - set a password - ![user information](https://i.imgur.com/40du7W5.png) -3. After creating your user, it will appear in the **Users** section, and a user's wallet in the **Wallets** section. -4. Next you can share the wallet with the corresponding user\ - ![user wallet](https://i.imgur.com/gAyajbx.png) -5. If you need to create more wallets for some user, click "NEW WALLET" at the top\ - ![multiple wallets](https://i.imgur.com/wovVnim.png) - - select the existing user you wish to add the wallet - - set a wallet name\ - ![new wallet](https://i.imgur.com/sGwG8dC.png) diff --git a/lnbits/extensions/usermanager/__init__.py b/lnbits/extensions/usermanager/__init__.py deleted file mode 100644 index 53154812d..000000000 --- a/lnbits/extensions/usermanager/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_usermanager") - -usermanager_ext: Blueprint = Blueprint( - "usermanager", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/usermanager/config.json b/lnbits/extensions/usermanager/config.json deleted file mode 100644 index 7391ec299..000000000 --- a/lnbits/extensions/usermanager/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "User Manager", - "short_description": "Generate users and wallets", - "icon": "person_add", - "contributors": ["benarc"] -} diff --git a/lnbits/extensions/usermanager/crud.py b/lnbits/extensions/usermanager/crud.py deleted file mode 100644 index a7854ad8f..000000000 --- a/lnbits/extensions/usermanager/crud.py +++ /dev/null @@ -1,122 +0,0 @@ -from typing import Optional, List - -from lnbits.core.models import Payment -from lnbits.core.crud import ( - create_account, - get_user, - get_payments, - create_wallet, - delete_wallet, -) - -from . import db -from .models import Users, Wallets - - -### Users - - -async def create_usermanager_user( - user_name: str, - wallet_name: str, - admin_id: str, - email: Optional[str] = None, - password: Optional[str] = None, -) -> Users: - account = await create_account() - user = await get_user(account.id) - assert user, "Newly created user couldn't be retrieved" - - wallet = await create_wallet(user_id=user.id, wallet_name=wallet_name) - - await db.execute( - """ - INSERT INTO usermanager.users (id, name, admin, email, password) - VALUES (?, ?, ?, ?, ?) - """, - (user.id, user_name, admin_id, email, password), - ) - - await db.execute( - """ - INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey) - VALUES (?, ?, ?, ?, ?, ?) - """, - (wallet.id, admin_id, wallet_name, user.id, wallet.adminkey, wallet.inkey), - ) - - user_created = await get_usermanager_user(user.id) - assert user_created, "Newly created user couldn't be retrieved" - return user_created - - -async def get_usermanager_user(user_id: str) -> Optional[Users]: - row = await db.fetchone("SELECT * FROM usermanager.users WHERE id = ?", (user_id,)) - return Users(**row) if row else None - - -async def get_usermanager_users(user_id: str) -> List[Users]: - rows = await db.fetchall( - "SELECT * FROM usermanager.users WHERE admin = ?", (user_id,) - ) - 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) - - await db.execute("DELETE FROM usermanager.users WHERE id = ?", (user_id,)) - await db.execute("""DELETE FROM usermanager.wallets WHERE "user" = ?""", (user_id,)) - - -### Wallets - - -async def create_usermanager_wallet( - user_id: str, wallet_name: str, admin_id: str -) -> Wallets: - wallet = await create_wallet(user_id=user_id, wallet_name=wallet_name) - await db.execute( - """ - INSERT INTO usermanager.wallets (id, admin, name, "user", adminkey, inkey) - VALUES (?, ?, ?, ?, ?, ?) - """, - (wallet.id, admin_id, wallet_name, user_id, wallet.adminkey, wallet.inkey), - ) - wallet_created = await get_usermanager_wallet(wallet.id) - assert wallet_created, "Newly created wallet couldn't be retrieved" - return wallet_created - - -async def get_usermanager_wallet(wallet_id: str) -> Optional[Wallets]: - row = await db.fetchone( - "SELECT * FROM usermanager.wallets WHERE id = ?", (wallet_id,) - ) - return Wallets(**row) if row else None - - -async def get_usermanager_wallets(admin_id: str) -> Optional[Wallets]: - rows = await db.fetchall( - "SELECT * FROM usermanager.wallets WHERE admin = ?", (admin_id,) - ) - return [Wallets(**row) for row in rows] - - -async def get_usermanager_users_wallets(user_id: str) -> Optional[Wallets]: - rows = await db.fetchall( - """SELECT * FROM usermanager.wallets WHERE "user" = ?""", (user_id,) - ) - return [Wallets(**row) for row in rows] - - -async def get_usermanager_wallet_transactions(wallet_id: str) -> Optional[Payment]: - return await get_payments( - wallet_id=wallet_id, complete=True, pending=False, outgoing=True, incoming=True - ) - - -async def delete_usermanager_wallet(wallet_id: str, user_id: str) -> None: - await delete_wallet(user_id=user_id, wallet_id=wallet_id) - await db.execute("DELETE FROM usermanager.wallets WHERE id = ?", (wallet_id,)) diff --git a/lnbits/extensions/usermanager/migrations.py b/lnbits/extensions/usermanager/migrations.py deleted file mode 100644 index 62a215752..000000000 --- a/lnbits/extensions/usermanager/migrations.py +++ /dev/null @@ -1,31 +0,0 @@ -async def m001_initial(db): - """ - Initial users table. - """ - await db.execute( - """ - CREATE TABLE usermanager.users ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - admin TEXT NOT NULL, - email TEXT, - password TEXT - ); - """ - ) - - """ - Initial wallets table. - """ - await db.execute( - """ - CREATE TABLE usermanager.wallets ( - id TEXT PRIMARY KEY, - admin TEXT NOT NULL, - name TEXT NOT NULL, - "user" TEXT NOT NULL, - adminkey TEXT NOT NULL, - inkey TEXT NOT NULL - ); - """ - ) diff --git a/lnbits/extensions/usermanager/models.py b/lnbits/extensions/usermanager/models.py deleted file mode 100644 index 484721197..000000000 --- a/lnbits/extensions/usermanager/models.py +++ /dev/null @@ -1,23 +0,0 @@ -from sqlite3 import Row -from pydantic import BaseModel - - -class Users(BaseModel): - id: str - name: str - admin: str - email: str - password: str - - -class Wallets(BaseModel): - id: str - admin: str - name: str - user: str - adminkey: str - inkey: str - - @classmethod - def from_row(cls, row: Row) -> "Wallets": - return cls(**dict(row)) diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html deleted file mode 100644 index 74640bb80..000000000 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ /dev/null @@ -1,259 +0,0 @@ - - - -
- User Manager: Make and manager users/wallets -
-

- To help developers use LNbits to manage their users, the User Manager - extension allows the creation and management of users and wallets. -
For example, a games developer may be developing a game that needs - each user to have their own wallet, LNbits can be included in the - develpoers stack as the user and wallet manager.
- - Created by, Ben Arc -

-
-
-
- - - - - GET - /usermanager/api/v1/users -
Body (application/json)
-
- Returns 201 CREATED (application/json) -
- JSON list of users -
Curl example
- curl -X GET {{ request.url_root }}usermanager/api/v1/users -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - GET - /usermanager/api/v1/users/<user_id> -
Body (application/json)
-
- Returns 201 CREATED (application/json) -
- JSON list of users -
Curl example
- curl -X GET {{ request.url_root }}usermanager/api/v1/users/<user_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - GET - /usermanager/api/v1/wallets/<user_id> -
Headers
- {"X-Api-Key": <string>} -
Body (application/json)
-
- Returns 201 CREATED (application/json) -
- JSON wallet data -
Curl example
- curl -X GET {{ request.url_root }}usermanager/api/v1/wallets/<user_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - GET - /usermanager/api/v1/wallets<wallet_id> -
Headers
- {"X-Api-Key": <string>} -
Body (application/json)
-
- Returns 201 CREATED (application/json) -
- JSON a wallets transactions -
Curl example
- curl -X GET {{ request.url_root }}usermanager/api/v1/wallets<wallet_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - POST - /usermanager/api/v1/users -
Headers
- {"X-Api-Key": <string>, "Content-type": - "application/json"} -
- Body (application/json) - "admin_id" is a YOUR user ID -
- {"admin_id": <string>, "user_name": <string>, - "wallet_name": <string>,"email": <Optional string> - ,"password": <Optional string>} -
- Returns 201 CREATED (application/json) -
- {"id": <string>, "name": <string>, "admin": - <string>, "email": <string>, "password": - <string>} -
Curl example
- curl -X POST {{ request.url_root }}usermanager/api/v1/users -d '{"admin_id": "{{ - g.user.id }}", "wallet_name": <string>, "user_name": - <string>, "email": <Optional string>, "password": < - Optional string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H - "Content-type: application/json" - -
-
-
- - - - POST - /usermanager/api/v1/wallets -
Headers
- {"X-Api-Key": <string>, "Content-type": - "application/json"} -
- Body (application/json) - "admin_id" is a YOUR user ID -
- {"user_id": <string>, "wallet_name": <string>, - "admin_id": <string>} -
- Returns 201 CREATED (application/json) -
- {"id": <string>, "admin": <string>, "name": - <string>, "user": <string>, "adminkey": <string>, - "inkey": <string>} -
Curl example
- curl -X POST {{ request.url_root }}usermanager/api/v1/wallets -d '{"user_id": - <string>, "wallet_name": <string>, "admin_id": "{{ - g.user.id }}"}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H - "Content-type: application/json" - -
-
-
- - - - DELETE - /usermanager/api/v1/users/<user_id> -
Headers
- {"X-Api-Key": <string>} -
Curl example
- curl -X DELETE {{ request.url_root }}usermanager/api/v1/users/<user_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - DELETE - /usermanager/api/v1/wallets/<wallet_id> -
Headers
- {"X-Api-Key": <string>} -
Curl example
- curl -X DELETE {{ request.url_root }}usermanager/api/v1/wallets/<wallet_id> - -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - POST - /usermanager/api/v1/extensions -
Headers
- {"X-Api-Key": <string>} -
Curl example
- curl -X POST {{ request.url_root }}usermanager/api/v1/extensions -d '{"userid": - <string>, "extension": <string>, "active": - <integer>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H - "Content-type: application/json" - -
-
-
-
diff --git a/lnbits/extensions/usermanager/templates/usermanager/index.html b/lnbits/extensions/usermanager/templates/usermanager/index.html deleted file mode 100644 index 446ee51d5..000000000 --- a/lnbits/extensions/usermanager/templates/usermanager/index.html +++ /dev/null @@ -1,473 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - New User - New Wallet - - - - - - -
-
-
Users
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
- - - -
-
-
Wallets
-
-
- Export to CSV -
-
- - {% raw %} - - - {% endraw %} - -
-
-
- -
- - -
- {{SITE_TITLE}} User Manager Extension -
-
- - - {% include "usermanager/_api_docs.html" %} - -
-
- - - - - - - - - - Create User - Cancel - - - - - - - - - - - Create Wallet - Cancel - - - -
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - -{% endblock %} diff --git a/lnbits/extensions/usermanager/views.py b/lnbits/extensions/usermanager/views.py deleted file mode 100644 index 5bd72a515..000000000 --- a/lnbits/extensions/usermanager/views.py +++ /dev/null @@ -1,15 +0,0 @@ -from quart import g, render_template - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import usermanager_ext -from fastapi import Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@usermanager_ext.get("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("usermanager/index.html", {"request":request,"user":g.user}) diff --git a/lnbits/extensions/usermanager/views_api.py b/lnbits/extensions/usermanager/views_api.py deleted file mode 100644 index 782175897..000000000 --- a/lnbits/extensions/usermanager/views_api.py +++ /dev/null @@ -1,148 +0,0 @@ -from typing import Optional -from quart import g, jsonify -from http import HTTPStatus - -from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from fastapi import FastAPI, Query -from fastapi.encoders import jsonable_encoder -from fastapi.responses import JSONResponse -from pydantic import BaseModel - -from . import usermanager_ext -from .crud import ( - create_usermanager_user, - get_usermanager_user, - get_usermanager_users, - get_usermanager_wallet_transactions, - delete_usermanager_user, - create_usermanager_wallet, - get_usermanager_wallet, - get_usermanager_wallets, - get_usermanager_users_wallets, - delete_usermanager_wallet, -) -from lnbits.core import update_user_extension - - -### Users - - -@usermanager_ext.get("/api/v1/users") -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_users(): - user_id = g.wallet.user - return ([user._asdict() for user in await get_usermanager_users(user_id)], - HTTPStatus.OK, - ) - - -@usermanager_ext.get("/api/v1/users/{user_id}") -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_user(user_id): - user = await get_usermanager_user(user_id) - return (user._asdict(), - HTTPStatus.OK, - ) - -class CreateUsersData(BaseModel): - domain: str - subdomain: str - email: str - ip: Optional[str] - sats: Optional[str] - -@usermanager_ext.post("/api/v1/users") -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_users_create(data: CreateUsersData): - user = await create_usermanager_user(**data) - full = user._asdict() - full["wallets"] = [wallet._asdict() for wallet in await get_usermanager_users_wallets(user.id)] - return jsonify(full), HTTPStatus.CREATED - - -@usermanager_ext.delete("/api/v1/users/{user_id}") -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_users_delete(user_id): - user = await get_usermanager_user(user_id) - if not user: - return {"message": "User does not exist."}, HTTPStatus.NOT_FOUND - await delete_usermanager_user(user_id) - return "", HTTPStatus.NO_CONTENT - - -###Activate Extension - -class CreateUsersData(BaseModel): - extension: str - userid: str - active: bool - -@usermanager_ext.post("/api/v1/extensions") -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_activate_extension(data: CreateUsersData): - user = await get_user(data.userid) - if not user: - return {"message": "no such user"}, HTTPStatus.NOT_FOUND - update_user_extension( - user_id=data.userid, extension=data.extension, active=data.active - ) - return {"extension": "updated"}, HTTPStatus.CREATED - - -###Wallets - -class CreateWalletsData(BaseModel): - user_id: str - wallet_name: str - admin_id: str - -@usermanager_ext.post("/api/v1/wallets") -@api_check_wallet_key(key_type="invoice") - -async def api_usermanager_wallets_create(data: CreateWalletsData): - user = await create_usermanager_wallet( - data.user_id, data.wallet_name, data.admin_id - ) - return user._asdict(), HTTPStatus.CREATED - - -@usermanager_ext.get("/api/v1/wallets") -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_wallets(): - admin_id = g.wallet.user - return ( - [wallet._asdict() for wallet in await get_usermanager_wallets(admin_id)], - HTTPStatus.OK, - ) - - -@usermanager_ext.get("/api/v1/wallets/{wallet_id}") -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_wallet_transactions(wallet_id): - return await get_usermanager_wallet_transactions(wallet_id), HTTPStatus.OK - - -@usermanager_ext.get("/api/v1/wallets/{user_id}") -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_users_wallets(user_id): - wallet = await get_usermanager_users_wallets(user_id) - return ( - [ - wallet._asdict() - for wallet in await get_usermanager_users_wallets(user_id) - ], - HTTPStatus.OK, - ) - - -@usermanager_ext.delete("/api/v1/wallets/{wallet_id}") -@api_check_wallet_key(key_type="invoice") -async def api_usermanager_wallets_delete(wallet_id): - wallet = await get_usermanager_wallet(wallet_id) - if not wallet: - return {"message": "Wallet does not exist."}, HTTPStatus.NOT_FOUND - - await delete_usermanager_wallet(wallet_id, wallet.user) - return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/watchonly/README.md b/lnbits/extensions/watchonly/README.md deleted file mode 100644 index d93f7162d..000000000 --- a/lnbits/extensions/watchonly/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Watch Only wallet - -## Monitor an onchain wallet and generate addresses for onchain payments - -Monitor an extended public key and generate deterministic fresh public keys with this simple watch only wallet. Invoice payments can also be generated, both through a publically shareable page and API. - -1. Start by clicking "NEW WALLET"\ - ![new wallet](https://i.imgur.com/vgbAB7c.png) -2. Fill the requested fields: - - give the wallet a name - - paste an Extended Public Key (xpub, ypub, zpub) - - click "CREATE WATCH-ONLY WALLET"\ - ![fill wallet form](https://i.imgur.com/UVoG7LD.png) -3. You can then access your onchain addresses\ - ![get address](https://i.imgur.com/zkxTQ6l.png) -4. You can then generate bitcoin onchain adresses from LNbits\ - ![onchain address](https://i.imgur.com/4KVSSJn.png) - -You can now use this wallet on the LNBits [SatsPayServer](https://github.com/lnbits/lnbits/blob/master/lnbits/extensions/satspay/README.md) extension diff --git a/lnbits/extensions/watchonly/__init__.py b/lnbits/extensions/watchonly/__init__.py deleted file mode 100644 index b8df31978..000000000 --- a/lnbits/extensions/watchonly/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from quart import Blueprint -from lnbits.db import Database - -db = Database("ext_watchonly") - - -watchonly_ext: Blueprint = Blueprint( - "watchonly", __name__, static_folder="static", template_folder="templates" -) - - -from .views_api import * # noqa -from .views import * # noqa diff --git a/lnbits/extensions/watchonly/config.json b/lnbits/extensions/watchonly/config.json deleted file mode 100644 index 48c19ef07..000000000 --- a/lnbits/extensions/watchonly/config.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Watch Only", - "short_description": "Onchain watch only wallets", - "icon": "visibility", - "contributors": [ - "arcbtc" - ] -} diff --git a/lnbits/extensions/watchonly/crud.py b/lnbits/extensions/watchonly/crud.py deleted file mode 100644 index bd301eb44..000000000 --- a/lnbits/extensions/watchonly/crud.py +++ /dev/null @@ -1,212 +0,0 @@ -from typing import List, Optional - -from . import db -from .models import Wallets, Addresses, Mempool - -from lnbits.helpers import urlsafe_short_hash - -from embit.descriptor import Descriptor, Key # type: ignore -from embit.descriptor.arguments import AllowedDerivation # type: ignore -from embit.networks import NETWORKS # type: ignore - - -##########################WALLETS#################### - - -def detect_network(k): - version = k.key.version - for network_name in NETWORKS: - net = NETWORKS[network_name] - # not found in this network - if version in [net["xpub"], net["ypub"], net["zpub"], net["Zpub"], net["Ypub"]]: - return net - - -def parse_key(masterpub: str): - """Parses masterpub or descriptor and returns a tuple: (Descriptor, network) - To create addresses use descriptor.derive(num).address(network=network) - """ - network = None - # probably a single key - if "(" not in masterpub: - k = Key.from_string(masterpub) - if not k.is_extended: - raise ValueError("The key is not a master public key") - if k.is_private: - raise ValueError("Private keys are not allowed") - # check depth - if k.key.depth != 3: - raise ValueError( - "Non-standard depth. Only bip44, bip49 and bip84 are supported with bare xpubs. For custom derivation paths use descriptors." - ) - # if allowed derivation is not provided use default /{0,1}/* - if k.allowed_derivation is None: - k.allowed_derivation = AllowedDerivation.default() - # get version bytes - version = k.key.version - for network_name in NETWORKS: - net = NETWORKS[network_name] - # not found in this network - if version in [net["xpub"], net["ypub"], net["zpub"]]: - network = net - if version == net["xpub"]: - desc = Descriptor.from_string("pkh(%s)" % str(k)) - elif version == net["ypub"]: - desc = Descriptor.from_string("sh(wpkh(%s))" % str(k)) - elif version == net["zpub"]: - desc = Descriptor.from_string("wpkh(%s)" % str(k)) - break - # we didn't find correct version - if network is None: - raise ValueError("Unknown master public key version") - else: - desc = Descriptor.from_string(masterpub) - if not desc.is_wildcard: - raise ValueError("Descriptor should have wildcards") - for k in desc.keys: - if k.is_extended: - net = detect_network(k) - if net is None: - raise ValueError(f"Unknown version: {k}") - if network is not None and network != net: - raise ValueError("Keys from different networks") - network = net - return desc, network - - -async def create_watch_wallet(*, user: str, masterpub: str, title: str) -> Wallets: - # check the masterpub is fine, it will raise an exception if not - parse_key(masterpub) - wallet_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO watchonly.wallets ( - id, - "user", - masterpub, - title, - address_no, - balance - ) - VALUES (?, ?, ?, ?, ?, ?) - """, - # address_no is -1 so fresh address on empty wallet can get address with index 0 - (wallet_id, user, masterpub, title, -1, 0), - ) - - return await get_watch_wallet(wallet_id) - - -async def get_watch_wallet(wallet_id: str) -> Optional[Wallets]: - row = await db.fetchone( - "SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,) - ) - return Wallets.from_row(row) if row else None - - -async def get_watch_wallets(user: str) -> List[Wallets]: - rows = await db.fetchall( - """SELECT * FROM watchonly.wallets WHERE "user" = ?""", (user,) - ) - return [Wallets(**row) for row in rows] - - -async def update_watch_wallet(wallet_id: str, **kwargs) -> Optional[Wallets]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - - await db.execute( - f"UPDATE watchonly.wallets SET {q} WHERE id = ?", (*kwargs.values(), wallet_id) - ) - row = await db.fetchone( - "SELECT * FROM watchonly.wallets WHERE id = ?", (wallet_id,) - ) - return Wallets.from_row(row) if row else None - - -async def delete_watch_wallet(wallet_id: str) -> None: - await db.execute("DELETE FROM watchonly.wallets WHERE id = ?", (wallet_id,)) - - ########################ADDRESSES####################### - - -async def get_derive_address(wallet_id: str, num: int): - wallet = await get_watch_wallet(wallet_id) - key = wallet[2] - desc, network = parse_key(key) - return desc.derive(num).address(network=network) - - -async def get_fresh_address(wallet_id: str) -> Optional[Addresses]: - wallet = await get_watch_wallet(wallet_id) - if not wallet: - return None - - address = await get_derive_address(wallet_id, wallet[4] + 1) - - await update_watch_wallet(wallet_id=wallet_id, address_no=wallet[4] + 1) - masterpub_id = urlsafe_short_hash() - await db.execute( - """ - INSERT INTO watchonly.addresses ( - id, - address, - wallet, - amount - ) - VALUES (?, ?, ?, ?) - """, - (masterpub_id, address, wallet_id, 0), - ) - - return await get_address(address) - - -async def get_address(address: str) -> Optional[Addresses]: - row = await db.fetchone( - "SELECT * FROM watchonly.addresses WHERE address = ?", (address,) - ) - return Addresses.from_row(row) if row else None - - -async def get_addresses(wallet_id: str) -> List[Addresses]: - rows = await db.fetchall( - "SELECT * FROM watchonly.addresses WHERE wallet = ?", (wallet_id,) - ) - return [Addresses(**row) for row in rows] - - -######################MEMPOOL####################### - - -async def create_mempool(user: str) -> Optional[Mempool]: - await db.execute( - """ - INSERT INTO watchonly.mempool ("user",endpoint) - VALUES (?, ?) - """, - (user, "https://mempool.space"), - ) - row = await db.fetchone( - """SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,) - ) - return Mempool.from_row(row) if row else None - - -async def update_mempool(user: str, **kwargs) -> Optional[Mempool]: - q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) - - await db.execute( - f"""UPDATE watchonly.mempool SET {q} WHERE "user" = ?""", - (*kwargs.values(), user), - ) - row = await db.fetchone( - """SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,) - ) - return Mempool.from_row(row) if row else None - - -async def get_mempool(user: str) -> Mempool: - row = await db.fetchone( - """SELECT * FROM watchonly.mempool WHERE "user" = ?""", (user,) - ) - return Mempool.from_row(row) if row else None diff --git a/lnbits/extensions/watchonly/migrations.py b/lnbits/extensions/watchonly/migrations.py deleted file mode 100644 index 05c229b53..000000000 --- a/lnbits/extensions/watchonly/migrations.py +++ /dev/null @@ -1,36 +0,0 @@ -async def m001_initial(db): - """ - Initial wallet table. - """ - await db.execute( - """ - CREATE TABLE watchonly.wallets ( - id TEXT NOT NULL PRIMARY KEY, - "user" TEXT, - masterpub TEXT NOT NULL, - title TEXT NOT NULL, - address_no INTEGER NOT NULL DEFAULT 0, - balance INTEGER NOT NULL - ); - """ - ) - - await db.execute( - """ - CREATE TABLE watchonly.addresses ( - id TEXT NOT NULL PRIMARY KEY, - address TEXT NOT NULL, - wallet TEXT NOT NULL, - amount INTEGER NOT NULL - ); - """ - ) - - await db.execute( - """ - CREATE TABLE watchonly.mempool ( - "user" TEXT NOT NULL, - endpoint TEXT NOT NULL - ); - """ - ) diff --git a/lnbits/extensions/watchonly/models.py b/lnbits/extensions/watchonly/models.py deleted file mode 100644 index cc63283b8..000000000 --- a/lnbits/extensions/watchonly/models.py +++ /dev/null @@ -1,35 +0,0 @@ -from sqlite3 import Row -from pydantic import BaseModel - - -class Wallets(BaseModel): - id: str - user: str - masterpub: str - title: str - address_no: int - balance: int - - @classmethod - def from_row(cls, row: Row) -> "Wallets": - return cls(**dict(row)) - - -class Mempool(BaseModel): - user: str - endpoint: str - - @classmethod - def from_row(cls, row: Row) -> "Mempool": - return cls(**dict(row)) - - -class Addresses(BaseModel): - id: str - address: str - wallet: str - amount: int - - @classmethod - def from_row(cls, row: Row) -> "Addresses": - return cls(**dict(row)) diff --git a/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html b/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html deleted file mode 100644 index 97fdb8a90..000000000 --- a/lnbits/extensions/watchonly/templates/watchonly/_api_docs.html +++ /dev/null @@ -1,244 +0,0 @@ - - -

- Watch Only extension uses mempool.space
- For use with "account Extended Public Key" - https://iancoleman.io/bip39/ - -
Created by, - Ben Arc (using, - Embit
) -

-
- - - - - - GET /watchonly/api/v1/wallet -
Headers
- {"X-Api-Key": <invoice_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<wallets_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/wallet -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - GET - /watchonly/api/v1/wallet/<wallet_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
- Body (application/json) -
-
- Returns 201 CREATED (application/json) -
- [<wallet_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/wallet/<wallet_id> - -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - POST /watchonly/api/v1/wallet -
Headers
- {"X-Api-Key": <admin_key>}
-
- Body (application/json) -
-
- Returns 201 CREATED (application/json) -
- [<wallet_object>, ...] -
Curl example
- curl -X POST {{ request.url_root }}api/v1/wallet -d '{"title": - <string>, "masterpub": <string>}' -H "Content-type: - application/json" -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" - -
-
-
- - - - DELETE - /watchonly/api/v1/wallet/<wallet_id> -
Headers
- {"X-Api-Key": <admin_key>}
-
Returns 204 NO CONTENT
- -
Curl example
- curl -X DELETE {{ request.url_root - }}api/v1/wallet/<wallet_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" - -
-
-
- - - - - GET - /watchonly/api/v1/addresses/<wallet_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<address_object>, ...] -
Curl example
- curl -X GET {{ request.url_root - }}api/v1/addresses/<wallet_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" - -
-
-
- - - - - GET - /watchonly/api/v1/address/<wallet_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<address_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/address/<wallet_id> - -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
-
- - - - - GET /watchonly/api/v1/mempool -
Headers
- {"X-Api-Key": <admin_key>}
-
- Body (application/json) -
-
- Returns 200 OK (application/json) -
- [<mempool_object>, ...] -
Curl example
- curl -X GET {{ request.url_root }}api/v1/mempool -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" - -
-
-
- - - - - POST - /watchonly/api/v1/mempool -
Headers
- {"X-Api-Key": <admin_key>}
-
- Body (application/json) -
-
- Returns 201 CREATED (application/json) -
- [<mempool_object>, ...] -
Curl example
- curl -X PUT {{ request.url_root }}api/v1/mempool -d '{"endpoint": - <string>}' -H "Content-type: application/json" -H "X-Api-Key: - {{ g.user.wallets[0].adminkey }}" - -
-
-
-
-
diff --git a/lnbits/extensions/watchonly/templates/watchonly/index.html b/lnbits/extensions/watchonly/templates/watchonly/index.html deleted file mode 100644 index 521c99fa8..000000000 --- a/lnbits/extensions/watchonly/templates/watchonly/index.html +++ /dev/null @@ -1,476 +0,0 @@ -{% extends "base.html" %} {% from "macros.jinja" import window_vars with context -%} {% block page %} -
-
- - - {% raw %} - New wallet - - -
- Point to another Mempool - {{ this.mempool.endpoint }} - - -
- set - cancel -
-
-
-
-
-
- - - -
-
-
Wallets
-
-
- - - -
-
- - - - -
-
-
- - {% endraw %} - -
- - -
- {{SITE_TITLE}} Watch Only Extension -
-
- - - {% include "watchonly/_api_docs.html" %} - -
-
- - - - - - - - -
- Create Watch-only Wallet - Cancel -
-
-
-
- - - - {% raw %} -
Addresses
-
-

- Current: - {{ currentaddress }} - -

- - - -

- - - - {{ data.address }} - - - - -

- -
- Get fresh address - Close -
-
-
- {% endraw %} -
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - - - -{% endblock %} diff --git a/lnbits/extensions/watchonly/views.py b/lnbits/extensions/watchonly/views.py deleted file mode 100644 index 59fad6ae8..000000000 --- a/lnbits/extensions/watchonly/views.py +++ /dev/null @@ -1,17 +0,0 @@ -from quart import g, abort, render_template -from http import HTTPStatus - -from lnbits.decorators import check_user_exists, validate_uuids - -from . import watchonly_ext -from fastapi import FastAPI, Request -from fastapi.templating import Jinja2Templates - -templates = Jinja2Templates(directory="templates") - -@watchonly_ext.get("/") -@validate_uuids(["usr"], required=True) -@check_user_exists() -async def index(request: Request): - return await templates.TemplateResponse("watchonly/index.html", {"request":request,"user":g.user}) - diff --git a/lnbits/extensions/watchonly/views_api.py b/lnbits/extensions/watchonly/views_api.py deleted file mode 100644 index e4598a973..000000000 --- a/lnbits/extensions/watchonly/views_api.py +++ /dev/null @@ -1,125 +0,0 @@ -import hashlib -from quart import g, jsonify, url_for, request -from http import HTTPStatus -import httpx -import json - -from lnbits.core.crud import get_user -from lnbits.decorators import api_check_wallet_key, api_validate_post_request - -from lnbits.extensions.watchonly import watchonly_ext -from .crud import ( - create_watch_wallet, - get_watch_wallet, - get_watch_wallets, - update_watch_wallet, - delete_watch_wallet, - get_fresh_address, - get_addresses, - create_mempool, - update_mempool, - get_mempool, -) - -###################WALLETS############################# - - -@watchonly_ext.get("/api/v1/wallet") -@api_check_wallet_key("invoice") -async def api_wallets_retrieve(): - - try: - return ( - [wallet._asdict() for wallet in await get_watch_wallets(g.wallet.user)], - HTTPStatus.OK, - ) - except: - return "" - - -@watchonly_ext.get("/api/v1/wallet/{wallet_id}") -@api_check_wallet_key("invoice") -async def api_wallet_retrieve(wallet_id): - wallet = await get_watch_wallet(wallet_id) - - if not wallet: - return {"message": "wallet does not exist"}, HTTPStatus.NOT_FOUND - - return wallet._asdict(), HTTPStatus.OK - - -@watchonly_ext.post("/api/v1/wallet") -@api_check_wallet_key("admin") -async def api_wallet_create_or_update(masterPub: str, Title: str, wallet_id=None): - try: - wallet = await create_watch_wallet( - user=g.wallet.user, masterpub=masterPub, title=Title - ) - except Exception as e: - return {"message": str(e)}, HTTPStatus.BAD_REQUEST - mempool = await get_mempool(g.wallet.user) - if not mempool: - create_mempool(user=g.wallet.user) - return wallet._asdict(), HTTPStatus.CREATED - - -@watchonly_ext.delete("/api/v1/wallet/{wallet_id}") -@api_check_wallet_key("admin") -async def api_wallet_delete(wallet_id): - wallet = await get_watch_wallet(wallet_id) - - if not wallet: - return {"message": "Wallet link does not exist."}, HTTPStatus.NOT_FOUND - - await delete_watch_wallet(wallet_id) - - return {"deleted": "true"}, HTTPStatus.NO_CONTENT - - -#############################ADDRESSES########################## - - -@watchonly_ext.get("/api/v1/address/{wallet_id}") -@api_check_wallet_key("invoice") -async def api_fresh_address(wallet_id): - await get_fresh_address(wallet_id) - - addresses = await get_addresses(wallet_id) - - return [address._asdict() for address in addresses], HTTPStatus.OK - - -@watchonly_ext.get("/api/v1/addresses/{wallet_id}") -@api_check_wallet_key("invoice") -async def api_get_addresses(wallet_id): - wallet = await get_watch_wallet(wallet_id) - - if not wallet: - return {"message": "wallet does not exist"}, HTTPStatus.NOT_FOUND - - addresses = await get_addresses(wallet_id) - - if not addresses: - await get_fresh_address(wallet_id) - addresses = await get_addresses(wallet_id) - - return [address._asdict() for address in addresses], HTTPStatus.OK - - -#############################MEMPOOL########################## - - -@watchonly_ext.put("/api/v1/mempool") -@api_check_wallet_key("admin") -async def api_update_mempool(endpoint: str): - mempool = await update_mempool(user=g.wallet.user, **endpoint) - return mempool._asdict(), HTTPStatus.OK - - -@watchonly_ext.get("/api/v1/mempool") -@api_check_wallet_key("admin") -async def api_get_mempool(): - mempool = await get_mempool(g.wallet.user) - if not mempool: - mempool = await create_mempool(user=g.wallet.user) - return mempool._asdict(), HTTPStatus.OK