diff --git a/config.json b/config.json
index d8b886b..78a00e6 100644
--- a/config.json
+++ b/config.json
@@ -2,6 +2,6 @@
"name": "Nostr Client",
"short_description": "Nostr client for extensions",
"tile": "/nostrclient/static/images/nostr-bitcoin.png",
- "contributors": ["calle"],
- "min_lnbits_version": "0.11.0"
+ "contributors": ["calle", "motorina0"],
+ "min_lnbits_version": "0.12.0"
}
diff --git a/crud.py b/crud.py
index 05ca907..3cabc25 100644
--- a/crud.py
+++ b/crud.py
@@ -1,7 +1,9 @@
-from typing import List
+from typing import List, Optional
+
+import json
from . import db
-from .models import Relay
+from .models import Config, Relay
async def get_relays() -> List[Relay]:
@@ -25,3 +27,37 @@ async def add_relay(relay: Relay) -> None:
async def delete_relay(relay: Relay) -> None:
await db.execute("DELETE FROM nostrclient.relays WHERE url = ?", (relay.url,))
+
+
+######################CONFIG#######################
+async def create_config() -> Config:
+ config = Config()
+ await db.execute(
+ """
+ INSERT INTO nostrclient.config (json_data)
+ VALUES (?)
+ """,
+ (json.dumps(config.dict())),
+ )
+ row = await db.fetchone(
+ "SELECT json_data FROM nostrclient.config", ()
+ )
+ return json.loads(row[0], object_hook=lambda d: Config(**d))
+
+
+async def update_config(config: Config) -> Optional[Config]:
+ await db.execute(
+ """UPDATE nostrclient.config SET json_data = ?""",
+ (json.dumps(config.dict())),
+ )
+ row = await db.fetchone(
+ "SELECT json_data FROM nostrclient.config", ()
+ )
+ return json.loads(row[0], object_hook=lambda d: Config(**d))
+
+
+async def get_config() -> Optional[Config]:
+ row = await db.fetchone(
+ "SELECT json_data FROM nostrclient.config", ()
+ )
+ return json.loads(row[0], object_hook=lambda d: Config(**d)) if row else None
diff --git a/migrations.py b/migrations.py
index 73b9ed8..804fd76 100644
--- a/migrations.py
+++ b/migrations.py
@@ -11,3 +11,15 @@ async def m001_initial(db):
);
"""
)
+
+
+async def m002_create_config_table(db):
+ """
+ Allow the extension to persist and retrieve any number of config values.
+ """
+
+ await db.execute(
+ """CREATE TABLE nostrclient.config (
+ json_data TEXT NOT NULL
+ );"""
+ )
diff --git a/models.py b/models.py
index e08ade3..a794230 100644
--- a/models.py
+++ b/models.py
@@ -42,3 +42,8 @@ class TestMessageResponse(BaseModel):
private_key: str
public_key: str
event_json: str
+
+
+class Config(BaseModel):
+ private_ws: bool = True
+ public_ws: bool = False
diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py
index ff7ca9c..2aa27c5 100644
--- a/nostr/relay_manager.py
+++ b/nostr/relay_manager.py
@@ -72,6 +72,7 @@ def add_subscription(self, id: str, filters: List[str]):
def close_subscription(self, id: str):
try:
+ logger.info(f"Closing subscription: '{id}'.")
with self._subscriptions_lock:
if id in self._cached_subscriptions:
self._cached_subscriptions.pop(id)
diff --git a/router.py b/router.py
index e6ccdef..cea4ece 100644
--- a/router.py
+++ b/router.py
@@ -42,7 +42,7 @@ async def stop(self):
pass
try:
- await self.websocket.close()
+ await self.websocket.close(reason="Websocket connection closed")
except Exception as _:
pass
@@ -113,7 +113,7 @@ async def _handle_received_subscription_events(self, s):
def _handle_notices(self):
while len(NostrRouter.received_subscription_notices):
my_event = NostrRouter.received_subscription_notices.pop(0)
- logger.info(f"[Relay '{my_event.url}'] Notice: '{my_event.content}']")
+ logger.debug(f"[Relay '{my_event.url}'] Notice: '{my_event.content}']")
# Note: we don't send it to the user because
# we don't know who should receive it
nostr_client.relay_manager.handle_notice(my_event)
@@ -136,6 +136,7 @@ async def _handle_client_to_nostr(self, json_str):
def _handle_client_req(self, json_data):
subscription_id = json_data[1]
+ logger.info(f"New subscription: '{subscription_id}'")
subscription_id_rewritten = urlsafe_short_hash()
self.original_subscription_ids[subscription_id_rewritten] = subscription_id
filters = json_data[2:]
@@ -154,5 +155,6 @@ def _handle_client_close(self, subscription_id):
if subscription_id_rewritten:
self.original_subscription_ids.pop(subscription_id_rewritten)
nostr_client.relay_manager.close_subscription(subscription_id_rewritten)
+ logger.info(f"Unsubscribe from '{subscription_id_rewritten}'. Original id: '{subscription_id}.'")
else:
- logger.debug(f"Failed to unsubscribe from '{subscription_id}.'")
+ logger.info(f"Failed to unsubscribe from '{subscription_id}.'")
diff --git a/templates/nostrclient/index.html b/templates/nostrclient/index.html
index db0f98e..f1b9456 100644
--- a/templates/nostrclient/index.html
+++ b/templates/nostrclient/index.html
@@ -4,38 +4,24 @@
-
-
-
+
+
+
-
@@ -46,36 +32,18 @@
Nostrclient
-
+
-
+
-
+
{{ col.label }}
@@ -84,12 +52,7 @@ Nostrclient
-
+
🟢
🔴
@@ -98,29 +61,17 @@
Nostrclient
⬆️ ⬇️
-
+
⚠️
-
+
ⓘ
-
+
{{ col.value }}
@@ -136,32 +87,15 @@
Nostrclient
- Copy address
+ Copy address
Your endpoint:
-
+
-
+
@@ -169,13 +103,8 @@
Nostrclient
Sender Private Key:
-
+
@@ -184,8 +113,7 @@
Nostrclient
No not use your real private key! Leave empty for a randomly
- generated key.
+ generated key.
@@ -194,13 +122,7 @@ Nostrclient
Sender Public Key:
-
+
@@ -208,15 +130,8 @@
Nostrclient
Test Message:
-
+
@@ -224,35 +139,22 @@
Nostrclient
Receiver Public Key:
-
+
- This is the recipient of the message. Field required.
+ This is the recipient of the message. Field required.
- Send Message
+ Send Message
@@ -264,14 +166,7 @@ Nostrclient
Sent Data:
-
+
@@ -279,14 +174,7 @@
Nostrclient
Received Data:
-
+
@@ -305,12 +193,8 @@ Nostrclient Extension
-
+
Only Admin users can manage this extension.
@@ -320,21 +204,27 @@ Nostrclient Extension
-
+
Close
+
+
+
+
+
+
+
+
+ Update
+ Cancel
+
+
+
+
{% endraw %} {% endblock %} {% block scripts %} {{ window_vars(user) }}
@@ -380,6 +270,10 @@ Nostrclient Extension
show: false,
data: null
},
+ config: {
+ showDialog: false,
+ data: {},
+ },
testData: {
show: false,
wsConnection: null,
@@ -477,7 +371,7 @@ Nostrclient Extension
'POST',
'/nostrclient/api/v1/relay?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
- {url: this.relayToAdd}
+ { url: this.relayToAdd }
)
.then(function (response) {
console.log('response:', response)
@@ -509,7 +403,7 @@ Nostrclient Extension
'DELETE',
'/nostrclient/api/v1/relay?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
- {url: url}
+ { url: url }
)
.then(response => {
const relayIndex = this.nostrrelayLinks.indexOf(r => r.url === url)
@@ -522,6 +416,34 @@ Nostrclient Extension
LNbits.utils.notifyApiError(error)
})
},
+ getConfig: async function () {
+ try {
+ const { data } = await LNbits.api
+ .request(
+ 'GET',
+ '/nostrclient/api/v1/config',
+ this.g.user.wallets[0].adminkey
+ )
+ this.config.data = data
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ },
+ updateConfig: async function () {
+ try {
+ const { data } = await LNbits.api.request(
+ 'PUT',
+ '/nostrclient/api/v1/config',
+ this.g.user.wallets[0].adminkey,
+ this.config.data
+ )
+ this.config.data = data
+
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ }
+ this.config.showDialog = false
+ },
toggleTestPanel: async function () {
if (this.testData.show) {
await this.hideTestPannel()
@@ -559,7 +481,7 @@ Nostrclient Extension
},
sendTestMessage: async function () {
try {
- const {data} = await LNbits.api.request(
+ const { data } = await LNbits.api.request(
'PUT',
'/nostrclient/api/v1/relay/test?usr=' + this.g.user.id,
this.g.user.wallets[0].adminkey,
@@ -580,7 +502,7 @@ Nostrclient Extension
const subscription = JSON.stringify([
'REQ',
'test-dms',
- {kinds: [4], '#p': [event.pubkey]}
+ { kinds: [4], '#p': [event.pubkey] }
])
this.testData.wsConnection.send(subscription)
} catch (error) {
@@ -642,9 +564,9 @@ Nostrclient Extension
},
sleep: ms => new Promise(r => setTimeout(r, ms))
},
- created: function () {
- var self = this
+ created: async function () {
this.getRelays()
+ await this.getConfig()
setInterval(this.getRelays, 5000)
}
})
diff --git a/views_api.py b/views_api.py
index 14642d0..a0ec45a 100644
--- a/views_api.py
+++ b/views_api.py
@@ -7,12 +7,12 @@
from starlette.exceptions import HTTPException
from lnbits.decorators import check_admin
-from lnbits.helpers import urlsafe_short_hash
+from lnbits.helpers import decrypt_internal_message, urlsafe_short_hash
from . import nostr_client, nostrclient_ext, scheduled_tasks
-from .crud import add_relay, delete_relay, get_relays
+from .crud import add_relay, create_config, delete_relay, get_config, get_relays, update_config
from .helpers import normalize_public_key
-from .models import Relay, TestMessage, TestMessageResponse
+from .models import Config, Relay, TestMessage, TestMessageResponse
from .nostr.key import EncryptedDirectMessage, PrivateKey
from .router import NostrRouter
@@ -20,7 +20,7 @@
all_routers: list[NostrRouter] = []
-@nostrclient_ext.get("/api/v1/relays")
+@nostrclient_ext.get("/api/v1/relays", dependencies=[Depends(check_admin)])
async def api_get_relays() -> List[Relay]:
relays = []
for url, r in nostr_client.relay_manager.relays.items():
@@ -133,19 +133,68 @@ async def api_stop():
return {"success": True}
-@nostrclient_ext.websocket("/api/v1/relay")
-async def ws_relay(websocket: WebSocket) -> None:
+@nostrclient_ext.websocket("/api/v1/{id}")
+async def ws_relay(id: str, websocket: WebSocket) -> None:
"""Relay multiplexer: one client (per endpoint) <-> multiple relays"""
- await websocket.accept()
- router = NostrRouter(websocket)
- router.start()
- all_routers.append(router)
- # we kill this websocket and the subscriptions
- # if the user disconnects and thus `connected==False`
- while router.connected:
- await asyncio.sleep(10)
+ logger.info("New websocket connection at: '/api/v1/relay'")
+ try:
+ config = await get_config()
+
+ if not config.private_ws and not config.public_ws:
+ raise ValueError("Websocket connections not accepted.")
+
+ if id == "relay":
+ if not config.public_ws:
+ raise ValueError("Public websocket connections not accepted.")
+ else:
+ if not config.private_ws:
+ raise ValueError("Private websocket connections not accepted.")
+ if decrypt_internal_message(id) != "relay":
+ raise ValueError("Invalid websocket endpoint.")
+
+
+ await websocket.accept()
+ router = NostrRouter(websocket)
+ router.start()
+ all_routers.append(router)
+
+ # we kill this websocket and the subscriptions
+ # if the user disconnects and thus `connected==False`
+ while router.connected:
+ await asyncio.sleep(10)
+
+ try:
+ await router.stop()
+ except Exception as e:
+ logger.debug(e)
+
+ all_routers.remove(router)
+ logger.info("Closed websocket connection at: '/api/v1/relay'")
+ except ValueError as ex:
+ logger.warning(ex)
+ await websocket.close(reason=str(ex))
+ except Exception as ex:
+ logger.warning(ex)
+ await websocket.close(reason="Websocket connection unexpected closed")
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail="Cannot accept websocket connection",
+ )
+
+
+@nostrclient_ext.get("/api/v1/config", dependencies=[Depends(check_admin)])
+async def api_get_relays() -> Config:
+ config = await get_config()
+ if not config:
+ await create_config()
- await router.stop()
- all_routers.remove(router)
+ return config
+@nostrclient_ext.put("/api/v1/config", dependencies=[Depends(check_admin)])
+async def api_update_config(
+ data: Config
+):
+ config = await update_config(data)
+ assert config
+ return config.dict()