diff --git a/.env.example b/.env.example index 060748a97..0d5b497e0 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,7 @@ PORT=5000 LNBITS_ALLOWED_USERS="" LNBITS_ADMIN_USERS="" # Extensions only admin can access -LNBITS_ADMIN_EXTENSIONS="ngrok" +LNBITS_ADMIN_EXTENSIONS="nostradmin" LNBITS_DEFAULT_WALLET_NAME="LNbits wallet" # Disable extensions for all users, use "all" to disable all extensions diff --git a/lnbits/extensions/nostr/config.json b/lnbits/extensions/nostr/config.json deleted file mode 100644 index a32e39a1a..000000000 --- a/lnbits/extensions/nostr/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "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 deleted file mode 100644 index 0b30bc9a7..000000000 --- a/lnbits/extensions/nostr/crud.py +++ /dev/null @@ -1,115 +0,0 @@ -from typing import List, Optional, Union - -from lnbits.helpers import urlsafe_short_hash -import shortuuid -from . import db -from .models import nostrKeys, nostrNotes, nostrCreateRelays, nostrRelays, nostrConnections, nostrCreateConnections - -###############KEYS################## - -async def create_nostrkeys( - data: nostrKeys -) -> nostrKeys: - await db.execute( - """ - INSERT INTO nostr.keys ( - pubkey, - privkey - ) - VALUES (?, ?) - """, - (data.pubkey, data.privkey), - ) - return await get_nostrkeys(nostrkey_id) - -async def get_nostrkeys(pubkey: str) -> nostrKeys: - row = await db.fetchone( - "SELECT * FROM nostr.keys WHERE pubkey = ?", - (pubkey,), - ) - 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( - data: nostrCreateRelays -) -> nostrCreateRelays: - nostrrelay_id = shortuuid.uuid(name=relay) - - if await get_nostrrelays(nostrrelay_id): - return "error" - await db.execute( - """ - INSERT INTO nostr.relays ( - id, - relay - ) - VALUES (?, ?) - """, - (nostrrelay_id, data.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: nostrCreateConnections -) -> nostrCreateConnections: - nostrrelay_id = shortuuid.uuid(name=data.relayid + data.pubkey) - await db.execute( - """ - INSERT INTO nostr.connections ( - id, - pubkey, - relayid - ) - VALUES (?, ?, ?) - """, - (data.id, data.pubkey, data.relayid), - ) - return await get_nostrconnections(data.id) - -async def get_nostrconnections(nostrconnections_id: str) -> nostrConnections: - row = await db.fetchone( - "SELECT * FROM nostr.connections WHERE id = ?", - (nostrconnections_id,), - ) - return nostrConnections(**row) if row else None \ No newline at end of file diff --git a/lnbits/extensions/nostr/views_api.py b/lnbits/extensions/nostr/views_api.py deleted file mode 100644 index 9e7ccfffa..000000000 --- a/lnbits/extensions/nostr/views_api.py +++ /dev/null @@ -1,24 +0,0 @@ -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.nostr import nostr_ext -from lnbits.utils.exchange_rates import currencies - -from . import nostr_ext -from .crud import ( - create_nostrkeys, - get_nostrkeys, - create_nostrnotes, - get_nostrnotes, - create_nostrrelays, - get_nostrrelays, - create_nostrconnections, - get_nostrconnections, -) -from .models import nostrKeys \ No newline at end of file diff --git a/lnbits/extensions/nostr/README.md b/lnbits/extensions/nostradmin/README.md similarity index 100% rename from lnbits/extensions/nostr/README.md rename to lnbits/extensions/nostradmin/README.md diff --git a/lnbits/extensions/nostr/__init__.py b/lnbits/extensions/nostradmin/__init__.py similarity index 51% rename from lnbits/extensions/nostr/__init__.py rename to lnbits/extensions/nostradmin/__init__.py index 9774a2790..7034ca46b 100644 --- a/lnbits/extensions/nostr/__init__.py +++ b/lnbits/extensions/nostradmin/__init__.py @@ -3,13 +3,14 @@ from fastapi import APIRouter from lnbits.db import Database from lnbits.helpers import template_renderer -db = Database("ext_nostr") +db = Database("ext_nostradmin") -nostr_ext: APIRouter = APIRouter(prefix="/nostr", tags=["nostr"]) +nostr_ext: APIRouter = APIRouter(prefix="/nostradmin", tags=["nostradmin"]) def nostr_renderer(): - return template_renderer(["lnbits/extensions/nostr/templates"]) + return template_renderer(["lnbits/extensions/nostradmin/templates"]) + from .views import * # noqa from .views_api import * # noqa diff --git a/lnbits/extensions/nostradmin/config.json b/lnbits/extensions/nostradmin/config.json new file mode 100644 index 000000000..2c4f76d35 --- /dev/null +++ b/lnbits/extensions/nostradmin/config.json @@ -0,0 +1,6 @@ +{ + "name": "NostrAdmin", + "short_description": "Admin daemon for Nostr", + "icon": "swap_horizontal_circle", + "contributors": ["arcbtc"] +} diff --git a/lnbits/extensions/nostradmin/crud.py b/lnbits/extensions/nostradmin/crud.py new file mode 100644 index 000000000..08908e856 --- /dev/null +++ b/lnbits/extensions/nostradmin/crud.py @@ -0,0 +1,156 @@ +from typing import List, Optional, Union + +from lnbits.helpers import urlsafe_short_hash +import shortuuid +from . import db +from .models import ( + nostrKeys, + nostrNotes, + nostrCreateRelays, + nostrRelays, + nostrConnections, + nostrCreateConnections, + nostrRelayList, +) + +###############KEYS################## + + +async def create_nostrkeys(data: nostrKeys) -> nostrKeys: + await db.execute( + """ + INSERT INTO nostradmin.keys ( + pubkey, + privkey + ) + VALUES (?, ?) + """, + (data.pubkey, data.privkey), + ) + return await get_nostrkeys(nostrkey_id) + + +async def get_nostrkeys(pubkey: str) -> nostrKeys: + row = await db.fetchone("SELECT * FROM nostradmin.keys WHERE pubkey = ?", (pubkey,)) + return nostrKeys(**row) if row else None + + +###############NOTES################## + + +async def create_nostrnotes(data: nostrNotes) -> nostrNotes: + await db.execute( + """ + INSERT INTO nostradmin.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 nostradmin.notes WHERE id = ?", (nostrnote_id,)) + return nostrNotes(**row) if row else None + + +###############RELAYS################## + + +async def create_nostrrelays(data: nostrCreateRelays) -> nostrCreateRelays: + nostrrelay_id = shortuuid.uuid(name=data.relay) + + if await get_nostrrelay(nostrrelay_id): + return "error" + await db.execute( + """ + INSERT INTO nostradmin.relays ( + id, + relay + ) + VALUES (?, ?) + """, + (nostrrelay_id, data.relay), + ) + return await get_nostrrelay(nostrrelay_id) + + +async def get_nostrrelays() -> nostrRelays: + rows = await db.fetchall("SELECT * FROM nostradmin.relays") + return [nostrRelays(**row) for row in rows] + + +async def get_nostrrelay(nostrrelay_id: str) -> nostrRelays: + row = await db.fetchone("SELECT * FROM nostradmin.relays WHERE id = ?", (nostrrelay_id,)) + return nostrRelays(**row) if row else None + + +async def update_nostrrelayallowlist(allowlist: str) -> nostrRelayList: + await db.execute( + """ + UPDATE nostradmin.relaylist SET + allowlist = ? + WHERE id = ? + """, + (allowlist, 1), + ) + return await get_nostrrelaylist() + +async def update_nostrrelaydenylist(denylist: str) -> nostrRelayList: + await db.execute( + """ + UPDATE nostradmin.relaylist SET + denylist = ? + WHERE id = ? + """, + (denylist, 1), + ) + return await get_nostrrelaylist() + +async def get_nostrrelaylist() -> nostrRelayList: + row = await db.fetchone("SELECT * FROM nostradmin.relaylist WHERE id = ?", (1,)) + return nostrRelayList(**row) if row else None + + +###############CONNECTIONS################## + + +async def create_nostrconnections( + data: nostrCreateConnections +) -> nostrCreateConnections: + nostrrelay_id = shortuuid.uuid(name=data.relayid + data.pubkey) + await db.execute( + """ + INSERT INTO nostradmin.connections ( + id, + pubkey, + relayid + ) + VALUES (?, ?, ?) + """, + (data.id, data.pubkey, data.relayid), + ) + return await get_nostrconnections(data.id) + + +async def get_nostrconnections(nostrconnections_id: str) -> nostrConnections: + row = await db.fetchone( + "SELECT * FROM nostradmin.connections WHERE id = ?", (nostrconnections_id,) + ) + return nostrConnections(**row) if row else None diff --git a/lnbits/extensions/nostr/migrations.py b/lnbits/extensions/nostradmin/migrations.py similarity index 54% rename from lnbits/extensions/nostr/migrations.py rename to lnbits/extensions/nostradmin/migrations.py index 0c616110e..09b28117d 100644 --- a/lnbits/extensions/nostr/migrations.py +++ b/lnbits/extensions/nostradmin/migrations.py @@ -9,7 +9,7 @@ async def m001_initial(db): """ await db.execute( f""" - CREATE TABLE nostr.keys ( + CREATE TABLE nostradmin.keys ( pubkey TEXT NOT NULL PRIMARY KEY, privkey TEXT NOT NULL ); @@ -17,7 +17,7 @@ async def m001_initial(db): ) await db.execute( f""" - CREATE TABLE nostr.notes ( + CREATE TABLE nostradmin.notes ( id TEXT NOT NULL PRIMARY KEY, pubkey TEXT NOT NULL, created_at TEXT NOT NULL, @@ -30,7 +30,7 @@ async def m001_initial(db): ) await db.execute( f""" - CREATE TABLE nostr.relays ( + CREATE TABLE nostradmin.relays ( id TEXT NOT NULL PRIMARY KEY, relay TEXT NOT NULL ); @@ -38,10 +38,32 @@ async def m001_initial(db): ) await db.execute( f""" - CREATE TABLE nostr.connections ( + CREATE TABLE nostradmin.relaylists ( + id TEXT NOT NULL PRIMARY KEY DEFAULT 1, + allowlist TEXT NOT NULL, + denylist TEXT NOT NULL + ); + """ + ) + try: + await db.execute( + """ + INSERT INTO nostradmin.relaylist ( + id, + denylist + ) + VALUES (?, ?,) + """, + (1, "\n".join(["wss://zucks-meta-relay.com", "wss://nostradmin.cia.gov"])), + ) + except: + return + await db.execute( + f""" + CREATE TABLE nostradmin.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/nostradmin/models.py similarity index 73% rename from lnbits/extensions/nostr/models.py rename to lnbits/extensions/nostradmin/models.py index e21e5d76f..fd89f5155 100644 --- a/lnbits/extensions/nostr/models.py +++ b/lnbits/extensions/nostradmin/models.py @@ -6,6 +6,7 @@ from fastapi import Request from pydantic import BaseModel from pydantic.main import BaseModel + class nostrKeys(BaseModel): pubkey: str privkey: str @@ -30,7 +31,18 @@ class nostrRelays(BaseModel): id: str relay: str +class nostrRelayList(BaseModel): + id: str + allowlist: str + denylist: str + +class nostrRelayDenyList(BaseModel): + denylist: str + +class nostrRelayAllowList(BaseModel): + allowlist: str + class nostrConnections(BaseModel): id: str pubkey: str - relayid: str \ No newline at end of file + relayid: str diff --git a/lnbits/extensions/nostr/templates/nostr/index.html b/lnbits/extensions/nostradmin/templates/nostradmin/index.html similarity index 50% rename from lnbits/extensions/nostr/templates/nostr/index.html rename to lnbits/extensions/nostradmin/templates/nostradmin/index.html index f17d0243b..adab98e2f 100644 --- a/lnbits/extensions/nostr/templates/nostr/index.html +++ b/lnbits/extensions/nostradmin/templates/nostradmin/index.html @@ -71,12 +71,77 @@ + + + + + + + + + + +
+ Deny List (denys use of relays in this list) +
+ +
+
+ +
+
+ + Update Deny List + + Reset +
+
+
+
+ + +
+ Allow List (denys any relays not in this list) +
+ +
+
+ +
+
+ + Update Allow List + + Reset +
+
+
+
+
+
{{SITE_TITLE}} Nostr Extension
+

