From 61cf79de691221ed631c58f77c25c76f522e9c85 Mon Sep 17 00:00:00 2001 From: benarc Date: Mon, 7 Feb 2022 13:22:43 +0000 Subject: [PATCH] Added nostr ext --- lnbits/extensions/nostr/README.md | 3 + lnbits/extensions/nostr/__init__.py | 17 + lnbits/extensions/nostr/config.json | 6 + lnbits/extensions/nostr/crud.py | 134 +++++ lnbits/extensions/nostr/migrations.py | 47 ++ lnbits/extensions/nostr/models.py | 34 ++ .../templates/lnurldevice/_api_docs.html | 169 ++++++ .../nostr/templates/lnurldevice/error.html | 34 ++ .../nostr/templates/lnurldevice/index.html | 534 ++++++++++++++++++ .../nostr/templates/lnurldevice/paid.html | 27 + lnbits/extensions/nostr/views.py | 24 + lnbits/extensions/nostr/views_api.py | 48 ++ 12 files changed, 1077 insertions(+) create mode 100644 lnbits/extensions/nostr/README.md create mode 100644 lnbits/extensions/nostr/__init__.py create mode 100644 lnbits/extensions/nostr/config.json create mode 100644 lnbits/extensions/nostr/crud.py create mode 100644 lnbits/extensions/nostr/migrations.py create mode 100644 lnbits/extensions/nostr/models.py create mode 100644 lnbits/extensions/nostr/templates/lnurldevice/_api_docs.html create mode 100644 lnbits/extensions/nostr/templates/lnurldevice/error.html create mode 100644 lnbits/extensions/nostr/templates/lnurldevice/index.html create mode 100644 lnbits/extensions/nostr/templates/lnurldevice/paid.html create mode 100644 lnbits/extensions/nostr/views.py create mode 100644 lnbits/extensions/nostr/views_api.py diff --git a/lnbits/extensions/nostr/README.md b/lnbits/extensions/nostr/README.md new file mode 100644 index 000000000..596cce9de --- /dev/null +++ b/lnbits/extensions/nostr/README.md @@ -0,0 +1,3 @@ +# Nostr + +Opens a Nostr daemon diff --git a/lnbits/extensions/nostr/__init__.py b/lnbits/extensions/nostr/__init__.py new file mode 100644 index 000000000..775960e3d --- /dev/null +++ b/lnbits/extensions/nostr/__init__.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter + +from lnbits.db import Database +from lnbits.helpers import template_renderer + +db = Database("ext_nostr") + +nostr_ext: APIRouter = APIRouter(prefix="/nostr", tags=["nostr"]) + + +def nostr_renderer(): + return template_renderer(["lnbits/extensions/nostr/templates"]) + + +from .lnurl import * # noqa +from .views import * # noqa +from .views_api import * # noqa diff --git a/lnbits/extensions/nostr/config.json b/lnbits/extensions/nostr/config.json new file mode 100644 index 000000000..a32e39a1a --- /dev/null +++ b/lnbits/extensions/nostr/config.json @@ -0,0 +1,6 @@ +{ + "name": "Nostr", + "short_description": "Daemon for Nostr", + "icon": "swap_horizontal_circle", + "contributors": ["arcbtc"] +} diff --git a/lnbits/extensions/nostr/crud.py b/lnbits/extensions/nostr/crud.py new file mode 100644 index 000000000..55e99ec67 --- /dev/null +++ b/lnbits/extensions/nostr/crud.py @@ -0,0 +1,134 @@ +from typing import List, Optional, Union + +from lnbits.helpers import urlsafe_short_hash + +from . import db +from .models import nostrKeys, nostrNotes, nostrRelays, nostrConnections + +###############KEYS################## + +async def create_nostrkeys( + data: nostrKeys +) -> nostrKeys: + nostrkey_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO nostr.keys ( + id, + pubkey, + privkey + ) + VALUES (?, ?, ?) + """, + (nostrkey_id, data.pubkey, data.privkey), + ) + return await get_nostrkeys(nostrkey_id) + +async def get_nostrkeys(nostrkey_id: str) -> nostrKeys: + row = await db.fetchone( + "SELECT * FROM nostr.keys WHERE id = ?", + (lnurldevicepayment_id,), + ) + return nostrKeys(**row) if row else None + + +###############NOTES################## + +async def create_nostrnotes( + data: nostrNotes +) -> nostrNotes: + await db.execute( + """ + INSERT INTO nostr.notes ( + id, + pubkey, + created_at, + kind, + tags, + content, + sig + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + (data.id, data.pubkey, data.created_at, data.kind, data.tags, data.content, data.sig), + ) + return await get_nostrnotes(data.id) + +async def get_nostrnotes(nostrnote_id: str) -> nostrNotes: + row = await db.fetchone( + "SELECT * FROM nostr.notes WHERE id = ?", + (nostrnote_id,), + ) + return nostrNotes(**row) if row else None + +###############RELAYS################## + +async def create_nostrrelays( + relay: str +) -> nostrRelays: + nostrrelay_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO nostr.relays ( + id, + relay + ) + VALUES (?, ?) + """, + (nostrrelay_id, relay), + ) + return await get_nostrnotes(nostrrelay_id) + +async def get_nostrrelays(nostrrelay_id: str) -> nostrRelays: + row = await db.fetchone( + "SELECT * FROM nostr.relays WHERE id = ?", + (nostrnote_id,), + ) + return nostrRelays(**row) if row else None + + +###############CONNECTIONS################## + +async def create_nostrconnections( + data: nostrNotes +) -> nostrNotes: + nostrkey_id = urlsafe_short_hash() + await db.execute( + """ + INSERT INTO nostr.notes ( + id, + pubkey, + created_at, + kind, + tags, + content, + sig + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, + (data.id, data.pubkey, data.created_at, data.kind, data.tags, data.content, data.sig), + ) + return await get_nostrnotes(data.id) + +async def get_nostrnotes(nostrnote_id: str) -> nostrNotes: + row = await db.fetchone( + "SELECT * FROM nostr.notes WHERE id = ?", + (nostrnote_id,), + ) + return nostrNotes(**row) if row else None + + + +async def update_lnurldevicepayment( + lnurldevicepayment_id: str, **kwargs +) -> Optional[lnurldevicepayment]: + q = ", ".join([f"{field[0]} = ?" for field in kwargs.items()]) + await db.execute( + f"UPDATE lnurldevice.lnurldevicepayment SET {q} WHERE id = ?", + (*kwargs.values(), lnurldevicepayment_id), + ) + row = await db.fetchone( + "SELECT * FROM lnurldevice.lnurldevicepayment WHERE id = ?", + (lnurldevicepayment_id,), + ) + return lnurldevicepayment(**row) if row else None \ No newline at end of file diff --git a/lnbits/extensions/nostr/migrations.py b/lnbits/extensions/nostr/migrations.py new file mode 100644 index 000000000..de32a5787 --- /dev/null +++ b/lnbits/extensions/nostr/migrations.py @@ -0,0 +1,47 @@ +from lnbits.db import Database + +db2 = Database("ext_nostr") + + +async def m001_initial(db): + """ + Initial nostr table. + """ + await db.execute( + f""" + CREATE TABLE nostr.keys ( + pubkey TEXT NOT NULL PRIMARY KEY, + privkey TEXT NOT NULL + ); + """ + ) + await db.execute( + f""" + CREATE TABLE nostr.notes ( + id TEXT NOT NULL PRIMARY KEY, + pubkey TEXT NOT NULL, + created_at TEXT NOT NULL, + kind INT NOT NULL, + tags TEXT NOT NULL, + content TEXT NOT NULL, + sig TEXT NOT NULL, + ); + """ + ) + await db.execute( + f""" + CREATE TABLE nostr.relays ( + id TEXT NOT NULL PRIMARY KEY, + relay TEXT NOT NULL + ); + """ + ) + await db.execute( + f""" + CREATE TABLE nostr.connections ( + id TEXT NOT NULL PRIMARY KEY, + publickey TEXT NOT NULL, + relayid TEXT NOT NULL + ); + """ + ) \ No newline at end of file diff --git a/lnbits/extensions/nostr/models.py b/lnbits/extensions/nostr/models.py new file mode 100644 index 000000000..ba0c87a5e --- /dev/null +++ b/lnbits/extensions/nostr/models.py @@ -0,0 +1,34 @@ +import json +from sqlite3 import Row +from typing import Optional + +from fastapi import Request +from lnurl import Lnurl +from lnurl import encode as lnurl_encode # type: ignore +from lnurl.models import LnurlPaySuccessAction, UrlAction # type: ignore +from lnurl.types import LnurlPayMetadata # type: ignore +from pydantic import BaseModel +from pydantic.main import BaseModel + +class nostrKeys(BaseModel): + id: str + pubkey: str + privkey: str + +class nostrNotes(BaseModel): + id: str + pubkey: str + created_at: str + kind: int + tags: str + content: str + sig: str + +class nostrRelays(BaseModel): + id: str + relay: str + +class nostrConnections(BaseModel): + id: str + pubkey: str + relayid: str \ No newline at end of file diff --git a/lnbits/extensions/nostr/templates/lnurldevice/_api_docs.html b/lnbits/extensions/nostr/templates/lnurldevice/_api_docs.html new file mode 100644 index 000000000..af69b76e3 --- /dev/null +++ b/lnbits/extensions/nostr/templates/lnurldevice/_api_docs.html @@ -0,0 +1,169 @@ + + +

