diff --git a/__init__.py b/__init__.py index b09d0e9..019df68 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,6 @@ import asyncio from typing import List + from fastapi import APIRouter from starlette.staticfiles import StaticFiles diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..bcf5c02 --- /dev/null +++ b/helpers.py @@ -0,0 +1,19 @@ +from bech32 import bech32_decode, convertbits + + +def normalize_public_key(pubkey: str) -> str: + if pubkey.startswith("npub1"): + _, decoded_data = bech32_decode(pubkey) + if not decoded_data: + raise ValueError("Public Key is not valid npub") + + decoded_data_bits = convertbits(decoded_data, 5, 8, False) + if not decoded_data_bits: + raise ValueError("Public Key is not valid npub") + return bytes(decoded_data_bits).hex() + + # check if valid hex + if len(pubkey) != 64: + raise ValueError("Public Key is not valid hex") + int(pubkey, 16) + return pubkey diff --git a/models.py b/models.py index 4ed1e30..1456d83 100644 --- a/models.py +++ b/models.py @@ -50,6 +50,16 @@ class Filters(BaseModel): __root__: List[Filter] +class TestMessage(BaseModel): + sender_private_key: Optional[str] + reciever_public_key: str + message: str + +class TestMessageResponse(BaseModel): + private_key: str + public_key: str + event_json: str + # class nostrKeys(BaseModel): # pubkey: str # privkey: str diff --git a/services.py b/services.py index 474bf90..82f6578 100644 --- a/services.py +++ b/services.py @@ -4,6 +4,7 @@ from fastapi import WebSocket, WebSocketDisconnect from loguru import logger + from lnbits.helpers import urlsafe_short_hash from .models import Event, Filter, Filters, Relay, RelayList diff --git a/tasks.py b/tasks.py index ab9a656..beff9db 100644 --- a/tasks.py +++ b/tasks.py @@ -1,6 +1,6 @@ import asyncio -import ssl import json +import ssl import threading from .crud import get_relays @@ -11,8 +11,8 @@ from .services import ( nostr, received_subscription_eosenotices, - received_subscription_notices, received_subscription_events, + received_subscription_notices, ) diff --git a/templates/nostrclient/index.html b/templates/nostrclient/index.html index abe5a87..10fd4a5 100644 --- a/templates/nostrclient/index.html +++ b/templates/nostrclient/index.html @@ -2,6 +2,26 @@ %} {% block page %} {% raw %}
+ + +
+
+ +
+
+ Add relay + +
+
+
+
@@ -42,7 +62,6 @@
Nostrclient
{{ col.label }}
- @@ -76,36 +95,167 @@
Nostrclient
- -
- Your endpoint: - -
-
- - +
-
- -
-
- Add relay +
+
+ Copy address + Your endpoint: + +
- + + + + +
+
+ Sender Private Key: +
+
+ +
+
+
+
+
+ + + No not use your real private key! Leave empty for a randomly + generated key. + +
+
+
+
+ Sender Public Key: +
+
+ +
+
+
+
+ Test Message: +
+
+ +
+
+
+
+ Receiver Public Key: +
+
+ +
+
+
+
+
+ + This is the recipient of the message. Field required. + +
+
+
+
+ Send Message +
+
+
+ + + +
+
+ Sent Data: +
+
+ +
+
+
+
+ Received Data: +
+
+ +
+
+
+
@@ -120,7 +270,6 @@
Nostrclient Extension

