From e73a508011e4d441c7ad7dfb04ccf186f32399d8 Mon Sep 17 00:00:00 2001 From: Eneko Illarramendi Date: Fri, 8 May 2020 21:05:32 +0200 Subject: [PATCH] feat(paywall): improved extension - make remember cookie optional - improve database - improve type casting --- lnbits/extensions/paywall/config.json | 2 +- lnbits/extensions/paywall/crud.py | 14 ++- lnbits/extensions/paywall/migrations.py | 48 ++++++++ lnbits/extensions/paywall/models.py | 16 ++- .../paywall/templates/paywall/_api_docs.html | 115 +++++++++++++++++- .../paywall/templates/paywall/display.html | 114 ++++++++++------- .../paywall/templates/paywall/index.html | 71 ++++++++--- lnbits/extensions/paywall/views_api.py | 17 ++- 8 files changed, 319 insertions(+), 78 deletions(-) diff --git a/lnbits/extensions/paywall/config.json b/lnbits/extensions/paywall/config.json index 5db7680d5..d08ce7bad 100644 --- a/lnbits/extensions/paywall/config.json +++ b/lnbits/extensions/paywall/config.json @@ -1,6 +1,6 @@ { "name": "Paywall", "short_description": "Create paywalls for content", - "icon": "vpn_lock", + "icon": "policy", "contributors": ["eillarra"] } diff --git a/lnbits/extensions/paywall/crud.py b/lnbits/extensions/paywall/crud.py index 529913f38..532f64386 100644 --- a/lnbits/extensions/paywall/crud.py +++ b/lnbits/extensions/paywall/crud.py @@ -6,15 +6,17 @@ from lnbits.helpers import urlsafe_short_hash from .models import Paywall -def create_paywall(*, wallet_id: str, url: str, memo: str, amount: int) -> Paywall: +def create_paywall( + *, wallet_id: str, url: str, memo: str, description: Optional[str] = None, amount: int = 0, remembers: bool = True +) -> Paywall: with open_ext_db("paywall") as db: paywall_id = urlsafe_short_hash() db.execute( """ - INSERT INTO paywalls (id, wallet, secret, url, memo, amount) - VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO paywalls (id, wallet, url, memo, description, amount, remembers) + VALUES (?, ?, ?, ?, ?, ?, ?) """, - (paywall_id, wallet_id, urlsafe_short_hash(), url, memo, amount), + (paywall_id, wallet_id, url, memo, description, amount, int(remembers)), ) return get_paywall(paywall_id) @@ -24,7 +26,7 @@ def get_paywall(paywall_id: str) -> Optional[Paywall]: with open_ext_db("paywall") as db: row = db.fetchone("SELECT * FROM paywalls WHERE id = ?", (paywall_id,)) - return Paywall(**row) if row else None + return Paywall.from_row(row) if row else None def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]: @@ -35,7 +37,7 @@ def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]: q = ",".join(["?"] * len(wallet_ids)) rows = db.fetchall(f"SELECT * FROM paywalls WHERE wallet IN ({q})", (*wallet_ids,)) - return [Paywall(**row) for row in rows] + return [Paywall.from_row(row) for row in rows] def delete_paywall(paywall_id: str) -> None: diff --git a/lnbits/extensions/paywall/migrations.py b/lnbits/extensions/paywall/migrations.py index 23fca00a0..aa63d0a99 100644 --- a/lnbits/extensions/paywall/migrations.py +++ b/lnbits/extensions/paywall/migrations.py @@ -1,3 +1,5 @@ +from sqlite3 import OperationalError + from lnbits.db import open_ext_db @@ -20,6 +22,52 @@ def m001_initial(db): ) +def m002_redux(db): + """ + Creates an improved paywalls table and migrates the existing data. + """ + try: + db.execute("SELECT remembers FROM paywalls") + + except OperationalError: + db.execute("ALTER TABLE paywalls RENAME TO paywalls_old") + db.execute( + """ + CREATE TABLE IF NOT EXISTS paywalls ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + url TEXT NOT NULL, + memo TEXT NOT NULL, + description TEXT NULL, + amount INTEGER DEFAULT 0, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')), + remembers INTEGER DEFAULT 0, + extras TEXT NULL + ); + """ + ) + db.execute("CREATE INDEX IF NOT EXISTS wallet_idx ON paywalls (wallet)") + + for row in [list(row) for row in db.fetchall("SELECT * FROM paywalls_old")]: + db.execute( + """ + INSERT INTO paywalls ( + id, + wallet, + url, + memo, + amount, + time + ) + VALUES (?, ?, ?, ?, ?, ?) + """, + (row[0], row[1], row[3], row[4], row[5], row[6]), + ) + + db.execute("DROP TABLE paywalls_old") + + def migrate(): with open_ext_db("paywall") as db: m001_initial(db) + m002_redux(db) diff --git a/lnbits/extensions/paywall/models.py b/lnbits/extensions/paywall/models.py index dbfa0afdb..d7f2451df 100644 --- a/lnbits/extensions/paywall/models.py +++ b/lnbits/extensions/paywall/models.py @@ -1,11 +1,23 @@ -from typing import NamedTuple +import json + +from sqlite3 import Row +from typing import NamedTuple, Optional class Paywall(NamedTuple): id: str wallet: str - secret: str url: str memo: str + description: str amount: int time: int + remembers: bool + extras: Optional[dict] + + @classmethod + def from_row(cls, row: Row) -> "Paywall": + data = dict(row) + data["remembers"] = bool(data["remembers"]) + data["extras"] = json.loads(data["extras"]) if data["extras"] else None + return cls(**data) diff --git a/lnbits/extensions/paywall/templates/paywall/_api_docs.html b/lnbits/extensions/paywall/templates/paywall/_api_docs.html index 74b4cb37a..eda9ba182 100644 --- a/lnbits/extensions/paywall/templates/paywall/_api_docs.html +++ b/lnbits/extensions/paywall/templates/paywall/_api_docs.html @@ -6,12 +6,106 @@ > - + + GET /paywall/api/v1/paywalls +
Headers
+ {"X-Api-Key": <invoice_key>}
+
Body (application/json)
+
+ Returns 200 OK (application/json) +
+ [<paywall_object>, ...] +
Curl example
+ curl -X GET {{ request.url_root }}paywall/api/v1/paywalls -H + "X-Api-Key: {{ g.user.wallets[0].inkey }}" + +
- + + POST + /paywall/api/v1/paywalls +
Headers
+ {"X-Api-Key": <admin_key>}
+
Body (application/json)
+ {"amount": <integer>, "description": <string>, + "memo": <string>, "remembers": <boolean>, + "url": <string>} +
+ Returns 201 CREATED (application/json) +
+ {"amount": <integer>, "description": <string>, + "id": <string>, "memo": <string>, + "remembers": <boolean>, "time": <int>, + "url": <string>, "wallet": <string>} +
Curl example
+ curl -X POST {{ request.url_root }}paywall/api/v1/paywalls -d + '{"url": <string>, "memo": <string>, + "description": <string>, "amount": <integer>, + "remembers": <boolean>}' -H + "Content-type: application/json" -H "X-Api-Key: {{ + g.user.wallets[0].adminkey }}" + +
+
+
+ + + + POST + /paywall/api/v1/paywalls/<paywall_id>/invoice +
Body (application/json)
+ {"amount": <integer>} +
+ Returns 201 CREATED (application/json) +
+ {"checking_id": <string>, "payment_request": <string>} +
Curl example
+ curl -X POST {{ request.url_root }}paywall/api/v1/paywalls/<paywall_id>/invoice -d + '{"amount": <integer>}' -H + "Content-type: application/json" + +
+
+
+ + + + POST + /paywall/api/v1/paywalls/<paywall_id>/check_invoice +
Body (application/json)
+ {"checking_id": <string>} +
+ Returns 200 OK (application/json) +
+ {"paid": false}
+ {"paid": true, "url": <string>, "remembers": <boolean>} +
Curl example
+ curl -X POST {{ request.url_root }}paywall/api/v1/paywalls/<paywall_id>/check_invoice -d + '{"checking_id": <string>}' -H + "Content-type: application/json" + +
- + + DELETE + /paywall/api/v1/paywalls/<paywall_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 204 NO CONTENT
+ +
Curl example
+ curl -X DELETE {{ request.url_root + }}paywall/api/v1/paywalls/<paywall_id> -H "X-Api-Key: {{ + g.user.wallets[0].adminkey }}" + +
diff --git a/lnbits/extensions/paywall/templates/paywall/display.html b/lnbits/extensions/paywall/templates/paywall/display.html index fd37f8da3..20b236458 100644 --- a/lnbits/extensions/paywall/templates/paywall/display.html +++ b/lnbits/extensions/paywall/templates/paywall/display.html @@ -1,31 +1,48 @@ {% extends "public.html" %} {% block page %}
-
+
-
{{ paywall.memo }}
- Price: - sat - -
- - - - - -
- Copy invoice{{ paywall.memo }} + {% if paywall.description %} +