+ Register LNURLDevice devices to receive payments in your LNbits wallet.
+ Build your own here + https://github.com/arcbtc/bitcoinpos
+ + Created by, Ben Arc +

+
+ + + + + /lnurldevice/api/v1/lnurlpos +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<lnurldevice_object>, ...] +
Curl example
+ curl -X POST {{ request.base_url }}api/v1/lnurldevice -d '{"title": + <string>, "message":<string>, "currency": + <integer>}' -H "Content-type: application/json" -H "X-Api-Key: + {{user.wallets[0].adminkey }}" + +
+
+
+ + + + PUT + /lnurldevice/api/v1/lnurlpos/<lnurldevice_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<lnurldevice_object>, ...] +
Curl example
+ curl -X POST {{ request.base_url + }}api/v1/lnurlpos/<lnurldevice_id> -d ''{"title": + <string>, "message":<string>, "currency": + <integer>} -H "Content-type: application/json" -H "X-Api-Key: + {{user.wallets[0].adminkey }}" + +
+
+
+ + + + + GET + /lnurldevice/api/v1/lnurlpos/<lnurldevice_id> +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<lnurldevice_object>, ...] +
Curl example
+ curl -X GET {{ request.base_url + }}api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: {{ + user.wallets[0].inkey }}" + +
+
+
+ + + + GET + /lnurldevice/api/v1/lnurlposs +
Headers
+ {"X-Api-Key": <invoice_key>}
+
+ Body (application/json) +
+
+ Returns 200 OK (application/json) +
+ [<lnurldevice_object>, ...] +
Curl example
+ curl -X GET {{ request.base_url }}api/v1/lnurldevices -H + "X-Api-Key: {{ user.wallets[0].inkey }}" + +
+
+
+ + + + DELETE + /lnurldevice/api/v1/lnurlpos/<lnurldevice_id> +
Headers
+ {"X-Api-Key": <admin_key>}
+
Returns 204 NO CONTENT
+ +
Curl example
+ curl -X DELETE {{ request.base_url + }}api/v1/lnurlpos/<lnurldevice_id> -H "X-Api-Key: {{ + user.wallets[0].adminkey }}" + +
+
+
+
+
diff --git a/lnbits/extensions/nostr/templates/lnurldevice/error.html b/lnbits/extensions/nostr/templates/lnurldevice/error.html new file mode 100644 index 000000000..d8e418329 --- /dev/null +++ b/lnbits/extensions/nostr/templates/lnurldevice/error.html @@ -0,0 +1,34 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +
+

LNURL-pay not paid

+
+ + +
+
+
+
+
+ + {% endblock %} {% block scripts %} + + + + {% endblock %} +
diff --git a/lnbits/extensions/nostr/templates/lnurldevice/index.html b/lnbits/extensions/nostr/templates/lnurldevice/index.html new file mode 100644 index 000000000..b51e25568 --- /dev/null +++ b/lnbits/extensions/nostr/templates/lnurldevice/index.html @@ -0,0 +1,534 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + {% raw %} + New LNURLDevice instance + + + + + + +
+
+
lNURLdevice
+
+ +
+ + + + Export to CSV +
+
+ + + + + {% endraw %} + +
+
+
+ +
+ + +
+ {{SITE_TITLE}} LNURLDevice Extension +
+
+ + + {% include "lnurldevice/_api_docs.html" %} + +
+
+ + + +
LNURLDevice device string
+ {% raw + %}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}}, + {{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw + %} Click to copy URL + + +
+ +
+
+
+ + + + + + + + + + + + +
+ Update lnurldevice + Create lnurldevice + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + + +{% endblock %} diff --git a/lnbits/extensions/nostr/templates/lnurldevice/paid.html b/lnbits/extensions/nostr/templates/lnurldevice/paid.html new file mode 100644 index 000000000..c185ecce6 --- /dev/null +++ b/lnbits/extensions/nostr/templates/lnurldevice/paid.html @@ -0,0 +1,27 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +
+

{{ pin }}

+
+
+
+
+
+ + {% endblock %} {% block scripts %} + + + + {% endblock %} +
diff --git a/lnbits/extensions/nostr/views.py b/lnbits/extensions/nostr/views.py new file mode 100644 index 000000000..1dfc07da7 --- /dev/null +++ b/lnbits/extensions/nostr/views.py @@ -0,0 +1,24 @@ +from http import HTTPStatus + +from fastapi import Request +from fastapi.param_functions import Query +from fastapi.params import Depends +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse + +from lnbits.core.crud import update_payment_status +from lnbits.core.models import User +from lnbits.core.views.api import api_payment +from lnbits.decorators import check_user_exists + +from . import nostr_ext, nostr_renderer + +templates = Jinja2Templates(directory="templates") + + +@nostr_ext.get("/", response_class=HTMLResponse) +async def index(request: Request, user: User = Depends(check_user_exists)): + return nostr_renderer().TemplateResponse( + "nostr/index.html", {"request": request, "user": user.dict()} + ) diff --git a/lnbits/extensions/nostr/views_api.py b/lnbits/extensions/nostr/views_api.py new file mode 100644 index 000000000..a479cad8f --- /dev/null +++ b/lnbits/extensions/nostr/views_api.py @@ -0,0 +1,48 @@ +from http import HTTPStatus + +from fastapi import Request +from fastapi.param_functions import Query +from fastapi.params import Depends +from starlette.exceptions import HTTPException + +from lnbits.core.crud import get_user +from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key +from lnbits.extensions.lnurldevice import lnurldevice_ext +from lnbits.utils.exchange_rates import currencies + +from . import lnurldevice_ext +from .crud import ( + create_lnurldevice, + delete_lnurldevice, + get_lnurldevice, + get_lnurldevices, + update_lnurldevice, +) +from .models import createLnurldevice + + +@nostr_ext.get("/api/v1/lnurlpos") +async def api_check_daemon(wallet: WalletTypeInfo = Depends(get_key_type)): + wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids + try: + return [ + {**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids) + ] + except: + return "" + +@nostr_ext.delete("/api/v1/lnurlpos/{lnurldevice_id}") +async def api_lnurldevice_delete( + wallet: WalletTypeInfo = Depends(require_admin_key), + lnurldevice_id: str = Query(None), +): + lnurldevice = await get_lnurldevice(lnurldevice_id) + + if not lnurldevice: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Wallet link does not exist." + ) + + await delete_lnurldevice(lnurldevice_id) + + return "", HTTPStatus.NO_CONTENT \ No newline at end of file