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 %}
-
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)]
)