From ae59c74c24904998dc930eacbd79571f78568abc Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 1 Dec 2020 22:35:06 +0000 Subject: [PATCH 01/46] Added if webhook to stop 500 in webhook absence --- lnbits/extensions/lnticket/crud.py | 31 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/lnbits/extensions/lnticket/crud.py b/lnbits/extensions/lnticket/crud.py index 5e987d216..b38d352f2 100644 --- a/lnbits/extensions/lnticket/crud.py +++ b/lnbits/extensions/lnticket/crud.py @@ -54,21 +54,22 @@ async def set_ticket_paid(payment_hash: str) -> Tickets: ) ticket = await get_ticket(payment_hash) - async with httpx.AsyncClient() as client: - try: - r = await client.post( - formdata.webhook, - json={ - "form": ticket.form, - "name": ticket.name, - "email": ticket.email, - "content": ticket.ltext - }, - timeout=40, - ) - except AssertionError: - webhook = None - return ticket + if formdata.webhook: + async with httpx.AsyncClient() as client: + try: + r = await client.post( + formdata.webhook, + json={ + "form": ticket.form, + "name": ticket.name, + "email": ticket.email, + "content": ticket.ltext + }, + timeout=40, + ) + except AssertionError: + webhook = None + return ticket ticket = await get_ticket(payment_hash) return From 31b1c0d1d532959664565037b3a2287b68954384 Mon Sep 17 00:00:00 2001 From: benarc Date: Tue, 1 Dec 2020 22:53:52 +0000 Subject: [PATCH 02/46] Fixed tpos links not being fetched --- lnbits/extensions/tpos/views_api.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lnbits/extensions/tpos/views_api.py b/lnbits/extensions/tpos/views_api.py index 717beaf3e..22980fcef 100644 --- a/lnbits/extensions/tpos/views_api.py +++ b/lnbits/extensions/tpos/views_api.py @@ -13,9 +13,8 @@ from .crud import create_tpos, get_tpos, get_tposs, delete_tpos @api_check_wallet_key("invoice") async def api_tposs(): wallet_ids = [g.wallet.id] - if "all_wallets" in request.args: - wallet_ids = await get_user(g.wallet.user).wallet_ids + wallet_ids = (await get_user(g.wallet.user)).wallet_ids return jsonify([tpos._asdict() for tpos in await get_tposs(wallet_ids)]), HTTPStatus.OK From 503c981bc970f4ab894f50198dbd6833cae8f6e0 Mon Sep 17 00:00:00 2001 From: benarc Date: Wed, 2 Dec 2020 10:45:12 +0000 Subject: [PATCH 03/46] Updated example extension --- lnbits/extensions/example/migrations.py | 11 +++++++++++ lnbits/extensions/example/models.py | 11 +++++++++++ lnbits/extensions/example/views_api.py | 4 ++-- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 lnbits/extensions/example/models.py diff --git a/lnbits/extensions/example/migrations.py b/lnbits/extensions/example/migrations.py index e69de29bb..04336b554 100644 --- a/lnbits/extensions/example/migrations.py +++ b/lnbits/extensions/example/migrations.py @@ -0,0 +1,11 @@ +#async def m001_initial(db): + +# await db.execute( +# """ +# CREATE TABLE IF NOT EXISTS example ( +# id TEXT PRIMARY KEY, +# wallet TEXT NOT NULL, +# time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) +# ); +# """ +# ) \ No newline at end of file diff --git a/lnbits/extensions/example/models.py b/lnbits/extensions/example/models.py new file mode 100644 index 000000000..15382e8ce --- /dev/null +++ b/lnbits/extensions/example/models.py @@ -0,0 +1,11 @@ +#from sqlite3 import Row +#from typing import NamedTuple + + +#class Example(NamedTuple): +# id: str +# wallet: str +# +# @classmethod +# def from_row(cls, row: Row) -> "Example": +# return cls(**dict(row)) \ No newline at end of file diff --git a/lnbits/extensions/example/views_api.py b/lnbits/extensions/example/views_api.py index c04f8c77e..e59c1072a 100644 --- a/lnbits/extensions/example/views_api.py +++ b/lnbits/extensions/example/views_api.py @@ -21,8 +21,8 @@ async def api_example(): """Try to add descriptions for others.""" tools = [ { - "name": "Flask", - "url": "https://flask.palletsprojects.com/", + "name": "Quart", + "url": "https://pgjones.gitlab.io/quart/", "language": "Python", }, { From 462322031669191f786c91c9b71689e54d64cbb5 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Fri, 18 Dec 2020 16:21:48 -0300 Subject: [PATCH 04/46] specify webhooks from invoice creation and call them. --- lnbits/core/tasks.py | 10 ++++++++++ lnbits/core/views/api.py | 34 +++++++++++----------------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index e0e28391f..bf4150e76 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -1,4 +1,5 @@ import trio # type: ignore +import httpx from typing import List from lnbits.tasks import register_invoice_listener @@ -14,9 +15,18 @@ async def register_listeners(): async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): async for payment in invoice_paid_chan: + # send information to sse channel for send_channel in sse_listeners: try: send_channel.send_nowait(payment) except trio.WouldBlock: print("removing sse listener", send_channel) sse_listeners.remove(send_channel) + + # dispatch webhook + if payment.extra and "webhook" in payment.extra: + async with httpx.AsyncClient() as client: + try: + await client.post(payment.extra["webhook"], json=payment._asdict(), timeout=40) + except (httpx.ConnectError, httpx.RequestError): + pass diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index 4e69275d2..a293f3425 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -21,13 +21,7 @@ from ..tasks import sse_listeners @api_check_wallet_key("invoice") async def api_wallet(): return ( - jsonify( - { - "id": g.wallet.id, - "name": g.wallet.name, - "balance": g.wallet.balance_msat, - } - ), + jsonify({"id": g.wallet.id, "name": g.wallet.name, "balance": g.wallet.balance_msat,}), HTTPStatus.OK, ) @@ -51,6 +45,7 @@ async def api_payments(): "memo": {"type": "string", "empty": False, "required": True, "excludes": "description_hash"}, "description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"}, "lnurl_callback": {"type": "string", "nullable": True, "required": False}, + "extra": {"type": "dict", "nullable": True, "required": False}, } ) async def api_payments_create_invoice(): @@ -63,7 +58,11 @@ async def api_payments_create_invoice(): try: payment_hash, payment_request = await create_invoice( - wallet_id=g.wallet.id, amount=g.data["amount"], memo=memo, description_hash=description_hash + wallet_id=g.wallet.id, + amount=g.data["amount"], + memo=memo, + description_hash=description_hash, + extra=g.data["extra"], ) except Exception as exc: await db.rollback() @@ -77,11 +76,7 @@ async def api_payments_create_invoice(): if g.data.get("lnurl_callback"): async with httpx.AsyncClient() as client: try: - r = await client.get( - g.data["lnurl_callback"], - params={"pr": payment_request}, - timeout=10, - ) + r = await client.get(g.data["lnurl_callback"], params={"pr": payment_request}, timeout=10,) if r.is_error: lnurl_response = r.text else: @@ -157,9 +152,7 @@ async def api_payments_pay_lnurl(): async with httpx.AsyncClient() as client: try: r = await client.get( - g.data["callback"], - params={"amount": g.data["amount"], "comment": g.data["comment"]}, - timeout=40, + g.data["callback"], params={"amount": g.data["amount"], "comment": g.data["comment"]}, timeout=40, ) if r.is_error: return jsonify({"message": "failed to connect"}), HTTPStatus.BAD_REQUEST @@ -199,10 +192,7 @@ async def api_payments_pay_lnurl(): extra["comment"] = g.data["comment"] payment_hash = await pay_invoice( - wallet_id=g.wallet.id, - payment_request=params["pr"], - description=g.data.get("description", ""), - extra=extra, + wallet_id=g.wallet.id, payment_request=params["pr"], description=g.data.get("description", ""), extra=extra, ) except Exception as exc: await db.rollback() @@ -362,9 +352,7 @@ async def api_lnurlscan(code: str): @core_app.route("/api/v1/lnurlauth", methods=["POST"]) @api_check_wallet_key("admin") @api_validate_post_request( - schema={ - "callback": {"type": "string", "required": True}, - } + schema={"callback": {"type": "string", "required": True},} ) async def api_perform_lnurlauth(): err = await perform_lnurlauth(g.data["callback"]) From 1c922a5ddcd4a3f27d5764bc39d3a5878588c7f8 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 24 Dec 2020 09:38:35 -0300 Subject: [PATCH 05/46] finish webhooks for normal invoices with two extra columns. --- lnbits/core/crud.py | 12 +++--- lnbits/core/migrations.py | 10 +++++ lnbits/core/models.py | 10 ++--- lnbits/core/services.py | 20 +++------- lnbits/core/tasks.py | 46 +++++++++++++++++------ lnbits/core/templates/core/_api_docs.html | 5 ++- lnbits/core/views/api.py | 4 +- lnbits/static/js/base.js | 5 ++- lnbits/static/js/components.js | 22 +++++++++++ 9 files changed, 92 insertions(+), 42 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index c9c3b1076..b66ab9bf7 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -253,13 +253,14 @@ async def create_payment( preimage: Optional[str] = None, pending: bool = True, extra: Optional[Dict] = None, + webhook: Optional[str] = None, ) -> Payment: await db.execute( """ INSERT INTO apipayments (wallet, checking_id, bolt11, hash, preimage, - amount, pending, memo, fee, extra) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + amount, pending, memo, fee, extra, webhook) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( wallet_id, @@ -272,6 +273,7 @@ async def create_payment( memo, fee, json.dumps(extra) if extra and extra != {} and type(extra) is dict else None, + webhook, ), ) @@ -283,11 +285,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/core/migrations.py b/lnbits/core/migrations.py index 5ec0c0a57..d04963228 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -120,3 +120,13 @@ async def m002_add_fields_to_apipayments(db): # catching errors like this won't be necessary in anymore now that we # keep track of db versions so no migration ever runs twice. pass + + +async def m003_add_invoice_webhook(db): + """ + Special column for webhook endpoints that can be assigned + to each different invoice. + """ + + await db.execute("ALTER TABLE apipayments ADD COLUMN webhook TEXT") + await db.execute("ALTER TABLE apipayments ADD COLUMN webhook_status TEXT") diff --git a/lnbits/core/models.py b/lnbits/core/models.py index d26c0aba5..a79d73f45 100644 --- a/lnbits/core/models.py +++ b/lnbits/core/models.py @@ -40,11 +40,7 @@ class Wallet(NamedTuple): hashing_key = hashlib.sha256(self.id.encode("utf-8")).digest() linking_key = hmac.digest(hashing_key, domain.encode("utf-8"), "sha256") - return SigningKey.from_string( - linking_key, - curve=SECP256k1, - hashfunc=hashlib.sha256, - ) + return SigningKey.from_string(linking_key, curve=SECP256k1, hashfunc=hashlib.sha256,) async def get_payment(self, payment_hash: str) -> Optional["Payment"]: from .crud import get_wallet_payment @@ -84,6 +80,8 @@ class Payment(NamedTuple): payment_hash: str extra: Dict wallet_id: str + webhook: str + webhook_status: int @classmethod def from_row(cls, row: Row): @@ -99,6 +97,8 @@ class Payment(NamedTuple): memo=row["memo"], time=row["time"], wallet_id=row["wallet"], + webhook=row["webhook"], + webhook_status=row["webhook_status"], ) @property diff --git a/lnbits/core/services.py b/lnbits/core/services.py index df93d6a5a..a5f7d9964 100644 --- a/lnbits/core/services.py +++ b/lnbits/core/services.py @@ -28,6 +28,7 @@ async def create_invoice( memo: str, description_hash: Optional[bytes] = None, extra: Optional[Dict] = None, + webhook: Optional[str] = None, ) -> Tuple[str, str]: await db.begin() invoice_memo = None if description_hash else memo @@ -50,6 +51,7 @@ async def create_invoice( amount=amount_msat, memo=storeable_memo, extra=extra, + webhook=webhook, ) await db.commit() @@ -131,10 +133,7 @@ async def pay_invoice( payment: PaymentResponse = WALLET.pay_invoice(payment_request) if payment.ok and payment.checking_id: await create_payment( - checking_id=payment.checking_id, - fee=payment.fee_msat, - preimage=payment.preimage, - **payment_kwargs, + checking_id=payment.checking_id, fee=payment.fee_msat, preimage=payment.preimage, **payment_kwargs, ) await delete_payment(temp_id) else: @@ -154,8 +153,7 @@ async def redeem_lnurl_withdraw(wallet_id: str, res: LnurlWithdrawResponse, memo async with httpx.AsyncClient() as client: await client.get( - res.callback.base, - params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}}, + res.callback.base, params={**res.callback.query_params, **{"k1": res.k1, "pr": payment_request}}, ) @@ -212,11 +210,7 @@ async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]: async with httpx.AsyncClient() as client: r = await client.get( callback, - params={ - "k1": k1.hex(), - "key": key.verifying_key.to_string("compressed").hex(), - "sig": sig.hex(), - }, + params={"k1": k1.hex(), "key": key.verifying_key.to_string("compressed").hex(), "sig": sig.hex(),}, ) try: resp = json.loads(r.text) @@ -225,9 +219,7 @@ async def perform_lnurlauth(callback: str) -> Optional[LnurlErrorResponse]: return LnurlErrorResponse(reason=resp["reason"]) except (KeyError, json.decoder.JSONDecodeError): - return LnurlErrorResponse( - reason=r.text[:200] + "..." if len(r.text) > 200 else r.text, - ) + return LnurlErrorResponse(reason=r.text[:200] + "..." if len(r.text) > 200 else r.text,) async def check_invoice_status(wallet_id: str, payment_hash: str) -> PaymentStatus: diff --git a/lnbits/core/tasks.py b/lnbits/core/tasks.py index bf4150e76..8d1d5a902 100644 --- a/lnbits/core/tasks.py +++ b/lnbits/core/tasks.py @@ -3,6 +3,8 @@ import httpx from typing import List from lnbits.tasks import register_invoice_listener +from . import db +from .models import Payment sse_listeners: List[trio.MemorySendChannel] = [] @@ -16,17 +18,37 @@ async def register_listeners(): async def wait_for_paid_invoices(invoice_paid_chan: trio.MemoryReceiveChannel): async for payment in invoice_paid_chan: # send information to sse channel - for send_channel in sse_listeners: - try: - send_channel.send_nowait(payment) - except trio.WouldBlock: - print("removing sse listener", send_channel) - sse_listeners.remove(send_channel) + await dispatch_sse(payment) # dispatch webhook - if payment.extra and "webhook" in payment.extra: - async with httpx.AsyncClient() as client: - try: - await client.post(payment.extra["webhook"], json=payment._asdict(), timeout=40) - except (httpx.ConnectError, httpx.RequestError): - pass + if payment.webhook and not payment.webhook_status: + await dispatch_webhook(payment) + + +async def dispatch_sse(payment: Payment): + for send_channel in sse_listeners: + try: + send_channel.send_nowait(payment) + except trio.WouldBlock: + print("removing sse listener", send_channel) + sse_listeners.remove(send_channel) + + +async def dispatch_webhook(payment: Payment): + async with httpx.AsyncClient() as client: + data = payment._asdict() + try: + r = await client.post(payment.webhook, json=data, timeout=40,) + await mark_webhook_sent(payment, r.status_code) + except (httpx.ConnectError, httpx.RequestError): + await mark_webhook_sent(payment, -1) + + +async def mark_webhook_sent(payment: Payment, status: int) -> None: + await db.execute( + """ + UPDATE apipayments SET webhook_status = ? + WHERE hash = ? + """, + (status, payment.payment_hash), + ) diff --git a/lnbits/core/templates/core/_api_docs.html b/lnbits/core/templates/core/_api_docs.html index 05bc125b6..43a2cc9de 100644 --- a/lnbits/core/templates/core/_api_docs.html +++ b/lnbits/core/templates/core/_api_docs.html @@ -55,8 +55,9 @@
Curl example
curl -X POST {{ request.url_root }}api/v1/payments -d '{"out": false, - "amount": <int>, "memo": <string>}' -H "X-Api-Key: - {{ wallet.inkey }}" -H "Content-type: application/json"{{ wallet.inkey }}" -H + "Content-type: application/json" diff --git a/lnbits/core/views/api.py b/lnbits/core/views/api.py index a293f3425..75749de33 100644 --- a/lnbits/core/views/api.py +++ b/lnbits/core/views/api.py @@ -46,6 +46,7 @@ async def api_payments(): "description_hash": {"type": "string", "empty": False, "required": True, "excludes": "memo"}, "lnurl_callback": {"type": "string", "nullable": True, "required": False}, "extra": {"type": "dict", "nullable": True, "required": False}, + "webhook": {"type": "string", "empty": False, "required": False}, } ) async def api_payments_create_invoice(): @@ -62,7 +63,8 @@ async def api_payments_create_invoice(): amount=g.data["amount"], memo=memo, description_hash=description_hash, - extra=g.data["extra"], + extra=g.data.get("extra"), + webhook=g.data.get("webhook"), ) except Exception as exc: await db.rollback() diff --git a/lnbits/static/js/base.js b/lnbits/static/js/base.js index 94041e386..ed0583e50 100644 --- a/lnbits/static/js/base.js +++ b/lnbits/static/js/base.js @@ -140,7 +140,10 @@ window.LNbits = { 'bolt11', 'preimage', 'payment_hash', - 'extra' + 'extra', + 'wallet_id', + 'webhook', + 'webhook_status' ], data ) diff --git a/lnbits/static/js/components.js b/lnbits/static/js/components.js index 682da275a..e1faf2fec 100644 --- a/lnbits/static/js/components.js +++ b/lnbits/static/js/components.js @@ -204,6 +204,15 @@ Vue.component('lnbits-payment-details', {
Payment hash:
{{ payment.payment_hash }}
+
+
Webhook:
+
+ {{ payment.webhook }} + + {{ webhookStatusText }} + +
+
Payment proof:
{{ payment.preimage }}
@@ -243,6 +252,19 @@ Vue.component('lnbits-payment-details', { this.payment.extra.success_action ) }, + webhookStatusColor() { + return this.payment.webhook_status >= 300 || + this.payment.webhook_status < 0 + ? 'red-10' + : !this.payment.webhook_status + ? 'cyan-7' + : 'green-10' + }, + webhookStatusText() { + return this.payment.webhook_status + ? this.payment.webhook_status + : 'not sent yet' + }, hasTag() { return this.payment.extra && !!this.payment.extra.tag }, From 3c398a82760a78d05674dd9d5b0ada6b0c4828b2 Mon Sep 17 00:00:00 2001 From: Kristjan Date: Mon, 28 Dec 2020 19:51:45 +0100 Subject: [PATCH 06/46] started working on subdomains extension --- lnbits/extensions/subdomains/README.md | 65 ++++ lnbits/extensions/subdomains/__init__.py | 10 + lnbits/extensions/subdomains/config.json | 6 + lnbits/extensions/subdomains/migrations.py | 34 ++ lnbits/extensions/subdomains/models.py | 26 ++ .../templates/subdomains/index.html | 296 ++++++++++++++++++ lnbits/extensions/subdomains/views.py | 12 + lnbits/extensions/subdomains/views_api.py | 40 +++ 8 files changed, 489 insertions(+) create mode 100644 lnbits/extensions/subdomains/README.md create mode 100644 lnbits/extensions/subdomains/__init__.py create mode 100644 lnbits/extensions/subdomains/config.json create mode 100644 lnbits/extensions/subdomains/migrations.py create mode 100644 lnbits/extensions/subdomains/models.py create mode 100644 lnbits/extensions/subdomains/templates/subdomains/index.html create mode 100644 lnbits/extensions/subdomains/views.py create mode 100644 lnbits/extensions/subdomains/views_api.py diff --git a/lnbits/extensions/subdomains/README.md b/lnbits/extensions/subdomains/README.md new file mode 100644 index 000000000..ca3fce5ae --- /dev/null +++ b/lnbits/extensions/subdomains/README.md @@ -0,0 +1,65 @@ +