{{ paywall.description }}

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

You can access the URL behind this paywall:
{% raw %}{{ redirectUrl }}{% endraw %} @@ -39,13 +56,6 @@

-
- - -
LNbits paywall
-
-
-
{% endblock %} {% block scripts %} @@ -57,25 +67,46 @@ mixins: [windowMixin], data: function () { return { + userAmount: {{ paywall.amount }}, + paywallAmount: {{ paywall.amount }}, paymentReq: null, - redirectUrl: null + redirectUrl: null, + paymentDialog: { + dismissMsg: null, + checker: null + } + } + }, + computed: { + amount: function () { + return (this.paywallAmount > this.userAmount) ? this.paywallAmount : this.userAmount } }, methods: { - getInvoice: function () { + cancelPayment: function () { + this.paymentReq = null + clearInterval(this.paymentDialog.checker) + if (this.paymentDialog.dismissMsg) { + this.paymentDialog.dismissMsg() + } + }, + createInvoice: function () { var self = this axios - .get('/paywall/api/v1/paywalls/{{ paywall.id }}/invoice') + .post( + '/paywall/api/v1/paywalls/{{ paywall.id }}/invoice', + {amount: this.amount} + ) .then(function (response) { - self.paymentReq = response.data.payment_request + self.paymentReq = response.data.payment_request.toUpperCase() - dismissMsg = self.$q.notify({ + self.paymentDialog.dismissMsg = self.$q.notify({ timeout: 0, message: 'Waiting for payment...' }) - paymentChecker = setInterval(function () { + self.paymentDialog.checker = setInterval(function () { axios .post( '/paywall/api/v1/paywalls/{{ paywall.id }}/check_invoice', @@ -83,13 +114,14 @@ ) .then(function (res) { if (res.data.paid) { - clearInterval(paymentChecker) - dismissMsg() + self.cancelPayment() self.redirectUrl = res.data.url - self.$q.localStorage.set( - 'lnbits.paywall.{{ paywall.id }}', - res.data.url - ) + if (res.data.remembers) { + self.$q.localStorage.set( + 'lnbits.paywall.{{ paywall.id }}', + res.data.url + ) + } self.$q.notify({ type: 'positive', @@ -113,8 +145,6 @@ if (url) { this.redirectUrl = url - } else { - this.getInvoice() } } }) diff --git a/lnbits/extensions/paywall/templates/paywall/index.html b/lnbits/extensions/paywall/templates/paywall/index.html index 552a9e10e..69f583d39 100644 --- a/lnbits/extensions/paywall/templates/paywall/index.html +++ b/lnbits/extensions/paywall/templates/paywall/index.html @@ -114,7 +114,21 @@ dense v-model.trim="formDialog.data.url" type="url" - label="Target URL *" + label="Redirect URL *" + > + + - + + + + + + + Remember payments + A succesful payment will be registered in the browser's storage, so the user doesn't need to pay again to access the URL. + + +
Create paywall @@ -168,13 +194,6 @@ columns: [ {name: 'id', align: 'left', label: 'ID', field: 'id'}, {name: 'memo', align: 'left', label: 'Memo', field: 'memo'}, - { - name: 'date', - align: 'left', - label: 'Date', - field: 'date', - sortable: true - }, { name: 'amount', align: 'right', @@ -184,6 +203,14 @@ sort: function (a, b, rowA, rowB) { return rowA.amount - rowB.amount } + }, + {name: 'remembers', align: 'left', label: 'Remember', field: 'remembers'}, + { + name: 'date', + align: 'left', + label: 'Date', + field: 'date', + sortable: true } ], pagination: { @@ -192,7 +219,9 @@ }, formDialog: { show: false, - data: {} + data: { + remembers: false + } } } }, @@ -216,7 +245,9 @@ var data = { url: this.formDialog.data.url, memo: this.formDialog.data.memo, - amount: this.formDialog.data.amount + amount: this.formDialog.data.amount, + description: this.formDialog.data.description, + remembers: this.formDialog.data.remembers } var self = this @@ -231,7 +262,9 @@ .then(function (response) { self.paywalls.push(mapPaywall(response.data)) self.formDialog.show = false - self.formDialog.data = {} + self.formDialog.data = { + remembers: false + } }) .catch(function (error) { LNbits.utils.notifyApiError(error) diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py index decad300c..012d355d5 100644 --- a/lnbits/extensions/paywall/views_api.py +++ b/lnbits/extensions/paywall/views_api.py @@ -27,7 +27,9 @@ def api_paywalls(): 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}, } ) def api_paywall_create(): @@ -52,18 +54,23 @@ def api_paywall_delete(paywall_id): return "", HTTPStatus.NO_CONTENT -@paywall_ext.route("/api/v1/paywalls//invoice", methods=["GET"]) -def api_paywall_get_invoice(paywall_id): +@paywall_ext.route("/api/v1/paywalls//invoice", methods=["POST"]) +@api_validate_post_request(schema={"amount": {"type": "integer", "min": 1, "required": True}}) +def api_paywall_create_invoice(paywall_id): paywall = get_paywall(paywall_id) + if g.data["amount"] < paywall.amount: + return jsonify({"message": f"Minimum amount is {paywall.amount} sat."}), HTTPStatus.BAD_REQUEST + try: + amount = g.data["amount"] if g.data["amount"] > paywall.amount else paywall.amount checking_id, payment_request = create_invoice( - wallet_id=paywall.wallet, amount=paywall.amount, memo=f"#paywall {paywall.memo}" + wallet_id=paywall.wallet, amount=amount, memo=f"#paywall {paywall.memo}" ) except Exception as e: return jsonify({"message": str(e)}), HTTPStatus.INTERNAL_SERVER_ERROR - return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.OK + return jsonify({"checking_id": checking_id, "payment_request": payment_request}), HTTPStatus.CREATED @paywall_ext.route("/api/v1/paywalls//check_invoice", methods=["POST"]) @@ -84,6 +91,6 @@ def api_paywal_check_invoice(paywall_id): payment = wallet.get_payment(g.data["checking_id"]) payment.set_pending(False) - return jsonify({"paid": True, "url": paywall.url}), HTTPStatus.OK + return jsonify({"paid": True, "url": paywall.url, "remembers": paywall.remembers}), HTTPStatus.OK return jsonify({"paid": False}), HTTPStatus.OK