From 13f01dfbe6b9153a66517cd929762ffc9a37fdd8 Mon Sep 17 00:00:00 2001 From: Arc <33088785+arcbtc@users.noreply.github.com> Date: Fri, 8 May 2020 21:03:18 +0100 Subject: [PATCH] Add files via upload --- lnbits/extensions/lnticket/README.md | 11 + lnbits/extensions/lnticket/__init__.py | 8 + .../extensions/lnticket/config.json.example | 6 + lnbits/extensions/lnticket/crud.py | 107 +++++ lnbits/extensions/lnticket/migrations.py | 36 ++ lnbits/extensions/lnticket/models.py | 23 + .../templates/lnticket/_api_docs.html | 27 ++ .../lnticket/templates/lnticket/display.html | 211 +++++++++ .../lnticket/templates/lnticket/index.html | 441 ++++++++++++++++++ lnbits/extensions/lnticket/views.py | 22 + lnbits/extensions/lnticket/views_api.py | 143 ++++++ 11 files changed, 1035 insertions(+) create mode 100644 lnbits/extensions/lnticket/README.md create mode 100644 lnbits/extensions/lnticket/__init__.py create mode 100644 lnbits/extensions/lnticket/config.json.example create mode 100644 lnbits/extensions/lnticket/crud.py create mode 100644 lnbits/extensions/lnticket/migrations.py create mode 100644 lnbits/extensions/lnticket/models.py create mode 100644 lnbits/extensions/lnticket/templates/lnticket/_api_docs.html create mode 100644 lnbits/extensions/lnticket/templates/lnticket/display.html create mode 100644 lnbits/extensions/lnticket/templates/lnticket/index.html create mode 100644 lnbits/extensions/lnticket/views.py create mode 100644 lnbits/extensions/lnticket/views_api.py diff --git a/lnbits/extensions/lnticket/README.md b/lnbits/extensions/lnticket/README.md new file mode 100644 index 0000000..2772945 --- /dev/null +++ b/lnbits/extensions/lnticket/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/lnticket/__init__.py b/lnbits/extensions/lnticket/__init__.py new file mode 100644 index 0000000..08ca29c --- /dev/null +++ b/lnbits/extensions/lnticket/__init__.py @@ -0,0 +1,8 @@ +from flask import Blueprint + + +lnticket_ext: Blueprint = Blueprint("lnticket", __name__, static_folder="static", template_folder="templates") + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/extensions/lnticket/config.json.example b/lnbits/extensions/lnticket/config.json.example new file mode 100644 index 0000000..8207de1 --- /dev/null +++ b/lnbits/extensions/lnticket/config.json.example @@ -0,0 +1,6 @@ +{ + "name": "LNTicket", + "short_description": "Pay-per-word LN ticket system", + "icon": "contact_support", + "contributors": ["benarc"] +} diff --git a/lnbits/extensions/lnticket/crud.py b/lnbits/extensions/lnticket/crud.py new file mode 100644 index 0000000..7b50bf2 --- /dev/null +++ b/lnbits/extensions/lnticket/crud.py @@ -0,0 +1,107 @@ +from typing import List, Optional, Union + +from lnbits.db import open_ext_db +from lnbits.helpers import urlsafe_short_hash + +from .models import Tickets, Forms + + + + +#######TICKETS######## + + +def create_ticket(wallet: str, form: str, name: str, email: str, ltext: str, sats: int) -> Tickets: + formdata = get_form(form) + amount = formdata.amountmade + sats + with open_ext_db("lnticket") as db: + ticket_id = urlsafe_short_hash() + db.execute( + """ + INSERT INTO tickets (id, form, email, ltext, name, wallet, sats) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + (ticket_id, form, email, ltext, name, wallet, sats), + ) + db.execute( + """ + UPDATE forms + SET amountmade = ? + WHERE id = ? + """, + (amount, form), + ) + return get_ticket(ticket_id) + + +def get_ticket(ticket_id: str) -> Optional[Tickets]: + with open_ext_db("lnticket") as db: + row = db.fetchone("SELECT * FROM tickets WHERE id = ?", (ticket_id,)) + + return Tickets(**row) if row else None + + +def get_tickets(wallet_ids: Union[str, List[str]]) -> List[Tickets]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + with open_ext_db("lnticket") as db: + q = ",".join(["?"] * len(wallet_ids)) + rows = db.fetchall(f"SELECT * FROM tickets WHERE wallet IN ({q})", (*wallet_ids,)) + + return [Tickets(**row) for row in rows] + + +def delete_ticket(ticket_id: str) -> None: + with open_ext_db("lnticket") as db: + db.execute("DELETE FROM tickets WHERE id = ?", (ticket_id,)) + + + + +########FORMS######### + + +def create_form(*, wallet: str, name: str, description: str, costpword: int) -> Forms: + with open_ext_db("lnticket") as db: + form_id = urlsafe_short_hash() + db.execute( + """ + INSERT INTO forms (id, wallet, name, description, costpword, amountmade) + VALUES (?, ?, ?, ?, ?, ?) + """, + (form_id, wallet, name, description, costpword, 0 ), + ) + + return get_form(form_id) + +def update_form(form_id: str, **kwargs) -> Forms: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + with open_ext_db("lnticket") as db: + db.execute(f"UPDATE forms SET {q} WHERE id = ?", (*kwargs.values(), form_id)) + row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,)) + + return Forms(**row) if row else None + + +def get_form(form_id: str) -> Optional[Forms]: + with open_ext_db("lnticket") as db: + row = db.fetchone("SELECT * FROM forms WHERE id = ?", (form_id,)) + + return Forms(**row) if row else None + + +def get_forms(wallet_ids: Union[str, List[str]]) -> List[Forms]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + with open_ext_db("lnticket") as db: + q = ",".join(["?"] * len(wallet_ids)) + rows = db.fetchall(f"SELECT * FROM forms WHERE wallet IN ({q})", (*wallet_ids,)) + + return [Forms(**row) for row in rows] + + +def delete_form(form_id: str) -> None: + with open_ext_db("lnticket") as db: + db.execute("DELETE FROM forms WHERE id = ?", (form_id,)) \ No newline at end of file diff --git a/lnbits/extensions/lnticket/migrations.py b/lnbits/extensions/lnticket/migrations.py new file mode 100644 index 0000000..e296b93 --- /dev/null +++ b/lnbits/extensions/lnticket/migrations.py @@ -0,0 +1,36 @@ +from lnbits.db import open_ext_db + +def m001_initial(db): + + db.execute( + """ + CREATE TABLE IF NOT EXISTS forms ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + costpword INTEGER NOT NULL, + amountmade INTEGER NOT NULL, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + ); + """ + ) + + db.execute( + """ + CREATE TABLE IF NOT EXISTS tickets ( + id TEXT PRIMARY KEY, + form TEXT NOT NULL, + email TEXT NOT NULL, + ltext TEXT NOT NULL, + name TEXT NOT NULL, + wallet TEXT NOT NULL, + sats INTEGER NOT NULL, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + ); + """ + ) + +def migrate(): + with open_ext_db("lnticket") as db: + m001_initial(db) \ No newline at end of file diff --git a/lnbits/extensions/lnticket/models.py b/lnbits/extensions/lnticket/models.py new file mode 100644 index 0000000..7da84a6 --- /dev/null +++ b/lnbits/extensions/lnticket/models.py @@ -0,0 +1,23 @@ +from typing import NamedTuple + + +class Forms(NamedTuple): + id: str + wallet: str + name: str + description: str + costpword: int + amountmade: int + time: int + + +class Tickets(NamedTuple): + id: str + form: str + email: str + ltext: str + name: str + wallet: str + sats: int + time: int + diff --git a/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html b/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html new file mode 100644 index 0000000..59bd1b3 --- /dev/null +++ b/lnbits/extensions/lnticket/templates/lnticket/_api_docs.html @@ -0,0 +1,27 @@ + + + + +
LNTickets: Get paid sats for questions
+