- Nostrclient Extension relayToAdd: '', nostrrelayLinks: [], filter: '', + testData: { + show: false, + wsConnection: null, + senderPrivateKey: null, + senderPublicKey: null, + recieverPublicKey: null, + message: null, + sentData: '', + receivedData: '' + }, relayTable: { columns: [ { @@ -273,10 +432,121 @@

Nostrclient Extension
LNbits.utils.notifyApiError(error) }) }, + toggleTestPanel: async function () { + if (this.testData.show) { + await this.hideTestPannel() + } else { + await this.showTestPanel() + } + }, + showTestPanel: async function () { + this.testData = { + show: true, + wsConnection: null, + senderPrivateKey: + this.$q.localStorage.getItem( + 'lnbits.nostrclient.senderPrivateKey' + ) || '', + recieverPublicKey: null, + message: null, + sentData: '', + receivedData: '' + } + await this.closeWebsocket() + this.connectToWebsocket() + }, + hideTestPannel: async function () { + await this.closeWebsocket() + this.testData = { + show: false, + wsConnection: null, + senderPrivateKey: null, + recieverPublicKey: null, + message: null, + sentData: '', + receivedData: '' + } + }, + sendTestMessage: async function () { + try { + const {data} = await LNbits.api.request( + 'PUT', + '/nostrclient/api/v1/relay/test?usr=' + this.g.user.id, + this.g.user.wallets[0].adminkey, + { + sender_private_key: this.testData.senderPrivateKey, + reciever_public_key: this.testData.recieverPublicKey, + message: this.testData.message + } + ) + this.testData.senderPrivateKey = data.private_key + this.$q.localStorage.set( + 'lnbits.nostrclient.senderPrivateKey', + data.private_key || '' + ) + const event = JSON.parse(data.event_json)[1] + this.testData.senderPublicKey = event.pubkey + await this.sendDataToWebSocket(data.event_json) + const subscription = JSON.stringify([ + 'REQ', + 'test-dms', + {kinds: [4], '#p': [event.pubkey]} + ]) + this.testData.wsConnection.send(subscription) + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + + sendDataToWebSocket: async function (data) { + try { + if (!this.testData.wsConnection) { + this.connectToWebsocket() + await this.sleep(500) + } + this.testData.wsConnection.send(data) + const separator = '='.repeat(80) + this.testData.sentData = + data + `\n\n${separator}\n` + this.testData.sentData + } catch (error) { + this.$q.notify({ + timeout: 5000, + type: 'warning', + message: 'Failed to connect to websocket', + caption: `${error}` + }) + } + }, + connectToWebsocket: function () { + const scheme = location.protocol === 'http:' ? 'ws' : 'wss' + const port = location.port ? `:${location.port}` : '' + const wsUrl = `${scheme}://${document.domain}${port}/nostrclient/api/v1/relay` + this.testData.wsConnection = new WebSocket(wsUrl) + const updateReciveData = async e => { + const separator = '='.repeat(80) + this.testData.receivedData = + e.data + `\n\n${separator}\n` + this.testData.receivedData + } + + this.testData.wsConnection.onmessage = updateReciveData + this.testData.wsConnection.onerror = updateReciveData + this.testData.wsConnection.onclose = updateReciveData + }, + closeWebsocket: async function () { + try { + if (this.testData.wsConnection) { + this.testData.wsConnection.close() + await this.sleep(100) + } + } catch (error) { + console.warn(error) + } + }, exportlnurldeviceCSV: function () { var self = this LNbits.utils.exportCSV(self.relayTable.columns, this.nostrLinks) - } + }, + sleep: ms => new Promise(r => setTimeout(r, ms)) }, created: function () { var self = this diff --git a/views.py b/views.py index 4214612..57b73a1 100644 --- a/views.py +++ b/views.py @@ -1,4 +1,4 @@ -from fastapi import Request, Depends +from fastapi import Depends, Request from fastapi.templating import Jinja2Templates from starlette.responses import HTMLResponse diff --git a/views_api.py b/views_api.py index c0be01e..12f4f79 100644 --- a/views_api.py +++ b/views_api.py @@ -1,4 +1,5 @@ import asyncio +import json from http import HTTPStatus from typing import Optional @@ -11,7 +12,9 @@ from . import nostrclient_ext, scheduled_tasks from .crud import add_relay, delete_relay, get_relays -from .models import Relay, RelayList +from .helpers import normalize_public_key +from .models import Relay, RelayList, TestMessage, TestMessageResponse +from .nostr.key import EncryptedDirectMessage, PrivateKey from .services import NostrRouter, nostr from .tasks import init_relays @@ -75,6 +78,36 @@ async def api_delete_relay(relay: Relay) -> None: await delete_relay(relay) +@nostrclient_ext.put( + "/api/v1/relay/test", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)] +) +async def api_test_endpoint(data: TestMessage) -> TestMessageResponse: + try: + to_public_key = normalize_public_key(data.reciever_public_key) + + pk = bytes.fromhex(data.sender_private_key) if data.sender_private_key else None + private_key = PrivateKey(pk) + + dm = EncryptedDirectMessage( + recipient_pubkey=to_public_key, cleartext_content=data.message + ) + private_key.sign_event(dm) + + return TestMessageResponse(private_key=private_key.hex(), public_key=to_public_key, event_json=dm.to_message()) + except (ValueError, AssertionError) as ex: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(ex), + ) + except Exception as ex: + logger.warning(ex) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Cannot generate test event", + ) + + + @nostrclient_ext.delete( "/api/v1", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)] )