init
This commit is contained in:
commit
69bf22c9ec
21 changed files with 778 additions and 0 deletions
3
README.md
Normal file
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Nostr
|
||||||
|
|
||||||
|
Opens a Nostr daemon
|
||||||
36
__init__.py
Normal file
36
__init__.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from starlette.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from lnbits.db import Database
|
||||||
|
from lnbits.helpers import template_renderer
|
||||||
|
from lnbits.tasks import catch_everything_and_restart
|
||||||
|
|
||||||
|
db = Database("ext_nostrclient")
|
||||||
|
|
||||||
|
nostrclient_static_files = [
|
||||||
|
{
|
||||||
|
"path": "/nostrclient/static",
|
||||||
|
"app": StaticFiles(directory="lnbits/extensions/nostrclient/static"),
|
||||||
|
"name": "nostrclient_static",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
nostrclient_ext: APIRouter = APIRouter(prefix="/nostrclient", tags=["nostrclient"])
|
||||||
|
|
||||||
|
|
||||||
|
def nostr_renderer():
|
||||||
|
return template_renderer(["lnbits/extensions/nostrclient/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
from .views import * # noqa
|
||||||
|
from .views_api import * # noqa
|
||||||
|
|
||||||
|
from .tasks import init_relays, subscribe_events
|
||||||
|
|
||||||
|
|
||||||
|
def nostrclient_start():
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(catch_everything_and_restart(init_relays))
|
||||||
|
# loop.create_task(catch_everything_and_restart(send_data))
|
||||||
|
# loop.create_task(catch_everything_and_restart(receive_data))
|
||||||
|
loop.create_task(catch_everything_and_restart(subscribe_events))
|
||||||
BIN
__pycache__/__init__.cpython-39.pyc
Normal file
BIN
__pycache__/__init__.cpython-39.pyc
Normal file
Binary file not shown.
BIN
__pycache__/crud.cpython-39.pyc
Normal file
BIN
__pycache__/crud.cpython-39.pyc
Normal file
Binary file not shown.
BIN
__pycache__/migrations.cpython-39.pyc
Normal file
BIN
__pycache__/migrations.cpython-39.pyc
Normal file
Binary file not shown.
BIN
__pycache__/models.cpython-39.pyc
Normal file
BIN
__pycache__/models.cpython-39.pyc
Normal file
Binary file not shown.
BIN
__pycache__/tasks.cpython-39.pyc
Normal file
BIN
__pycache__/tasks.cpython-39.pyc
Normal file
Binary file not shown.
BIN
__pycache__/views.cpython-39.pyc
Normal file
BIN
__pycache__/views.cpython-39.pyc
Normal file
Binary file not shown.
BIN
__pycache__/views_api.cpython-39.pyc
Normal file
BIN
__pycache__/views_api.cpython-39.pyc
Normal file
Binary file not shown.
26
cbc.py
Normal file
26
cbc.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from Cryptodome.Cipher import AES
|
||||||
|
|
||||||
|
BLOCK_SIZE = 16
|
||||||
|
|
||||||
|
|
||||||
|
class AESCipher(object):
|
||||||
|
"""This class is compatible with crypto.createCipheriv('aes-256-cbc')"""
|
||||||
|
|
||||||
|
def __init__(self, key=None):
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
def pad(self, data):
|
||||||
|
length = BLOCK_SIZE - (len(data) % BLOCK_SIZE)
|
||||||
|
return data + (chr(length) * length).encode()
|
||||||
|
|
||||||
|
def unpad(self, data):
|
||||||
|
return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))]
|
||||||
|
|
||||||
|
def encrypt(self, plain_text):
|
||||||
|
cipher = AES.new(self.key, AES.MODE_CBC)
|
||||||
|
b = plain_text.encode("UTF-8")
|
||||||
|
return cipher.iv, cipher.encrypt(self.pad(b))
|
||||||
|
|
||||||
|
def decrypt(self, iv, enc_text):
|
||||||
|
cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
|
||||||
|
return self.unpad(cipher.decrypt(enc_text).decode("UTF-8"))
|
||||||
6
config.json
Normal file
6
config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Nostr Client",
|
||||||
|
"short_description": "Nostr client for extensions",
|
||||||
|
"tile": "/nostr-client/static/images/nostr-bitcoin.png",
|
||||||
|
"contributors": ["calle"]
|
||||||
|
}
|
||||||
29
crud.py
Normal file
29
crud.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
import shortuuid
|
||||||
|
from . import db
|
||||||
|
from .models import Relay, RelayList
|
||||||
|
|
||||||
|
|
||||||
|
async def get_relays() -> RelayList:
|
||||||
|
row = await db.fetchall("SELECT * FROM nostradmin.relays")
|
||||||
|
return RelayList(__root__=row)
|
||||||
|
|
||||||
|
|
||||||
|
async def add_relay(relay: Relay) -> None:
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO nostradmin.relays (
|
||||||
|
id,
|
||||||
|
url,
|
||||||
|
active
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(relay.id, relay.url, relay.active),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_relay(relay: Relay) -> None:
|
||||||
|
await db.execute("DELETE FROM nostradmin.relays WHERE id = ?", (relay.id,))
|
||||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"repos": [
|
||||||
|
{
|
||||||
|
"id": "nostr-client",
|
||||||
|
"organisation": "lnbits",
|
||||||
|
"repository": "nostr-client-extension"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
migrations.py
Normal file
13
migrations.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
async def m001_initial(db):
|
||||||
|
"""
|
||||||
|
Initial nostrclient table.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE nostrclient.relays (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
active BOOLEAN DEFAULT true
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
92
models.py
Normal file
92
models.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
from typing import List, Dict
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
|
|
||||||
|
class Relay(BaseModel):
|
||||||
|
id: Optional[str] = None
|
||||||
|
url: Optional[str] = None
|
||||||
|
connected: Optional[bool] = None
|
||||||
|
connected_string: Optional[str] = None
|
||||||
|
status: Optional[str] = None
|
||||||
|
active: Optional[bool] = None
|
||||||
|
ping: Optional[int] = None
|
||||||
|
|
||||||
|
def _init__(self):
|
||||||
|
if not self.id:
|
||||||
|
self.id = urlsafe_short_hash()
|
||||||
|
|
||||||
|
|
||||||
|
class RelayList(BaseModel):
|
||||||
|
__root__: List[Relay]
|
||||||
|
|
||||||
|
|
||||||
|
class Event(BaseModel):
|
||||||
|
content: str
|
||||||
|
pubkey: str
|
||||||
|
created_at: Optional[int]
|
||||||
|
kind: int
|
||||||
|
tags: Optional[List[List[str]]]
|
||||||
|
sig: str
|
||||||
|
|
||||||
|
|
||||||
|
class Filter(BaseModel):
|
||||||
|
ids: Optional[List[str]]
|
||||||
|
kinds: Optional[List[int]]
|
||||||
|
authors: Optional[List[str]]
|
||||||
|
since: Optional[int]
|
||||||
|
until: Optional[int]
|
||||||
|
e: Optional[List[str]] = Field(alias="#e")
|
||||||
|
p: Optional[List[str]] = Field(alias="#p")
|
||||||
|
limit: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class Filters(BaseModel):
|
||||||
|
__root__: List[Filter]
|
||||||
|
|
||||||
|
|
||||||
|
# class nostrKeys(BaseModel):
|
||||||
|
# pubkey: str
|
||||||
|
# privkey: str
|
||||||
|
|
||||||
|
# class nostrNotes(BaseModel):
|
||||||
|
# id: str
|
||||||
|
# pubkey: str
|
||||||
|
# created_at: str
|
||||||
|
# kind: int
|
||||||
|
# tags: str
|
||||||
|
# content: str
|
||||||
|
# sig: str
|
||||||
|
|
||||||
|
# class nostrCreateRelays(BaseModel):
|
||||||
|
# relay: str = Query(None)
|
||||||
|
|
||||||
|
# class nostrCreateConnections(BaseModel):
|
||||||
|
# pubkey: str = Query(None)
|
||||||
|
# relayid: str = Query(None)
|
||||||
|
|
||||||
|
# class nostrRelays(BaseModel):
|
||||||
|
# id: Optional[str]
|
||||||
|
# relay: Optional[str]
|
||||||
|
# status: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
|
# class nostrRelaySetList(BaseModel):
|
||||||
|
# allowlist: Optional[str]
|
||||||
|
# denylist: Optional[str]
|
||||||
|
|
||||||
|
# class nostrConnections(BaseModel):
|
||||||
|
# id: str
|
||||||
|
# pubkey: Optional[str]
|
||||||
|
# relayid: Optional[str]
|
||||||
|
|
||||||
|
# class nostrSubscriptions(BaseModel):
|
||||||
|
# id: str
|
||||||
|
# userPubkey: Optional[str]
|
||||||
|
# subscribedPubkey: Optional[str]
|
||||||
1
nostr
Submodule
1
nostr
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit f598039e440f1d57c3b5d993ff44473649ffac3d
|
||||||
BIN
static/images/nostr-bitcoin.png
Normal file
BIN
static/images/nostr-bitcoin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
88
tasks.py
Normal file
88
tasks.py
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import asyncio
|
||||||
|
import ssl
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from .nostr.nostr.client.client import NostrClient
|
||||||
|
from .nostr.nostr.event import Event
|
||||||
|
from .nostr.nostr.key import PublicKey
|
||||||
|
from .nostr.nostr.relay_manager import RelayManager
|
||||||
|
|
||||||
|
# relays = [
|
||||||
|
# "wss://nostr.mom",
|
||||||
|
# "wss://nostr-pub.wellorder.net",
|
||||||
|
# "wss://nostr.zebedee.cloud",
|
||||||
|
# "wss://relay.damus.io",
|
||||||
|
# "wss://relay.nostr.info",
|
||||||
|
# "wss://nostr.onsats.org",
|
||||||
|
# "wss://nostr-relay.untethr.me",
|
||||||
|
# "wss://relay.snort.social",
|
||||||
|
# "wss://lnbits.link/nostrrelay/client",
|
||||||
|
# ]
|
||||||
|
client = NostrClient(
|
||||||
|
connect=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# client = NostrClient(
|
||||||
|
# connect=False,
|
||||||
|
# privatekey_hex="211aac75a687ad96cca402406f8147a2726e31c5fc838e22ce30640ca1f3a6fe",
|
||||||
|
# )
|
||||||
|
|
||||||
|
received_event_queue: asyncio.Queue[Event] = asyncio.Queue(0)
|
||||||
|
|
||||||
|
from .crud import get_relays
|
||||||
|
|
||||||
|
|
||||||
|
async def init_relays():
|
||||||
|
relays = await get_relays()
|
||||||
|
client.relays = [r.url for r in relays.__root__]
|
||||||
|
client.connect()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# async def send_data():
|
||||||
|
# while not any([r.connected for r in client.relay_manager.relays.values()]):
|
||||||
|
# print("no relays connected yet")
|
||||||
|
# await asyncio.sleep(0.5)
|
||||||
|
# while True:
|
||||||
|
# client.dm("test", PublicKey(bytes.fromhex(client.public_key.hex())))
|
||||||
|
# print("sent DM")
|
||||||
|
# await asyncio.sleep(5)
|
||||||
|
# return
|
||||||
|
|
||||||
|
|
||||||
|
# async def receive_data():
|
||||||
|
# while not any([r.connected for r in client.relay_manager.relays.values()]):
|
||||||
|
# print("no relays connected yet")
|
||||||
|
# await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# def callback(event: Event, decrypted_content=None):
|
||||||
|
# print(
|
||||||
|
# f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content or event.content}"
|
||||||
|
# )
|
||||||
|
|
||||||
|
# t = threading.Thread(
|
||||||
|
# target=client.get_dm,
|
||||||
|
# args=(
|
||||||
|
# client.public_key,
|
||||||
|
# callback,
|
||||||
|
# ),
|
||||||
|
# name="Nostr DM",
|
||||||
|
# )
|
||||||
|
# t.start()
|
||||||
|
|
||||||
|
|
||||||
|
async def subscribe_events():
|
||||||
|
while not any([r.connected for r in client.relay_manager.relays.values()]):
|
||||||
|
print("no relays connected yet")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
def callback(event: Event):
|
||||||
|
print(f"From {event.public_key[:3]}..{event.public_key[-3:]}: {event.content}")
|
||||||
|
asyncio.run(received_event_queue.put(event))
|
||||||
|
|
||||||
|
t = threading.Thread(
|
||||||
|
target=client.subscribe,
|
||||||
|
args=(callback,),
|
||||||
|
name="Nostr-event-subscription",
|
||||||
|
)
|
||||||
|
t.start()
|
||||||
257
templates/nostr-client/index.html
Normal file
257
templates/nostr-client/index.html
Normal file
|
|
@ -0,0 +1,257 @@
|
||||||
|
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
|
||||||
|
%} {% block page %}
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<div class="row items-center no-wrap q-mb-md">
|
||||||
|
<div class="col">
|
||||||
|
<h5 class="text-subtitle1 q-my-none">nostr relays</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-auto">
|
||||||
|
<q-input
|
||||||
|
borderless
|
||||||
|
dense
|
||||||
|
debounce="300"
|
||||||
|
v-model="filter"
|
||||||
|
placeholder="Search"
|
||||||
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon name="search"></q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<q-table
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
:data="nostrrelayLinks"
|
||||||
|
row-key="id"
|
||||||
|
:columns="relayTable.columns"
|
||||||
|
:pagination.sync="relayTable.pagination"
|
||||||
|
:filter="filter"
|
||||||
|
>
|
||||||
|
{% raw %}
|
||||||
|
<template v-slot:header="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-th
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
auto-width
|
||||||
|
>
|
||||||
|
<div v-if="col.name == 'id'"></div>
|
||||||
|
<div v-else>{{ col.label }}</div>
|
||||||
|
</q-th>
|
||||||
|
<!-- <q-th auto-width></q-th> -->
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:body="props">
|
||||||
|
<q-tr :props="props">
|
||||||
|
<q-td
|
||||||
|
v-for="col in props.cols"
|
||||||
|
:key="col.name"
|
||||||
|
:props="props"
|
||||||
|
auto-width
|
||||||
|
>
|
||||||
|
<div v-if="col.name == 'id'"></div>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="col.value == true" style="background-color: green">
|
||||||
|
{{ col.value }}
|
||||||
|
</div>
|
||||||
|
<div v-else>{{ col.value }}</div>
|
||||||
|
</div>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="xs"
|
||||||
|
@click="deleteRelay(props.row.id)"
|
||||||
|
icon="cancel"
|
||||||
|
color="pink"
|
||||||
|
></q-btn>
|
||||||
|
</q-tr>
|
||||||
|
</template>
|
||||||
|
{% endraw %}
|
||||||
|
</q-table>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
<q-card>
|
||||||
|
<!-- <q-tabs
|
||||||
|
v-model="listSelection"
|
||||||
|
dense
|
||||||
|
class="text-grey"
|
||||||
|
active-color="primary"
|
||||||
|
indicator-color="primary"
|
||||||
|
align="justify"
|
||||||
|
narrow-indicator
|
||||||
|
>
|
||||||
|
<q-tab name="denylist" label="Deny List"></q-tab>
|
||||||
|
<q-tab name="allowlist" label="Allow List"></q-tab>
|
||||||
|
</q-tabs> -->
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-form class="q-gutter-md q-y-md" @submit="addRelay">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col q-mx-lg q-my-sm">
|
||||||
|
<q-input v-model="relayToAdd" dense filled autogrow></q-input>
|
||||||
|
</div>
|
||||||
|
<div class="col q-mx-lg items-align flex items-center justify-right">
|
||||||
|
<q-btn unelevated color="primary" type="submit"> Add relay </q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-form>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Nostr Extension</h6>
|
||||||
|
<p>Only Admin users can manage this extension</p>
|
||||||
|
|
||||||
|
<q-card-section></q-card-section>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
var maplrelays = obj => {
|
||||||
|
obj._data = _.clone(obj)
|
||||||
|
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
|
||||||
|
obj.time = obj.time + 'mins'
|
||||||
|
|
||||||
|
obj.ping = obj.ping + ' ms'
|
||||||
|
|
||||||
|
if (obj.time_elapsed) {
|
||||||
|
obj.date = 'Time elapsed'
|
||||||
|
} else {
|
||||||
|
obj.date = Quasar.utils.date.formatDate(
|
||||||
|
new Date((obj.theTime - 3600) * 1000),
|
||||||
|
'HH:mm:ss'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
relayToAdd: '',
|
||||||
|
nostrrelayLinks: [],
|
||||||
|
filter: '',
|
||||||
|
relayTable: {
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'connected_string',
|
||||||
|
align: 'left',
|
||||||
|
label: '',
|
||||||
|
field: 'connected_string'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'relay',
|
||||||
|
align: 'left',
|
||||||
|
label: 'URL',
|
||||||
|
field: 'url'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
align: 'center',
|
||||||
|
label: 'Status',
|
||||||
|
field: 'status'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ping',
|
||||||
|
align: 'center',
|
||||||
|
label: 'Ping',
|
||||||
|
field: 'ping'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
rowsPerPage: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getRelays: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/nostradmin/api/v1/relays',
|
||||||
|
self.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data) {
|
||||||
|
console.log(response.data)
|
||||||
|
response.data.map(maplrelays)
|
||||||
|
self.nostrrelayLinks = response.data
|
||||||
|
console.log(self.nostrrelayLinks)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addRelay: function () {
|
||||||
|
console.log("ADD RELAY " + this.relayToAdd)
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'POST',
|
||||||
|
'/nostradmin/api/v1/relay',
|
||||||
|
self.g.user.wallets[0].adminkey,
|
||||||
|
{url:this.relayToAdd},
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data) {
|
||||||
|
console.log(response.data)
|
||||||
|
response.data.map(maplrelays)
|
||||||
|
self.nostrrelayLinks = response.data
|
||||||
|
console.log(self.nostrrelayLinks)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteRelay: function (relay_id) {
|
||||||
|
console.log("DELETE RELAY " + relay_id)
|
||||||
|
var self = this
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'DELETE',
|
||||||
|
'/nostradmin/api/v1/relay',
|
||||||
|
self.g.user.wallets[0].adminkey,
|
||||||
|
{id:relay_id},
|
||||||
|
)
|
||||||
|
.then(function (response) {
|
||||||
|
if (response.data) {
|
||||||
|
console.log(response.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
LNbits.utils.notifyApiError(error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
exportlnurldeviceCSV: function () {
|
||||||
|
var self = this
|
||||||
|
LNbits.utils.exportCSV(self.relayTable.columns, this.nostrLinks)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
var self = this
|
||||||
|
this.getRelays()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
100
views.py
Normal file
100
views.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
import asyncio
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from starlette.responses import HTMLResponse
|
||||||
|
from . import nostrclient_ext, nostr_renderer
|
||||||
|
|
||||||
|
# FastAPI good for incoming
|
||||||
|
from fastapi import Request
|
||||||
|
from lnbits.core.crud import update_payment_status
|
||||||
|
from lnbits.core.models import User
|
||||||
|
from lnbits.core.views.api import api_payment
|
||||||
|
from lnbits.decorators import check_user_exists, check_admin
|
||||||
|
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
|
@nostrclient_ext.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request, user: User = Depends(check_admin)):
|
||||||
|
return nostr_renderer().TemplateResponse(
|
||||||
|
"nostrclient/index.html", {"request": request, "user": user.dict()}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# #####################################################################
|
||||||
|
# #################### NOSTR WEBSOCKET THREAD #########################
|
||||||
|
# ##### THE QUEUE LOOP THREAD THING THAT LISTENS TO BUNCH OF ##########
|
||||||
|
# ### WEBSOCKET CONNECTIONS, STORING DATA IN DB/PUSHING TO FRONTEND ###
|
||||||
|
# ################### VIA updater() FUNCTION ##########################
|
||||||
|
# #####################################################################
|
||||||
|
|
||||||
|
# websocket_queue = asyncio.Queue(1000)
|
||||||
|
|
||||||
|
# # while True:
|
||||||
|
# async def nostr_subscribe():
|
||||||
|
# return
|
||||||
|
# # for the relays:
|
||||||
|
# # async with websockets.connect("ws://localhost:8765") as websocket:
|
||||||
|
# # for the public keys:
|
||||||
|
# # await websocket.send("subscribe to events")
|
||||||
|
# # await websocket.recv()
|
||||||
|
|
||||||
|
# #####################################################################
|
||||||
|
# ################### LNBITS WEBSOCKET ROUTES #########################
|
||||||
|
# #### HERE IS WHERE LNBITS FRONTEND CAN RECEIVE AND SEND MESSAGES ####
|
||||||
|
# #####################################################################
|
||||||
|
|
||||||
|
# class ConnectionManager:
|
||||||
|
# def __init__(self):
|
||||||
|
# self.active_connections: List[WebSocket] = []
|
||||||
|
|
||||||
|
# async def connect(self, websocket: WebSocket, nostr_id: str):
|
||||||
|
# await websocket.accept()
|
||||||
|
# websocket.id = nostr_id
|
||||||
|
# self.active_connections.append(websocket)
|
||||||
|
|
||||||
|
# def disconnect(self, websocket: WebSocket):
|
||||||
|
# self.active_connections.remove(websocket)
|
||||||
|
|
||||||
|
# async def send_personal_message(self, message: str, nostr_id: str):
|
||||||
|
# for connection in self.active_connections:
|
||||||
|
# if connection.id == nostr_id:
|
||||||
|
# await connection.send_text(message)
|
||||||
|
|
||||||
|
# async def broadcast(self, message: str):
|
||||||
|
# for connection in self.active_connections:
|
||||||
|
# await connection.send_text(message)
|
||||||
|
|
||||||
|
|
||||||
|
# manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
|
# @nostrclient_ext.websocket("/nostrclient/ws/relayevents/{nostr_id}", name="nostr_id.websocket_by_id")
|
||||||
|
# async def websocket_endpoint(websocket: WebSocket, nostr_id: str):
|
||||||
|
# await manager.connect(websocket, nostr_id)
|
||||||
|
# try:
|
||||||
|
# while True:
|
||||||
|
# data = await websocket.receive_text()
|
||||||
|
# except WebSocketDisconnect:
|
||||||
|
# manager.disconnect(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
# async def updater(nostr_id, message):
|
||||||
|
# copilot = await get_copilot(nostr_id)
|
||||||
|
# if not copilot:
|
||||||
|
# return
|
||||||
|
# await manager.send_personal_message(f"{message}", nostr_id)
|
||||||
|
|
||||||
|
|
||||||
|
# async def relay_check(relay: str):
|
||||||
|
# async with websockets.connect(relay) as websocket:
|
||||||
|
# if str(websocket.state) == "State.OPEN":
|
||||||
|
# print(str(websocket.state))
|
||||||
|
# return True
|
||||||
|
# else:
|
||||||
|
# return False
|
||||||
118
views_api.py
Normal file
118
views_api.py
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
import asyncio
|
||||||
|
import ssl
|
||||||
|
import json
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.param_functions import Query
|
||||||
|
from fastapi.params import Depends
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from sse_starlette.sse import EventSourceResponse
|
||||||
|
|
||||||
|
from . import nostrclient_ext
|
||||||
|
|
||||||
|
from .tasks import client, received_event_queue
|
||||||
|
|
||||||
|
from .crud import get_relays, add_relay, delete_relay
|
||||||
|
from .models import RelayList, Relay, Event, Filter, Filters
|
||||||
|
|
||||||
|
from .nostr.nostr.event import Event as NostrEvent
|
||||||
|
from .nostr.nostr.event import EncryptedDirectMessage
|
||||||
|
from .nostr.nostr.filter import Filter as NostrFilter
|
||||||
|
from .nostr.nostr.filter import Filters as NostrFilters
|
||||||
|
from .nostr.nostr.message_type import ClientMessageType
|
||||||
|
|
||||||
|
from lnbits.decorators import (
|
||||||
|
WalletTypeInfo,
|
||||||
|
get_key_type,
|
||||||
|
require_admin_key,
|
||||||
|
check_admin,
|
||||||
|
)
|
||||||
|
|
||||||
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
from .tasks import init_relays
|
||||||
|
|
||||||
|
|
||||||
|
@nostrclient_ext.get("/api/v1/relays")
|
||||||
|
async def api_get_relays(): # type: ignore
|
||||||
|
relays = RelayList(__root__=[])
|
||||||
|
for url, r in client.relay_manager.relays.items():
|
||||||
|
status_text = (
|
||||||
|
f"⬆️ {r.num_sent_events} ⬇️ {r.num_received_events} ⚠️ {r.error_counter}"
|
||||||
|
)
|
||||||
|
connected_text = "🟢" if r.connected else "🔴"
|
||||||
|
relay_id = urlsafe_short_hash()
|
||||||
|
relays.__root__.append(
|
||||||
|
Relay(
|
||||||
|
id=relay_id,
|
||||||
|
url=url,
|
||||||
|
connected_string=connected_text,
|
||||||
|
status=status_text,
|
||||||
|
ping=r.ping,
|
||||||
|
connected=True,
|
||||||
|
active=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return relays
|
||||||
|
|
||||||
|
|
||||||
|
@nostrclient_ext.post("/api/v1/relay")
|
||||||
|
async def api_add_relay(relay: Relay): # type: ignore
|
||||||
|
assert relay.url, "no URL"
|
||||||
|
relay.id = urlsafe_short_hash()
|
||||||
|
await add_relay(relay)
|
||||||
|
await init_relays()
|
||||||
|
|
||||||
|
|
||||||
|
@nostrclient_ext.delete("/api/v1/relay")
|
||||||
|
async def api_delete_relay(relay: Relay): # type: ignore
|
||||||
|
await delete_relay(relay)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrclient_ext.post("/api/v1/publish")
|
||||||
|
async def api_post_event(event: Event):
|
||||||
|
nostr_event = NostrEvent(
|
||||||
|
content=event.content,
|
||||||
|
public_key=event.pubkey,
|
||||||
|
created_at=event.created_at, # type: ignore
|
||||||
|
kind=event.kind,
|
||||||
|
tags=event.tags or None, # type: ignore
|
||||||
|
signature=event.sig,
|
||||||
|
)
|
||||||
|
client.relay_manager.publish_event(nostr_event)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrclient_ext.post("/api/v1/filter")
|
||||||
|
async def api_subscribe(filter: Filter):
|
||||||
|
nostr_filter = NostrFilter(
|
||||||
|
event_ids=filter.ids,
|
||||||
|
kinds=filter.kinds, # type: ignore
|
||||||
|
authors=filter.authors,
|
||||||
|
since=filter.since,
|
||||||
|
until=filter.until,
|
||||||
|
event_refs=filter.e,
|
||||||
|
pubkey_refs=filter.p,
|
||||||
|
limit=filter.limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
filters = NostrFilters([nostr_filter])
|
||||||
|
subscription_id = urlsafe_short_hash()
|
||||||
|
client.relay_manager.add_subscription(subscription_id, filters)
|
||||||
|
|
||||||
|
request = [ClientMessageType.REQUEST, subscription_id]
|
||||||
|
request.extend(filters.to_json_array())
|
||||||
|
message = json.dumps(request)
|
||||||
|
client.relay_manager.publish_message(message)
|
||||||
|
|
||||||
|
async def event_getter():
|
||||||
|
while True:
|
||||||
|
event = await received_event_queue.get()
|
||||||
|
if filters.match(event):
|
||||||
|
yield event.to_message()
|
||||||
|
|
||||||
|
return EventSourceResponse(
|
||||||
|
event_getter(),
|
||||||
|
ping=20,
|
||||||
|
media_type="text/event-stream",
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue