Merge pull request #16 from lnbits/add_test_button

Test Endpoint
This commit is contained in:
callebtc 2023-05-08 12:29:53 +02:00 committed by GitHub
commit 91078341c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 366 additions and 32 deletions

View file

@ -1,5 +1,6 @@
import asyncio import asyncio
from typing import List from typing import List
from fastapi import APIRouter from fastapi import APIRouter
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles

19
helpers.py Normal file
View file

@ -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

View file

@ -50,6 +50,16 @@ class Filters(BaseModel):
__root__: List[Filter] __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): # class nostrKeys(BaseModel):
# pubkey: str # pubkey: str
# privkey: str # privkey: str

View file

@ -4,6 +4,7 @@ from typing import List, Union
from fastapi import WebSocket, WebSocketDisconnect from fastapi import WebSocket, WebSocketDisconnect
from loguru import logger from loguru import logger
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from .models import Event, Filter, Filters, Relay, RelayList from .models import Event, Filter, Filters, Relay, RelayList

View file

@ -1,6 +1,6 @@
import asyncio import asyncio
import ssl
import json import json
import ssl
import threading import threading
from .crud import get_relays from .crud import get_relays
@ -11,8 +11,8 @@ from .nostr.relay_manager import RelayManager
from .services import ( from .services import (
nostr, nostr,
received_subscription_eosenotices, received_subscription_eosenotices,
received_subscription_notices,
received_subscription_events, received_subscription_events,
received_subscription_notices,
) )

View file