Only Admin users can manage this extension

Okay
@@ -109,6 +174,9 @@ mixins: [windowMixin], data: function () { return { + listSelection: 'denylist', + allowList: [], + denyList: [], nostrTable: { columns: [ { @@ -136,12 +204,66 @@ LNbits.api .request( 'GET', - '/nostr/api/v1/relays', + '/nostradmin/api/v1/relays', self.g.user.wallets[0].adminkey ) .then(function (response) { if (response.data) { - self.lnurldeviceLinks = response.data.map(maplnurldevice) + self.nostrrelayLinks = response.data.map(maprelays) + } + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + setAllowList: function () { + var self = this + LNbits.api + .request( + 'POST', + '/nostradmin/api/v1/allowlist', + self.g.user.wallets[0].adminkey, + self.allowList + ) + .then(function (response) { + if (response.data) { + self.allowList = response.data + } + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + setDenyList: function () { + var self = this + LNbits.api + .request( + 'POST', + '/nostradmin/api/v1/denylist', + self.g.user.wallets[0].adminkey, + self.allowList + ) + .then(function (response) { + if (response.data) { + self.denyList = response.data + } + }) + .catch(function (error) { + LNbits.utils.notifyApiError(error) + }) + }, + getLists: function () { + var self = this + LNbits.api + .request( + 'GET', + '/nostradmin/api/v1/relaylist', + self.g.user.wallets[0].adminkey + ) + .then(function (response) { + if (response.data) { + self.denyList = response.data.denylist + self.allowList = response.data.allowlist } }) .catch(function (error) { @@ -151,8 +273,8 @@ }, created: function () { var self = this - var getrelays = this.getrelays - getrelays() + this.getrelays() + this.getLists() } }) diff --git a/lnbits/extensions/nostr/views.py b/lnbits/extensions/nostradmin/views.py similarity index 90% rename from lnbits/extensions/nostr/views.py rename to lnbits/extensions/nostradmin/views.py index 1dfc07da7..5609c2185 100644 --- a/lnbits/extensions/nostr/views.py +++ b/lnbits/extensions/nostradmin/views.py @@ -20,5 +20,5 @@ 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()} + "nostradmin/index.html", {"request": request, "user": user.dict()} ) diff --git a/lnbits/extensions/nostradmin/views_api.py b/lnbits/extensions/nostradmin/views_api.py new file mode 100644 index 000000000..da6e140ba --- /dev/null +++ b/lnbits/extensions/nostradmin/views_api.py @@ -0,0 +1,87 @@ +from http import HTTPStatus +import asyncio +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.nostr import nostr_ext +from lnbits.utils.exchange_rates import currencies + +from . import nostr_ext +from lnbits.settings import LNBITS_ADMIN_USERS +from .crud import ( + create_nostrkeys, + get_nostrkeys, + create_nostrnotes, + get_nostrnotes, + create_nostrrelays, + get_nostrrelays, + get_nostrrelaylist, + update_nostrrelayallowlist, + update_nostrrelaydenylist, + create_nostrconnections, + get_nostrconnections, +) +from .models import nostrKeys, nostrCreateRelays, nostrRelayAllowList, nostrRelayDenyList + + +# while True: +async def nostr_subscribe(): + return + # for the relays: + # async with websockets.connect("ws://localhost:8765") as websocket: + # for the public keys: + # await websocket.send("subscribe to events") + # await websocket.recv() + + +websocket_queue = asyncio.Queue(1000) + + +async def internal_invoice_listener(): + while True: + checking_id = await internal_invoice_queue.get() + asyncio.create_task(invoice_callback_dispatcher(checking_id)) + + +@nostr_ext.get("/api/v1/relays") +async def api_relays_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): + + relays = await get_nostrrelays() + if not relays: + await create_nostrrelays(nostrCreateRelays(relay="wss://relayer.fiatjaf.com")) + await create_nostrrelays( + nostrCreateRelays(relay="wss://nostr-pub.wellorder.net") + ) + relays = await get_nostrrelays() + try: + return [{**relays.dict()} for relays in await relays] + except: + None + +@nostr_ext.get("/api/v1/relaylist") +async def api_relaylist(wallet: WalletTypeInfo = Depends(get_key_type)): + if wallet.wallet.user not in LNBITS_ADMIN_USERS: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." + ) + return await get_nostrrelaylist() + +@nostr_ext.post("/api/v1/allowlist") +async def api_relaysallowed(data: nostrRelayAllowList, wallet: WalletTypeInfo = Depends(get_key_type)): + if wallet.wallet.user not in LNBITS_ADMIN_USERS: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." + ) + return await update_nostrrelayallowlist(data) + +@nostr_ext.post("/api/v1/denylist") +async def api_relaysdenyed(data: nostrRelayDenyList, wallet: WalletTypeInfo = Depends(get_key_type)): + if wallet.wallet.user not in LNBITS_ADMIN_USERS: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized." + ) + return await update_nostrrelaydenylist(data) \ No newline at end of file