Subdomains Extension

+ +#TODO - fix formatting etc... +on lnbits there should be an interface with input fields: +subdomain (for example: subdomain1) +ip address (for example: 192.168.21.21) +duration (1 month / 1 year etc...) + +then when user presses SUBMIT button the ln invoice is shown that has to be paid... + +when invoice is paid, the lnbits backend send request to the cloudflare domain registration service, that creates a new A record for that subdomain + +for example, i am hosting lnbits on +lnbits.grmkris.com + +and i am selling my subdomains +subdomain1.grmkris.com +subdomain2.grmkris.com +subdomain3.grmkris.com + +there should be checks if that subdomain is already taken + +and maybe an option to blacklist certain subdomains that i don't want to sell + + +

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":"subdomains"}' -H "X-Api-Key: YOUR_WALLET-ADMIN/INVOICE-KEY" + +## cloudflare + +- Cloudflare offers programmatic subdomain registration... (create new A record) +- you can keep your existing domain's registrar, you just have to transfer dns records to the cloudflare (free service) +- more information: + - https://api.cloudflare.com/#getting-started-requests + - API endpoints needed for our project: + - https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records + - https://api.cloudflare.com/#dns-records-for-a-zone-create-dns-record + - https://api.cloudflare.com/#dns-records-for-a-zone-delete-dns-record + - https://api.cloudflare.com/#dns-records-for-a-zone-update-dns-record +- api can be used by providing authorization token OR authorization key + - check API Tokens and API Keys : https://api.cloudflare.com/#getting-started-requests + + + +example curls: +List dns records +```bash +curl --location --request GET 'https://api.cloudflare.com/client/v4/zones/bf3c1e516b35878c9f6532db2f2705ee/dns_records?type=A' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer mS3gGFC3ySLqBe2ERtRTlh7H2YiGbFp2KLDK62uu' +``` + +```bash +curl --location --request POST 'https://api.cloudflare.com/client/v4/zones/bf3c1e516b35878c9f6532db2f2705ee/dns_records' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer mS3gGFC3ySLqBe2ERtRTlh7H2YiGbFp2KLDK62uu' \ +--data-raw '{ + "type":"A", + "name":"subdomain1.grmkris.com", + "content":"31.15.150.237", + "ttl":0, + "proxied":true +}' +``` \ No newline at end of file diff --git a/lnbits/extensions/subdomains/__init__.py b/lnbits/extensions/subdomains/__init__.py new file mode 100644 index 000000000..51a821174 --- /dev/null +++ b/lnbits/extensions/subdomains/__init__.py @@ -0,0 +1,10 @@ +from quart import Blueprint +from lnbits.db import Database + +db = Database("ext_subdomains") + +subdomains_ext: Blueprint = Blueprint("subdomains", __name__, static_folder="static", template_folder="templates") + + +from .views_api import * # noqa +from .views import * # noqa diff --git a/lnbits/extensions/subdomains/config.json b/lnbits/extensions/subdomains/config.json new file mode 100644 index 000000000..4a34be565 --- /dev/null +++ b/lnbits/extensions/subdomains/config.json @@ -0,0 +1,6 @@ +{ + "name": "Subdomains", + "short_description": "Sell subdomains of your domain", + "icon": "domain", + "contributors": ["grmkris"] +} diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py new file mode 100644 index 000000000..00010f60f --- /dev/null +++ b/lnbits/extensions/subdomains/migrations.py @@ -0,0 +1,34 @@ +async def m001_initial(db): + + await db.execute( + """ + CREATE TABLE IF NOT EXISTS domain ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + domain_name TEXT NOT NULL, + webhook TEXT, + cf_token TEXT NOT NULL, + cf_zone_id TEXT NOT NULL, + description TEXT NOT NULL, + cost INTEGER NOT NULL, + amountmade INTEGER NOT NULL, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + ); + """ + ) + + await db.execute( + """ + CREATE TABLE IF NOT EXISTS subdomain ( + id TEXT PRIMARY KEY, + domain_name TEXT NOT NULL, + email TEXT NOT NULL, + subdomain TEXT NOT NULL, + ip TEXT NOT NULL, + wallet TEXT NOT NULL, + sats INTEGER NOT NULL, + paid BOOLEAN NOT NULL, + time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) + ); + """ + ) \ No newline at end of file diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py new file mode 100644 index 000000000..9b330b858 --- /dev/null +++ b/lnbits/extensions/subdomains/models.py @@ -0,0 +1,26 @@ +from typing import NamedTuple + + +class Domains(NamedTuple): + id: str + wallet: str + domainName: str + cfToken: str + cfZoneId: str + webhook: str + description: str + cost: int + amountmade: int + time: int + + +class Subdomains(NamedTuple): + id: str + domainName: str + email: str + subdomain: str + ip: str + wallet: str + sats: int + paid: bool + time: int diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html new file mode 100644 index 000000000..cc6a37362 --- /dev/null +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -0,0 +1,296 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} + +
+
+ + + New Domain + + + + + +
+
+
Domains
+
+
+ Export to CSV +
+
+ + {% raw %} + + + {% endraw %} + +
+
+
+ + + + + + + + + + + + + + +
+ Update Form + Create Domain + Cancel +
+
+
+
+ +
+ +{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} \ No newline at end of file diff --git a/lnbits/extensions/subdomains/views.py b/lnbits/extensions/subdomains/views.py new file mode 100644 index 000000000..b75c4906b --- /dev/null +++ b/lnbits/extensions/subdomains/views.py @@ -0,0 +1,12 @@ +from quart import g, render_template + +from lnbits.decorators import check_user_exists, validate_uuids + +from . import subdomains_ext + + +@subdomains_ext.route("/") +@validate_uuids(["usr"], required=True) +@check_user_exists() +async def index(): + return await render_template("subdomains/index.html", user=g.user) diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py new file mode 100644 index 000000000..bfcac16c7 --- /dev/null +++ b/lnbits/extensions/subdomains/views_api.py @@ -0,0 +1,40 @@ +# views_api.py is for you API endpoints that could be hit by another service + +# add your dependencies here + +# import json +# import httpx +# (use httpx just like requests, except instead of response.ok there's only the +# response.is_error that is its inverse) + +from quart import jsonify +from http import HTTPStatus + +from . import subdomains_ext + + +# add your endpoints here + + +@subdomains_ext.route("/api/v1/tools", methods=["GET"]) +async def api_subdomains(): + """Try to add descriptions for others.""" + tools = [ + { + "name": "Quart", + "url": "https://pgjones.gitlab.io/quart/", + "language": "Python", + }, + { + "name": "Vue.js", + "url": "https://vuejs.org/", + "language": "JavaScript", + }, + { + "name": "Quasar Framework", + "url": "https://quasar.dev/", + "language": "JavaScript", + }, + ] + + return jsonify(tools), HTTPStatus.OK From 307a919d179a0b456afbcc0db9d0793eab6dfa76 Mon Sep 17 00:00:00 2001 From: Kristjan Date: Mon, 28 Dec 2020 22:32:04 +0100 Subject: [PATCH 07/46] added CRUD operations --- lnbits/extensions/subdomains/crud.py | 140 ++++++++++++++++++ lnbits/extensions/subdomains/migrations.py | 4 +- lnbits/extensions/subdomains/models.py | 10 +- .../templates/subdomains/index.html | 8 +- lnbits/extensions/subdomains/views_api.py | 1 + 5 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 lnbits/extensions/subdomains/crud.py diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py new file mode 100644 index 000000000..217d6f025 --- /dev/null +++ b/lnbits/extensions/subdomains/crud.py @@ -0,0 +1,140 @@ +from typing import List, Optional, Union + +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import Domains, Subdomains +import httpx + +from lnbits.extensions import subdomains + +async def create_subdomain( + payment_hash: str, + wallet: str, + domain: str, + subdomain: str, + email: str, + ip: str, + sats: int, +) -> Subdomains: + await db.execute( + """ + INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, paid) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + (payment_hash, domain, email, subdomain, ip, wallet, sats, False), + ) + + subdomain = await get_subdomain(payment_hash) + assert subdomain, "Newly created subdomain couldn't be retrieved" + return subdomain + + +async def set_subdomain_paid(payment_hash: str) -> Subdomains: + row = await db.fetchone("SELECT * FROM subdomain WHERE id = ?", (payment_hash,)) + if row[7] == False: + await db.execute( + """ + UPDATE subdomain + SET paid = true + WHERE id = ? + """, + (payment_hash,), + ) + + domaindata = await get_domain(row[1]) + assert domaindata, "Couldn't get domain from paid subdomain" + + amount = domaindata.amountmade + row[7] + await db.execute( + """ + UPDATE domain + SET amountmade = ? + WHERE id = ? + """, + (amount, row[1]), + ) + + subdomain = await get_subdomain(payment_hash) + if domaindata.webhook: + async with httpx.AsyncClient() as client: + try: + r = await client.post( + domaindata.webhook, + json={ + "domain": subdomain.domain, + "subdomain": subdomain.subdomain, + "email": subdomain.email, + "ip": subdomain.ip + }, + timeout=40, + ) + except AssertionError: + webhook = None + return subdomain + subdomain = await get_subdomain(payment_hash) + return + + +async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]: + row = await db.fetchone("SELECT * FROM subdomain WHERE id = ?", (subdomain_id,)) + return Subdomains(**row) if row else None + + +async def get_subdomains(wallet_ids: Union[str, List[str]]) -> List[Subdomains]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall(f"SELECT * FROM subdomain WHERE wallet IN ({q})", (*wallet_ids,)) + + return [Subdomains(**row) for row in rows] + + +async def delete_subdomain(subdomain_id: str) -> None: + await db.execute("DELETE FROM subdomain WHERE id = ?", (subdomain_id,)) + + +# Domains + + +async def create_domain(*, wallet: str, domain: str, cfToken: str, cfZoneId: str, webhook: Optional[str] = None, description: str, cost: int) -> Domains: + domain_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO domains (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade) + VALUES (?, ?, ?, ?, ?, ?, ?. ?, ?) + """, + (domain_id, wallet, domain, webhook, cfToken, cfZoneId, description, cost, 0), + ) + + domain = await get_domain(domain_id) + assert domain, "Newly created domain couldn't be retrieved" + return domain + + +async def update_domain(domain_id: str, **kwargs) -> Domains: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute(f"UPDATE domain SET {q} WHERE id = ?", (*kwargs.values(), domain_id)) + row = await db.fetchone("SELECT * FROM domain WHERE id = ?", (domain_id,)) + assert row, "Newly updated domain couldn't be retrieved" + return Domains(**row) + + +async def get_domain(domain_id: str) -> Optional[Domains]: + row = await db.fetchone("SELECT * FROM domain WHERE id = ?", (domain_id,)) + return Domains(**row) if row else None + + +async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domains]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall(f"SELECT * FROM domain WHERE wallet IN ({q})", (*wallet_ids,)) + + return [Domains(**row) for row in rows] + + +async def delete_domain(domain_id: str) -> None: + await db.execute("DELETE FROM domain WHERE id = ?", (domain_id,)) diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py index 00010f60f..7d4fa4d71 100644 --- a/lnbits/extensions/subdomains/migrations.py +++ b/lnbits/extensions/subdomains/migrations.py @@ -5,7 +5,7 @@ async def m001_initial(db): CREATE TABLE IF NOT EXISTS domain ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, - domain_name TEXT NOT NULL, + domain TEXT NOT NULL, webhook TEXT, cf_token TEXT NOT NULL, cf_zone_id TEXT NOT NULL, @@ -21,7 +21,7 @@ async def m001_initial(db): """ CREATE TABLE IF NOT EXISTS subdomain ( id TEXT PRIMARY KEY, - domain_name TEXT NOT NULL, + domain TEXT NOT NULL, email TEXT NOT NULL, subdomain TEXT NOT NULL, ip TEXT NOT NULL, diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py index 9b330b858..0a71a1dd8 100644 --- a/lnbits/extensions/subdomains/models.py +++ b/lnbits/extensions/subdomains/models.py @@ -4,7 +4,7 @@ from typing import NamedTuple class Domains(NamedTuple): id: str wallet: str - domainName: str + domain: str cfToken: str cfZoneId: str webhook: str @@ -16,11 +16,11 @@ class Domains(NamedTuple): class Subdomains(NamedTuple): id: str - domainName: str - email: str - subdomain: str - ip: str wallet: str + domain: str + subdomain: str + email: str + ip: str sats: int paid: bool time: int diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index cc6a37362..183fc0221 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -207,18 +207,18 @@ createDomain: function (wallet, data) { var self = this - /* + LNbits.api - .request('POST', '/lnticket/api/v1/forms', wallet.inkey, data) + .request('POST', '/subdomains/api/v1/domains', wallet.inkey, data) .then(function (response) { - self.forms.push(mapLNTicket(response.data)) + self.forms.push(mapLNDomain(response.data)) self.domainDialog.show = false self.domainDialog.data = {} }) .catch(function (error) { LNbits.utils.notifyApiError(error) }) - */ + }, updateDomainDialog: function (formId) { var link = _.findWhere(this.forms, {id: formId}) diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py index bfcac16c7..dc9263ca2 100644 --- a/lnbits/extensions/subdomains/views_api.py +++ b/lnbits/extensions/subdomains/views_api.py @@ -38,3 +38,4 @@ async def api_subdomains(): ] return jsonify(tools), HTTPStatus.OK + From 2eb44674d0bb66bdf333a021b07c91281e630d16 Mon Sep 17 00:00:00 2001 From: Kristjan Date: Mon, 28 Dec 2020 22:40:46 +0100 Subject: [PATCH 08/46] added public facing api --- lnbits/extensions/subdomains/views_api.py | 185 ++++++++++++++++++---- 1 file changed, 153 insertions(+), 32 deletions(-) diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py index dc9263ca2..7f2ffb7b4 100644 --- a/lnbits/extensions/subdomains/views_api.py +++ b/lnbits/extensions/subdomains/views_api.py @@ -1,41 +1,162 @@ -# views_api.py is for you API endpoints that could be hit by another service - -# add your dependencies here - -# import json -# import httpx -# (use httpx just like requests, except instead of response.ok there's only the -# response.is_error that is its inverse) - -from quart import jsonify +import re +from quart import g, jsonify, request from http import HTTPStatus -from . import subdomains_ext +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 lnsubdomain_ext +from .crud import ( + create_subdomain, + set_subdomain_paid, + get_subdomain, + get_subdomains, + delete_subdomain, + create_domain, + update_domain, + get_domain, + get_domains, + delete_domain, +) -# add your endpoints here +# domainS -@subdomains_ext.route("/api/v1/tools", methods=["GET"]) +@lnsubdomain_ext.route("/api/v1/domains", methods=["GET"]) +@api_check_wallet_key("invoice") +async def api_domains(): + wallet_ids = [g.wallet.id] + + if "all_wallets" in request.args: + wallet_ids = (await get_user(g.wallet.user)).wallet_ids + + return jsonify([domain._asdict() for domain in await get_domains(wallet_ids)]), HTTPStatus.OK + + +@lnsubdomain_ext.route("/api/v1/domains", methods=["POST"]) +@lnsubdomain_ext.route("/api/v1/domains/", methods=["PUT"]) +@api_check_wallet_key("invoice") +@api_validate_post_request( + schema={ + "wallet": {"type": "string", "empty": False, "required": True}, + "domain": {"type": "string", "empty": False, "required": True}, + "cfToken": {"type": "string", "empty": False, "required": True}, + "cfZoneId": {"type": "string", "empty": False, "required": True}, + "webhook": {"type": "string", "empty": False, "required": False}, + "description": {"type": "string", "min": 0, "required": True}, + "cost": {"type": "integer", "min": 0, "required": True}, + } +) +async def api_domain_create(domain_id=None): + if domain_id: + domain = await get_domain(domain_id) + + if not domain: + return jsonify({"message": "domain does not exist."}), HTTPStatus.NOT_FOUND + + if domain.wallet != g.wallet.id: + return jsonify({"message": "Not your domain."}), HTTPStatus.FORBIDDEN + + domain = await update_domain(domain_id, **g.data) + else: + domain = await create_domain(**g.data) + return jsonify(domain._asdict()), HTTPStatus.CREATED + + +@lnsubdomain_ext.route("/api/v1/domains/", methods=["DELETE"]) +@api_check_wallet_key("invoice") +async def api_domain_delete(domain_id): + domain = await get_domain(domain_id) + + if not domain: + return jsonify({"message": "domain does not exist."}), HTTPStatus.NOT_FOUND + + if domain.wallet != g.wallet.id: + return jsonify({"message": "Not your domain."}), HTTPStatus.FORBIDDEN + + await delete_domain(domain_id) + + return "", HTTPStatus.NO_CONTENT + + +#########subdomains########## + + +@lnsubdomain_ext.route("/api/v1/subdomains", methods=["GET"]) +@api_check_wallet_key("invoice") async def api_subdomains(): - """Try to add descriptions for others.""" - tools = [ - { - "name": "Quart", - "url": "https://pgjones.gitlab.io/quart/", - "language": "Python", - }, - { - "name": "Vue.js", - "url": "https://vuejs.org/", - "language": "JavaScript", - }, - { - "name": "Quasar Framework", - "url": "https://quasar.dev/", - "language": "JavaScript", - }, - ] + wallet_ids = [g.wallet.id] - return jsonify(tools), HTTPStatus.OK + if "all_wallets" in request.args: + wallet_ids = (await get_user(g.wallet.user)).wallet_ids + return jsonify([domain._asdict() for domain in await get_subdomains(wallet_ids)]), HTTPStatus.OK + + +@lnsubdomain_ext.route("/api/v1/subdomains/", methods=["POST"]) +@api_validate_post_request( + schema={ + "domain": {"type": "string", "empty": False, "required": True}, + "subdomain": {"type": "string", "empty": False, "required": True}, + "email": {"type": "string", "empty": True, "required": True}, + "ip": {"type": "string", "empty": False, "required": True}, + "sats": {"type": "integer", "min": 0, "required": True}, + } +) +async def api_subdomain_make_subdomain(domain_id): + domain = await get_domain(domain_id) + if not domain: + return jsonify({"message": "LNsubdomain does not exist."}), HTTPStatus.NOT_FOUND + + subdomain = len(re.split(r"\s+", g.data["subdomain"])) + sats = g.data["sats"] + payment_hash, payment_request = await create_invoice( + wallet_id=domain.wallet, + amount=sats, + memo=f"subdomain with {subdomain} words on {domain_id}", + extra={"tag": "lnsubdomain"}, + ) + + subdomain = await create_subdomain(payment_hash=payment_hash, wallet=domain.wallet, **g.data) + + if not subdomain: + return jsonify({"message": "LNsubdomain could not be fetched."}), HTTPStatus.NOT_FOUND + + return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK + + +@lnsubdomain_ext.route("/api/v1/subdomains/", methods=["GET"]) +async def api_subdomain_send_subdomain(payment_hash): + subdomain = await get_subdomain(payment_hash) + try: + status = await check_invoice_status(subdomain.wallet, payment_hash) + is_paid = not status.pending + except Exception: + return jsonify({"paid": False}), HTTPStatus.OK + + if is_paid: + wallet = await get_wallet(subdomain.wallet) + payment = await wallet.get_payment(payment_hash) + await payment.set_pending(False) + subdomain = await set_subdomain_paid(payment_hash=payment_hash) + return jsonify({"paid": True}), HTTPStatus.OK + + return jsonify({"paid": False}), HTTPStatus.OK + + +@lnsubdomain_ext.route("/api/v1/subdomains/", methods=["DELETE"]) +@api_check_wallet_key("invoice") +async def api_subdomain_delete(subdomain_id): + subdomain = await get_subdomain(subdomain_id) + + if not subdomain: + return jsonify({"message": "Paywall does not exist."}), HTTPStatus.NOT_FOUND + + if subdomain.wallet != g.wallet.id: + return jsonify({"message": "Not your subdomain."}), HTTPStatus.FORBIDDEN + + await delete_subdomain(subdomain_id) + + return "", HTTPStatus.NO_CONTENT From e96ec08f44e4b36299509d8f075ecdd98dbd57ee Mon Sep 17 00:00:00 2001 From: Kristjan Date: Mon, 28 Dec 2020 23:24:47 +0100 Subject: [PATCH 09/46] code refactor --- lnbits/extensions/subdomains/crud.py | 4 +- lnbits/extensions/subdomains/models.py | 4 +- .../templates/subdomains/index.html | 71 +++++++++---------- lnbits/extensions/subdomains/views_api.py | 18 ++--- 4 files changed, 48 insertions(+), 49 deletions(-) diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py index 217d6f025..ca46d2fba 100644 --- a/lnbits/extensions/subdomains/crud.py +++ b/lnbits/extensions/subdomains/crud.py @@ -102,8 +102,8 @@ async def create_domain(*, wallet: str, domain: str, cfToken: str, cfZoneId: str domain_id = urlsafe_short_hash() await db.execute( """ - INSERT INTO domains (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade) - VALUES (?, ?, ?, ?, ?, ?, ?. ?, ?) + INSERT INTO domain (id, wallet, domain, webhook, cf_token, cf_zone_id, description, cost, amountmade) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, (domain_id, wallet, domain, webhook, cfToken, cfZoneId, description, cost, 0), ) diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py index 0a71a1dd8..e248811d9 100644 --- a/lnbits/extensions/subdomains/models.py +++ b/lnbits/extensions/subdomains/models.py @@ -5,8 +5,8 @@ class Domains(NamedTuple): id: str wallet: str domain: str - cfToken: str - cfZoneId: str + cf_token: str + cf_zone_id: str webhook: str description: str cost: int diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index 183fc0221..e2d08a63e 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -61,7 +61,7 @@ - + @@ -73,7 +73,7 @@
Update Form Create Domain Cancel
@@ -104,7 +104,7 @@ domainsTable: { columns: [ {name: 'id', align: 'left', label: 'ID', field: 'id'}, - {name: 'domainName', align: 'left', label: 'Domain name', field: 'name'}, + {name: 'domain', align: 'left', label: 'Domain name', field: 'domain'}, {name: 'wallet', align: 'left', label: 'Wallet', field: 'wallet'}, {name: 'webhook', align: 'left', label: 'Webhook', field: 'webhook'}, { @@ -133,33 +133,32 @@ methods: { getSubdomains: function () { var self = this - /* + LNbits.api .request( 'GET', - '/lnticket/api/v1/tickets?all_wallets', + '/subdomains/api/v1/subdomains?all_wallets', this.g.user.wallets[0].inkey ) .then(function (response) { self.tickets = response.data.map(function (obj) { - return mapLNTicket(obj) + return mapLNSubdomain(obj) }) }) - */ }, deleteSubdomain: function (subdomainId) { var self = this var tickets = _.findWhere(this.tickets, {id: ticketId}) - /* + LNbits.utils .confirmDialog('Are you sure you want to delete this ticket') .onOk(function () { LNbits.api .request( 'DELETE', - '/lnticket/api/v1/tickets/' + ticketId, - _.findWhere(self.g.user.wallets, {id: tickets.wallet}).inkey + '/subdomain/api/v1/subdomains/' + subdomainId, + _.findWhere(self.g.user.wallets, {id: subdomains.wallet}).inkey ) .then(function (response) { self.tickets = _.reject(self.tickets, function (obj) { @@ -170,7 +169,7 @@ LNbits.utils.notifyApiError(error) }) }) - */ + }, exportSubdomainsCSV: function () { LNbits.utils.exportCSV(this.domainsTable.columns, this.tickets) @@ -178,19 +177,19 @@ getDomains: function () { var self = this - /* + LNbits.api .request( 'GET', - '/lnticket/api/v1/forms?all_wallets', + '/subdomains/api/v1/domains?all_wallets', this.g.user.wallets[0].inkey ) .then(function (response) { - self.forms = response.data.map(function (obj) { - return mapLNTicket(obj) + self.domains = response.data.map(function (obj) { + return mapLNDomain(obj) }) }) - */ + }, sendFormData: function () { var wallet = _.findWhere(this.g.user.wallets, { @@ -199,9 +198,9 @@ var data = this.domainDialog.data if (data.id) { - this.updateForm(wallet, data) + this.updateDomain(wallet, data) } else { - this.createForm(wallet, data) + this.createDomain(wallet, data) } }, @@ -211,7 +210,7 @@ LNbits.api .request('POST', '/subdomains/api/v1/domains', wallet.inkey, data) .then(function (response) { - self.forms.push(mapLNDomain(response.data)) + self.domains.push(mapLNDomain(response.data)) self.domainDialog.show = false self.domainDialog.data = {} }) @@ -221,13 +220,13 @@ }, updateDomainDialog: function (formId) { - var link = _.findWhere(this.forms, {id: formId}) + var link = _.findWhere(this.domains, {id: formId}) console.log(link.id) this.domainDialog.data.id = link.id this.domainDialog.data.wallet = link.wallet - this.domainDialog.data.domainName = link.domainName + this.domainDialog.data.domain = link.domain this.domainDialog.data.description = link.description - this.domainDialog.domainDialog.data.cfToken = link.cfToken + this.domainDialog.data.cfToken = link.cfToken this.domainDialog.cfZoneId = link.cfZoneId this.domainDialog.data.cost = link.cost this.domainDialog.show = true @@ -235,51 +234,51 @@ updateDomain: function (wallet, data) { var self = this console.log(data) - /* + LNbits.api .request( 'PUT', - '/lnticket/api/v1/forms/' + data.id, + '/subdomains/api/v1/domains/' + data.id, wallet.inkey, data ) .then(function (response) { - self.forms = _.reject(self.forms, function (obj) { + self.domains = _.reject(self.domains, function (obj) { return obj.id == data.id }) - self.forms.push(mapLNTicket(response.data)) + self.domains.push(mapLNDomain(response.data)) self.domainDialog.show = false self.domainDialog.data = {} }) .catch(function (error) { LNbits.utils.notifyApiError(error) }) - */ + }, - deleteDomain: function (formsId) { + deleteDomain: function (domainId) { var self = this - var forms = _.findWhere(this.forms, {id: formsId}) - /* + var domains = _.findWhere(this.domains, {id: domainId}) + LNbits.utils - .confirmDialog('Are you sure you want to delete this form link?') + .confirmDialog('Are you sure you want to delete this domain link?') .onOk(function () { LNbits.api .request( 'DELETE', - '/lnticket/api/v1/forms/' + formsId, - _.findWhere(self.g.user.wallets, {id: forms.wallet}).inkey + '/subdomains/api/v1/domains/' + domainId, + _.findWhere(self.g.user.wallets, {id: domains.wallet}).inkey ) .then(function (response) { - self.forms = _.reject(self.forms, function (obj) { - return obj.id == formsId + self.domains = _.reject(self.domains, function (obj) { + return obj.id == domainId }) }) .catch(function (error) { LNbits.utils.notifyApiError(error) }) }) - */ + }, exportDomainsCSV: function () { LNbits.utils.exportCSV(this.domainsTable.columns, this.domains) diff --git a/lnbits/extensions/subdomains/views_api.py b/lnbits/extensions/subdomains/views_api.py index 7f2ffb7b4..a05c32c22 100644 --- a/lnbits/extensions/subdomains/views_api.py +++ b/lnbits/extensions/subdomains/views_api.py @@ -6,7 +6,7 @@ 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 lnsubdomain_ext +from . import subdomains_ext from .crud import ( create_subdomain, set_subdomain_paid, @@ -24,7 +24,7 @@ from .crud import ( # domainS -@lnsubdomain_ext.route("/api/v1/domains", methods=["GET"]) +@subdomains_ext.route("/api/v1/domains", methods=["GET"]) @api_check_wallet_key("invoice") async def api_domains(): wallet_ids = [g.wallet.id] @@ -35,8 +35,8 @@ async def api_domains(): return jsonify([domain._asdict() for domain in await get_domains(wallet_ids)]), HTTPStatus.OK -@lnsubdomain_ext.route("/api/v1/domains", methods=["POST"]) -@lnsubdomain_ext.route("/api/v1/domains/", methods=["PUT"]) +@subdomains_ext.route("/api/v1/domains", methods=["POST"]) +@subdomains_ext.route("/api/v1/domains/", methods=["PUT"]) @api_check_wallet_key("invoice") @api_validate_post_request( schema={ @@ -65,7 +65,7 @@ async def api_domain_create(domain_id=None): return jsonify(domain._asdict()), HTTPStatus.CREATED -@lnsubdomain_ext.route("/api/v1/domains/", methods=["DELETE"]) +@subdomains_ext.route("/api/v1/domains/", methods=["DELETE"]) @api_check_wallet_key("invoice") async def api_domain_delete(domain_id): domain = await get_domain(domain_id) @@ -84,7 +84,7 @@ async def api_domain_delete(domain_id): #########subdomains########## -@lnsubdomain_ext.route("/api/v1/subdomains", methods=["GET"]) +@subdomains_ext.route("/api/v1/subdomains", methods=["GET"]) @api_check_wallet_key("invoice") async def api_subdomains(): wallet_ids = [g.wallet.id] @@ -95,7 +95,7 @@ async def api_subdomains(): return jsonify([domain._asdict() for domain in await get_subdomains(wallet_ids)]), HTTPStatus.OK -@lnsubdomain_ext.route("/api/v1/subdomains/", methods=["POST"]) +@subdomains_ext.route("/api/v1/subdomains/", methods=["POST"]) @api_validate_post_request( schema={ "domain": {"type": "string", "empty": False, "required": True}, @@ -127,7 +127,7 @@ async def api_subdomain_make_subdomain(domain_id): return jsonify({"payment_hash": payment_hash, "payment_request": payment_request}), HTTPStatus.OK -@lnsubdomain_ext.route("/api/v1/subdomains/", methods=["GET"]) +@subdomains_ext.route("/api/v1/subdomains/", methods=["GET"]) async def api_subdomain_send_subdomain(payment_hash): subdomain = await get_subdomain(payment_hash) try: @@ -146,7 +146,7 @@ async def api_subdomain_send_subdomain(payment_hash): return jsonify({"paid": False}), HTTPStatus.OK -@lnsubdomain_ext.route("/api/v1/subdomains/", methods=["DELETE"]) +@subdomains_ext.route("/api/v1/subdomains/", methods=["DELETE"]) @api_check_wallet_key("invoice") async def api_subdomain_delete(subdomain_id): subdomain = await get_subdomain(subdomain_id) From c0d73711370b8decb9d9fb27f9e81cbaf601b147 Mon Sep 17 00:00:00 2001 From: Kristjan Date: Tue, 29 Dec 2020 19:16:04 +0100 Subject: [PATCH 10/46] subdomains public window --- lnbits/extensions/subdomains/crud.py | 3 + .../templates/subdomains/display.html | 168 ++++++++++++++++++ .../templates/subdomains/index.html | 5 +- lnbits/extensions/subdomains/views.py | 19 +- 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 lnbits/extensions/subdomains/templates/subdomains/display.html diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py index ca46d2fba..4bad6768f 100644 --- a/lnbits/extensions/subdomains/crud.py +++ b/lnbits/extensions/subdomains/crud.py @@ -138,3 +138,6 @@ async def get_domains(wallet_ids: Union[str, List[str]]) -> List[Domains]: async def delete_domain(domain_id: str) -> None: await db.execute("DELETE FROM domain WHERE id = ?", (domain_id,)) + + + diff --git a/lnbits/extensions/subdomains/templates/subdomains/display.html b/lnbits/extensions/subdomains/templates/subdomains/display.html new file mode 100644 index 000000000..f57e079f9 --- /dev/null +++ b/lnbits/extensions/subdomains/templates/subdomains/display.html @@ -0,0 +1,168 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +

{{ form_domain }}

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

{% raw %}{{amountSats}}{% endraw %}

+
+ Submit + Cancel +
+
+
+
+
+ + + + + + +
+ Copy invoice + Close +
+
+
+
+ +{% endblock %} {% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index e2d08a63e..7428023b4 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -226,8 +226,9 @@ this.domainDialog.data.wallet = link.wallet this.domainDialog.data.domain = link.domain this.domainDialog.data.description = link.description - this.domainDialog.data.cfToken = link.cfToken - this.domainDialog.cfZoneId = link.cfZoneId + this.domainDialog.data.cfToken = link.cf_token + this.domainDialog.data.cfZoneId = link.cf_zone_id + this.domainDialog.data.webhook = link.webhook this.domainDialog.data.cost = link.cost this.domainDialog.show = true }, diff --git a/lnbits/extensions/subdomains/views.py b/lnbits/extensions/subdomains/views.py index b75c4906b..c90c4dbc8 100644 --- a/lnbits/extensions/subdomains/views.py +++ b/lnbits/extensions/subdomains/views.py @@ -1,12 +1,27 @@ -from quart import g, render_template +from quart import g, abort, render_template from lnbits.decorators import check_user_exists, validate_uuids +from http import HTTPStatus from . import subdomains_ext - +from .crud import get_domain @subdomains_ext.route("/") @validate_uuids(["usr"], required=True) @check_user_exists() async def index(): return await render_template("subdomains/index.html", user=g.user) + +@subdomains_ext.route("/") +async def display(domain_id): + domain = await get_domain(domain_id) + if not domain: + abort(HTTPStatus.NOT_FOUND, "Domain does not exist.") + + return await render_template( + "subdomains/display.html", + domain_id=domain.id, + domain_domain=domain.domain, + form_desc=domain.description, + form_cost=domain.cost, + ) From 6c4b5ea406ac2f8c19dcdda6d9356945f0c884aa Mon Sep 17 00:00:00 2001 From: Kristjan Date: Tue, 29 Dec 2020 20:52:54 +0100 Subject: [PATCH 11/46] working subdomains frontend, table, popup, payments --- lnbits/extensions/subdomains/crud.py | 15 +- lnbits/extensions/subdomains/migrations.py | 1 + lnbits/extensions/subdomains/models.py | 2 + .../templates/subdomains/display.html | 27 ++-- .../templates/subdomains/index.html | 140 ++++++++++++++---- lnbits/extensions/subdomains/views.py | 4 +- lnbits/extensions/subdomains/views_api.py | 6 +- 7 files changed, 143 insertions(+), 52 deletions(-) diff --git a/lnbits/extensions/subdomains/crud.py b/lnbits/extensions/subdomains/crud.py index 4bad6768f..0ecb88147 100644 --- a/lnbits/extensions/subdomains/crud.py +++ b/lnbits/extensions/subdomains/crud.py @@ -16,13 +16,14 @@ async def create_subdomain( email: str, ip: str, sats: int, + duration: int ) -> Subdomains: await db.execute( """ - INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, paid) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO subdomain (id, domain, email, subdomain, ip, wallet, sats, duration, paid) + VALUES (?, ?, ?, ?, ?, ?, ?, ?,?) """, - (payment_hash, domain, email, subdomain, ip, wallet, sats, False), + (payment_hash, domain, email, subdomain, ip, wallet, sats, duration, False), ) subdomain = await get_subdomain(payment_hash) @@ -32,7 +33,7 @@ async def create_subdomain( async def set_subdomain_paid(payment_hash: str) -> Subdomains: row = await db.fetchone("SELECT * FROM subdomain WHERE id = ?", (payment_hash,)) - if row[7] == False: + if row[8] == False: await db.execute( """ UPDATE subdomain @@ -45,7 +46,7 @@ async def set_subdomain_paid(payment_hash: str) -> Subdomains: domaindata = await get_domain(row[1]) assert domaindata, "Couldn't get domain from paid subdomain" - amount = domaindata.amountmade + row[7] + amount = domaindata.amountmade + row[8] await db.execute( """ UPDATE domain @@ -77,7 +78,7 @@ async def set_subdomain_paid(payment_hash: str) -> Subdomains: async def get_subdomain(subdomain_id: str) -> Optional[Subdomains]: - row = await db.fetchone("SELECT * FROM subdomain WHERE id = ?", (subdomain_id,)) + row = await db.fetchone("SELECT * FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.id = ?", (subdomain_id,)) return Subdomains(**row) if row else None @@ -86,7 +87,7 @@ async def get_subdomains(wallet_ids: Union[str, List[str]]) -> List[Subdomains]: wallet_ids = [wallet_ids] q = ",".join(["?"] * len(wallet_ids)) - rows = await db.fetchall(f"SELECT * FROM subdomain WHERE wallet IN ({q})", (*wallet_ids,)) + rows = await db.fetchall(f"SELECT s.*, d.domain as domain_name FROM subdomain s INNER JOIN domain d ON (s.domain = d.id) WHERE s.wallet IN ({q})", (*wallet_ids,)) return [Subdomains(**row) for row in rows] diff --git a/lnbits/extensions/subdomains/migrations.py b/lnbits/extensions/subdomains/migrations.py index 7d4fa4d71..75080280f 100644 --- a/lnbits/extensions/subdomains/migrations.py +++ b/lnbits/extensions/subdomains/migrations.py @@ -27,6 +27,7 @@ async def m001_initial(db): ip TEXT NOT NULL, wallet TEXT NOT NULL, sats INTEGER NOT NULL, + duration INTEGER NOT NULL, paid BOOLEAN NOT NULL, time TIMESTAMP NOT NULL DEFAULT (strftime('%s', 'now')) ); diff --git a/lnbits/extensions/subdomains/models.py b/lnbits/extensions/subdomains/models.py index e248811d9..a1d14070b 100644 --- a/lnbits/extensions/subdomains/models.py +++ b/lnbits/extensions/subdomains/models.py @@ -18,9 +18,11 @@ class Subdomains(NamedTuple): id: str wallet: str domain: str + domain_name: str subdomain: str email: str ip: str sats: int + duration: int paid: bool time: int diff --git a/lnbits/extensions/subdomains/templates/subdomains/display.html b/lnbits/extensions/subdomains/templates/subdomains/display.html index f57e079f9..90c91b581 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/display.html +++ b/lnbits/extensions/subdomains/templates/subdomains/display.html @@ -3,9 +3,9 @@
-

{{ form_domain }}

+

{{ domain_domain }}


-
{{ form_desc }}
+
{{ domain_desc }}

- +

{% raw %}{{amountSats}}{% endraw %}

@@ -48,7 +48,7 @@ {% endblock %} {% block scripts %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/lnbits/extensions/subdomains/templates/subdomains/index.html b/lnbits/extensions/subdomains/templates/subdomains/index.html index 8294f65f8..05168a798 100644 --- a/lnbits/extensions/subdomains/templates/subdomains/index.html +++ b/lnbits/extensions/subdomains/templates/subdomains/index.html @@ -5,7 +5,9 @@
- New Domain + New Domain @@ -16,11 +18,19 @@
Domains
- Export to CSV + Export to CSV
- + {% raw %}