From b91271315a29979fd1e46cc1370ab5a51a2d6366 Mon Sep 17 00:00:00 2001 From: pseudozach Date: Wed, 30 Dec 2020 15:50:08 -0800 Subject: [PATCH 01/63] captcha extension added --- lnbits/app.py | 2 +- lnbits/extensions/captcha/README.md | 11 + lnbits/extensions/captcha/__init__.py | 10 + lnbits/extensions/captcha/config.json | 6 + lnbits/extensions/captcha/crud.py | 43 ++ lnbits/extensions/captcha/migrations.py | 65 +++ lnbits/extensions/captcha/models.py | 23 + .../extensions/captcha/static/js/captcha.js | 61 +++ .../captcha/templates/captcha/_api_docs.html | 147 +++++++ .../captcha/templates/captcha/display.html | 172 ++++++++ .../captcha/templates/captcha/index.html | 415 ++++++++++++++++++ lnbits/extensions/captcha/views.py | 20 + lnbits/extensions/captcha/views_api.py | 95 ++++ 13 files changed, 1069 insertions(+), 1 deletion(-) create mode 100644 lnbits/extensions/captcha/README.md create mode 100644 lnbits/extensions/captcha/__init__.py create mode 100644 lnbits/extensions/captcha/config.json create mode 100644 lnbits/extensions/captcha/crud.py create mode 100644 lnbits/extensions/captcha/migrations.py create mode 100644 lnbits/extensions/captcha/models.py create mode 100644 lnbits/extensions/captcha/static/js/captcha.js create mode 100644 lnbits/extensions/captcha/templates/captcha/_api_docs.html create mode 100644 lnbits/extensions/captcha/templates/captcha/display.html create mode 100644 lnbits/extensions/captcha/templates/captcha/index.html create mode 100644 lnbits/extensions/captcha/views.py create mode 100644 lnbits/extensions/captcha/views_api.py diff --git a/lnbits/app.py b/lnbits/app.py index b1562f629..b5fe71bd7 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -14,7 +14,7 @@ from .proxy_fix import ASGIProxyFix from .tasks import run_deferred_async, invoice_listener, internal_invoice_listener, webhook_handler, grab_app_for_later from .settings import WALLET -secure_headers = SecureHeaders(hsts=False) +secure_headers = SecureHeaders(hsts=False,xfo=False) def create_app(config_object="lnbits.settings") -> QuartTrio: diff --git a/lnbits/extensions/captcha/README.md b/lnbits/extensions/captcha/README.md new file mode 100644 index 000000000..277294592 --- /dev/null +++ b/lnbits/extensions/captcha/README.md @@ -0,0 +1,11 @@ +

Example Extension

+

*tagline*

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

If your extension has API endpoints, include useful ones here

