commit
91078341c7
8 changed files with 366 additions and 32 deletions
|
|
@ -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
19
helpers.py
Normal 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
|
||||||
10
models.py
10
models.py
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
4
tasks.py
4
tasks.py
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
2
views.py
2
views.py
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
35
views_api.py
35
views_api.py
|
|
@ -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)]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue