diff --git a/lnbits/extensions/paywall/README.md b/lnbits/extensions/paywall/README.md new file mode 100644 index 000000000..f3c20fb89 --- /dev/null +++ b/lnbits/extensions/paywall/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 "Grpc-Metadata-macaroon: YOUR_WALLET-ADMIN/INVOICE-KEY" diff --git a/lnbits/extensions/paywall/__init__.py b/lnbits/extensions/paywall/__init__.py new file mode 100644 index 000000000..86e90f979 --- /dev/null +++ b/lnbits/extensions/paywall/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + + +paywall_ext = Blueprint("paywall", __name__, static_folder="static", template_folder="templates") + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/extensions/paywall/config.json b/lnbits/extensions/paywall/config.json new file mode 100644 index 000000000..8a763eafd --- /dev/null +++ b/lnbits/extensions/paywall/config.json @@ -0,0 +1,6 @@ +{ + "name": "Paywall", + "short_description": "BLah blah blah.", + "icon": "vpn_lock", + "contributors": ["eillarra"] +} diff --git a/lnbits/extensions/paywall/crud.py b/lnbits/extensions/paywall/crud.py new file mode 100644 index 000000000..55a874ff9 --- /dev/null +++ b/lnbits/extensions/paywall/crud.py @@ -0,0 +1,44 @@ +from base64 import urlsafe_b64encode +from uuid import uuid4 +from typing import List, Optional, Union + +from lnbits.db import open_ext_db + +from .models import Paywall + + +def create_paywall(*, wallet_id: str, url: str, memo: str, amount: int) -> Paywall: + with open_ext_db("paywall") as db: + paywall_id = urlsafe_b64encode(uuid4().bytes_le).decode('utf-8') + db.execute( + """ + INSERT INTO paywalls (id, wallet, url, memo, amount) + VALUES (?, ?, ?, ?, ?) + """, + (paywall_id, wallet_id, url, memo, amount), + ) + + return get_paywall(paywall_id) + + +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 + + +def get_paywalls(wallet_ids: Union[str, List[str]]) -> List[Paywall]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + with open_ext_db("paywall") as db: + 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] + + +def delete_paywall(paywall_id: str) -> None: + with open_ext_db("paywall") as db: + db.execute("DELETE FROM paywalls WHERE id = ?", (paywall_id,)) diff --git a/lnbits/extensions/paywall/models.py b/lnbits/extensions/paywall/models.py new file mode 100644 index 000000000..4f12d7e6b --- /dev/null +++ b/lnbits/extensions/paywall/models.py @@ -0,0 +1,10 @@ +from typing import NamedTuple + + +class Paywall(NamedTuple): + id: str + wallet: str + url: str + memo: str + amount: int + time: int diff --git a/lnbits/extensions/paywall/schema.sql b/lnbits/extensions/paywall/schema.sql new file mode 100644 index 000000000..2f6ffb8fb --- /dev/null +++ b/lnbits/extensions/paywall/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS paywalls ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + url TEXT NOT NULL, + memo TEXT NOT NULL, + amount INTEGER NOT NULL, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) +); diff --git a/lnbits/extensions/paywall/templates/paywall/_api_docs.html b/lnbits/extensions/paywall/templates/paywall/_api_docs.html new file mode 100644 index 000000000..8e25509a7 --- /dev/null +++ b/lnbits/extensions/paywall/templates/paywall/_api_docs.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lnbits/extensions/paywall/templates/paywall/index.html b/lnbits/extensions/paywall/templates/paywall/index.html new file mode 100644 index 000000000..8b0af6ea9 --- /dev/null +++ b/lnbits/extensions/paywall/templates/paywall/index.html @@ -0,0 +1,222 @@ +{% extends "base.html" %} + +{% from "macros.jinja" import window_vars with context %} + + +{% block page %} +
+
+ + + New Paywall + + + + + +
+
+
Paywalls
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ +
+ + +
LNbits paywall extension
+
+ + + + {% include "paywall/_api_docs.html" %} + + +
+
+ + + + + + + + + + Create paywall + Cancel + + + +
+{% endblock %} + +{% block scripts %} + {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/paywall/templates/paywall/wall.html b/lnbits/extensions/paywall/templates/paywall/wall.html new file mode 100644 index 000000000..423f41a7c --- /dev/null +++ b/lnbits/extensions/paywall/templates/paywall/wall.html @@ -0,0 +1 @@ +{{ paywall.url }} diff --git a/lnbits/extensions/paywall/views.py b/lnbits/extensions/paywall/views.py new file mode 100644 index 000000000..4e8702df1 --- /dev/null +++ b/lnbits/extensions/paywall/views.py @@ -0,0 +1,21 @@ +from flask import g, abort, render_template + +from lnbits.decorators import check_user_exists, validate_uuids +from lnbits.extensions.paywall import paywall_ext +from lnbits.helpers import Status + +from .crud import get_paywall + + +@paywall_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +def index(): + return render_template("paywall/index.html", user=g.user) + + +@paywall_ext.route("/") +def wall(paywall_id): + paywall = get_paywall(paywall_id) or abort(Status.NOT_FOUND, "Paywall does not exist.") + + return render_template("paywall/wall.html", paywall=paywall) diff --git a/lnbits/extensions/paywall/views_api.py b/lnbits/extensions/paywall/views_api.py new file mode 100644 index 000000000..11afd2114 --- /dev/null +++ b/lnbits/extensions/paywall/views_api.py @@ -0,0 +1,51 @@ +from flask import g, jsonify, request + +from lnbits.core.crud import get_user +from lnbits.decorators import api_check_wallet_macaroon, api_validate_post_request +from lnbits.helpers import Status + +from lnbits.extensions.paywall import paywall_ext +from .crud import create_paywall, get_paywall, get_paywalls, delete_paywall + + +@paywall_ext.route("/api/v1/paywalls", methods=["GET"]) +@api_check_wallet_macaroon(key_type="invoice") +def api_paywalls(): + wallet_ids = [g.wallet.id] + + if "all_wallets" in request.args: + wallet_ids = get_user(g.wallet.user).wallet_ids + + return jsonify([paywall._asdict() for paywall in get_paywalls(wallet_ids)]), Status.OK + + +@paywall_ext.route("/api/v1/paywalls", methods=["POST"]) +@api_check_wallet_macaroon(key_type="invoice") +@api_validate_post_request(required_params=["url", "memo", "amount"]) +def api_paywall_create(): + if not isinstance(g.data["amount"], int) or g.data["amount"] < 0: + return jsonify({"message": "`amount` needs to be a positive integer, or zero."}), Status.BAD_REQUEST + + for var in ["url", "memo"]: + if not isinstance(g.data[var], str) or not g.data[var].strip(): + return jsonify({"message": f"`{var}` needs to be a valid string."}), Status.BAD_REQUEST + + paywall = create_paywall(wallet_id=g.wallet.id, url=g.data["url"], memo=g.data["memo"], amount=g.data["amount"]) + + return jsonify(paywall._asdict()), Status.CREATED + + +@paywall_ext.route("/api/v1/paywalls/", methods=["DELETE"]) +@api_check_wallet_macaroon(key_type="invoice") +def api_paywall_delete(paywall_id): + paywall = get_paywall(paywall_id) + + if not paywall: + return jsonify({"message": "Paywall does not exist."}), Status.NOT_FOUND + + if paywall.wallet != g.wallet.id: + return jsonify({"message": "Not your paywall."}), Status.FORBIDDEN + + delete_paywall(paywall_id) + + return '', Status.NO_CONTENT