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
from typing import List
from fastapi import APIRouter
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]
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

View file

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

View file

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

View file

@ -2,6 +2,26 @@
%} {% block page %} {% raw %}
<div class="row q-col-gutter-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-section>
<div class="row items-center no-wrap q-mb-md">
@ -42,7 +62,6 @@
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div>
</q-th>
<!-- <q-th auto-width></q-th> -->
</q-tr>
</template>
@ -76,36 +95,167 @@
</template>
</q-table>
</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-separator></q-separator>
<q-form class="q-gutter-md q-y-md" @submit="addRelay">
<q-card-section>
<div class="row">
<div class="col q-mx-md q-my-sm">
<q-input
outlined
v-model="relayToAdd"
dense
filled
label="Relay URL"
></q-input>
</div>
<div class="col q-mx-md items-align flex items-center justify-right">
<q-btn unelevated color="primary" type="submit">Add relay </q-btn>
<div class="col">
<div class="text-weight-bold">
<q-btn
flat
dense
size="0.6rem"
class="q-px-none q-mx-none"
color="grey"
icon="content_copy"
@click="copyText(`wss://${host}/nostrclient/api/v1/relay`)"
><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>
</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>
</div>
@ -120,7 +270,6 @@
</p>
<p>
<!-- wss://{{host}}nostrclient/api/v1/relay -->
<q-badge
outline
class="q-ml-sm text-subtitle2"
@ -167,6 +316,16 @@
relayToAdd: '',
nostrrelayLinks: [],
filter: '',
testData: {
show: false,
wsConnection: null,
senderPrivateKey: null,
senderPublicKey: null,
recieverPublicKey: null,
message: null,
sentData: '',
receivedData: ''
},
relayTable: {
columns: [
{
@ -273,10 +432,121 @@
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

View file

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

View file

@ -1,4 +1,5 @@
import asyncio
import json
from http import HTTPStatus
from typing import Optional
@ -11,7 +12,9 @@ from lnbits.helpers import urlsafe_short_hash
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)]
)