+ +curl -H "Content-type: application/json" -X POST https://YOUR-LNBITS/YOUR-EXTENSION/api/v1/EXAMPLE -d '{"amount":"100","memo":"example"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY" diff --git a/lnbits/extensions/captcha/__init__.py b/lnbits/extensions/captcha/__init__.py new file mode 100644 index 000000000..66eed22b3 --- /dev/null +++ b/lnbits/extensions/captcha/__init__.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..4ef7c43fb --- /dev/null +++ b/lnbits/extensions/captcha/config.json @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 000000000..735d05a79 --- /dev/null +++ b/lnbits/extensions/captcha/crud.py @@ -0,0 +1,43 @@ +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 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 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 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 captchas WHERE id = ?", (captcha_id,)) diff --git a/lnbits/extensions/captcha/migrations.py b/lnbits/extensions/captcha/migrations.py new file mode 100644 index 000000000..455cf0ff3 --- /dev/null +++ b/lnbits/extensions/captcha/migrations.py @@ -0,0 +1,65 @@ +from sqlalchemy.exc import OperationalError # type: ignore + + +async def m001_initial(db): + """ + Initial captchas table. + """ + await db.execute( + """ + CREATE TABLE IF NOT EXISTS 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 (strftime('%s', 'now')) + ); + """ + ) + + +async def m002_redux(db): + """ + Creates an improved captchas table and migrates the existing data. + """ + try: + await db.execute("SELECT remembers FROM captchas") + + except OperationalError: + await db.execute("ALTER TABLE captchas RENAME TO captchas_old") + await db.execute( + """ + CREATE TABLE IF NOT EXISTS 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 (strftime('%s', 'now')), + remembers INTEGER DEFAULT 0, + extras TEXT NULL + ); + """ + ) + await db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON captchas (wallet)") + + for row in [list(row) for row in await db.fetchall("SELECT * FROM captchas_old")]: + await db.execute( + """ + INSERT INTO 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 captchas_old") diff --git a/lnbits/extensions/captcha/models.py b/lnbits/extensions/captcha/models.py new file mode 100644 index 000000000..3179d5c18 --- /dev/null +++ b/lnbits/extensions/captcha/models.py @@ -0,0 +1,23 @@ +import json + +from sqlite3 import Row +from typing import NamedTuple, Optional + + +class Captcha(NamedTuple): + 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 new file mode 100644 index 000000000..0b09c0f4b --- /dev/null +++ b/lnbits/extensions/captcha/static/js/captcha.js @@ -0,0 +1,61 @@ +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"); + return e.src = "http://localhost:5000/captcha/" + captchaid, e +} +document.addEventListener("DOMContentLoaded", function() { + if (captchaStyleAdded) console.log("Captcha stuff already added!"); + else { + console.log("Adding captcha stuff"), 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("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 new file mode 100644 index 000000000..dfe2f32f8 --- /dev/null +++ b/lnbits/extensions/captcha/templates/captcha/_api_docs.html @@ -0,0 +1,147 @@ + + + + + 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 new file mode 100644 index 000000000..08ee2a2e8 --- /dev/null +++ b/lnbits/extensions/captcha/templates/captcha/display.html @@ -0,0 +1,172 @@ +{% 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 new file mode 100644 index 000000000..5fb50513a --- /dev/null +++ b/lnbits/extensions/captcha/templates/captcha/index.html @@ -0,0 +1,415 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + New captcha + + + + + +
+
+
Captchas
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
LNbits 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 new file mode 100644 index 000000000..9e4cbe9eb --- /dev/null +++ b/lnbits/extensions/captcha/views.py @@ -0,0 +1,20 @@ +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 + + +@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("/") +async def display(captcha_id): + captcha = await get_captcha(captcha_id) or abort(HTTPStatus.NOT_FOUND, "captcha does not exist.") + return await render_template("captcha/display.html", captcha=captcha) diff --git a/lnbits/extensions/captcha/views_api.py b/lnbits/extensions/captcha/views_api.py new file mode 100644 index 000000000..2062e541d --- /dev/null +++ b/lnbits/extensions/captcha/views_api.py @@ -0,0 +1,95 @@ +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.route("/api/v1/captchas", methods=["GET"]) +@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.route("/api/v1/captchas", methods=["POST"]) +@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.route("/api/v1/captchas/", methods=["DELETE"]) +@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.route("/api/v1/captchas//invoice", methods=["POST"]) +@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.route("/api/v1/captchas//check_invoice", methods=["POST"]) +@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 jsonify({"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 jsonify({"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 jsonify({"paid": True, "url": captcha.url, "remembers": captcha.remembers}), HTTPStatus.OK + + return jsonify({"paid": False}), HTTPStatus.OK From c5c5bdb287eedd5a476d44d06f5e771662b4d0fd Mon Sep 17 00:00:00 2001 From: pseudozach Date: Wed, 30 Dec 2020 17:06:30 -0800 Subject: [PATCH 02/63] payment hash added to captcha for server-side verification --- lnbits/extensions/captcha/static/js/captcha.js | 4 ++++ lnbits/extensions/captcha/templates/captcha/display.html | 2 ++ lnbits/extensions/captcha/templates/captcha/index.html | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/captcha/static/js/captcha.js b/lnbits/extensions/captcha/static/js/captcha.js index 0b09c0f4b..6d86e865a 100644 --- a/lnbits/extensions/captcha/static/js/captcha.js +++ b/lnbits/extensions/captcha/static/js/captcha.js @@ -44,6 +44,10 @@ document.addEventListener("DOMContentLoaded", function() { }); 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 diff --git a/lnbits/extensions/captcha/templates/captcha/display.html b/lnbits/extensions/captcha/templates/captcha/display.html index 08ee2a2e8..af40ff4a2 100644 --- a/lnbits/extensions/captcha/templates/captcha/display.html +++ b/lnbits/extensions/captcha/templates/captcha/display.html @@ -143,6 +143,8 @@ ) } + parent.window.postMessage("paymenthash_"+response.data.payment_hash, "*"); + self.$q.notify({ type: 'positive', message: 'Payment received!', diff --git a/lnbits/extensions/captcha/templates/captcha/index.html b/lnbits/extensions/captcha/templates/captcha/index.html index 5fb50513a..a83e1029a 100644 --- a/lnbits/extensions/captcha/templates/captcha/index.html +++ b/lnbits/extensions/captcha/templates/captcha/index.html @@ -365,7 +365,8 @@ var captchasnippet = '\n' + '\n' - + '\n' + + '
\n' + + '\n' + ' + +{% endblock %} {% block page %} +
+
+ + + Add Bleskomat + + + + + +
+
+
Bleskomats
+
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
LNbits 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 new file mode 100644 index 000000000..16e986eee --- /dev/null +++ b/lnbits/extensions/bleskomat/views.py @@ -0,0 +1,19 @@ +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 + +@bleskomat_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + bleskomat_vars = { + "callback_url": get_callback_url(), + "exchange_rate_providers": exchange_rate_providers_serializable, + "fiat_currencies": fiat_currencies + } + return await render_template("bleskomat/index.html", user=g.user, bleskomat_vars=bleskomat_vars) diff --git a/lnbits/extensions/bleskomat/views_api.py b/lnbits/extensions/bleskomat/views_api.py new file mode 100644 index 000000000..aed4d02ea --- /dev/null +++ b/lnbits/extensions/bleskomat/views_api.py @@ -0,0 +1,90 @@ +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.route("/api/v1/bleskomats", methods=["GET"]) +@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 jsonify([bleskomat._asdict() for bleskomat in await get_bleskomats(wallet_ids)]), HTTPStatus.OK + + +@bleskomat_ext.route("/api/v1/bleskomat/", methods=["GET"]) +@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 + + +@bleskomat_ext.route("/api/v1/bleskomat", methods=["POST"]) +@bleskomat_ext.route("/api/v1/bleskomat/", methods=["PUT"]) +@api_check_wallet_key("admin") +@api_validate_post_request( + schema={ + "name": {"type": "string", "empty": False, "required": True}, + "fiat_currency": {"type": "string", "allowed": fiat_currencies.keys(), "required": True}, + "exchange_rate_provider": {"type": "string", "allowed": exchange_rate_providers.keys(), "required": True}, + "fee": {"type": ["string", "float", "number", "integer"], "required": True}, + } +) +async def api_bleskomat_create_or_update(bleskomat_id=None): + + try: + fiat_currency = g.data["fiat_currency"] + exchange_rate_provider = g.data["exchange_rate_provider"] + rate = await fetch_fiat_exchange_rate( + currency=fiat_currency, + provider=exchange_rate_provider + ) + except Exception as e: + print(e) + return jsonify({"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, **g.data) + else: + bleskomat = await create_bleskomat(wallet_id=g.wallet.id, **g.data) + + return jsonify(bleskomat._asdict()), HTTPStatus.OK if bleskomat_id else HTTPStatus.CREATED + + +@bleskomat_ext.route("/api/v1/bleskomat/", methods=["DELETE"]) +@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 From c03c463171946eadb0108eab3416fdc5f757b65e Mon Sep 17 00:00:00 2001 From: benarc Date: Fri, 26 Feb 2021 16:54:57 +0000 Subject: [PATCH 04/63] Added LNURLw as image view --- Pipfile | 1 + Pipfile.lock | 418 ++++++++++-------- .../withdraw/templates/withdraw/index.html | 10 + lnbits/extensions/withdraw/views.py | 9 +- 4 files changed, 247 insertions(+), 191 deletions(-) diff --git a/Pipfile b/Pipfile index 6d125eebd..eaa183791 100644 --- a/Pipfile +++ b/Pipfile @@ -24,6 +24,7 @@ quart-trio = "*" trio = "==0.16.0" hypercorn = {extras = ["trio"], version = "*"} sqlalchemy-aio = "*" +pyqrcode = "*" [dev-packages] black = "==20.8b1" diff --git a/Pipfile.lock b/Pipfile.lock index 8c4d75c51..a8206d437 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4e34dce2635dc6cc5260a95c959810b290aabaa772a1fe7a9ce02b23fea440c9" + "sha256": "259c5d4b87c631fa28c4df9714e3cac58e4d73e64375752b4d564140e220a22b" }, "pipfile-spec": 6, "requires": { @@ -81,6 +81,7 @@ "sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296", "sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12", "sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452", + "sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761", "sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea", "sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a", "sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5", @@ -89,6 +90,7 @@ "sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb", "sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b", "sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4", + "sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3", "sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7", "sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1", "sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1" @@ -104,10 +106,10 @@ }, "certifi": { "hashes": [ - "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd", - "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.11.8" + "version": "==2020.12.5" }, "click": { "hashes": [ @@ -127,18 +129,19 @@ }, "environs": { "hashes": [ - "sha256:10dca340bff9c912e99d237905909390365e32723c2785a9f3afa6ef426c53bc", - "sha256:36081033ab34a725c2414f48ee7ec7f7c57e498d8c9255d61fbc7f2d4bf60865" + "sha256:2da44b7c30114415aa858577fa6396ee326fc76a0a60f0f15e8260ba554f19dc", + "sha256:3f6def554abb5455141b540e6e0b72fda3853404f2b0d31658aab1bf95410db3" ], "index": "pypi", - "version": "==9.2.0" + "version": "==9.3.1" }, "h11": { "hashes": [ - "sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab", - "sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87" + "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", + "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" ], - "version": "==0.11.0" + "markers": "python_version >= '3.6'", + "version": "==0.12.0" }, "h2": { "hashes": [ @@ -158,11 +161,11 @@ }, "httpcore": { "hashes": [ - "sha256:420700af11db658c782f7e8fda34f9dcd95e3ee93944dd97d78cb70247e0cd06", - "sha256:dd1d762d4f7c2702149d06be2597c35fb154c5eff9789a8c5823fbcf4d2978d6" + "sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9", + "sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc" ], "markers": "python_version >= '3.6'", - "version": "==0.12.2" + "version": "==0.12.3" }, "httpx": { "hashes": [ @@ -177,11 +180,11 @@ "trio" ], "hashes": [ - "sha256:81c69dd84a87b8e8b3ebf06ef5dd92836a8238f0ac65ded3d86befb8ba9acfeb", - "sha256:e3f317d6d64d15ce589f49e4f5057947259fa35332d169e62cb060e9997189e4" + "sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a", + "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821" ], "index": "pypi", - "version": "==0.11.1" + "version": "==0.11.2" }, "hyperframe": { "hashes": [ @@ -193,10 +196,10 @@ }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", + "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" ], - "version": "==2.10" + "version": "==3.1" }, "itsdangerous": { "hashes": [ @@ -208,11 +211,11 @@ }, "jinja2": { "hashes": [ - "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", - "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" + "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419", + "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==2.11.2" + "version": "==2.11.3" }, "lnurl": { "hashes": [ @@ -228,8 +231,12 @@ "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", + "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f", + "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014", + "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", @@ -238,35 +245,50 @@ "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85", + "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850", + "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0", "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb", "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1", + "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2", "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7", "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8", "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5", + "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c", + "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032", "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", - "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be", + "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, "marshmallow": { "hashes": [ - "sha256:73facc37462dfc0b27f571bdaffbef7709e19f7a616beb3802ea425b07843f4e", - "sha256:e26763201474b588d144dae9a32bdd945cd26a06c943bc746a6882e850475378" + "sha256:4ab2fdb7f36eb61c3665da67a7ce281c8900db08d72ba6bf0e695828253581f7", + "sha256:eca81d53aa4aafbc0e20566973d0d2e50ce8bf0ee15165bb799bec0df1e50177" ], "markers": "python_version >= '3.5'", - "version": "==3.9.1" + "version": "==3.10.0" }, "outcome": { "hashes": [ @@ -285,31 +307,39 @@ }, "pydantic": { "hashes": [ - "sha256:01f0291f4951580f320f7ae3f2ecaf0044cdebcc9b45c5f882a7e84453362420", - "sha256:0fe8b45d31ae53d74a6aa0bf801587bd49970070eac6a6326f9fa2a302703b8a", - "sha256:2182ba2a9290964b278bcc07a8d24207de709125d520efec9ad6fa6f92ee058d", - "sha256:2c1673633ad1eea78b1c5c420a47cd48717d2ef214c8230d96ca2591e9e00958", - "sha256:388c0c26c574ff49bad7d0fd6ed82fbccd86a0473fa3900397d3354c533d6ebb", - "sha256:4ba6b903e1b7bd3eb5df0e78d7364b7e831ed8b4cd781ebc3c4f1077fbcb72a4", - "sha256:6665f7ab7fbbf4d3c1040925ff4d42d7549a8c15fe041164adfe4fc2134d4cce", - "sha256:95d4410c4e429480c736bba0db6cce5aaa311304aea685ebcf9ee47571bfd7c8", - "sha256:a2fc7bf77ed4a7a961d7684afe177ff59971828141e608f142e4af858e07dddc", - "sha256:a3c274c49930dc047a75ecc865e435f3df89715c775db75ddb0186804d9b04d0", - "sha256:ab1d5e4d8de00575957e1c982b951bffaedd3204ddd24694e3baca3332e53a23", - "sha256:b11fc9530bf0698c8014b2bdb3bbc50243e82a7fa2577c8cfba660bcc819e768", - "sha256:b9572c0db13c8658b4a4cb705dcaae6983aeb9842248b36761b3fbc9010b740f", - "sha256:c68b5edf4da53c98bb1ccb556ae8f655575cb2e676aef066c12b08c724a3f1a1", - "sha256:c8200aecbd1fb914e1bd061d71a4d1d79ecb553165296af0c14989b89e90d09b", - "sha256:c9760d1556ec59ff745f88269a8f357e2b7afc75c556b3a87b8dda5bc62da8ba", - "sha256:ce2d452961352ba229fe1e0b925b41c0c37128f08dddb788d0fd73fd87ea0f66", - "sha256:dfaa6ed1d509b5aef4142084206584280bb6e9014f01df931ec6febdad5b200a", - "sha256:e5fece30e80087d9b7986104e2ac150647ec1658c4789c89893b03b100ca3164", - "sha256:f045cf7afb3352a03bc6cb993578a34560ac24c5d004fa33c76efec6ada1361a", - "sha256:f83f679e727742b0c465e7ef992d6da4a7e5268b8edd8fdaf5303276374bef52", - "sha256:fc21a37ff3f545de80b166e1735c4172b41b017948a3fb2d5e2f03c219eac50a" + "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f", + "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef", + "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9", + "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec", + "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229", + "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af", + "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e", + "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f", + "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1", + "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2", + "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b", + "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009", + "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c", + "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd", + "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e", + "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127", + "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23", + "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef", + "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730", + "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608", + "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e", + "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95" ], "markers": "python_version >= '3.6'", - "version": "==1.7.2" + "version": "==1.7.3" + }, + "pyqrcode": { + "hashes": [ + "sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6", + "sha256:fdbf7634733e56b72e27f9bce46e4550b75a3a2c420414035cae9d9d26b234d5" + ], + "index": "pypi", + "version": "==1.2.1" }, "pyscss": { "hashes": [ @@ -327,11 +357,11 @@ }, "quart": { "hashes": [ - "sha256:9c634e4c1e4b21b824003c676de1583581258c72b0ac4d2ba747db846e97ff56", - "sha256:d885d782edd9d5dcfd2c4a56e020db3b82493d4c3950f91c221b7d88d239ac93" + "sha256:429c5b4ff27e1d2f9ca0aacc38f6aba0ff49b38b815448bf24b613d3de12ea02", + "sha256:7b13786e07541cc9ce1466fdc6a6ccd5f36eb39118edd25a42d617593cd17707" ], "index": "pypi", - "version": "==0.13.1" + "version": "==0.14.1" }, "quart-compress": { "hashes": [ @@ -351,18 +381,19 @@ }, "quart-trio": { "hashes": [ - "sha256:8262e82d01ff63a1e74f9a95e5980f9658bfd5facf119d99e11c7bfe23427d69", - "sha256:ce63f8b21c6795579f0206138ee67487259359d8e9341b2924fa635f7672de32" + "sha256:1e7fce0df41afc3038bf0431b20614f90984de50341b19f9d4d3b9ba1ac7574a", + "sha256:933e3c18e232ece30ccbac7579fdc5f62f2f9c79c3273d6c341f5a1686791eb1" ], "index": "pypi", - "version": "==0.6.0" + "version": "==0.7.0" }, "represent": { "hashes": [ - "sha256:293dfec8b2e9e2150a21a49bfec2cd009ecb600c8c04f9186d2ad222c3cef78a", - "sha256:6000c24f317dbf8b57a116ce4d7e4459fc5900af6a2915c9a2d74456bcc33d3c" + "sha256:026c0de2ee8385d1255b9c2426cd4f03fe9177ac94c09979bc601946c8493aa0", + "sha256:99142650756ef1998ce0661568f54a47dac8c638fb27e3816c02536575dbba8c" ], - "version": "==1.6.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.6.0.post0" }, "rfc3986": { "extras": [ @@ -395,7 +426,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "sniffio": { @@ -415,47 +446,47 @@ }, "sqlalchemy": { "hashes": [ - "sha256:009e8388d4d551a2107632921320886650b46332f61dc935e70c8bcf37d8e0d6", - "sha256:0157c269701d88f5faf1fa0e4560e4d814f210c01a5b55df3cab95e9346a8bcc", - "sha256:0a92745bb1ebbcb3985ed7bda379b94627f0edbc6c82e9e4bac4fb5647ae609a", - "sha256:0cca1844ba870e81c03633a99aa3dc62256fb96323431a5dec7d4e503c26372d", - "sha256:166917a729b9226decff29416f212c516227c2eb8a9c9f920d69ced24e30109f", - "sha256:1f5f369202912be72fdf9a8f25067a5ece31a2b38507bb869306f173336348da", - "sha256:2909dffe5c9a615b7e6c92d1ac2d31e3026dc436440a4f750f4749d114d88ceb", - "sha256:2b5dafed97f778e9901b79cc01b88d39c605e0545b4541f2551a2fd785adc15b", - "sha256:2e9bd5b23bba8ae8ce4219c9333974ff5e103c857d9ff0e4b73dc4cb244c7d86", - "sha256:3aa6d45e149a16aa1f0c46816397e12313d5e37f22205c26e06975e150ffcf2a", - "sha256:4bdbdb8ca577c6c366d15791747c1de6ab14529115a2eb52774240c412a7b403", - "sha256:53fd857c6c8ffc0aa6a5a3a2619f6a74247e42ec9e46b836a8ffa4abe7aab327", - "sha256:5cdfe54c1e37279dc70d92815464b77cd8ee30725adc9350f06074f91dbfeed2", - "sha256:5d92c18458a4aa27497a986038d5d797b5279268a2de303cd00910658e8d149c", - "sha256:632b32183c0cb0053194a4085c304bc2320e5299f77e3024556fa2aa395c2a8b", - "sha256:7c735c7a6db8ee9554a3935e741cf288f7dcbe8706320251eb38c412e6a4281d", - "sha256:7cd40cb4bc50d9e87b3540b23df6e6b24821ba7e1f305c1492b0806c33dbdbec", - "sha256:84f0ac4a09971536b38cc5d515d6add7926a7e13baa25135a1dbb6afa351a376", - "sha256:8dcbf377529a9af167cbfc5b8acec0fadd7c2357fc282a1494c222d3abfc9629", - "sha256:950f0e17ffba7a7ceb0dd056567bc5ade22a11a75920b0e8298865dc28c0eff6", - "sha256:9e379674728f43a0cd95c423ac0e95262500f9bfd81d33b999daa8ea1756d162", - "sha256:b15002b9788ffe84e42baffc334739d3b68008a973d65fad0a410ca5d0531980", - "sha256:b6f036ecc017ec2e2cc2a40615b41850dc7aaaea6a932628c0afc73ab98ba3fb", - "sha256:bad73f9888d30f9e1d57ac8829f8a12091bdee4949b91db279569774a866a18e", - "sha256:bbc58fca72ce45a64bb02b87f73df58e29848b693869e58bd890b2ddbb42d83b", - "sha256:bca4d367a725694dae3dfdc86cf1d1622b9f414e70bd19651f5ac4fb3aa96d61", - "sha256:be41d5de7a8e241864189b7530ca4aaf56a5204332caa70555c2d96379e18079", - "sha256:bf53d8dddfc3e53a5bda65f7f4aa40fae306843641e3e8e701c18a5609471edf", - "sha256:c092fe282de83d48e64d306b4bce03114859cdbfe19bf8a978a78a0d44ddadb1", - "sha256:c3ab23ee9674336654bf9cac30eb75ac6acb9150dc4b1391bec533a7a4126471", - "sha256:ce64a44c867d128ab8e675f587aae7f61bd2db836a3c4ba522d884cd7c298a77", - "sha256:d05cef4a164b44ffda58200efcb22355350979e000828479971ebca49b82ddb1", - "sha256:d2f25c7f410338d31666d7ddedfa67570900e248b940d186b48461bd4e5569a1", - "sha256:d3b709d64b5cf064972b3763b47139e4a0dc4ae28a36437757f7663f67b99710", - "sha256:e32e3455db14602b6117f0f422f46bc297a3853ae2c322ecd1e2c4c04daf6ed5", - "sha256:ed53209b5f0f383acb49a927179fa51a6e2259878e164273ebc6815f3a752465", - "sha256:f605f348f4e6a2ba00acb3399c71d213b92f27f2383fc4abebf7a37368c12142", - "sha256:fcdb3755a7c355bc29df1b5e6fb8226d5c8b90551d202d69d0076a8a5649d68b" + "sha256:040bdfc1d76a9074717a3f43455685f781c581f94472b010cd6c4754754e1862", + "sha256:1fe5d8d39118c2b018c215c37b73fd6893c3e1d4895be745ca8ff6eb83333ed3", + "sha256:23927c3981d1ec6b4ea71eb99d28424b874d9c696a21e5fbd9fa322718be3708", + "sha256:24f9569e82a009a09ce2d263559acb3466eba2617203170e4a0af91e75b4f075", + "sha256:2578dbdbe4dbb0e5126fb37ffcd9793a25dcad769a95f171a2161030bea850ff", + "sha256:269990b3ab53cb035d662dcde51df0943c1417bdab707dc4a7e4114a710504b4", + "sha256:29cccc9606750fe10c5d0e8bd847f17a97f3850b8682aef1f56f5d5e1a5a64b1", + "sha256:37b83bf81b4b85dda273aaaed5f35ea20ad80606f672d94d2218afc565fb0173", + "sha256:63677d0c08524af4c5893c18dbe42141de7178001360b3de0b86217502ed3601", + "sha256:639940bbe1108ac667dcffc79925db2966826c270112e9159439ab6bb14f8d80", + "sha256:6a939a868fdaa4b504e8b9d4a61f21aac11e3fecc8a8214455e144939e3d2aea", + "sha256:6b8b8c80c7f384f06825612dd078e4a31f0185e8f1f6b8c19e188ff246334205", + "sha256:6c9e6cc9237de5660bcddea63f332428bb83c8e2015c26777281f7ffbd2efb84", + "sha256:6ec1044908414013ebfe363450c22f14698803ce97fbb47e53284d55c5165848", + "sha256:6fca33672578666f657c131552c4ef8979c1606e494f78cd5199742dfb26918b", + "sha256:751934967f5336a3e26fc5993ccad1e4fee982029f9317eb6153bc0bc3d2d2da", + "sha256:8be835aac18ec85351385e17b8665bd4d63083a7160a017bef3d640e8e65cadb", + "sha256:927ce09e49bff3104459e1451ce82983b0a3062437a07d883a4c66f0b344c9b5", + "sha256:94208867f34e60f54a33a37f1c117251be91a47e3bfdb9ab8a7847f20886ad06", + "sha256:94f667d86be82dd4cb17d08de0c3622e77ca865320e0b95eae6153faa7b4ecaf", + "sha256:9e9c25522933e569e8b53ccc644dc993cab87e922fb7e142894653880fdd419d", + "sha256:a0e306e9bb76fd93b29ae3a5155298e4c1b504c7cbc620c09c20858d32d16234", + "sha256:a8bfc1e1afe523e94974132d7230b82ca7fa2511aedde1f537ec54db0399541a", + "sha256:ac2244e64485c3778f012951fdc869969a736cd61375fde6096d08850d8be729", + "sha256:b4b0e44d586cd64b65b507fa116a3814a1a53d55dce4836d7c1a6eb2823ff8d1", + "sha256:baeb451ee23e264de3f577fee5283c73d9bbaa8cb921d0305c0bbf700094b65b", + "sha256:c7dc052432cd5d060d7437e217dd33c97025287f99a69a50e2dc1478dd610d64", + "sha256:d1a85dfc5dee741bf49cb9b6b6b8d2725a268e4992507cf151cba26b17d97c37", + "sha256:d90010304abb4102123d10cbad2cdf2c25a9f2e66a50974199b24b468509bad5", + "sha256:ddfb511e76d016c3a160910642d57f4587dc542ce5ee823b0d415134790eeeb9", + "sha256:e273367f4076bd7b9a8dc2e771978ef2bfd6b82526e80775a7db52bff8ca01dd", + "sha256:e5bb3463df697279e5459a7316ad5a60b04b0107f9392e88674d0ece70e9cf70", + "sha256:e8a1750b44ad6422ace82bf3466638f1aa0862dbb9689690d5f2f48cce3476c8", + "sha256:eab063a70cca4a587c28824e18be41d8ecc4457f8f15b2933584c6c6cccd30f0", + "sha256:ecce8c021894a77d89808222b1ff9687ad84db54d18e4bd0500ca766737faaf6", + "sha256:f4d972139d5000105fcda9539a76452039434013570d6059993120dc2a65e447", + "sha256:fd3b96f8c705af8e938eaa99cbd8fd1450f632d38cad55e7367c33b263bf98ec", + "sha256:fdd2ed7395df8ac2dbb10cefc44737b66c6a5cd7755c92524733d7a443e5b7e2" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.3.20" + "version": "==1.3.23" }, "sqlalchemy-aio": { "hashes": [ @@ -470,7 +501,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "trio": { @@ -548,50 +579,65 @@ }, "coverage": { "hashes": [ - "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", - "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", - "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", - "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", - "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", - "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", - "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", - "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", - "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", - "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", - "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", - "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", - "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", - "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", - "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", - "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", - "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", - "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", - "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", - "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", - "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", - "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", - "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", - "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", - "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", - "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", - "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", - "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", - "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", - "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", - "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", - "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", - "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", - "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7", + "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5", + "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f", + "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde", + "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f", + "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f", + "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c", + "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66", + "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90", + "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337", + "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d", + "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4", + "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409", + "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37", + "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1", + "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247", + "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39", + "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c", + "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994", + "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c", + "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb", + "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc", + "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f", + "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca", + "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135", + "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3", + "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339", + "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9", + "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9", + "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af", + "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370", + "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19", + "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3", + "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44", + "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3", + "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a", + "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c", + "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b", + "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9", + "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8", + "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22", + "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f", + "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345", + "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880", + "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0", + "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b", + "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec", + "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3", + "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==5.3" + "version": "==5.4" }, "idna": { "hashes": [ - "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", - "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + "sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16", + "sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1" ], - "version": "==2.10" + "version": "==3.1" }, "iniconfig": { "hashes": [ @@ -637,11 +683,11 @@ }, "packaging": { "hashes": [ - "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", - "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.4" + "version": "==20.9" }, "pathspec": { "hashes": [ @@ -660,35 +706,35 @@ }, "py": { "hashes": [ - "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", - "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.9.0" + "version": "==1.10.0" }, "pyparsing": { "hashes": [ "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.4.7" }, "pytest": { "hashes": [ - "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe", - "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e" + "sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9", + "sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839" ], "index": "pypi", - "version": "==6.1.2" + "version": "==6.2.2" }, "pytest-cov": { "hashes": [ - "sha256:45ec2d5182f89a81fc3eb29e3d1ed3113b9e9a873bcddb2a71faaab066110191", - "sha256:47bd0ce14056fdd79f93e1713f88fad7bdcc583dcd7783da86ef2f085a0bb88e" + "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7", + "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da" ], "index": "pypi", - "version": "==2.10.1" + "version": "==2.11.1" }, "pytest-trio": { "hashes": [ @@ -743,14 +789,6 @@ ], "version": "==2020.11.13" }, - "six": { - "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", - "version": "==1.15.0" - }, "sniffio": { "hashes": [ "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", @@ -771,7 +809,7 @@ "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==0.10.2" }, "trio": { @@ -784,38 +822,38 @@ }, "typed-ast": { "hashes": [ - "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", - "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", - "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", - "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", - "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", - "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", - "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", - "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", - "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", - "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", - "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", - "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", - "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", - "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", - "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", - "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", - "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", - "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", - "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", - "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", - "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", - "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", - "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", - "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", - "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", - "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", - "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", - "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", - "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", - "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" + "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", + "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", + "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", + "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", + "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", + "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", + "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", + "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", + "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", + "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", + "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", + "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", + "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", + "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", + "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", + "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", + "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", + "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", + "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", + "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", + "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", + "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", + "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", + "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", + "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", + "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", + "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", + "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", + "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", + "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" ], - "version": "==1.4.1" + "version": "==1.4.2" }, "typing-extensions": { "hashes": [ diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html index 9e142a49d..6ffdf99a0 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/index.html +++ b/lnbits/extensions/withdraw/templates/withdraw/index.html @@ -59,6 +59,16 @@ :href="props.row.withdraw_url" target="_blank" > + ") +async def img(link_id): + link = await get_withdraw_link(link_id, 0) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") + qr = pyqrcode.create(link.lnurl) + qrimage = qr.png('qrimage.png', scale=5) + return '' + @withdraw_ext.route("/print/") async def print_qr(link_id): link = await get_withdraw_link(link_id) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") From 0ed281e0fb07ae83f9f981527e181e6f769cb124 Mon Sep 17 00:00:00 2001 From: benarc Date: Fri, 26 Feb 2021 17:42:26 +0000 Subject: [PATCH 05/63] bug --- lnbits/extensions/withdraw/templates/withdraw/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html index 6ffdf99a0..98e00562f 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/index.html +++ b/lnbits/extensions/withdraw/templates/withdraw/index.html @@ -66,7 +66,7 @@ icon="web_asset" :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" type="a" - :href="'/img/' + props.row.id" + :href="'/withdraw/img/' + props.row.id" target="_blank" > Date: Fri, 26 Feb 2021 17:44:32 +0000 Subject: [PATCH 06/63] link broken --- lnbits/extensions/withdraw/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index 11b4c34fe..0d788ab19 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -23,8 +23,11 @@ async def display(link_id): @withdraw_ext.route("/img/") async def img(link_id): link = await get_withdraw_link(link_id, 0) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") + print(link) qr = pyqrcode.create(link.lnurl) + print(qr) qrimage = qr.png('qrimage.png', scale=5) + print(qrimage) return '' @withdraw_ext.route("/print/") From 2d4e9202f14cafd624ae2f2776f1b1994dc8697d Mon Sep 17 00:00:00 2001 From: benarc Date: Fri, 26 Feb 2021 17:48:51 +0000 Subject: [PATCH 07/63] return img --- lnbits/extensions/withdraw/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index 0d788ab19..609cdffa9 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -28,7 +28,7 @@ async def img(link_id): print(qr) qrimage = qr.png('qrimage.png', scale=5) print(qrimage) - return '' + return qrimage @withdraw_ext.route("/print/") async def print_qr(link_id): From 13f440d5174e6305de504b013ef2fccb6cf2028f Mon Sep 17 00:00:00 2001 From: benarc Date: Fri, 26 Feb 2021 17:54:50 +0000 Subject: [PATCH 08/63] added pypng --- Pipfile | 1 + Pipfile.lock | 57 ++++++++++++++++------------- lnbits/extensions/withdraw/views.py | 1 + 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Pipfile b/Pipfile index eaa183791..ca45cc6f9 100644 --- a/Pipfile +++ b/Pipfile @@ -25,6 +25,7 @@ trio = "==0.16.0" hypercorn = {extras = ["trio"], version = "*"} sqlalchemy-aio = "*" pyqrcode = "*" +pypng = "*" [dev-packages] black = "==20.8b1" diff --git a/Pipfile.lock b/Pipfile.lock index a8206d437..38fd9900e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "259c5d4b87c631fa28c4df9714e3cac58e4d73e64375752b4d564140e220a22b" + "sha256": "0ce59c4683840d106bd734853c04d6ce18ac2eee8ef001d663111833bb51931d" }, "pipfile-spec": 6, "requires": { @@ -307,31 +307,38 @@ }, "pydantic": { "hashes": [ - "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f", - "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef", - "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9", - "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec", - "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229", - "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af", - "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e", - "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f", - "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1", - "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2", - "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b", - "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009", - "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c", - "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd", - "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e", - "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127", - "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23", - "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef", - "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730", - "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608", - "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e", - "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95" + "sha256:0b71ca069c16470cb00be0acaf0657eb74cbc4ff5f11b42e79647f170956cda3", + "sha256:12ed0b175bba65e29dfc5859cd539d3512f58bb776bf620a3d3338501fd0f389", + "sha256:22fe5756c6c57279234e4c4027a3549507aca29e9ee832d6aa39c367cb43c99f", + "sha256:26821f61623b01d618bd8b3243f790ac8bd7ae31b388c0e41aa586002cf350eb", + "sha256:2bc9e9f5d91a29dec53346efc5c719d82297885d89c8a62b971492fba222c68d", + "sha256:42b8fb1e4e4783c4aa31df44b64714f96aa4deeacbacf3713a8a238ee7df3b2b", + "sha256:4a83d24bcf9ce8e6fa55c379bba1359461eedb85721bfb3151e240871e2b13a8", + "sha256:5759a4b276bda5ac2360f00e9b1e711aaac51fabd155b422d27f3339710f4264", + "sha256:77e04800d19acc2a8fbb95fe3d47ff397ce137aa5a2b32cc23a87bac70dda343", + "sha256:865410a6df71fb60294887770d19c67d499689f7ce64245182653952cdbd4183", + "sha256:91baec8ed771d4c53d71ef549d8e36b0f92a31c32296062d562d1d7074dd1d6e", + "sha256:999cc108933425752e45d1bf2f57d3cf091f2a5e8b9b8afab5b8872d2cc7645f", + "sha256:a0ff36e3f929d76b91d1624c6673dbdc1407358700d117bb7f29d5696c52d288", + "sha256:a989924324513215ad2b2cfd187426e6372f76f507b17361142c0b792294960c", + "sha256:ad2fae68e185cfae5b6d83e7915352ff0b6e5fa84d84bc6a94c3e2de58327114", + "sha256:b4e03c84f4e96e3880c9d34508cccbd0f0df6e7dc14b17f960ea8c71448823a3", + "sha256:c26d380af3e9a8eb9abe3b6337cea28f057b5425330817c918cf74d0a0a2303d", + "sha256:c8a3600435b83a4f28f5379f3bb574576521180f691828268268e9f172f1b1eb", + "sha256:ccc2ab0a240d01847f3d5f0f9e1582d450a2fc3389db33a7af8e7447b205a935", + "sha256:d361d181a3fb53ebfdc2fb1e3ca55a6b2ad717578a5e119c99641afd11b31a47", + "sha256:d5aeab86837f8799df0d84bec1190e6cc0062d5c5374636b5599234f2b39fe0a", + "sha256:edf37d30ea60179ef067add9772cf42299ea6cd490b3c94335a68f1021944ac4" ], - "markers": "python_version >= '3.6'", - "version": "==1.7.3" + "markers": "python_full_version >= '3.6.1'", + "version": "==1.8" + }, + "pypng": { + "hashes": [ + "sha256:1032833440c91bafee38a42c38c02d00431b24c42927feb3e63b104d8550170b" + ], + "index": "pypi", + "version": "==0.0.20" }, "pyqrcode": { "hashes": [ diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index 609cdffa9..566f83db8 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -1,6 +1,7 @@ from quart import g, abort, render_template from http import HTTPStatus import pyqrcode +import png from lnbits.decorators import check_user_exists, validate_uuids from . import withdraw_ext From 3bfca5b7b7f4a1e7e60766f60e3c00b12f20d1e1 Mon Sep 17 00:00:00 2001 From: benarc Date: Fri, 26 Feb 2021 18:23:17 +0000 Subject: [PATCH 09/63] Image loading as SVG --- lnbits/extensions/withdraw/views.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index 566f83db8..bd5b9d89f 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -1,7 +1,7 @@ from quart import g, abort, render_template from http import HTTPStatus import pyqrcode -import png +from io import BytesIO from lnbits.decorators import check_user_exists, validate_uuids from . import withdraw_ext @@ -27,9 +27,13 @@ async def img(link_id): print(link) qr = pyqrcode.create(link.lnurl) print(qr) - qrimage = qr.png('qrimage.png', scale=5) - print(qrimage) - return qrimage + stream = BytesIO() + qr.svg(stream, scale=3) + return stream.getvalue(), 200, { + 'Content-Type': 'image/svg+xml', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0'} @withdraw_ext.route("/print/") async def print_qr(link_id): From 97e519d569ca703ad56d1eba80f1d81d621fadaa Mon Sep 17 00:00:00 2001 From: benarc Date: Fri, 26 Feb 2021 18:52:45 +0000 Subject: [PATCH 10/63] Deleted some prints --- lnbits/extensions/withdraw/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index bd5b9d89f..6b7ed3f7c 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -24,9 +24,7 @@ async def display(link_id): @withdraw_ext.route("/img/") async def img(link_id): link = await get_withdraw_link(link_id, 0) or abort(HTTPStatus.NOT_FOUND, "Withdraw link does not exist.") - print(link) qr = pyqrcode.create(link.lnurl) - print(qr) stream = BytesIO() qr.svg(stream, scale=3) return stream.getvalue(), 200, { From 4fee785229b29145e203b6025f12b50003d08214 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 3 Mar 2021 13:36:26 +0000 Subject: [PATCH 11/63] Changed "LNURL voucher" to the LNURL title --- lnbits/extensions/withdraw/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 7e80a789b..9a4dfb7c2 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -57,5 +57,5 @@ class WithdrawLink(NamedTuple): k1=self.k1, min_withdrawable=self.min_withdrawable * 1000, max_withdrawable=self.max_withdrawable * 1000, - default_description="LNbits voucher", + default_description=self.title, ) From 1f4218d5c2a62817b2e1cf5c9a47b4359a260b1d Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 3 Mar 2021 13:42:36 +0000 Subject: [PATCH 12/63] Changed "LNURL voucher" to the LNURL title --- lnbits/extensions/withdraw/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 7e80a789b..9a4dfb7c2 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -57,5 +57,5 @@ class WithdrawLink(NamedTuple): k1=self.k1, min_withdrawable=self.min_withdrawable * 1000, max_withdrawable=self.max_withdrawable * 1000, - default_description="LNbits voucher", + default_description=self.title, ) From 33a90a8de33b84708265f1fdeb1be73c2ab311bd Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 3 Mar 2021 13:51:31 +0000 Subject: [PATCH 13/63] prettier --- .../withdraw/templates/withdraw/index.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html index 98e00562f..674ef0000 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/index.html +++ b/lnbits/extensions/withdraw/templates/withdraw/index.html @@ -60,15 +60,15 @@ target="_blank" > + unelevated + dense + size="xs" + icon="web_asset" + :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" + type="a" + :href="'/withdraw/img/' + props.row.id" + target="_blank" + > Date: Wed, 3 Mar 2021 13:59:31 +0000 Subject: [PATCH 14/63] black format --- lnbits/extensions/withdraw/views.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lnbits/extensions/withdraw/views.py b/lnbits/extensions/withdraw/views.py index 6b7ed3f7c..473ccbeaf 100644 --- a/lnbits/extensions/withdraw/views.py +++ b/lnbits/extensions/withdraw/views.py @@ -27,11 +27,17 @@ async def img(link_id): qr = pyqrcode.create(link.lnurl) stream = BytesIO() qr.svg(stream, scale=3) - return stream.getvalue(), 200, { - 'Content-Type': 'image/svg+xml', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0'} + return ( + stream.getvalue(), + 200, + { + "Content-Type": "image/svg+xml", + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + }, + ) + @withdraw_ext.route("/print/") async def print_qr(link_id): From f571f5f8405a2e1a41f88b384a8444d980362529 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 3 Mar 2021 14:22:39 +0000 Subject: [PATCH 15/63] Added some tooltips --- .../extensions/withdraw/templates/withdraw/index.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/withdraw/templates/withdraw/index.html b/lnbits/extensions/withdraw/templates/withdraw/index.html index 674ef0000..7442ca96a 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/index.html +++ b/lnbits/extensions/withdraw/templates/withdraw/index.html @@ -58,7 +58,9 @@ type="a" :href="props.row.withdraw_url" target="_blank" - > + > + shareable link + > embeddable image + > view LNURL {{ col.value }} From 732d06c1e515417d7d1345adb7eeeb9826c537fe Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 7 Mar 2021 00:08:36 -0300 Subject: [PATCH 16/63] basic offlineshop functionality. --- lnbits/core/crud.py | 13 +- lnbits/extensions/offlineshop/README.md | 1 + lnbits/extensions/offlineshop/__init__.py | 12 ++ lnbits/extensions/offlineshop/config.json | 8 + lnbits/extensions/offlineshop/crud.py | 80 +++++++ lnbits/extensions/offlineshop/helpers.py | 48 +++++ lnbits/extensions/offlineshop/lnurl.py | 63 ++++++ lnbits/extensions/offlineshop/migrations.py | 28 +++ lnbits/extensions/offlineshop/models.py | 65 ++++++ .../extensions/offlineshop/static/js/index.js | 157 ++++++++++++++ .../templates/offlineshop/_api_docs.html | 46 +++++ .../templates/offlineshop/index.html | 195 ++++++++++++++++++ lnbits/extensions/offlineshop/views.py | 34 +++ lnbits/extensions/offlineshop/views_api.py | 72 +++++++ lnbits/extensions/offlineshop/wordlists.py | 28 +++ 15 files changed, 842 insertions(+), 8 deletions(-) create mode 100644 lnbits/extensions/offlineshop/README.md create mode 100644 lnbits/extensions/offlineshop/__init__.py create mode 100644 lnbits/extensions/offlineshop/config.json create mode 100644 lnbits/extensions/offlineshop/crud.py create mode 100644 lnbits/extensions/offlineshop/helpers.py create mode 100644 lnbits/extensions/offlineshop/lnurl.py create mode 100644 lnbits/extensions/offlineshop/migrations.py create mode 100644 lnbits/extensions/offlineshop/models.py create mode 100644 lnbits/extensions/offlineshop/static/js/index.js create mode 100644 lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html create mode 100644 lnbits/extensions/offlineshop/templates/offlineshop/index.html create mode 100644 lnbits/extensions/offlineshop/views.py create mode 100644 lnbits/extensions/offlineshop/views_api.py create mode 100644 lnbits/extensions/offlineshop/wordlists.py diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 5b0d572c5..f9321056c 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -131,14 +131,15 @@ async def get_wallet_for_key(key: str, key_type: str = "invoice") -> Optional[Wa # --------------- -async def get_standalone_payment(checking_id: str) -> Optional[Payment]: +async def get_standalone_payment(checking_id_or_hash: str) -> Optional[Payment]: row = await db.fetchone( """ SELECT * FROM apipayments - WHERE checking_id = ? + WHERE checking_id = ? OR payment_hash = ? + LIMIT 1 """, - (checking_id,), + (checking_id_or_hash, checking_id_or_hash), ) return Payment.from_row(row) if row else None @@ -285,11 +286,7 @@ async def create_payment( async def update_payment_status(checking_id: str, pending: bool) -> None: await db.execute( - "UPDATE apipayments SET pending = ? WHERE checking_id = ?", - ( - int(pending), - checking_id, - ), + "UPDATE apipayments SET pending = ? WHERE checking_id = ?", (int(pending), checking_id,), ) diff --git a/lnbits/extensions/offlineshop/README.md b/lnbits/extensions/offlineshop/README.md new file mode 100644 index 000000000..254bc6884 --- /dev/null +++ b/lnbits/extensions/offlineshop/README.md @@ -0,0 +1 @@ +# Offline Shop diff --git a/lnbits/extensions/offlineshop/__init__.py b/lnbits/extensions/offlineshop/__init__.py new file mode 100644 index 000000000..e24fa14b5 --- /dev/null +++ b/lnbits/extensions/offlineshop/__init__.py @@ -0,0 +1,12 @@ +from quart import Blueprint + +from lnbits.db import Database + +db = Database("ext_offlineshop") + +offlineshop_ext: Blueprint = Blueprint("offlineshop", __name__, static_folder="static", template_folder="templates") + + +from .views_api import * # noqa +from .views import * # noqa +from .lnurl import * # noqa diff --git a/lnbits/extensions/offlineshop/config.json b/lnbits/extensions/offlineshop/config.json new file mode 100644 index 000000000..3748c9f45 --- /dev/null +++ b/lnbits/extensions/offlineshop/config.json @@ -0,0 +1,8 @@ +{ + "name": "Offline Shop", + "short_description": "Sell stuff with Lightning and lnurlpay on a shop without internet or any electronic device.", + "icon": "nature_people", + "contributors": [ + "fiatjaf" + ] +} diff --git a/lnbits/extensions/offlineshop/crud.py b/lnbits/extensions/offlineshop/crud.py new file mode 100644 index 000000000..0544edeaa --- /dev/null +++ b/lnbits/extensions/offlineshop/crud.py @@ -0,0 +1,80 @@ +from typing import List, Optional + +from . import db +from .wordlists import animals +from .models import Shop, Item + + +async def create_shop(*, wallet_id: str) -> int: + result = await db.execute( + """ + INSERT INTO shops (wallet, wordlist) + VALUES (?, ?) + """, + (wallet_id, "\n".join(animals)), + ) + return result._result_proxy.lastrowid + + +async def get_shop(id: int) -> Optional[Shop]: + row = await db.fetchone("SELECT * FROM shops WHERE id = ?", (id,)) + return Shop(**dict(row)) if row else None + + +async def get_or_create_shop_by_wallet(wallet: str) -> Optional[Shop]: + row = await db.fetchone("SELECT * FROM shops WHERE wallet = ?", (wallet,)) + + if not row: + # create on the fly + ls_id = await create_shop(wallet_id=wallet) + return await get_shop(ls_id) + + return Shop(**dict(row)) if row else None + + +async def add_item(shop: int, name: str, description: str, image: Optional[str], price: int, unit: str,) -> int: + result = await db.execute( + """ + INSERT INTO items (shop, name, description, image, price, unit) + VALUES (?, ?, ?, ?, ?, ?) + """, + (shop, name, description, image, price, unit), + ) + return result._result_proxy.lastrowid + + +async def update_item( + shop: int, item_id: int, name: str, description: str, image: Optional[str], price: int, unit: str, +) -> int: + await db.execute( + """ + UPDATE items SET + name = ?, + description = ?, + image = ?, + price = ?, + unit = ? + WHERE shop = ? AND id = ? + """, + (name, description, image, price, unit, shop, item_id), + ) + return item_id + + +async def get_item(id: int) -> Optional[Item]: + row = await db.fetchone("SELECT * FROM items WHERE id = ? LIMIT 1", (id,)) + return Item(**dict(row)) if row else None + + +async def get_items(shop: int) -> List[Item]: + rows = await db.fetchall("SELECT * FROM items WHERE shop = ?", (shop,)) + return [Item(**dict(row)) for row in rows] + + +async def delete_item_from_shop(shop: int, item_id: int): + await db.execute( + """ + DELETE FROM items WHERE shop = ? AND id = ? + """, + (shop, item_id), + ) diff --git a/lnbits/extensions/offlineshop/helpers.py b/lnbits/extensions/offlineshop/helpers.py new file mode 100644 index 000000000..13e748574 --- /dev/null +++ b/lnbits/extensions/offlineshop/helpers.py @@ -0,0 +1,48 @@ +import trio # type: ignore +import httpx + + +async def get_fiat_rate(currency: str): + assert currency == "USD", "Only USD is supported as fiat currency." + return await get_usd_rate() + + +async def get_usd_rate(): + """ + Returns an average satoshi price from multiple sources. + """ + + satoshi_prices = [None, None, None] + + async def fetch_price(index, url, getter): + try: + async with httpx.AsyncClient() as client: + r = await client.get(url) + r.raise_for_status() + satoshi_price = int(100_000_000 / float(getter(r.json()))) + satoshi_prices[index] = satoshi_price + except Exception: + pass + + async with trio.open_nursery() as nursery: + nursery.start_soon( + fetch_price, + 0, + "https://api.kraken.com/0/public/Ticker?pair=XXBTZUSD", + lambda d: d["result"]["XXBTCZUSD"]["c"][0], + ) + nursery.start_soon( + fetch_price, + 1, + "https://www.bitstamp.net/api/v2/ticker/btcusd", + lambda d: d["last"], + ) + nursery.start_soon( + fetch_price, + 2, + "https://api.coincap.io/v2/rates/bitcoin", + lambda d: d["data"]["rateUsd"], + ) + + satoshi_prices = [x for x in satoshi_prices if x] + return sum(satoshi_prices) / len(satoshi_prices) diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py new file mode 100644 index 000000000..780593ced --- /dev/null +++ b/lnbits/extensions/offlineshop/lnurl.py @@ -0,0 +1,63 @@ +import hashlib +from quart import jsonify, url_for, request +from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore + +from lnbits.core.services import create_invoice + +from . import offlineshop_ext +from .crud import get_shop, get_item +from .helpers import get_fiat_rate + + +@offlineshop_ext.route("/lnurl/", methods=["GET"]) +async def lnurl_response(item_id): + item = await get_item(item_id) + if not item: + return jsonify({"status": "ERROR", "reason": "Item not found."}) + + rate = await get_fiat_rate(item.unit) if item.unit != "sat" else 1 + price_msat = item.price * 1000 * rate + + resp = LnurlPayResponse( + callback=url_for("shop.lnurl_callback", item_id=item.id, _external=True), + min_sendable=price_msat, + max_sendable=price_msat, + metadata=await item.lnurlpay_metadata(), + ) + + return jsonify(resp.dict()) + + +@offlineshop_ext.route("/lnurl/cb/", methods=["GET"]) +async def lnurl_callback(item_id): + item = await get_item(item_id) + if not item: + return jsonify({"status": "ERROR", "reason": "Couldn't find item."}) + + if item.unit == "sat": + min = item.price * 1000 + max = item.price * 1000 + else: + rate = await get_fiat_rate(item.unit) + # allow some fluctuation (the fiat price may have changed between the calls) + min = rate * 995 * item.price + max = rate * 1010 * item.price + + amount_received = int(request.args.get("amount")) + if amount_received < min: + return jsonify(LnurlErrorResponse(reason=f"Amount {amount_received} is smaller than minimum {min}.").dict()) + elif amount_received > max: + return jsonify(LnurlErrorResponse(reason=f"Amount {amount_received} is greater than maximum {max}.").dict()) + + shop = await get_shop(item.shop) + payment_hash, payment_request = await create_invoice( + wallet_id=shop.wallet, + amount=int(amount_received / 1000), + memo=await item.name, + description_hash=hashlib.sha256((await item.lnurlpay_metadata()).encode("utf-8")).digest(), + extra={"tag": "offlineshop", "item": item.id}, + ) + + resp = LnurlPayActionResponse(pr=payment_request, success_action=item.success_action(payment_hash, shop), routes=[]) + + return jsonify(resp.dict()) diff --git a/lnbits/extensions/offlineshop/migrations.py b/lnbits/extensions/offlineshop/migrations.py new file mode 100644 index 000000000..72e9e501d --- /dev/null +++ b/lnbits/extensions/offlineshop/migrations.py @@ -0,0 +1,28 @@ +async def m001_initial(db): + """ + Initial offlineshop tables. + """ + await db.execute( + """ + CREATE TABLE shops ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + wallet TEXT NOT NULL, + wordlist TEXT + ); + """ + ) + + await db.execute( + """ + CREATE TABLE items ( + shop INTEGER NOT NULL REFERENCES shop (id), + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT NOT NULL, + image TEXT, -- image/png;base64,... + enabled BOOLEAN NOT NULL DEFAULT true, + price INTEGER NOT NULL, + unit TEXT NOT NULL DEFAULT 'sat' + ); + """ + ) diff --git a/lnbits/extensions/offlineshop/models.py b/lnbits/extensions/offlineshop/models.py new file mode 100644 index 000000000..5a0dcee11 --- /dev/null +++ b/lnbits/extensions/offlineshop/models.py @@ -0,0 +1,65 @@ +import json +from collections import OrderedDict +from quart import url_for +from typing import NamedTuple, Optional +from lnurl import encode as lnurl_encode # type: ignore +from lnurl.types import LnurlPayMetadata # type: ignore +from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore + + +class Shop(NamedTuple): + id: int + wallet: str + wordlist: str + + def get_word(self, payment_hash): + # initialize confirmation words cache + self.fulfilled_payments = self.words or OrderedDict() + + if payment_hash in self.fulfilled_payments: + return self.fulfilled_payments[payment_hash] + + # get a new word + self.counter = (self.counter or -1) + 1 + wordlist = self.wordlist.split("\n") + word = [self.counter % len(wordlist)] + + # cleanup confirmation words cache + to_remove = self.fulfilled_payments - 23 + if to_remove > 0: + for i in range(to_remove): + self.fulfilled_payments.popitem(False) + + return word + + +class Item(NamedTuple): + shop: int + id: int + name: str + description: str + image: str + enabled: bool + price: int + unit: str + + @property + def lnurl(self) -> str: + return lnurl_encode(url_for("offlineshop.lnurl_response", item_id=self.id)) + + async def lnurlpay_metadata(self) -> LnurlPayMetadata: + metadata = [["text/plain", self.description]] + + if self.image: + metadata.append(self.image.split(",")) + + return LnurlPayMetadata(json.dumps(metadata)) + + def success_action(self, shop: Shop, payment_hash: str) -> Optional[LnurlPaySuccessAction]: + if not self.wordlist: + return None + + return UrlAction( + url=url_for("offlineshop.confirmation_code", p=payment_hash, _external=True), + description="Open to get the confirmation code for your purchase.", + ) diff --git a/lnbits/extensions/offlineshop/static/js/index.js b/lnbits/extensions/offlineshop/static/js/index.js new file mode 100644 index 000000000..7cbdf3052 --- /dev/null +++ b/lnbits/extensions/offlineshop/static/js/index.js @@ -0,0 +1,157 @@ +/* globals Quasar, Vue, _, VueQrcode, windowMixin, LNbits, LOCALE */ + +Vue.component(VueQrcode.name, VueQrcode) + +const pica = window.pica() + +const defaultItemData = { + unit: 'sat' +} + +new Vue({ + el: '#vue', + mixins: [windowMixin], + data() { + return { + selectedWallet: null, + offlineshop: { + wordlist: [], + items: [] + }, + itemDialog: { + show: false, + data: {...defaultItemData}, + units: ['sat', 'USD'] + } + } + }, + methods: { + async imageAdded(file) { + let image = new Image() + image.src = URL.createObjectURL(file) + let canvas = document.getElementById('uploading-image') + image.onload = async () => { + canvas.setAttribute('width', 300) + canvas.setAttribute('height', 300) + await pica.resize(image, canvas) + this.itemDialog.data.image = canvas.toDataURL() + } + }, + imageCleared() { + this.itemDialog.data.image = null + let canvas = document.getElementById('uploading-image') + canvas.setAttribute('height', 0) + canvas.setAttribute('width', 0) + let ctx = canvas.getContext('2d') + ctx.clearRect(0, 0, canvas.width, canvas.height) + }, + disabledAddItemButton() { + return ( + !this.itemDialog.data.name || + this.itemDialog.data.name.length === 0 || + !this.itemDialog.data.price || + !this.itemDialog.data.description || + !this.itemDialog.data.unit || + this.itemDialog.data.unit.length === 0 + ) + }, + changedWallet(wallet) { + this.selectedWallet = wallet + this.loadShop() + }, + loadShop() { + LNbits.api + .request( + 'GET', + '/offlineshop/api/v1/offlineshop', + this.selectedWallet.inkey + ) + .then(response => { + this.offlineshop = response.data + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + addItem() { + let {name, image, description, price, unit} = this.itemDialog.data + + LNbits.api + .request( + 'POST', + '/offlineshop/api/v1/offlineshop/items', + this.selectedWallet.inkey, + { + name, + description, + image, + price, + unit + } + ) + .then(response => { + this.$q.notify({ + message: `Item '${this.itemDialog.data.name}' added.`, + timeout: 700 + }) + this.loadShop() + this.itemsDialog.show = false + this.itemsDialog.data = {...defaultItemData} + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + toggleItem(itemId) { + let item = this.offlineshop.items.find(item => item.id === itemId) + item.enabled = !item.enabled + + LNbits.api + .request( + 'PUT', + '/offlineshop/api/v1/offlineshop/items/' + itemId, + this.selectedWallet.inkey, + item + ) + .then(response => { + this.$q.notify({ + message: `Item ${item.enabled ? 'enabled' : 'disabled'}.`, + timeout: 700 + }) + this.offlineshop.items = this.offlineshop.items + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }, + deleteItem(itemId) { + LNbits.utils + .confirmDialog('Are you sure you want to delete this item?') + .onOk(() => { + LNbits.api + .request( + 'DELETE', + '/offlineshop/api/v1/offlineshop/items/' + itemId, + this.selectedWallet.inkey + ) + .then(response => { + this.$q.notify({ + message: `Item deleted.`, + timeout: 700 + }) + this.offlineshop.items.splice( + this.offlineshop.items.findIndex(item => item.id === itemId), + 1 + ) + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) + }) + } + }, + created() { + this.selectedWallet = this.g.user.wallets[0] + this.loadShop() + } +}) diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html b/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html new file mode 100644 index 000000000..944c746a0 --- /dev/null +++ b/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html @@ -0,0 +1,46 @@ + + + +

+ Sell stuff offline, accept Lightning payments. Powered by LNURL-pay. +

+
+
+
+ + + + + + GET +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ [<offlineshop object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}/offlineshop/api/v1/offlineshop -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
+
diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/index.html b/lnbits/extensions/offlineshop/templates/offlineshop/index.html new file mode 100644 index 000000000..3f75f8d28 --- /dev/null +++ b/lnbits/extensions/offlineshop/templates/offlineshop/index.html @@ -0,0 +1,195 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + +
+
+
Items
+
+
+ Add new item +
+
+ {% raw %} + + + + + {% endraw %} +
+
+ + + + + + + + + +
+ +
+ + +
LNbits OfflineShop extension
+
+ + + {% include "offlineshop/_api_docs.html" %} + +
+
+ + + + + + + + + + + + + + +
+
+ Add item +
+
+ Cancel +
+
+
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + +{% endblock %} diff --git a/lnbits/extensions/offlineshop/views.py b/lnbits/extensions/offlineshop/views.py new file mode 100644 index 000000000..140c1a766 --- /dev/null +++ b/lnbits/extensions/offlineshop/views.py @@ -0,0 +1,34 @@ +import time +from quart import g, render_template, request +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_standalone_payment + +from . import offlineshop_ext +from .crud import get_item, get_shop + + +@offlineshop_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("offlineshop/index.html", user=g.user) + + +@offlineshop_ext.route("/confirmation") +async def confirmation_code(): + payment_hash = request.args.get("p") + payment: Payment = await get_standalone_payment(payment_hash) + if not payment: + return f"Couldn't find the payment {payment_hash}.", HTTPStatus.NOT_FOUND + if payment.pending: + return f"Payment {payment_hash} wasn't received yet. Please try again in a minute.", HTTPStatus.PAYMENT_REQUIRED + + if payment.time + 60 * 15 < time.time(): + return "too much time has passed." + + item = await get_item(payment.extra.get("item")) + shop = await get_shop(item.shop) + return shop.next_word(payment_hash) diff --git a/lnbits/extensions/offlineshop/views_api.py b/lnbits/extensions/offlineshop/views_api.py new file mode 100644 index 000000000..749eec427 --- /dev/null +++ b/lnbits/extensions/offlineshop/views_api.py @@ -0,0 +1,72 @@ +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 . import offlineshop_ext +from .crud import ( + get_or_create_shop_by_wallet, + add_item, + update_item, + get_items, + delete_item_from_shop, +) + + +@offlineshop_ext.route("/api/v1/offlineshop", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_shop_from_wallet(): + shop = await get_or_create_shop_by_wallet(g.wallet.id) + items = await get_items(shop.id) + + try: + return ( + jsonify({**shop._asdict(), **{"items": [item._asdict() for item in items],},}), + HTTPStatus.OK, + ) + except LnurlInvalidUrl: + return ( + jsonify({"message": "LNURLs need to be delivered over a publically accessible `https` domain or Tor."}), + HTTPStatus.UPGRADE_REQUIRED, + ) + + +@offlineshop_ext.route("/api/v1/offlineshop/items", methods=["POST"]) +@offlineshop_ext.route("/api/v1/offlineshop/items/", methods=["PUT"]) +@api_check_wallet_key("invoice") +@api_validate_post_request( + schema={ + "name": {"type": "string", "empty": False, "required": True}, + "description": {"type": "string", "empty": False, "required": True}, + "image": {"type": "string", "required": False}, + "price": {"type": "number", "required": True}, + "unit": {"type": "string", "allowed": ["sat", "USD"], "required": True}, + } +) +async def api_add_or_update_item(item_id=None): + shop = await get_or_create_shop_by_wallet(g.wallet.id) + if item_id == None: + await add_item( + shop.id, g.data["name"], g.data["description"], g.data.get("image"), g.data["price"], g.data["unit"], + ) + return "", HTTPStatus.CREATED + else: + await update_item( + shop.id, + item_id, + g.data["name"], + g.data["description"], + g.data.get("image"), + g.data["price"], + g.data["unit"], + ) + return "", HTTPStatus.OK + + +@offlineshop_ext.route("/api/v1/offlineshop/items/", methods=["DELETE"]) +@api_check_wallet_key("invoice") +async def api_delete_item(item_id): + shop = await get_or_create_shop_by_wallet(g.wallet.id) + await delete_item_from_shop(shop.id, item_id) + return "", HTTPStatus.NO_CONTENT diff --git a/lnbits/extensions/offlineshop/wordlists.py b/lnbits/extensions/offlineshop/wordlists.py new file mode 100644 index 000000000..ee3663e34 --- /dev/null +++ b/lnbits/extensions/offlineshop/wordlists.py @@ -0,0 +1,28 @@ +animals = [ + "albatross", + "bison", + "chicken", + "duck", + "eagle", + "flamingo", + "gorila", + "hamster", + "iguana", + "jaguar", + "koala", + "llama", + "macaroni penguim", + "numbat", + "octopus", + "platypus", + "quetzal", + "rabbit", + "salmon", + "tuna", + "unicorn", + "vulture", + "wolf", + "xenops", + "yak", + "zebra", +] From cda0819f64359ee620419e72c59f5e8bb48a9550 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 7 Mar 2021 14:37:31 -0300 Subject: [PATCH 17/63] improve and finish basic UI. --- .../lnurlp/templates/lnurlp/index.html | 2 +- lnbits/extensions/offlineshop/lnurl.py | 3 + .../extensions/offlineshop/static/js/index.js | 79 +++++++++++-------- .../templates/offlineshop/index.html | 36 +++++---- 4 files changed, 74 insertions(+), 46 deletions(-) diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/index.html b/lnbits/extensions/lnurlp/templates/lnurlp/index.html index 79c44a508..c235dc7f3 100644 --- a/lnbits/extensions/lnurlp/templates/lnurlp/index.html +++ b/lnbits/extensions/lnurlp/templates/lnurlp/index.html @@ -133,7 +133,7 @@ - + item.id === itemId) + this.itemDialog.data = item + }, + imageAdded(file) { + let blobURL = URL.createObjectURL(file) let image = new Image() - image.src = URL.createObjectURL(file) - let canvas = document.getElementById('uploading-image') + image.src = blobURL image.onload = async () => { + let canvas = document.createElement('canvas') canvas.setAttribute('width', 300) canvas.setAttribute('height', 300) await pica.resize(image, canvas) this.itemDialog.data.image = canvas.toDataURL() + this.itemDialog = {...this.itemDialog} } }, imageCleared() { this.itemDialog.data.image = null - let canvas = document.getElementById('uploading-image') - canvas.setAttribute('height', 0) - canvas.setAttribute('width', 0) - let ctx = canvas.getContext('2d') - ctx.clearRect(0, 0, canvas.width, canvas.height) + this.itemDialog = {...this.itemDialog} }, disabledAddItemButton() { return ( @@ -73,34 +80,44 @@ new Vue({ LNbits.utils.notifyApiError(err) }) }, - addItem() { - let {name, image, description, price, unit} = this.itemDialog.data + async sendItem() { + let {id, name, image, description, price, unit} = this.itemDialog.data + const data = { + name, + description, + image, + price, + unit + } - LNbits.api - .request( - 'POST', - '/offlineshop/api/v1/offlineshop/items', - this.selectedWallet.inkey, - { - name, - description, - image, - price, - unit - } - ) - .then(response => { + try { + if (id) { + await LNbits.api.request( + 'PUT', + '/offlineshop/api/v1/offlineshop/items/' + id, + this.selectedWallet.inkey, + data + ) + } else { + await LNbits.api.request( + 'POST', + '/offlineshop/api/v1/offlineshop/items', + this.selectedWallet.inkey, + data + ) this.$q.notify({ message: `Item '${this.itemDialog.data.name}' added.`, timeout: 700 }) - this.loadShop() - this.itemsDialog.show = false - this.itemsDialog.data = {...defaultItemData} - }) - .catch(err => { - LNbits.utils.notifyApiError(err) - }) + } + } catch (err) { + LNbits.utils.notifyApiError(err) + return + } + + this.loadShop() + this.itemDialog.show = false + this.itemDialog.data = {...defaultItemData} }, toggleItem(itemId) { let item = this.offlineshop.items.find(item => item.id === itemId) diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/index.html b/lnbits/extensions/offlineshop/templates/offlineshop/index.html index 3f75f8d28..8522dcff4 100644 --- a/lnbits/extensions/offlineshop/templates/offlineshop/index.html +++ b/lnbits/extensions/offlineshop/templates/offlineshop/index.html @@ -9,10 +9,7 @@
Items
- Add new item
@@ -51,20 +48,27 @@ target="_blank" >
- {{ props.row.name }} + {{ props.row.name }} {{ props.row.description }} - - + {{ props.row.price }} {{ props.row.unit }} + - + - + + - Add item + {% raw %}{{ itemDialog.data.id ? 'Update' : 'Add' }}{% endraw %} + Item +
Date: Sun, 7 Mar 2021 16:13:20 -0300 Subject: [PATCH 18/63] QR codes, printing, success-action and other fixes. --- lnbits/core/crud.py | 2 +- lnbits/extensions/offlineshop/config.json | 2 +- lnbits/extensions/offlineshop/lnurl.py | 6 +- lnbits/extensions/offlineshop/models.py | 76 +++++++++++++------ .../extensions/offlineshop/static/js/index.js | 5 ++ .../templates/offlineshop/index.html | 35 +++++++++ .../templates/offlineshop/print.html | 25 ++++++ lnbits/extensions/offlineshop/views.py | 25 +++++- lnbits/extensions/offlineshop/views_api.py | 4 +- 9 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 lnbits/extensions/offlineshop/templates/offlineshop/print.html diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index f9321056c..2c0c5c3d5 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -136,7 +136,7 @@ async def get_standalone_payment(checking_id_or_hash: str) -> Optional[Payment]: """ SELECT * FROM apipayments - WHERE checking_id = ? OR payment_hash = ? + WHERE checking_id = ? OR hash = ? LIMIT 1 """, (checking_id_or_hash, checking_id_or_hash), diff --git a/lnbits/extensions/offlineshop/config.json b/lnbits/extensions/offlineshop/config.json index 3748c9f45..507b1d146 100644 --- a/lnbits/extensions/offlineshop/config.json +++ b/lnbits/extensions/offlineshop/config.json @@ -1,5 +1,5 @@ { - "name": "Offline Shop", + "name": "OfflineShop", "short_description": "Sell stuff with Lightning and lnurlpay on a shop without internet or any electronic device.", "icon": "nature_people", "contributors": [ diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py index 171ff970b..e53a145cd 100644 --- a/lnbits/extensions/offlineshop/lnurl.py +++ b/lnbits/extensions/offlineshop/lnurl.py @@ -22,7 +22,7 @@ async def lnurl_response(item_id): price_msat = item.price * 1000 * rate resp = LnurlPayResponse( - callback=url_for("shop.lnurl_callback", item_id=item.id, _external=True), + callback=url_for("offlineshop.lnurl_callback", item_id=item.id, _external=True), min_sendable=price_msat, max_sendable=price_msat, metadata=await item.lnurlpay_metadata(), @@ -56,11 +56,11 @@ async def lnurl_callback(item_id): payment_hash, payment_request = await create_invoice( wallet_id=shop.wallet, amount=int(amount_received / 1000), - memo=await item.name, + memo=item.name, description_hash=hashlib.sha256((await item.lnurlpay_metadata()).encode("utf-8")).digest(), extra={"tag": "offlineshop", "item": item.id}, ) - resp = LnurlPayActionResponse(pr=payment_request, success_action=item.success_action(payment_hash, shop), routes=[]) + resp = LnurlPayActionResponse(pr=payment_request, success_action=item.success_action(shop, payment_hash), routes=[]) return jsonify(resp.dict()) diff --git a/lnbits/extensions/offlineshop/models.py b/lnbits/extensions/offlineshop/models.py index 5a0dcee11..490b8a6a9 100644 --- a/lnbits/extensions/offlineshop/models.py +++ b/lnbits/extensions/offlineshop/models.py @@ -1,11 +1,54 @@ import json from collections import OrderedDict from quart import url_for -from typing import NamedTuple, Optional +from typing import NamedTuple, Optional, List from lnurl import encode as lnurl_encode # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore +shop_counters = {} + + +class ShopCounter(object): + fulfilled_payments: OrderedDict + counter: int + + @classmethod + def invoke(cls, shop: "Shop"): + shop_counter = shop_counters.get(shop.id) + if not shop_counter: + shop_counter = cls(wordlist=shop.wordlist.split("\n")) + shop_counters[shop.id] = shop_counter + return shop_counter + + @classmethod + def reset(cls, shop: "Shop"): + shop_counter = cls.invoke(shop) + shop_counter.counter = -1 + shop_counter.wordlist = shop.wordlist.split("\n") + + def __init__(self, wordlist: List[str]): + self.wordlist = wordlist + self.fulfilled_payments = OrderedDict() + self.counter = -1 + + def get_word(self, payment_hash): + if payment_hash in self.fulfilled_payments: + return self.fulfilled_payments[payment_hash] + + # get a new word + self.counter += 1 + word = self.wordlist[self.counter % len(self.wordlist)] + self.fulfilled_payments[payment_hash] = word + + # cleanup confirmation words cache + to_remove = len(self.fulfilled_payments) - 23 + if to_remove > 0: + for i in range(to_remove): + self.fulfilled_payments.popitem(False) + + return word + class Shop(NamedTuple): id: int @@ -13,24 +56,8 @@ class Shop(NamedTuple): wordlist: str def get_word(self, payment_hash): - # initialize confirmation words cache - self.fulfilled_payments = self.words or OrderedDict() - - if payment_hash in self.fulfilled_payments: - return self.fulfilled_payments[payment_hash] - - # get a new word - self.counter = (self.counter or -1) + 1 - wordlist = self.wordlist.split("\n") - word = [self.counter % len(wordlist)] - - # cleanup confirmation words cache - to_remove = self.fulfilled_payments - 23 - if to_remove > 0: - for i in range(to_remove): - self.fulfilled_payments.popitem(False) - - return word + sc = ShopCounter.invoke(self) + return sc.get_word(payment_hash) class Item(NamedTuple): @@ -45,18 +72,23 @@ class Item(NamedTuple): @property def lnurl(self) -> str: - return lnurl_encode(url_for("offlineshop.lnurl_response", item_id=self.id)) + return lnurl_encode(url_for("offlineshop.lnurl_response", item_id=self.id, _external=True)) + + def values(self): + values = self._asdict() + values["lnurl"] = self.lnurl + return values async def lnurlpay_metadata(self) -> LnurlPayMetadata: metadata = [["text/plain", self.description]] if self.image: - metadata.append(self.image.split(",")) + metadata.append(self.image.split(":")[1].split(",")) return LnurlPayMetadata(json.dumps(metadata)) def success_action(self, shop: Shop, payment_hash: str) -> Optional[LnurlPaySuccessAction]: - if not self.wordlist: + if not shop.wordlist: return None return UrlAction( diff --git a/lnbits/extensions/offlineshop/static/js/index.js b/lnbits/extensions/offlineshop/static/js/index.js index f0c1445bc..a55cf4368 100644 --- a/lnbits/extensions/offlineshop/static/js/index.js +++ b/lnbits/extensions/offlineshop/static/js/index.js @@ -25,6 +25,11 @@ new Vue({ } } }, + computed: { + printItems() { + return this.offlineshop.items.filter(({enabled}) => enabled) + } + }, methods: { openNewDialog() { this.itemDialog.show = true diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/index.html b/lnbits/extensions/offlineshop/templates/offlineshop/index.html index 8522dcff4..6ede54544 100644 --- a/lnbits/extensions/offlineshop/templates/offlineshop/index.html +++ b/lnbits/extensions/offlineshop/templates/offlineshop/index.html @@ -18,6 +18,7 @@ + +
+ Print QR Codes +
@@ -120,6 +131,30 @@ +
+
Adding a new item
+ + + + + +
+ Copy LNURL +
+
+
{{ item.name }}
+ +
{{ item.price }}
+
+ +{% endraw %} {% endblock %} {% block scripts %} + +{% endblock %} diff --git a/lnbits/extensions/offlineshop/views.py b/lnbits/extensions/offlineshop/views.py index 140c1a766..ace818b00 100644 --- a/lnbits/extensions/offlineshop/views.py +++ b/lnbits/extensions/offlineshop/views.py @@ -17,18 +17,35 @@ async def index(): return await render_template("offlineshop/index.html", user=g.user) +@offlineshop_ext.route("/print") +async def print_qr_codes(): + items = [] + for item_id in request.args.get("items").split(","): + item = await get_item(item_id) + if item: + items.append({"lnurl": item.lnurl, "name": item.name, "price": f"{item.price} {item.unit}"}) + + return await render_template("offlineshop/print.html", items=items) + + @offlineshop_ext.route("/confirmation") async def confirmation_code(): + style = "" + payment_hash = request.args.get("p") payment: Payment = await get_standalone_payment(payment_hash) if not payment: - return f"Couldn't find the payment {payment_hash}.", HTTPStatus.NOT_FOUND + return f"Couldn't find the payment {payment_hash}." + style, 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 ( + f"Payment {payment_hash} wasn't received yet. Please try again in a minute." + style, + HTTPStatus.PAYMENT_REQUIRED, + ) if payment.time + 60 * 15 < time.time(): - return "too much time has passed." + return "too much time has passed." + style item = await get_item(payment.extra.get("item")) shop = await get_shop(item.shop) - return shop.next_word(payment_hash) + + return shop.get_word(payment_hash) + style diff --git a/lnbits/extensions/offlineshop/views_api.py b/lnbits/extensions/offlineshop/views_api.py index 749eec427..430b23f0e 100644 --- a/lnbits/extensions/offlineshop/views_api.py +++ b/lnbits/extensions/offlineshop/views_api.py @@ -22,7 +22,7 @@ async def api_shop_from_wallet(): try: return ( - jsonify({**shop._asdict(), **{"items": [item._asdict() for item in items],},}), + jsonify({**shop._asdict(), **{"items": [item.values() for item in items],},}), HTTPStatus.OK, ) except LnurlInvalidUrl: @@ -39,7 +39,7 @@ async def api_shop_from_wallet(): schema={ "name": {"type": "string", "empty": False, "required": True}, "description": {"type": "string", "empty": False, "required": True}, - "image": {"type": "string", "required": False}, + "image": {"type": "string", "required": False, "nullable": True}, "price": {"type": "number", "required": True}, "unit": {"type": "string", "allowed": ["sat", "USD"], "required": True}, } From 773103f8938adf041c1816b3011903794b629fa8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 7 Mar 2021 16:38:23 -0300 Subject: [PATCH 19/63] instructions and API docs. --- .../templates/offlineshop/_api_docs.html | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html b/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html index 944c746a0..f0d4a151a 100644 --- a/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html +++ b/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html @@ -6,8 +6,21 @@ > +
    +
  1. Register items.
  2. +
  3. Print QR codes and paste them on your store, your menu, somewhere, somehow.
  4. +
  5. Clients scan the QR codes and get information about the items plus the price on their phones directly (they must have internet)
  6. +
  7. Once they decide to pay, they'll get an invoice on their phones automatically
  8. +
  9. When the payment is confirmed, a confirmation code will be issued for them.
  10. +

- Sell stuff offline, accept Lightning payments. Powered by LNURL-pay. + The confirmation codes are words from a predefined sequential word list. Each new payment bumps the words sequence by 1. So you can check the confirmation codes manually by just looking at them. +

+

+ For example, if your wordlist is [apple, banana, coconut] the first purchase will be apple, the second banana and so on. When it gets to the end it starts from the beginning again. +

+

+ Powered by LNURL-pay.

@@ -19,6 +32,31 @@ label="API info" :content-inset-level="0.5" > + + + + POST +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
Returns 201 OK
+
Curl example
+ curl -X GET {{ request.url_root + }}/offlineshop/api/v1/offlineshop/items -H "Content-Type: + application/json" -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -d + '{"name": <string>, "description": <string>, "image": + <data-uri string>, "price": <integer>, "unit": <"sat" or + "USD">}' + +
+
+
Returns 200 OK (application/json) - [<offlineshop object>, ...] + {"id": <integer>, "wallet": <string>, "wordlist": + <string>, "items": [{"id": <integer>, "name": <string>, "description": <string>, "image": <string>, "enabled": <boolean>, "price": <integer>, "unit": <string>, "lnurl": <string>}, ...]}</code + >
Curl example
curl -X GET {{ request.url_root }}/offlineshop/api/v1/offlineshop -H @@ -43,4 +84,46 @@
+ + + + PUT +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
Returns 200 OK
+
Curl example
+ curl -X GET {{ request.url_root + }}/offlineshop/api/v1/offlineshop/items/<item_id> -H + "Content-Type: application/json" -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" -d '{"name": <string>, + "description": <string>, "image": <data-uri string>, + "price": <integer>, "unit": <"sat" or "USD">}' + +
+
+
+ + + + DELETE +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
Returns 200 OK
+
Curl example
+ curl -X GET {{ request.url_root + }}/offlineshop/api/v1/offlineshop/items/<item_id> -H "X-Api-Key: + {{ g.user.wallets[0].inkey }}" + +
+
+
From c7717a611a06ab357982f32627a06248e92c2b15 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 7 Mar 2021 17:03:01 -0300 Subject: [PATCH 20/63] UI to change the wordlist. --- lnbits/extensions/offlineshop/crud.py | 7 +++++ .../extensions/offlineshop/static/js/index.js | 19 +++++++++++++ .../templates/offlineshop/index.html | 27 +++++++++++++++++++ lnbits/extensions/offlineshop/views_api.py | 23 ++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/lnbits/extensions/offlineshop/crud.py b/lnbits/extensions/offlineshop/crud.py index 0544edeaa..2d9376dd0 100644 --- a/lnbits/extensions/offlineshop/crud.py +++ b/lnbits/extensions/offlineshop/crud.py @@ -32,6 +32,13 @@ async def get_or_create_shop_by_wallet(wallet: str) -> Optional[Shop]: return Shop(**dict(row)) if row else None +async def set_wordlist(shop: int, wordlist: str) -> Optional[Shop]: + await db.execute( + "UPDATE shops SET wordlist = ? WHERE id = ?", (wordlist, shop), + ) + return await get_shop(shop) + + async def add_item(shop: int, name: str, description: str, image: Optional[str], price: int, unit: str,) -> int: result = await db.execute( """ diff --git a/lnbits/extensions/offlineshop/static/js/index.js b/lnbits/extensions/offlineshop/static/js/index.js index a55cf4368..84b6f8650 100644 --- a/lnbits/extensions/offlineshop/static/js/index.js +++ b/lnbits/extensions/offlineshop/static/js/index.js @@ -85,6 +85,25 @@ new Vue({ LNbits.utils.notifyApiError(err) }) }, + async updateWordlist() { + try { + await LNbits.api.request( + 'PUT', + '/offlineshop/api/v1/offlineshop/wordlist', + this.selectedWallet.inkey, + {wordlist: this.offlineshop.wordlist} + ) + this.$q.notify({ + message: `Wordlist updated. Counter reset.`, + timeout: 700 + }) + } catch (err) { + LNbits.utils.notifyApiError(err) + return + } + + this.loadShop() + }, async sendItem() { let {id, name, image, description, price, unit} = this.itemDialog.data const data = { diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/index.html b/lnbits/extensions/offlineshop/templates/offlineshop/index.html index 6ede54544..8de5a8ede 100644 --- a/lnbits/extensions/offlineshop/templates/offlineshop/index.html +++ b/lnbits/extensions/offlineshop/templates/offlineshop/index.html @@ -90,6 +90,10 @@ +
+
Wallet Shop
+
+
+ + + +
+
Wordlist
+
+ +
+
+ +
+
+ + Update Wordlist + + Reset +
+
+
+
+
diff --git a/lnbits/extensions/offlineshop/views_api.py b/lnbits/extensions/offlineshop/views_api.py index 430b23f0e..41aa2cf6f 100644 --- a/lnbits/extensions/offlineshop/views_api.py +++ b/lnbits/extensions/offlineshop/views_api.py @@ -7,11 +7,13 @@ from lnbits.decorators import api_check_wallet_key, api_validate_post_request from . import offlineshop_ext from .crud import ( get_or_create_shop_by_wallet, + set_wordlist, add_item, update_item, get_items, delete_item_from_shop, ) +from .models import ShopCounter @offlineshop_ext.route("/api/v1/offlineshop", methods=["GET"]) @@ -70,3 +72,24 @@ async def api_delete_item(item_id): shop = await get_or_create_shop_by_wallet(g.wallet.id) await delete_item_from_shop(shop.id, item_id) return "", HTTPStatus.NO_CONTENT + + +@offlineshop_ext.route("/api/v1/offlineshop/wordlist", methods=["PUT"]) +@api_check_wallet_key("invoice") +@api_validate_post_request( + schema={"wordlist": {"type": "string", "empty": True, "nullable": True, "required": True},} +) +async def api_set_wordlist(): + wordlist = g.data["wordlist"].split("\n") if g.data["wordlist"] else None + wordlist = [word.strip() for word in wordlist if word.strip()] + + shop = await get_or_create_shop_by_wallet(g.wallet.id) + if not shop: + return "", HTTPStatus.NOT_FOUND + + updated_shop = await set_wordlist(shop.id, "\n".join(wordlist)) + if not updated_shop: + return "", HTTPStatus.NOT_FOUND + + ShopCounter.reset(updated_shop) + return "", HTTPStatus.OK From 1630a28da0aa8d79c4936c9f3b67bb73b31d7287 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 7 Mar 2021 19:18:02 -0300 Subject: [PATCH 21/63] prettier and black stuff. --- lnbits/core/crud.py | 6 ++- lnbits/extensions/offlineshop/crud.py | 20 +++++++-- .../templates/offlineshop/_api_docs.html | 42 +++++++++++++------ lnbits/extensions/offlineshop/views_api.py | 20 +++++++-- 4 files changed, 69 insertions(+), 19 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index 2c0c5c3d5..2c91bc22a 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -286,7 +286,11 @@ async def create_payment( async def update_payment_status(checking_id: str, pending: bool) -> None: await db.execute( - "UPDATE apipayments SET pending = ? WHERE checking_id = ?", (int(pending), checking_id,), + "UPDATE apipayments SET pending = ? WHERE checking_id = ?", + ( + int(pending), + checking_id, + ), ) diff --git a/lnbits/extensions/offlineshop/crud.py b/lnbits/extensions/offlineshop/crud.py index 2d9376dd0..5da350a5e 100644 --- a/lnbits/extensions/offlineshop/crud.py +++ b/lnbits/extensions/offlineshop/crud.py @@ -34,12 +34,20 @@ async def get_or_create_shop_by_wallet(wallet: str) -> Optional[Shop]: async def set_wordlist(shop: int, wordlist: str) -> Optional[Shop]: await db.execute( - "UPDATE shops SET wordlist = ? WHERE id = ?", (wordlist, shop), + "UPDATE shops SET wordlist = ? WHERE id = ?", + (wordlist, shop), ) return await get_shop(shop) -async def add_item(shop: int, name: str, description: str, image: Optional[str], price: int, unit: str,) -> int: +async def add_item( + shop: int, + name: str, + description: str, + image: Optional[str], + price: int, + unit: str, +) -> int: result = await db.execute( """ INSERT INTO items (shop, name, description, image, price, unit) @@ -51,7 +59,13 @@ async def add_item(shop: int, name: str, description: str, image: Optional[str], async def update_item( - shop: int, item_id: int, name: str, description: str, image: Optional[str], price: int, unit: str, + shop: int, + item_id: int, + name: str, + description: str, + image: Optional[str], + price: int, + unit: str, ) -> int: await db.execute( """ diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html b/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html index f0d4a151a..1e3bf0519 100644 --- a/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html +++ b/lnbits/extensions/offlineshop/templates/offlineshop/_api_docs.html @@ -8,20 +8,35 @@
  1. Register items.
  2. -
  3. Print QR codes and paste them on your store, your menu, somewhere, somehow.
  4. -
  5. Clients scan the QR codes and get information about the items plus the price on their phones directly (they must have internet)
  6. -
  7. Once they decide to pay, they'll get an invoice on their phones automatically
  8. -
  9. When the payment is confirmed, a confirmation code will be issued for them.
  10. +
  11. + Print QR codes and paste them on your store, your menu, somewhere, + somehow. +
  12. +
  13. + Clients scan the QR codes and get information about the items plus the + price on their phones directly (they must have internet) +
  14. +
  15. + Once they decide to pay, they'll get an invoice on their phones + automatically +
  16. +
  17. + When the payment is confirmed, a confirmation code will be issued for + them. +

- The confirmation codes are words from a predefined sequential word list. Each new payment bumps the words sequence by 1. So you can check the confirmation codes manually by just looking at them. + The confirmation codes are words from a predefined sequential word list. + Each new payment bumps the words sequence by 1. So you can check the + confirmation codes manually by just looking at them.

- For example, if your wordlist is [apple, banana, coconut] the first purchase will be apple, the second banana and so on. When it gets to the end it starts from the beginning again. -

-

- Powered by LNURL-pay. + For example, if your wordlist is + [apple, banana, coconut] the first purchase will be + apple, the second banana and so on. When it + gets to the end it starts from the beginning again.

+

Powered by LNURL-pay.

@@ -51,8 +66,8 @@ }}/offlineshop/api/v1/offlineshop/items -H "Content-Type: application/json" -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -d '{"name": <string>, "description": <string>, "image": - <data-uri string>, "price": <integer>, "unit": <"sat" or - "USD">}' + <data-uri string>, "price": <integer>, "unit": <"sat" + or "USD">}'
@@ -74,7 +89,10 @@ {"id": <integer>, "wallet": <string>, "wordlist": - <string>, "items": [{"id": <integer>, "name": <string>, "description": <string>, "image": <string>, "enabled": <boolean>, "price": <integer>, "unit": <string>, "lnurl": <string>}, ...]}</code + <string>, "items": [{"id": <integer>, "name": + <string>, "description": <string>, "image": + <string>, "enabled": <boolean>, "price": <integer>, + "unit": <string>, "lnurl": <string>}, ...]}<
Curl example
Date: Sun, 7 Mar 2021 19:20:39 -0300 Subject: [PATCH 22/63] prettier changes its rules everyday. --- .../paywall/templates/paywall/_api_docs.html | 14 +++--- .../templates/usermanager/_api_docs.html | 44 +++++++++---------- .../templates/withdraw/_api_docs.html | 23 +++++----- 3 files changed, 37 insertions(+), 44 deletions(-) diff --git a/lnbits/extensions/paywall/templates/paywall/_api_docs.html b/lnbits/extensions/paywall/templates/paywall/_api_docs.html index 3884c3b52..1157fa467 100644 --- a/lnbits/extensions/paywall/templates/paywall/_api_docs.html +++ b/lnbits/extensions/paywall/templates/paywall/_api_docs.html @@ -17,8 +17,8 @@ [<paywall_object>, ...]
Curl example
curl -X GET {{ request.url_root }}api/v1/paywalls -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root }}api/v1/paywalls -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" @@ -48,11 +48,11 @@ >
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 }}" + >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 }}" diff --git a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html index 7b1925a50..fbd13e725 100644 --- a/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html +++ b/lnbits/extensions/usermanager/templates/usermanager/_api_docs.html @@ -42,8 +42,8 @@ JSON list of users
Curl example
curl -X GET {{ request.url_root }}api/v1/users -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root }}api/v1/users -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" @@ -64,9 +64,8 @@ JSON wallet data
Curl example
curl -X GET {{ request.url_root - }}api/v1/wallets/<user_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root }}api/v1/wallets/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" @@ -87,9 +86,8 @@ JSON a wallets transactions
Curl example
curl -X GET {{ request.url_root - }}api/v1/wallets<wallet_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root }}api/v1/wallets<wallet_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" @@ -128,10 +126,10 @@ >
Curl example
curl -X POST {{ request.url_root }}api/v1/users -d - '{"admin_id": "{{ g.user.id }}", "wallet_name": <string>, - "user_name": <string>}' -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" -H "Content-type: application/json" + >curl -X POST {{ request.url_root }}api/v1/users -d '{"admin_id": "{{ + g.user.id }}", "wallet_name": <string>, "user_name": + <string>}' -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" -H + "Content-type: application/json" @@ -165,10 +163,10 @@ >
Curl example
curl -X POST {{ request.url_root }}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" + >curl -X POST {{ request.url_root }}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" @@ -189,9 +187,8 @@ {"X-Api-Key": <string>}
Curl example
curl -X DELETE {{ request.url_root - }}api/v1/users/<user_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + >curl -X DELETE {{ request.url_root }}api/v1/users/<user_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" @@ -207,9 +204,8 @@ {"X-Api-Key": <string>}
Curl example
curl -X DELETE {{ request.url_root - }}api/v1/wallets/<wallet_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + >curl -X DELETE {{ request.url_root }}api/v1/wallets/<wallet_id> + -H "X-Api-Key: {{ g.user.wallets[0].inkey }}" @@ -230,8 +226,8 @@ {"X-Api-Key": <string>}
Curl example
curl -X POST {{ request.url_root }}api/v1/extensions -d - '{"userid": <string>, "extension": <string>, "active": + >curl -X POST {{ request.url_root }}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/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html index bc1aac2b2..18a0a5420 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html +++ b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html @@ -22,8 +22,8 @@ [<withdraw_link_object>, ...]
Curl example
curl -X GET {{ request.url_root }}api/v1/links -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root }}api/v1/links -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" @@ -49,9 +49,8 @@ {"lnurl": <string>}
Curl example
curl -X GET {{ request.url_root - }}api/v1/links/<withdraw_id> -H "X-Api-Key: {{ - g.user.wallets[0].inkey }}" + >curl -X GET {{ request.url_root }}api/v1/links/<withdraw_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" @@ -79,8 +78,8 @@ {"lnurl": <string>}
Curl example
curl -X POST {{ request.url_root }}api/v1/links -d - '{"title": <string>, "min_withdrawable": <integer>, + >curl -X POST {{ request.url_root }}api/v1/links -d '{"title": + <string>, "min_withdrawable": <integer>, "max_withdrawable": <integer>, "uses": <integer>, "wait_time": <integer>, "is_unique": <boolean>}' -H "Content-type: application/json" -H "X-Api-Key: {{ @@ -115,9 +114,8 @@ {"lnurl": <string>}
Curl example
curl -X PUT {{ request.url_root - }}api/v1/links/<withdraw_id> -d '{"title": - <string>, "min_withdrawable": <integer>, + >curl -X PUT {{ request.url_root }}api/v1/links/<withdraw_id> -d + '{"title": <string>, "min_withdrawable": <integer>, "max_withdrawable": <integer>, "uses": <integer>, "wait_time": <integer>, "is_unique": <boolean>}' -H "Content-type: application/json" -H "X-Api-Key: {{ @@ -145,9 +143,8 @@
Curl example
curl -X DELETE {{ request.url_root - }}api/v1/links/<withdraw_id> -H "X-Api-Key: {{ - g.user.wallets[0].adminkey }}" + >curl -X DELETE {{ request.url_root }}api/v1/links/<withdraw_id> + -H "X-Api-Key: {{ g.user.wallets[0].adminkey }}" From d9b9d1e9b2fb559b584458750fb5a212f4348f7f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Mar 2021 13:01:19 -0300 Subject: [PATCH 23/63] more info on confirmation code screen. --- lnbits/extensions/offlineshop/models.py | 4 ++-- lnbits/extensions/offlineshop/views.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/offlineshop/models.py b/lnbits/extensions/offlineshop/models.py index 490b8a6a9..175b440b0 100644 --- a/lnbits/extensions/offlineshop/models.py +++ b/lnbits/extensions/offlineshop/models.py @@ -1,12 +1,12 @@ import json from collections import OrderedDict from quart import url_for -from typing import NamedTuple, Optional, List +from typing import NamedTuple, Optional, List, Dict from lnurl import encode as lnurl_encode # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore -shop_counters = {} +shop_counters: Dict = {} class ShopCounter(object): diff --git a/lnbits/extensions/offlineshop/views.py b/lnbits/extensions/offlineshop/views.py index ace818b00..86c9d9c0a 100644 --- a/lnbits/extensions/offlineshop/views.py +++ b/lnbits/extensions/offlineshop/views.py @@ -1,4 +1,5 @@ import time +from datetime import datetime from quart import g, render_template, request from http import HTTPStatus @@ -48,4 +49,12 @@ async def confirmation_code(): item = await get_item(payment.extra.get("item")) shop = await get_shop(item.shop) - return shop.get_word(payment_hash) + style + return ( + f""" +[{shop.get_word(payment_hash)}] +{item.name} +{item.price} {item.unit} +{datetime.utcfromtimestamp(payment.time).strftime('%Y-%m-%d %H:%M:%S')} + """ + + style + ) From a653a5327be396d69fb92e0db798f7a0f4d0ea1b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Mar 2021 14:21:26 -0300 Subject: [PATCH 24/63] support totp confirmation method. --- lnbits/extensions/offlineshop/crud.py | 6 +- lnbits/extensions/offlineshop/helpers.py | 17 ++++ lnbits/extensions/offlineshop/lnurl.py | 6 +- lnbits/extensions/offlineshop/migrations.py | 1 + lnbits/extensions/offlineshop/models.py | 23 ++++- .../extensions/offlineshop/static/js/index.js | 21 +++-- .../templates/offlineshop/index.html | 83 +++++++++++++++++-- lnbits/extensions/offlineshop/views.py | 6 +- lnbits/extensions/offlineshop/views_api.py | 14 ++-- 9 files changed, 147 insertions(+), 30 deletions(-) diff --git a/lnbits/extensions/offlineshop/crud.py b/lnbits/extensions/offlineshop/crud.py index 5da350a5e..365015a3e 100644 --- a/lnbits/extensions/offlineshop/crud.py +++ b/lnbits/extensions/offlineshop/crud.py @@ -32,10 +32,10 @@ async def get_or_create_shop_by_wallet(wallet: str) -> Optional[Shop]: return Shop(**dict(row)) if row else None -async def set_wordlist(shop: int, wordlist: str) -> Optional[Shop]: +async def set_method(shop: int, method: str, wordlist: str = "") -> Optional[Shop]: await db.execute( - "UPDATE shops SET wordlist = ? WHERE id = ?", - (wordlist, shop), + "UPDATE shops SET method = ?, wordlist = ? WHERE id = ?", + (method, wordlist, shop), ) return await get_shop(shop) diff --git a/lnbits/extensions/offlineshop/helpers.py b/lnbits/extensions/offlineshop/helpers.py index 13e748574..2930809dc 100644 --- a/lnbits/extensions/offlineshop/helpers.py +++ b/lnbits/extensions/offlineshop/helpers.py @@ -1,5 +1,9 @@ import trio # type: ignore import httpx +import base64 +import struct +import hmac +import time async def get_fiat_rate(currency: str): @@ -46,3 +50,16 @@ async def get_usd_rate(): satoshi_prices = [x for x in satoshi_prices if x] return sum(satoshi_prices) / len(satoshi_prices) + + +def hotp(key, counter, digits=6, digest="sha1"): + key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8)) + counter = struct.pack(">Q", counter) + mac = hmac.new(key, counter, digest).digest() + offset = mac[-1] & 0x0F + binary = struct.unpack(">L", mac[offset : offset + 4])[0] & 0x7FFFFFFF + return str(binary)[-digits:].zfill(digits) + + +def totp(key, time_step=30, digits=6, digest="sha1"): + return hotp(key, int(time.time() / time_step), digits, digest) diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py index e53a145cd..f58a59ebe 100644 --- a/lnbits/extensions/offlineshop/lnurl.py +++ b/lnbits/extensions/offlineshop/lnurl.py @@ -61,6 +61,10 @@ async def lnurl_callback(item_id): extra={"tag": "offlineshop", "item": item.id}, ) - resp = LnurlPayActionResponse(pr=payment_request, success_action=item.success_action(shop, payment_hash), routes=[]) + resp = LnurlPayActionResponse( + pr=payment_request, + success_action=item.success_action(shop, payment_hash) if shop.method else None, + routes=[], + ) return jsonify(resp.dict()) diff --git a/lnbits/extensions/offlineshop/migrations.py b/lnbits/extensions/offlineshop/migrations.py index 72e9e501d..8e8a4877b 100644 --- a/lnbits/extensions/offlineshop/migrations.py +++ b/lnbits/extensions/offlineshop/migrations.py @@ -7,6 +7,7 @@ async def m001_initial(db): CREATE TABLE shops ( id INTEGER PRIMARY KEY AUTOINCREMENT, wallet TEXT NOT NULL, + method TEXT NOT NULL, wordlist TEXT ); """ diff --git a/lnbits/extensions/offlineshop/models.py b/lnbits/extensions/offlineshop/models.py index 175b440b0..52cf95f28 100644 --- a/lnbits/extensions/offlineshop/models.py +++ b/lnbits/extensions/offlineshop/models.py @@ -1,4 +1,6 @@ import json +import base64 +import hashlib from collections import OrderedDict from quart import url_for from typing import NamedTuple, Optional, List, Dict @@ -6,6 +8,8 @@ from lnurl import encode as lnurl_encode # type: ignore from lnurl.types import LnurlPayMetadata # type: ignore from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore +from .helpers import totp + shop_counters: Dict = {} @@ -53,11 +57,24 @@ class ShopCounter(object): class Shop(NamedTuple): id: int wallet: str + method: str wordlist: str - def get_word(self, payment_hash): - sc = ShopCounter.invoke(self) - return sc.get_word(payment_hash) + @property + def otp_key(self) -> str: + return base64.b32encode( + hashlib.sha256( + ("otpkey" + str(self.id) + self.wallet).encode("ascii"), + ).digest() + ).decode("ascii") + + def get_code(self, payment_hash: str) -> str: + if self.method == "wordlist": + sc = ShopCounter.invoke(self) + return sc.get_word(payment_hash) + elif self.method == "totp": + return totp(self.otp_key) + return "" class Item(NamedTuple): diff --git a/lnbits/extensions/offlineshop/static/js/index.js b/lnbits/extensions/offlineshop/static/js/index.js index 84b6f8650..f1d612f4e 100644 --- a/lnbits/extensions/offlineshop/static/js/index.js +++ b/lnbits/extensions/offlineshop/static/js/index.js @@ -14,7 +14,10 @@ new Vue({ data() { return { selectedWallet: null, + confirmationMethod: 'wordlist', + wordlistTainted: false, offlineshop: { + method: null, wordlist: [], items: [] }, @@ -80,28 +83,32 @@ new Vue({ ) .then(response => { this.offlineshop = response.data + this.confirmationMethod = response.data.method + this.wordlistTainted = false }) .catch(err => { LNbits.utils.notifyApiError(err) }) }, - async updateWordlist() { + async setMethod() { try { await LNbits.api.request( 'PUT', - '/offlineshop/api/v1/offlineshop/wordlist', + '/offlineshop/api/v1/offlineshop/method', this.selectedWallet.inkey, - {wordlist: this.offlineshop.wordlist} + {method: this.confirmationMethod, wordlist: this.offlineshop.wordlist} ) - this.$q.notify({ - message: `Wordlist updated. Counter reset.`, - timeout: 700 - }) } catch (err) { LNbits.utils.notifyApiError(err) return } + this.$q.notify({ + message: + `Method set to ${this.confirmationMethod}.` + + (this.confirmationMethod === 'wordlist' ? ' Counter reset.' : ''), + timeout: 700 + }) this.loadShop() }, async sendItem() { diff --git a/lnbits/extensions/offlineshop/templates/offlineshop/index.html b/lnbits/extensions/offlineshop/templates/offlineshop/index.html index 8de5a8ede..6dfbc9933 100644 --- a/lnbits/extensions/offlineshop/templates/offlineshop/index.html +++ b/lnbits/extensions/offlineshop/templates/offlineshop/index.html @@ -120,17 +120,41 @@ - -
-
Wordlist
-
- + + + + + + + +
- +
-
- +
+ Update Wordlist
+ +
+
+
+ + + +
+
+ + Set TOTP + +
+
+
+ +
+

+ Setting this option disables the confirmation code message that + appears in the consumer wallet after a purchase is paid for. It's ok + if the consumer is to be trusted when they claim to have paid. +

+ + + Disable Confirmation Codes + +
diff --git a/lnbits/extensions/offlineshop/views.py b/lnbits/extensions/offlineshop/views.py index 86c9d9c0a..aa4ac3a13 100644 --- a/lnbits/extensions/offlineshop/views.py +++ b/lnbits/extensions/offlineshop/views.py @@ -51,9 +51,9 @@ async def confirmation_code(): return ( f""" -[{shop.get_word(payment_hash)}] -{item.name} -{item.price} {item.unit} +[{shop.get_code(payment_hash)}]
+{item.name}
+{item.price} {item.unit}
{datetime.utcfromtimestamp(payment.time).strftime('%Y-%m-%d %H:%M:%S')} """ + style diff --git a/lnbits/extensions/offlineshop/views_api.py b/lnbits/extensions/offlineshop/views_api.py index 211306e54..fd60014a4 100644 --- a/lnbits/extensions/offlineshop/views_api.py +++ b/lnbits/extensions/offlineshop/views_api.py @@ -7,7 +7,7 @@ from lnbits.decorators import api_check_wallet_key, api_validate_post_request from . import offlineshop_ext from .crud import ( get_or_create_shop_by_wallet, - set_wordlist, + set_method, add_item, update_item, get_items, @@ -28,6 +28,7 @@ async def api_shop_from_wallet(): { **shop._asdict(), **{ + "otp_key": shop.otp_key, "items": [item.values() for item in items], }, } @@ -86,14 +87,17 @@ async def api_delete_item(item_id): return "", HTTPStatus.NO_CONTENT -@offlineshop_ext.route("/api/v1/offlineshop/wordlist", methods=["PUT"]) +@offlineshop_ext.route("/api/v1/offlineshop/method", methods=["PUT"]) @api_check_wallet_key("invoice") @api_validate_post_request( schema={ - "wordlist": {"type": "string", "empty": True, "nullable": True, "required": True}, + "method": {"type": "string", "required": True, "nullable": False}, + "wordlist": {"type": "string", "empty": True, "nullable": True, "required": False}, } ) -async def api_set_wordlist(): +async def api_set_method(): + method = g.data["method"] + wordlist = g.data["wordlist"].split("\n") if g.data["wordlist"] else None wordlist = [word.strip() for word in wordlist if word.strip()] @@ -101,7 +105,7 @@ async def api_set_wordlist(): if not shop: return "", HTTPStatus.NOT_FOUND - updated_shop = await set_wordlist(shop.id, "\n".join(wordlist)) + updated_shop = await set_method(shop.id, method, "\n".join(wordlist)) if not updated_shop: return "", HTTPStatus.NOT_FOUND From d13ca2afdb622f939fce73242ea3147679b86d10 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Mar 2021 14:34:47 -0300 Subject: [PATCH 25/63] round USD rate to satoshis. --- lnbits/extensions/offlineshop/lnurl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py index f58a59ebe..7abf070e1 100644 --- a/lnbits/extensions/offlineshop/lnurl.py +++ b/lnbits/extensions/offlineshop/lnurl.py @@ -19,7 +19,7 @@ async def lnurl_response(item_id): return jsonify({"status": "ERROR", "reason": "Item disabled."}) rate = await get_fiat_rate(item.unit) if item.unit != "sat" else 1 - price_msat = item.price * 1000 * rate + price_msat = int(item.price * rate) * 1000 resp = LnurlPayResponse( callback=url_for("offlineshop.lnurl_callback", item_id=item.id, _external=True), @@ -43,8 +43,8 @@ async def lnurl_callback(item_id): else: rate = await get_fiat_rate(item.unit) # allow some fluctuation (the fiat price may have changed between the calls) - min = rate * 995 * item.price - max = rate * 1010 * item.price + min = int(rate * item.price) * 995 + max = int(rate * item.price) * 1010 amount_received = int(request.args.get("amount")) if amount_received < min: From 5142f1e29f8175eb8274aa7ef3fe16828fc4bf2f Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Mar 2021 16:47:01 -0300 Subject: [PATCH 26/63] reduce image quality even more. --- lnbits/extensions/offlineshop/static/js/index.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/offlineshop/static/js/index.js b/lnbits/extensions/offlineshop/static/js/index.js index f1d612f4e..f7dbba9bc 100644 --- a/lnbits/extensions/offlineshop/static/js/index.js +++ b/lnbits/extensions/offlineshop/static/js/index.js @@ -49,9 +49,15 @@ new Vue({ image.src = blobURL image.onload = async () => { let canvas = document.createElement('canvas') - canvas.setAttribute('width', 300) - canvas.setAttribute('height', 300) - await pica.resize(image, canvas) + canvas.setAttribute('width', 100) + canvas.setAttribute('height', 100) + await pica.resize(image, canvas, { + quality: 0, + alpha: true, + unsharpAmount: 95, + unsharpRadius: 0.9, + unsharpThreshold: 70 + }) this.itemDialog.data.image = canvas.toDataURL() this.itemDialog = {...this.itemDialog} } From adc3e625739f8a9a20a355ed500aa84606ec48ee Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Mar 2021 20:43:39 -0300 Subject: [PATCH 27/63] abstract exchange rates code into a "util". --- lnbits/extensions/lnurlp/helpers.py | 48 ----- lnbits/extensions/lnurlp/lnurl.py | 6 +- lnbits/extensions/lnurlp/views_api.py | 4 +- lnbits/extensions/offlineshop/helpers.py | 48 ----- lnbits/extensions/offlineshop/lnurl.py | 11 +- lnbits/utils/__init__.py | 0 lnbits/utils/exchange_rates.py | 262 +++++++++++++++++++++++ 7 files changed, 272 insertions(+), 107 deletions(-) delete mode 100644 lnbits/extensions/lnurlp/helpers.py create mode 100644 lnbits/utils/__init__.py create mode 100644 lnbits/utils/exchange_rates.py diff --git a/lnbits/extensions/lnurlp/helpers.py b/lnbits/extensions/lnurlp/helpers.py deleted file mode 100644 index 13e748574..000000000 --- a/lnbits/extensions/lnurlp/helpers.py +++ /dev/null @@ -1,48 +0,0 @@ -import trio # type: ignore -import httpx - - -async def get_fiat_rate(currency: str): - assert currency == "USD", "Only USD is supported as fiat currency." - return await get_usd_rate() - - -async def get_usd_rate(): - """ - Returns an average satoshi price from multiple sources. - """ - - satoshi_prices = [None, None, None] - - async def fetch_price(index, url, getter): - try: - async with httpx.AsyncClient() as client: - r = await client.get(url) - r.raise_for_status() - satoshi_price = int(100_000_000 / float(getter(r.json()))) - satoshi_prices[index] = satoshi_price - except Exception: - pass - - async with trio.open_nursery() as nursery: - nursery.start_soon( - fetch_price, - 0, - "https://api.kraken.com/0/public/Ticker?pair=XXBTZUSD", - lambda d: d["result"]["XXBTCZUSD"]["c"][0], - ) - nursery.start_soon( - fetch_price, - 1, - "https://www.bitstamp.net/api/v2/ticker/btcusd", - lambda d: d["last"], - ) - nursery.start_soon( - fetch_price, - 2, - "https://api.coincap.io/v2/rates/bitcoin", - lambda d: d["data"]["rateUsd"], - ) - - satoshi_prices = [x for x in satoshi_prices if x] - return sum(satoshi_prices) / len(satoshi_prices) diff --git a/lnbits/extensions/lnurlp/lnurl.py b/lnbits/extensions/lnurlp/lnurl.py index 74dd6e352..31b855595 100644 --- a/lnbits/extensions/lnurlp/lnurl.py +++ b/lnbits/extensions/lnurlp/lnurl.py @@ -5,10 +5,10 @@ from quart import jsonify, url_for, request from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore from lnbits.core.services import create_invoice +from lnbits.utils.exchange_rates import get_fiat_rate_satoshis from . import lnurlp_ext from .crud import increment_pay_link -from .helpers import get_fiat_rate @lnurlp_ext.route("/api/v1/lnurl/", methods=["GET"]) @@ -17,7 +17,7 @@ async def api_lnurl_response(link_id): if not link: return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK - rate = await get_fiat_rate(link.currency) if link.currency else 1 + rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1 resp = LnurlPayResponse( callback=url_for("lnurlp.api_lnurl_callback", link_id=link.id, _external=True), min_sendable=math.ceil(link.min * rate) * 1000, @@ -39,7 +39,7 @@ async def api_lnurl_callback(link_id): return jsonify({"status": "ERROR", "reason": "LNURL-pay not found."}), HTTPStatus.OK min, max = link.min, link.max - rate = await get_fiat_rate(link.currency) if link.currency else 1 + rate = await get_fiat_rate_satoshis(link.currency) if link.currency else 1 if link.currency: # allow some fluctuation (as the fiat price may have changed between the calls) min = rate * 995 * link.min diff --git a/lnbits/extensions/lnurlp/views_api.py b/lnbits/extensions/lnurlp/views_api.py index f68bc5b30..a0fe2ffd8 100644 --- a/lnbits/extensions/lnurlp/views_api.py +++ b/lnbits/extensions/lnurlp/views_api.py @@ -4,6 +4,7 @@ from lnurl.exceptions import InvalidUrl as LnurlInvalidUrl # type: ignore from lnbits.core.crud import get_user from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from lnbits.utils.exchange_rates import get_fiat_rate_satoshis from . import lnurlp_ext from .crud import ( @@ -13,7 +14,6 @@ from .crud import ( update_pay_link, delete_pay_link, ) -from .helpers import get_fiat_rate @lnurlp_ext.route("/api/v1/links", methods=["GET"]) @@ -109,7 +109,7 @@ async def api_link_delete(link_id): @lnurlp_ext.route("/api/v1/rate/", methods=["GET"]) async def api_check_fiat_rate(currency): try: - rate = await get_fiat_rate(currency) + rate = await get_fiat_rate_satoshis(currency) except AssertionError: rate = None diff --git a/lnbits/extensions/offlineshop/helpers.py b/lnbits/extensions/offlineshop/helpers.py index 2930809dc..6b56cf559 100644 --- a/lnbits/extensions/offlineshop/helpers.py +++ b/lnbits/extensions/offlineshop/helpers.py @@ -1,57 +1,9 @@ -import trio # type: ignore -import httpx import base64 import struct import hmac import time -async def get_fiat_rate(currency: str): - assert currency == "USD", "Only USD is supported as fiat currency." - return await get_usd_rate() - - -async def get_usd_rate(): - """ - Returns an average satoshi price from multiple sources. - """ - - satoshi_prices = [None, None, None] - - async def fetch_price(index, url, getter): - try: - async with httpx.AsyncClient() as client: - r = await client.get(url) - r.raise_for_status() - satoshi_price = int(100_000_000 / float(getter(r.json()))) - satoshi_prices[index] = satoshi_price - except Exception: - pass - - async with trio.open_nursery() as nursery: - nursery.start_soon( - fetch_price, - 0, - "https://api.kraken.com/0/public/Ticker?pair=XXBTZUSD", - lambda d: d["result"]["XXBTCZUSD"]["c"][0], - ) - nursery.start_soon( - fetch_price, - 1, - "https://www.bitstamp.net/api/v2/ticker/btcusd", - lambda d: d["last"], - ) - nursery.start_soon( - fetch_price, - 2, - "https://api.coincap.io/v2/rates/bitcoin", - lambda d: d["data"]["rateUsd"], - ) - - satoshi_prices = [x for x in satoshi_prices if x] - return sum(satoshi_prices) / len(satoshi_prices) - - def hotp(key, counter, digits=6, digest="sha1"): key = base64.b32decode(key.upper() + "=" * ((8 - len(key)) % 8)) counter = struct.pack(">Q", counter) diff --git a/lnbits/extensions/offlineshop/lnurl.py b/lnbits/extensions/offlineshop/lnurl.py index 7abf070e1..d1e11c0c5 100644 --- a/lnbits/extensions/offlineshop/lnurl.py +++ b/lnbits/extensions/offlineshop/lnurl.py @@ -3,10 +3,10 @@ from quart import jsonify, url_for, request from lnurl import LnurlPayResponse, LnurlPayActionResponse, LnurlErrorResponse # type: ignore from lnbits.core.services import create_invoice +from lnbits.utils.exchange_rates import fiat_amount_as_satoshis from . import offlineshop_ext from .crud import get_shop, get_item -from .helpers import get_fiat_rate @offlineshop_ext.route("/lnurl/", methods=["GET"]) @@ -18,8 +18,7 @@ async def lnurl_response(item_id): if not item.enabled: return jsonify({"status": "ERROR", "reason": "Item disabled."}) - rate = await get_fiat_rate(item.unit) if item.unit != "sat" else 1 - price_msat = int(item.price * rate) * 1000 + price_msat = (await fiat_amount_as_satoshis(item.price, item.unit) if item.unit != "sat" else item.price) * 1000 resp = LnurlPayResponse( callback=url_for("offlineshop.lnurl_callback", item_id=item.id, _external=True), @@ -41,10 +40,10 @@ async def lnurl_callback(item_id): min = item.price * 1000 max = item.price * 1000 else: - rate = await get_fiat_rate(item.unit) + price = await fiat_amount_as_satoshis(item.price, item.unit) # allow some fluctuation (the fiat price may have changed between the calls) - min = int(rate * item.price) * 995 - max = int(rate * item.price) * 1010 + min = price * 995 + max = price * 1010 amount_received = int(request.args.get("amount")) if amount_received < min: diff --git a/lnbits/utils/__init__.py b/lnbits/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lnbits/utils/exchange_rates.py b/lnbits/utils/exchange_rates.py new file mode 100644 index 000000000..506f6daf0 --- /dev/null +++ b/lnbits/utils/exchange_rates.py @@ -0,0 +1,262 @@ +import trio # type: ignore +import httpx +from typing import Callable, NamedTuple + +currencies = { + "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", +} + + +class Provider(NamedTuple): + name: str + domain: str + api_url: str + getter: Callable + + +exchange_rate_providers = { + "bitfinex": Provider( + "Bitfinex", + "bitfinex.com", + "https://api.bitfinex.com/v1/pubticker/{from}{to}", + lambda data, replacements: data["last_price"], + ), + "bitstamp": Provider( + "Bitstamp", + "bitstamp.net", + "https://www.bitstamp.net/api/v2/ticker/{from}{to}/", + lambda data, replacements: data["last"], + ), + "coinbase": Provider( + "Coinbase", + "coinbase.com", + "https://api.coinbase.com/v2/exchange-rates?currency={FROM}", + lambda data, replacements: data["data"]["rates"][replacements["TO"]], + ), + "coinmate": Provider( + "CoinMate", + "coinmate.io", + "https://coinmate.io/api/ticker?currencyPair={FROM}_{TO}", + lambda data, replacements: data["data"]["last"], + ), + "kraken": Provider( + "Kraken", + "kraken.com", + "https://api.kraken.com/0/public/Ticker?pair=XBT{TO}", + lambda data, replacements: data["result"]["XXBTZ" + replacements["TO"]]["c"][0], + ), +} + + +async def btc_price(currency: str) -> float: + replacements = {"FROM": "BTC", "from": "btc", "TO": currency.upper(), "to": currency.lower()} + rates = [] + send_channel, receive_channel = trio.open_memory_channel(0) + + async def controller(nursery): + failures = 0 + while True: + rate = await receive_channel.receive() + if rate: + rates.append(rate) + else: + failures += 1 + if len(rates) >= 2 or len(rates) == 1 and failures >= 2: + nursery.cancel_scope.cancel() + break + if failures == len(exchange_rate_providers): + nursery.cancel_scope.cancel() + break + + async def fetch_price(key: str, provider: Provider): + try: + url = provider.api_url.format(**replacements) + async with httpx.AsyncClient() as client: + r = await client.get(url, timeout=0.5) + r.raise_for_status() + data = r.json() + rate = float(provider.getter(data, replacements)) + await send_channel.send(rate) + except Exception: + await send_channel.send(None) + + async with trio.open_nursery() as nursery: + nursery.start_soon(controller, nursery) + for key, provider in exchange_rate_providers.items(): + nursery.start_soon(fetch_price, key, provider) + + if not rates: + return 9999999999 + + return sum([rate for rate in rates]) / len(rates) + + +async def get_fiat_rate_satoshis(currency: str) -> float: + return int(100_000_000 / (await btc_price(currency))) + + +async def fiat_amount_as_satoshis(amount: float, currency: str) -> int: + return int(amount * (await get_fiat_rate_satoshis(currency))) From 0bc66ea15052572bbbd0016c851967c676502cb9 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Mar 2021 20:58:49 -0300 Subject: [PATCH 28/63] support all the currencies. --- lnbits/extensions/offlineshop/static/js/index.js | 11 ++++++++++- lnbits/extensions/offlineshop/views_api.py | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/offlineshop/static/js/index.js b/lnbits/extensions/offlineshop/static/js/index.js index f7dbba9bc..00e932416 100644 --- a/lnbits/extensions/offlineshop/static/js/index.js +++ b/lnbits/extensions/offlineshop/static/js/index.js @@ -24,7 +24,7 @@ new Vue({ itemDialog: { show: false, data: {...defaultItemData}, - units: ['sat', 'USD'] + units: ['sat'] } } }, @@ -207,5 +207,14 @@ new Vue({ created() { this.selectedWallet = this.g.user.wallets[0] this.loadShop() + + LNbits.api + .request('GET', '/offlineshop/api/v1/currencies') + .then(response => { + this.itemDialog = {...this.itemDialog, units: ['sat', ...response.data]} + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) } }) diff --git a/lnbits/extensions/offlineshop/views_api.py b/lnbits/extensions/offlineshop/views_api.py index fd60014a4..20a9eced0 100644 --- a/lnbits/extensions/offlineshop/views_api.py +++ b/lnbits/extensions/offlineshop/views_api.py @@ -3,6 +3,7 @@ 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 lnbits.utils.exchange_rates import currencies from . import offlineshop_ext from .crud import ( @@ -16,6 +17,11 @@ from .crud import ( from .models import ShopCounter +@offlineshop_ext.route("/api/v1/currencies", methods=["GET"]) +async def api_list_currencies_available(): + return jsonify(list(currencies.keys())) + + @offlineshop_ext.route("/api/v1/offlineshop", methods=["GET"]) @api_check_wallet_key("invoice") async def api_shop_from_wallet(): @@ -51,7 +57,7 @@ async def api_shop_from_wallet(): "description": {"type": "string", "empty": False, "required": True}, "image": {"type": "string", "required": False, "nullable": True}, "price": {"type": "number", "required": True}, - "unit": {"type": "string", "allowed": ["sat", "USD"], "required": True}, + "unit": {"type": "string", "required": True}, } ) async def api_add_or_update_item(item_id=None): From 1bc59974a8ec327c9ca6f57deb9d956f0fa55de8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Sun, 14 Mar 2021 21:50:05 -0300 Subject: [PATCH 29/63] also support all currencies in lnurlp. --- lnbits/extensions/lnurlp/static/js/index.js | 10 ++++++++++ lnbits/extensions/lnurlp/templates/lnurlp/index.html | 2 +- lnbits/extensions/lnurlp/views_api.py | 9 +++++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lnbits/extensions/lnurlp/static/js/index.js b/lnbits/extensions/lnurlp/static/js/index.js index bbf5baf08..dbc0df1e3 100644 --- a/lnbits/extensions/lnurlp/static/js/index.js +++ b/lnbits/extensions/lnurlp/static/js/index.js @@ -26,6 +26,7 @@ new Vue({ mixins: [windowMixin], data() { return { + currencies: [], fiatRates: {}, checker: null, payLinks: [], @@ -203,5 +204,14 @@ new Vue({ getPayLinks() }, 20000) } + + LNbits.api + .request('GET', '/lnurlp/api/v1/currencies') + .then(response => { + this.currencies = ['satoshis', ...response.data] + }) + .catch(err => { + LNbits.utils.notifyApiError(err) + }) } }) diff --git a/lnbits/extensions/lnurlp/templates/lnurlp/index.html b/lnbits/extensions/lnurlp/templates/lnurlp/index.html index c235dc7f3..c7d60667b 100644 --- a/lnbits/extensions/lnurlp/templates/lnurlp/index.html +++ b/lnbits/extensions/lnurlp/templates/lnurlp/index.html @@ -182,7 +182,7 @@
Date: Sun, 14 Mar 2021 22:14:52 -0300 Subject: [PATCH 30/63] update requirements.txt --- requirements.txt | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/requirements.txt b/requirements.txt index 182500512..fec167af4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,40 +6,42 @@ bitstring==3.1.7 blinker==1.4 brotli==1.0.9 cerberus==1.3.2 -certifi==2020.11.8 +certifi==2020.12.5 click==7.1.2 ecdsa==0.16.1 -environs==9.2.0 -h11==0.11.0 +environs==9.3.1 +h11==0.12.0 h2==4.0.0 hpack==4.0.0 -httpcore==0.12.2 +httpcore==0.12.3 httpx==0.16.1 -hypercorn==0.11.1 +hypercorn==0.11.2 hyperframe==6.0.0 -idna==2.10 +idna==3.1 itsdangerous==1.1.0 -jinja2==2.11.2 +jinja2==2.11.3 lnurl==0.3.5 markupsafe==1.1.1 -marshmallow==3.9.1 +marshmallow==3.10.0 outcome==1.1.0 priority==1.3.0 -pydantic==1.7.2 +pydantic==1.8 +pypng==0.0.20 +pyqrcode==1.2.1 pyscss==1.3.7 python-dotenv==0.15.0 -quart==0.13.1 +quart==0.14.1 quart-compress==0.2.1 quart-cors==0.3.0 -quart-trio==0.6.0 -represent==1.6.0 +quart-trio==0.7.0 +represent==1.6.0.post0 rfc3986==1.4.0 secure==0.2.1 shortuuid==1.0.1 six==1.15.0 sniffio==1.2.0 sortedcontainers==2.3.0 -sqlalchemy==1.3.20 +sqlalchemy==1.3.23 sqlalchemy-aio==0.16.0 toml==0.10.2 trio==0.16.0 From b5c4fe905f5067ef379f34f234139e24fe823e71 Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 16 Mar 2021 13:38:42 +0000 Subject: [PATCH 31/63] Extension summary must be small --- lnbits/extensions/bleskomat/config.json | 2 +- lnbits/extensions/offlineshop/config.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/bleskomat/config.json b/lnbits/extensions/bleskomat/config.json index 8004b7fb6..99244df14 100644 --- a/lnbits/extensions/bleskomat/config.json +++ b/lnbits/extensions/bleskomat/config.json @@ -1,6 +1,6 @@ { "name": "Bleskomat", - "short_description": "This extension allows you to connect a Bleskomat ATM to an lnbits wallet.", + "short_description": "Connect a Bleskomat ATM to an lnbits", "icon": "money", "contributors": ["chill117"] } diff --git a/lnbits/extensions/offlineshop/config.json b/lnbits/extensions/offlineshop/config.json index 507b1d146..0dcb1d6b0 100644 --- a/lnbits/extensions/offlineshop/config.json +++ b/lnbits/extensions/offlineshop/config.json @@ -1,6 +1,6 @@ { "name": "OfflineShop", - "short_description": "Sell stuff with Lightning and lnurlpay on a shop without internet or any electronic device.", + "short_description": "Receive payments for products offline!", "icon": "nature_people", "contributors": [ "fiatjaf" From 1a460a2ce57dbd63094bc415cdcaa34239827b6b Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 17 Mar 2021 09:02:47 -0300 Subject: [PATCH 32/63] fix issue #159 --- lnbits/extensions/offlineshop/crud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/offlineshop/crud.py b/lnbits/extensions/offlineshop/crud.py index 365015a3e..e6bd0d6c6 100644 --- a/lnbits/extensions/offlineshop/crud.py +++ b/lnbits/extensions/offlineshop/crud.py @@ -8,8 +8,8 @@ from .models import Shop, Item async def create_shop(*, wallet_id: str) -> int: result = await db.execute( """ - INSERT INTO shops (wallet, wordlist) - VALUES (?, ?) + INSERT INTO shops (wallet, wordlist, method) + VALUES (?, ?, 'wordlist') """, (wallet_id, "\n".join(animals)), ) From 7cd3487bc9cfee4730708f6e4fc58071148a4172 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 17 Mar 2021 15:38:32 +0000 Subject: [PATCH 33/63] Added hash check to db --- lnbits/extensions/withdraw/migrations.py | 13 +++++++++++++ lnbits/extensions/withdraw/models.py | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/lnbits/extensions/withdraw/migrations.py b/lnbits/extensions/withdraw/migrations.py index 4af24f8fa..b0cf0b8f0 100644 --- a/lnbits/extensions/withdraw/migrations.py +++ b/lnbits/extensions/withdraw/migrations.py @@ -94,3 +94,16 @@ async def m002_change_withdraw_table(db): ), ) await db.execute("DROP TABLE withdraw_links") + +async def m003_make_hash_check(db): + """ + Creates a hash check table. + """ + await db.execute( + """ + CREATE TABLE IF NOT EXISTS hash_check ( + id TEXT PRIMARY KEY, + lnurl_id TEXT + ); + """ + ) \ No newline at end of file diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 9a4dfb7c2..1903dba69 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -59,3 +59,11 @@ class WithdrawLink(NamedTuple): max_withdrawable=self.max_withdrawable * 1000, default_description=self.title, ) + +class HashCheck(NamedTuple): + id: str + lnurl_id: str + + @classmethod + def from_row(cls, row: Row) -> "Hash": + return cls(**dict(row)) \ No newline at end of file From 514329045f2cf9ab570b250e460542dd61562fd4 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 17 Mar 2021 16:33:12 +0000 Subject: [PATCH 34/63] Should work --- lnbits/extensions/withdraw/crud.py | 25 +++++++++++++++++++++++++ lnbits/extensions/withdraw/models.py | 1 - lnbits/extensions/withdraw/views_api.py | 16 ++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index 78fd7f566..64d4373e2 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -98,3 +98,28 @@ async def delete_withdraw_link(link_id: str) -> None: def chunks(lst, n): for i in range(0, len(lst), n): yield lst[i : i + n] + +async def create_hash_check( + the_hash: str, + lnurl_id: str, +) -> HashCheck: + await db.execute( + """ + INSERT INTO hash_check ( + id, + lnurl_id + ) + VALUES (?, ?) + """, + ( + the_hash, + lnurl_id, + ), + ) + hashCheck = await get_hash_check(the_hash, lnurl_id) + row = await db.fetchone("SELECT * FROM hash_check WHERE id = ?", (the_hash,)) + return HashCheck.from_row(row) if row else None + +async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: + row = await db.fetchone("SELECT * FROM hash_check WHERE id = ?", (the_hash,)) + return HashCheck.from_row(row) if row else None \ No newline at end of file diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 1903dba69..4d147f254 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -63,7 +63,6 @@ class WithdrawLink(NamedTuple): class HashCheck(NamedTuple): id: str lnurl_id: str - @classmethod def from_row(cls, row: Row) -> "Hash": return cls(**dict(row)) \ No newline at end of file diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py index cb8b7f0a7..d34422baa 100644 --- a/lnbits/extensions/withdraw/views_api.py +++ b/lnbits/extensions/withdraw/views_api.py @@ -12,6 +12,8 @@ from .crud import ( get_withdraw_links, update_withdraw_link, delete_withdraw_link, + create_hash_check, + get_hash_check, ) @@ -111,3 +113,17 @@ async def api_link_delete(link_id): await delete_withdraw_link(link_id) return "", HTTPStatus.NO_CONTENT + +@withdraw_ext.route("/api/v1/links//", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_hash_retrieve(the_hash, lnurl_id): + hashCheck = await get_hash_check(the_hash, lnurl_id) + + if not hashCheck: + hashCheck = await create_hash_check(the_hash, lnurl_id) + return jsonify({"status": False}), HTTPStatus.OK + + if link.wallet != g.wallet.id: + return jsonify({"status": True}), HTTPStatus.OK + + return jsonify({"status": True}), HTTPStatus.OK From 86744ebcedadca6c54619c3c73ac8267b24efbc3 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 17 Mar 2021 16:56:23 +0000 Subject: [PATCH 35/63] Bug --- lnbits/extensions/withdraw/crud.py | 2 +- .../templates/withdraw/_api_docs.html | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index 64d4373e2..cc7fe3878 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -3,7 +3,7 @@ from typing import List, Optional, Union from lnbits.helpers import urlsafe_short_hash from . import db -from .models import WithdrawLink +from .models import WithdrawLink, HashCheck async def create_withdraw_link( diff --git a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html index 18a0a5420..75ed9e02e 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html +++ b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html @@ -150,3 +150,30 @@ + + + + GET + /withdraw/api/v1/links/<the_hash>/<lnurl_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ {"status": <bool>} +
Curl example
+ curl -X GET {{ request.url_root }}api/v1/links/<the_hash>/<lnurl_id> -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
+
+
From f42dab5e309c5e3d90e597492d7063035705f816 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 17 Mar 2021 17:55:00 +0000 Subject: [PATCH 36/63] Added api docs --- .../templates/withdraw/_api_docs.html | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html index 75ed9e02e..13d2c1d3e 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html +++ b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html @@ -149,8 +149,7 @@ - - + + + + GET + /withdraw/img/<lnurl_id> +
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ {"status": <bool>} +
Curl example
+ curl -X GET {{ request.url_root }}/withdraw/img/<lnurl_id>" + +
+
+
+ + + From 09f89d07dd32fd0b0a75f3e09674e38593ea4120 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 17 Mar 2021 17:57:12 +0000 Subject: [PATCH 37/63] API docs --- .../extensions/withdraw/templates/withdraw/_api_docs.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html index 13d2c1d3e..e15355c39 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html +++ b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html @@ -129,7 +129,7 @@ dense expand-separator label="Delete a withdraw link" - class="q-pb-md" + > @@ -181,6 +181,7 @@ group="api" dense expand-separator label="Get image to embed" +class="q-pb-md" > @@ -188,10 +189,6 @@ label="Get image to embed" >GET /withdraw/img/<lnurl_id>
-
Body (application/json)
-
- Returns 201 CREATED (application/json) -
{"status": <bool>}
Curl example
Date: Wed, 17 Mar 2021 17:58:03 +0000 Subject: [PATCH 38/63] api doc --- lnbits/extensions/withdraw/templates/withdraw/_api_docs.html | 1 - 1 file changed, 1 deletion(-) diff --git a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html index e15355c39..b8fa8231c 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html +++ b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html @@ -189,7 +189,6 @@ class="q-pb-md" >GET /withdraw/img/<lnurl_id> - {"status": <bool>}
Curl example
curl -X GET {{ request.url_root }}/withdraw/img/<lnurl_id>" From 66cde0154b591fcfeaffc71796cb046f8862d2cf Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 17 Mar 2021 18:02:52 +0000 Subject: [PATCH 39/63] api docs --- lnbits/extensions/withdraw/views_api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py index d34422baa..757603c6b 100644 --- a/lnbits/extensions/withdraw/views_api.py +++ b/lnbits/extensions/withdraw/views_api.py @@ -122,8 +122,5 @@ async def api_hash_retrieve(the_hash, lnurl_id): if not hashCheck: hashCheck = await create_hash_check(the_hash, lnurl_id) return jsonify({"status": False}), HTTPStatus.OK - - if link.wallet != g.wallet.id: - return jsonify({"status": True}), HTTPStatus.OK return jsonify({"status": True}), HTTPStatus.OK From ad545e7fe13e3b9dc94cfd39e745acd960f7e49a Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 17 Mar 2021 19:27:52 +0000 Subject: [PATCH 40/63] prettier/black --- lnbits/extensions/withdraw/crud.py | 2 + lnbits/extensions/withdraw/migrations.py | 1 + lnbits/extensions/withdraw/models.py | 2 + .../templates/withdraw/_api_docs.html | 91 +++++++++---------- lnbits/extensions/withdraw/views_api.py | 1 + 5 files changed, 48 insertions(+), 49 deletions(-) diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index cc7fe3878..11f7d949d 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -99,6 +99,7 @@ def chunks(lst, n): for i in range(0, len(lst), n): yield lst[i : i + n] + async def create_hash_check( the_hash: str, lnurl_id: str, @@ -120,6 +121,7 @@ async def create_hash_check( row = await db.fetchone("SELECT * FROM hash_check WHERE id = ?", (the_hash,)) return HashCheck.from_row(row) if row else None + async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: row = await db.fetchone("SELECT * FROM hash_check WHERE id = ?", (the_hash,)) return HashCheck.from_row(row) if row else None \ No newline at end of file diff --git a/lnbits/extensions/withdraw/migrations.py b/lnbits/extensions/withdraw/migrations.py index b0cf0b8f0..2cb802d44 100644 --- a/lnbits/extensions/withdraw/migrations.py +++ b/lnbits/extensions/withdraw/migrations.py @@ -95,6 +95,7 @@ async def m002_change_withdraw_table(db): ) await db.execute("DROP TABLE withdraw_links") + async def m003_make_hash_check(db): """ Creates a hash check table. diff --git a/lnbits/extensions/withdraw/models.py b/lnbits/extensions/withdraw/models.py index 4d147f254..4c485ea33 100644 --- a/lnbits/extensions/withdraw/models.py +++ b/lnbits/extensions/withdraw/models.py @@ -60,9 +60,11 @@ class WithdrawLink(NamedTuple): default_description=self.title, ) + class HashCheck(NamedTuple): id: str lnurl_id: str + @classmethod def from_row(cls, row: Row) -> "Hash": return cls(**dict(row)) \ No newline at end of file diff --git a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html index b8fa8231c..c303b091c 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html +++ b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html @@ -129,7 +129,6 @@ dense expand-separator label="Delete a withdraw link" - > @@ -149,53 +148,47 @@ + + + + GET + /withdraw/api/v1/links/<the_hash>/<lnurl_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 201 CREATED (application/json) +
+ {"status": <bool>} +
Curl example
+ curl -X GET {{ request.url_root + }}api/v1/links/<the_hash>/<lnurl_id> -H "X-Api-Key: {{ + g.user.wallets[0].inkey }}" + +
+
+
- - - GET - /withdraw/api/v1/links/<the_hash>/<lnurl_id> -
Headers
- {"X-Api-Key": <invoice_key>}
-
Body (application/json)
-
- Returns 201 CREATED (application/json) -
- {"status": <bool>} -
Curl example
- curl -X GET {{ request.url_root }}api/v1/links/<the_hash>/<lnurl_id> -H - "X-Api-Key: {{ g.user.wallets[0].inkey }}" - -
-
+ group="api" + dense + expand-separator + label="Get image to embed" + class="q-pb-md" + > + + + GET + /withdraw/img/<lnurl_id> +
Curl example
+ curl -X GET {{ request.url_root }}/withdraw/img/<lnurl_id>" + +
+
+
- - - - GET - /withdraw/img/<lnurl_id> -
Curl example
- curl -X GET {{ request.url_root }}/withdraw/img/<lnurl_id>" - -
-
-
- - - diff --git a/lnbits/extensions/withdraw/views_api.py b/lnbits/extensions/withdraw/views_api.py index 757603c6b..2f80aab5e 100644 --- a/lnbits/extensions/withdraw/views_api.py +++ b/lnbits/extensions/withdraw/views_api.py @@ -114,6 +114,7 @@ async def api_link_delete(link_id): return "", HTTPStatus.NO_CONTENT + @withdraw_ext.route("/api/v1/links//", methods=["GET"]) @api_check_wallet_key("invoice") async def api_hash_retrieve(the_hash, lnurl_id): From c86bd74506c054e8a0f6c271918d15d81c4cbdf3 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 17 Mar 2021 19:32:37 +0000 Subject: [PATCH 41/63] edited api --- .../extensions/withdraw/templates/withdraw/_api_docs.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html index c303b091c..484464baf 100644 --- a/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html +++ b/lnbits/extensions/withdraw/templates/withdraw/_api_docs.html @@ -148,7 +148,12 @@ - + Date: Wed, 17 Mar 2021 19:54:17 +0000 Subject: [PATCH 42/63] Ran black on bleskomat --- lnbits/extensions/bleskomat/crud.py | 29 +++++++++-- lnbits/extensions/bleskomat/exchange_rates.py | 23 ++++---- lnbits/extensions/bleskomat/helpers.py | 52 ++++++------------- lnbits/extensions/bleskomat/lnurl_api.py | 13 ++--- lnbits/extensions/bleskomat/models.py | 14 ++--- lnbits/extensions/bleskomat/views.py | 3 +- lnbits/extensions/bleskomat/views_api.py | 10 ++-- lnbits/extensions/withdraw/crud.py | 1 + 8 files changed, 70 insertions(+), 75 deletions(-) diff --git a/lnbits/extensions/bleskomat/crud.py b/lnbits/extensions/bleskomat/crud.py index 470d87cbb..690793501 100644 --- a/lnbits/extensions/bleskomat/crud.py +++ b/lnbits/extensions/bleskomat/crud.py @@ -6,19 +6,30 @@ 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"; + api_key_encoding = "hex" await db.execute( """ INSERT INTO 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_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" @@ -65,7 +76,19 @@ async def create_bleskomat_lnurl( INSERT INTO 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_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" diff --git a/lnbits/extensions/bleskomat/exchange_rates.py b/lnbits/extensions/bleskomat/exchange_rates.py index 15547370c..6d9297c60 100644 --- a/lnbits/extensions/bleskomat/exchange_rates.py +++ b/lnbits/extensions/bleskomat/exchange_rates.py @@ -2,39 +2,41 @@ 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')) +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"] + "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"] + "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"]] + "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"] + "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] - } + "getter": lambda data, replacements: data["result"]["XXBTZ" + replacements["TO"]]["c"][0], + }, } exchange_rate_providers_serializable = {} @@ -48,12 +50,7 @@ for ref, exchange_rate_provider in exchange_rate_providers.items(): async def fetch_fiat_exchange_rate(currency: str, provider: str): - replacements = { - "FROM" : "BTC", - "from" : "btc", - "TO" : currency.upper(), - "to" : currency.lower() - } + replacements = {"FROM": "BTC", "from": "btc", "TO": currency.upper(), "to": currency.lower()} url = exchange_rate_providers[provider]["api_url"] for key in replacements.keys(): diff --git a/lnbits/extensions/bleskomat/helpers.py b/lnbits/extensions/bleskomat/helpers.py index 439795be0..9b745c797 100644 --- a/lnbits/extensions/bleskomat/helpers.py +++ b/lnbits/extensions/bleskomat/helpers.py @@ -21,11 +21,7 @@ def generate_bleskomat_lnurl_signature(payload: str, api_key_secret: str, api_ke 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() + return hmac.new(key=key, msg=payload.encode(), digestmod=hashlib.sha256).hexdigest() def generate_bleskomat_lnurl_secret(api_key_id: str, signature: str): @@ -58,19 +54,21 @@ class LnurlValidationError(Exception): def prepare_lnurl_params(tag: str, query: Dict[str, str]): params = {} if not is_supported_lnurl_subprotocol(tag): - raise LnurlValidationError(f"Unsupported 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") + 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\"") + 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) @@ -84,35 +82,17 @@ def query_to_signing_payload(query: Dict[str, str]) -> str: unshorten_rules = { - "query": { - "n": "nonce", - "s": "signature", - "t": "tag" - }, - "tags": { - "c": "channelRequest", - "l": "login", - "p": "payRequest", - "w": "withdrawRequest" - }, + "query": {"n": "nonce", "s": "signature", "t": "tag"}, + "tags": {"c": "channelRequest", "l": "login", "p": "payRequest", "w": "withdrawRequest"}, "params": { - "channelRequest": { - "pl": "localAmt", - "pp": "pushAmt" - }, + "channelRequest": {"pl": "localAmt", "pp": "pushAmt"}, "login": {}, - "payRequest": { - "pn": "minSendable", - "px": "maxSendable", - "pm": "metadata" - }, - "withdrawRequest": { - "pn": "minWithdrawable", - "px": "maxWithdrawable", - "pd": "defaultDescription" - } - } + "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 @@ -121,14 +101,14 @@ def unshorten_lnurl_query(query: Dict[str, str]) -> Dict[str, str]: elif "t" in query: tag = query["t"] else: - raise LnurlValidationError("Missing required query parameter: \"tag\"") + 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}\"") + raise LnurlValidationError(f'Unknown tag: "{tag}"') for key in query: if key in rules["params"][tag]: short_param_key = key diff --git a/lnbits/extensions/bleskomat/lnurl_api.py b/lnbits/extensions/bleskomat/lnurl_api.py index 35d93032d..c7df81d1b 100644 --- a/lnbits/extensions/bleskomat/lnurl_api.py +++ b/lnbits/extensions/bleskomat/lnurl_api.py @@ -47,7 +47,7 @@ async def api_bleskomat_lnurl(): # 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) + 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 @@ -72,8 +72,7 @@ async def api_bleskomat_lnurl(): params = prepare_lnurl_params(tag, query) if "f" in query: rate = await fetch_fiat_exchange_rate( - currency=query["f"], - provider=bleskomat.exchange_rate_provider + currency=query["f"], provider=bleskomat.exchange_rate_provider ) # Convert fee (%) to decimal: fee = float(bleskomat.fee) / 100 @@ -88,13 +87,7 @@ async def api_bleskomat_lnurl(): 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 - ) + 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 diff --git a/lnbits/extensions/bleskomat/models.py b/lnbits/extensions/bleskomat/models.py index 8a671a716..8ea973385 100644 --- a/lnbits/extensions/bleskomat/models.py +++ b/lnbits/extensions/bleskomat/models.py @@ -39,7 +39,7 @@ class BleskomatLnurl(NamedTuple): def get_info_response_object(self, secret: str) -> Dict[str, str]: tag = self.tag params = json.loads(self.params) - response = { "tag": tag } + response = {"tag": tag} if tag == "withdrawRequest": for key in ["minWithdrawable", "maxWithdrawable", "defaultDescription"]: response[key] = params[key] @@ -54,7 +54,7 @@ class BleskomatLnurl(NamedTuple): if tag == "withdrawRequest": for field in ["pr"]: if not field in query: - raise LnurlValidationError(f"Missing required parameter: \"{field}\"") + raise LnurlValidationError(f'Missing required parameter: "{field}"') # Check the bolt11 invoice(s) provided. pr = query["pr"] if "," in pr: @@ -62,13 +62,13 @@ class BleskomatLnurl(NamedTuple): try: invoice = bolt11.decode(pr) except ValueError as e: - raise LnurlValidationError("Invalid parameter (\"pr\"): Lightning payment request expected") + 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\"") + 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\"") + raise LnurlValidationError('Amount in invoice must be less than or equal to "maxWithdrawable"') else: - raise LnurlValidationError(f"Unknown subprotocol: \"{tag}\"") + raise LnurlValidationError(f'Unknown subprotocol: "{tag}"') async def execute_action(self, query: Dict[str, str]): self.validate_action(query) @@ -105,6 +105,6 @@ class BleskomatLnurl(NamedTuple): WHERE id = ? AND remaining_uses > 0 """, - (now, self.id) + (now, self.id), ) return result.rowcount > 0 diff --git a/lnbits/extensions/bleskomat/views.py b/lnbits/extensions/bleskomat/views.py index 16e986eee..52f63499f 100644 --- a/lnbits/extensions/bleskomat/views.py +++ b/lnbits/extensions/bleskomat/views.py @@ -7,6 +7,7 @@ from . import bleskomat_ext from .exchange_rates import exchange_rate_providers_serializable, fiat_currencies from .helpers import get_callback_url + @bleskomat_ext.route("/") @validate_uuids(["usr"], required=True) @check_user_exists() @@ -14,6 +15,6 @@ async def index(): bleskomat_vars = { "callback_url": get_callback_url(), "exchange_rate_providers": exchange_rate_providers_serializable, - "fiat_currencies": fiat_currencies + "fiat_currencies": fiat_currencies, } return await render_template("bleskomat/index.html", user=g.user, bleskomat_vars=bleskomat_vars) diff --git a/lnbits/extensions/bleskomat/views_api.py b/lnbits/extensions/bleskomat/views_api.py index aed4d02ea..8256ece86 100644 --- a/lnbits/extensions/bleskomat/views_api.py +++ b/lnbits/extensions/bleskomat/views_api.py @@ -58,13 +58,13 @@ async def api_bleskomat_create_or_update(bleskomat_id=None): try: fiat_currency = g.data["fiat_currency"] exchange_rate_provider = g.data["exchange_rate_provider"] - rate = await fetch_fiat_exchange_rate( - currency=fiat_currency, - provider=exchange_rate_provider - ) + rate = await fetch_fiat_exchange_rate(currency=fiat_currency, provider=exchange_rate_provider) except Exception as e: print(e) - return jsonify({"message": f"Failed to fetch BTC/{fiat_currency} currency pair from \"{exchange_rate_provider}\""}), HTTPStatus.INTERNAL_SERVER_ERROR + return ( + jsonify({"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) diff --git a/lnbits/extensions/withdraw/crud.py b/lnbits/extensions/withdraw/crud.py index 11f7d949d..18f77db03 100644 --- a/lnbits/extensions/withdraw/crud.py +++ b/lnbits/extensions/withdraw/crud.py @@ -124,4 +124,5 @@ async def create_hash_check( async def get_hash_check(the_hash: str, lnurl_id: str) -> Optional[HashCheck]: row = await db.fetchone("SELECT * FROM hash_check WHERE id = ?", (the_hash,)) + return HashCheck.from_row(row) if row else None \ No newline at end of file From 1e9151cedcae72fd768d998230ba88dea5c87bdc Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 17 Mar 2021 19:59:00 +0000 Subject: [PATCH 43/63] ran prettier on captcha --- .../extensions/captcha/static/js/captcha.js | 131 ++++++++++-------- .../captcha/templates/captcha/display.html | 8 +- .../captcha/templates/captcha/index.html | 45 +++--- 3 files changed, 106 insertions(+), 78 deletions(-) diff --git a/lnbits/extensions/captcha/static/js/captcha.js b/lnbits/extensions/captcha/static/js/captcha.js index 6d86e865a..b23872897 100644 --- a/lnbits/extensions/captcha/static/js/captcha.js +++ b/lnbits/extensions/captcha/static/js/captcha.js @@ -1,65 +1,80 @@ var ciframeLoaded = !1, - captchaStyleAdded = !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"); - return e.src = "http://localhost:5000/captcha/" + captchaid, e + 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') + return (e.src = 'http://localhost:5000/captcha/' + captchaid), e } -document.addEventListener("DOMContentLoaded", function() { - if (captchaStyleAdded) console.log("Captcha stuff already added!"); - else { - console.log("Adding captcha stuff"), 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() - } - }) - } +document.addEventListener('DOMContentLoaded', function () { + if (captchaStyleAdded) console.log('Captcha stuff already added!') + else { + console.log('Adding captcha stuff'), (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 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); - } +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); - - +window.addEventListener('message', receiveMessage, false) diff --git a/lnbits/extensions/captcha/templates/captcha/display.html b/lnbits/extensions/captcha/templates/captcha/display.html index af40ff4a2..80e59e63d 100644 --- a/lnbits/extensions/captcha/templates/captcha/display.html +++ b/lnbits/extensions/captcha/templates/captcha/display.html @@ -46,7 +46,11 @@ Copy invoice - Cancel
@@ -58,7 +62,7 @@ Captcha accepted. You are probably human.

- diff --git a/lnbits/extensions/captcha/templates/captcha/index.html b/lnbits/extensions/captcha/templates/captcha/index.html index a83e1029a..2250bcedf 100644 --- a/lnbits/extensions/captcha/templates/captcha/index.html +++ b/lnbits/extensions/captcha/templates/captcha/index.html @@ -106,7 +106,7 @@ label="Wallet *" > -\n' - + '\n' - + '
\n' - + '\n' - + '