@ -2,6 +2,26 @@
%} {% block page %} {% raw %} %} {% block page %} {% raw %}
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md"> <div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-form @submit="addRelay">
<div class="row q-pa-md">
<div class="col-9">
<q-input
outlined
v-model="relayToAdd"
dense
filled
label="Relay URL"
></q-input>
</div>
<div class="col-3">
<q-btn unelevated color="primary" class="float-right" type="submit"
>Add relay
</q-btn>
</div>
</div>
</q-form>
</q-card>
<q-card> <q-card>
<q-card-section> <q-card-section>
<div class="row items-center no-wrap q-mb-md"> <div class="row items-center no-wrap q-mb-md">
@ -42,7 +62,6 @@
<div v-if="col.name == 'id'"></div> <div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div> <div v-else>{{ col.label }}</div>
</q-th> </q-th>
<!-- <q-th auto-width></q-th> -->
</q-tr> </q-tr>
</template> </template>
@ -76,36 +95,167 @@
</template> </template>
</q-table> </q-table>
</q-card-section> </q-card-section>
<q-card-section>
<div class="text-weight-bold">
Your endpoint:
<q-badge
outline
class="q-ml-sm text-subtitle2"
color="primary"
:label="`wss://${host}/nostrclient/api/v1/relay`"
/>
</div>
</q-card-section>
</q-card> </q-card>
<q-card> <q-card>
<q-separator></q-separator> <q-card-section>
<q-form class="q-gutter-md q-y-md" @submit="addRelay">
<div class="row"> <div class="row">
<div class="col q-mx-md q-my-sm"> <div class="col">
<q-input <div class="text-weight-bold">
outlined <q-btn
v-model="relayToAdd" flat
dense dense
filled size="0.6rem"
label="Relay URL" class="q-px-none q-mx-none"
></q-input> color="grey"
</div> icon="content_copy"
<div class="col q-mx-md items-align flex items-center justify-right"> @click="copyText(`wss://${host}/nostrclient/api/v1/relay`)"
<q-btn unelevated color="primary" type="submit">Add relay </q-btn> ><q-tooltip>Copy address</q-tooltip></q-btn
>
Your endpoint:
<q-badge
outline
class="q-ml-sm text-subtitle2"
:label="`wss://${host}/nostrclient/api/v1/relay`"
/>
</div>
</div> </div>
</div> </div>
</q-form> </q-card-section>
<q-expansion-item
group="advanced"
icon="settings"
label="Test this endpoint"
@click="toggleTestPanel"
>
<q-separator></q-separator>
<q-card-section>
<div class="row">
<div class="col-3">
<span>Sender Private Key:</span>
</div>
<div class="col-9">
<q-input
outlined
v-model="testData.senderPrivateKey"
dense
filled
label="Private Key (optional)"
></q-input>
</div>
</div>
<div class="row q-mt-sm q-mb-lg">
<div class="col-3"></div>
<div class="col-9">
<q-badge color="yellow" text-color="black">
<span>
No not use your real private key! Leave empty for a randomly
generated key.</span
>
</q-badge>
</div>
</div>
<div v-if="testData.senderPublicKey" class="row">
<div class="col-3">
<span>Sender Public Key:</span>
</div>
<div class="col-9">
<q-input
outlined
v-model="testData.senderPublicKey"
dense
readonly
filled
></q-input>
</div>
</div>
<div class="row q-mt-md">
<div class="col-3">
<span>Test Message:</span>
</div>
<div class="col-9">
<q-input
outlined
v-model="testData.message"
dense
filled
rows="3"
type="textarea"
label="Test Message *"
></q-input>
</div>
</div>
<div class="row q-mt-md">
<div class="col-3">
<span>Receiver Public Key:</span>
</div>
<div class="col-9">
<q-input
outlined
v-model="testData.recieverPublicKey"
dense
filled
label="Public Key (hex or npub) *"
></q-input>
</div>
</div>
<div class="row q-mt-sm q-mb-lg">
<div class="col-3"></div>
<div class="col-9">
<q-badge color="yellow" text-color="black">
<span
>This is the recipient of the message. Field required.</span
>
</q-badge>
</div>
</div>
<div class="row">
<div class="col-12">
<q-btn
:disabled="!testData.recieverPublicKey || !testData.message"
@click="sendTestMessage"
unelevated
color="primary"
class="float-right"
>Send Message</q-btn
>
</div>
</div>
</q-card-section>
<q-separator></q-separator>
<q-card-section>
<div class="row q-mt-md">
<div class="col-3">
<span>Sent Data:</span>
</div>
<div class="col-9">
<q-input
outlined
v-model="testData.sentData"
dense
filled
rows="5"
type="textarea"
></q-input>
</div>
</div>
<div class="row q-mt-md">
<div class="col-3">
<span>Received Data:</span>
</div>
<div class="col-9">
<q-input
outlined
v-model="testData.receivedData"
dense
filled
rows="5"
type="textarea"
></q-input>
</div>
</div>
</q-card-section>
</q-expansion-item>
</q-card> </q-card>
</div> </div>
@ -120,7 +270,6 @@
</p> </p>
<p> <p>
<!-- wss://{{host}}nostrclient/api/v1/relay -->
<q-badge <q-badge
outline outline
class="q-ml-sm text-subtitle2" class="q-ml-sm text-subtitle2"
@ -167,6 +316,16 @@
relayToAdd: '', relayToAdd: '',
nostrrelayLinks: [], nostrrelayLinks: [],
filter: '', filter: '',
testData: {
show: false,
wsConnection: null,
senderPrivateKey: null,
senderPublicKey: null,
recieverPublicKey: null,
message: null,
sentData: '',
receivedData: ''
},
relayTable: { relayTable: {
columns: [ columns: [
{ {
@ -273,10 +432,121 @@
LNbits.utils.notifyApiError(error) 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 () { exportlnurldeviceCSV: function () {
var self = this var self = this
LNbits.utils.exportCSV(self.relayTable.columns, this.nostrLinks) LNbits.utils.exportCSV(self.relayTable.columns, this.nostrLinks)
} },
sleep: ms => new Promise(r => setTimeout(r, ms))
}, },
created: function () { created: function () {
var self = this var self = this

View file

@ -1,4 +1,4 @@
from fastapi import Request, Depends from fastapi import Depends, Request
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse

View file

@ -1,4 +1,5 @@
import asyncio import asyncio
import json
from http import HTTPStatus from http import HTTPStatus
from typing import Optional from typing import Optional
@ -11,7 +12,9 @@ from lnbits.helpers import urlsafe_short_hash
from . import nostrclient_ext, scheduled_tasks from . import nostrclient_ext, scheduled_tasks
from .crud import add_relay, delete_relay, get_relays 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 .services import NostrRouter, nostr
from .tasks import init_relays from .tasks import init_relays
@ -75,6 +78,36 @@ async def api_delete_relay(relay: Relay) -> None:
await delete_relay(relay) 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( @nostrclient_ext.delete(
"/api/v1", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)] "/api/v1", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
) )