LNTickets allow you to charge people per word for contacting you. Applications incude, paid support ticketting, PAYG language services, such as translation, spam protection (people have to pay to contact you).
+ Created by, Ben Arc

+
+ + + +
+ + + + + diff --git a/lnbits/extensions/lnticket/templates/lnticket/display.html b/lnbits/extensions/lnticket/templates/lnticket/display.html new file mode 100644 index 0000000..a1808ea --- /dev/null +++ b/lnbits/extensions/lnticket/templates/lnticket/display.html @@ -0,0 +1,211 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +

{{ form_name }}

+
+
{{ form_desc }}
+
+ + + + +

{% raw %}{{amountWords}}{% endraw %}

+
+ Submit + Cancel +
+
+
+
+
+ + + + + + +
+ Copy invoice + Close +
+
+
+
+ +{% endblock %} {% block scripts %} + + +{% endblock %} diff --git a/lnbits/extensions/lnticket/templates/lnticket/index.html b/lnbits/extensions/lnticket/templates/lnticket/index.html new file mode 100644 index 0000000..bc3cc50 --- /dev/null +++ b/lnbits/extensions/lnticket/templates/lnticket/index.html @@ -0,0 +1,441 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + New Form + + + + + +
+
+
Forms
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+ + + +
+
+
Tickets
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+
+ + +
LNbits LNTicket extension
+
+ + + + {% include "lnticket/_api_docs.html" %} + + +
+
+ + + + + + + + + +
+ Update Form + + Create Form + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/lnbits/extensions/lnticket/views.py b/lnbits/extensions/lnticket/views.py new file mode 100644 index 0000000..5b5d998 --- /dev/null +++ b/lnbits/extensions/lnticket/views.py @@ -0,0 +1,22 @@ +from flask import g, abort, render_template + +from lnbits.decorators import check_user_exists, validate_uuids +from http import HTTPStatus + +from lnbits.extensions.lnticket import lnticket_ext +from .crud import get_form + + +@lnticket_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +def index(): + return render_template("lnticket/index.html", user=g.user) + + +@lnticket_ext.route("/") +def display(form_id): + form = get_form(form_id) or abort(HTTPStatus.NOT_FOUND, "LNTicket does not exist.") + print(form.id) + + return render_template("lnticket/display.html", form_id=form.id, form_name=form.name, form_desc=form.description, form_costpword=form.costpword) diff --git a/lnbits/extensions/lnticket/views_api.py b/lnbits/extensions/lnticket/views_api.py new file mode 100644 index 0000000..da0d4d7 --- /dev/null +++ b/lnbits/extensions/lnticket/views_api.py @@ -0,0 +1,143 @@ +from flask import g, jsonify, request +from http import HTTPStatus + +from lnbits.core.crud import get_user, get_wallet +from lnbits.core.services import create_invoice +from lnbits.decorators import api_check_wallet_key, api_validate_post_request +from lnbits.settings import WALLET + +from lnbits.extensions.lnticket import lnticket_ext +from .crud import create_ticket, get_ticket, get_tickets, delete_ticket, create_form, update_form, get_form, get_forms, delete_form + + +#########FORMS########## + +@lnticket_ext.route("/api/v1/forms", methods=["GET"]) +@api_check_wallet_key("invoice") +def api_forms(): + wallet_ids = [g.wallet.id] + + if "all_wallets" in request.args: + wallet_ids = get_user(g.wallet.user).wallet_ids + + return jsonify([form._asdict() for form in get_forms(wallet_ids)]), HTTPStatus.OK + + +@lnticket_ext.route("/api/v1/forms", methods=["POST"]) +@lnticket_ext.route("/api/v1/forms/", methods=["PUT"]) +@api_check_wallet_key("invoice") +@api_validate_post_request( + schema={ + "wallet": {"type": "string", "empty": False, "required": True}, + "name": {"type": "string", "empty": False, "required": True}, + "description": {"type": "string", "min": 0, "required": True}, + "costpword": {"type": "integer", "min": 0, "required": True} + } +) +def api_form_create(form_id=None): + if form_id: + form = get_form(form_id) + print(g.data) + + if not form: + return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND + + if form.wallet != g.wallet.id: + return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN + + form = update_form(form_id, **g.data) + else: + form = create_form(**g.data) + return jsonify(form._asdict()), HTTPStatus.CREATED + + +@lnticket_ext.route("/api/v1/forms/", methods=["DELETE"]) +@api_check_wallet_key("invoice") +def api_form_delete(form_id): + form = get_form(form_id) + + if not form: + return jsonify({"message": "Form does not exist."}), HTTPStatus.NOT_FOUND + + if form.wallet != g.wallet.id: + return jsonify({"message": "Not your form."}), HTTPStatus.FORBIDDEN + + delete_form(form_id) + + return "", HTTPStatus.NO_CONTENT + + + + +#########tickets########## + +@lnticket_ext.route("/api/v1/tickets", methods=["GET"]) +@api_check_wallet_key("invoice") +def api_tickets(): + wallet_ids = [g.wallet.id] + + if "all_wallets" in request.args: + wallet_ids = get_user(g.wallet.user).wallet_ids + + return jsonify([form._asdict() for form in get_tickets(wallet_ids)]), HTTPStatus.OK + + +@lnticket_ext.route("/api/v1/tickets//", methods=["GET"]) +def api_ticket_create(form_id, sats): + form = get_form(form_id) + + try: + checking_id, payment_request = create_invoice( + wallet_id=form.wallet, amount=sats, memo=f"#lnticket {form_id}" + ) + 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 + + + +@lnticket_ext.route("/api/v1/tickets/", methods=["POST"]) +@api_validate_post_request( + schema={ + "form": {"type": "string", "empty": False, "required": True}, + "name": {"type": "string", "empty": False, "required": True}, + "email": {"type": "string", "empty": False, "required": True}, + "ltext": {"type": "string", "empty": False, "required": True}, + "sats": {"type": "integer", "min": 0, "required": True} + }) +def api_ticket_send_ticket(checking_id): + + form = get_form(g.data['form']) + if not form: + return jsonify({"message": "LNTicket does not exist."}), HTTPStatus.NOT_FOUND + try: + is_paid = not WALLET.get_invoice_status(checking_id).pending + except Exception: + return jsonify({"message": "Not paid."}), HTTPStatus.NOT_FOUND + + if is_paid: + wallet = get_wallet(form.wallet) + payment = wallet.get_payment(checking_id) + payment.set_pending(False) + create_ticket(wallet=form.wallet, **g.data) + + return jsonify({"paid": True}), HTTPStatus.OK + + return jsonify({"paid": False}), HTTPStatus.OK + + +@lnticket_ext.route("/api/v1/tickets/", methods=["DELETE"]) +@api_check_wallet_key("invoice") +def api_ticket_delete(ticket_id): + ticket = get_ticket(ticket_id) + + if not ticket: + return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND + + if ticket.wallet != g.wallet.id: + return jsonify({"message": "Not your ticket."}), HTTPStatus.FORBIDDEN + + delete_ticket(ticket_id) + + return "", HTTPStatus.NO_CONTENT \ No newline at end of file