feat: extracted
This commit is contained in:
parent
462770be40
commit
4b82905f78
12 changed files with 573 additions and 0 deletions
12
README.md
Normal file
12
README.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Nostr Relay
|
||||||
|
|
||||||
|
## One click and spin up your own Nostr relay. Share with the world, or use privately.
|
||||||
|
|
||||||
|
A simple UI wrapper for the great python relay library <a href="https://code.pobblelabs.org/fossil/nostr_relay/">nostr_relay</a>.
|
||||||
|
|
||||||
|
UI for diagnostics and management (key alow/ban lists, rate limiting) coming soon!
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
1. Enable extension
|
||||||
|
2. Enable relay
|
||||||
27
__init__.py
Normal file
27
__init__.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from lnbits.db import Database
|
||||||
|
from lnbits.helpers import template_renderer
|
||||||
|
|
||||||
|
db = Database("ext_nostrrelay")
|
||||||
|
|
||||||
|
nostrrelay_ext: APIRouter = APIRouter(prefix="/nostrrelay", tags=["NostrRelay"])
|
||||||
|
|
||||||
|
nostrrelay_static_files = [
|
||||||
|
{
|
||||||
|
"path": "/nostrrelay/static",
|
||||||
|
"app": StaticFiles(directory="lnbits/extensions/nostrrelay/static"),
|
||||||
|
"name": "nostrrelay_static",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def nostrrelay_renderer():
|
||||||
|
return template_renderer(["lnbits/extensions/nostrrelay/templates"])
|
||||||
|
|
||||||
|
|
||||||
|
from .views import * # noqa
|
||||||
|
from .views_api import * # noqa
|
||||||
94
client_manager.py
Normal file
94
client_manager.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
import json
|
||||||
|
from typing import Callable, List
|
||||||
|
|
||||||
|
from fastapi import WebSocket
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from .crud import create_event, get_events
|
||||||
|
from .models import NostrEvent, NostrEventType, NostrFilter
|
||||||
|
|
||||||
|
|
||||||
|
class NostrClientManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.clients: List["NostrClientConnection"] = []
|
||||||
|
|
||||||
|
def add_client(self, client: "NostrClientConnection"):
|
||||||
|
setattr(client, "broadcast_event", self.broadcast_event)
|
||||||
|
self.clients.append(client)
|
||||||
|
print("### client count:", len(self.clients))
|
||||||
|
|
||||||
|
def remove_client(self, client: "NostrClientConnection"):
|
||||||
|
self.clients.remove(client)
|
||||||
|
|
||||||
|
async def broadcast_event(self, source: "NostrClientConnection", event: NostrEvent):
|
||||||
|
print("### broadcast_event", len(self.clients))
|
||||||
|
for client in self.clients:
|
||||||
|
if client != source:
|
||||||
|
await client.notify_event(event)
|
||||||
|
|
||||||
|
|
||||||
|
class NostrClientConnection:
|
||||||
|
broadcast_event: Callable
|
||||||
|
|
||||||
|
def __init__(self, websocket: WebSocket):
|
||||||
|
self.websocket = websocket
|
||||||
|
self.filters: List[NostrFilter] = []
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
await self.websocket.accept()
|
||||||
|
while True:
|
||||||
|
json_data = await self.websocket.receive_text()
|
||||||
|
try:
|
||||||
|
data = json.loads(json_data)
|
||||||
|
|
||||||
|
resp = await self.__handle_message(data)
|
||||||
|
if resp:
|
||||||
|
for r in resp:
|
||||||
|
# print("### start send content: ", json.dumps(r))
|
||||||
|
await self.websocket.send_text(json.dumps(r))
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
|
||||||
|
async def notify_event(self, event: NostrEvent):
|
||||||
|
for filter in self.filters:
|
||||||
|
if filter.matches(event):
|
||||||
|
r = [NostrEventType.EVENT, filter.subscription_id, dict(event)]
|
||||||
|
print("### notify send content: ", json.dumps(r))
|
||||||
|
await self.websocket.send_text(json.dumps(r))
|
||||||
|
|
||||||
|
async def __handle_message(self, data: List):
|
||||||
|
if len(data) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
message_type = data[0]
|
||||||
|
if message_type == NostrEventType.EVENT:
|
||||||
|
return await self.__handle_event(NostrEvent.parse_obj(data[1]))
|
||||||
|
if message_type == NostrEventType.REQ:
|
||||||
|
if len(data) != 3:
|
||||||
|
return
|
||||||
|
return await self.__handle_request(data[1], NostrFilter.parse_obj(data[2]))
|
||||||
|
if message_type == NostrEventType.CLOSE:
|
||||||
|
return self.__handle_close(data[1])
|
||||||
|
|
||||||
|
async def __handle_event(self, e: "NostrEvent") -> None:
|
||||||
|
# print('### __handle_event', e)
|
||||||
|
e.check_signature()
|
||||||
|
await create_event("111", e)
|
||||||
|
await self.broadcast_event(self, e)
|
||||||
|
|
||||||
|
async def __handle_request(self, subscription_id: str, filter: NostrFilter) -> List:
|
||||||
|
filter.subscription_id = subscription_id
|
||||||
|
self.remove_filter(subscription_id)
|
||||||
|
self.filters.append(filter)
|
||||||
|
events = await get_events("111", filter)
|
||||||
|
return [
|
||||||
|
[NostrEventType.EVENT, subscription_id, dict(event)] for event in events
|
||||||
|
]
|
||||||
|
|
||||||
|
def __handle_close(self, subscription_id: str) -> None:
|
||||||
|
print("### __handle_close", len(self.filters), subscription_id)
|
||||||
|
self.remove_filter(subscription_id)
|
||||||
|
print("### __handle_close", len(self.filters))
|
||||||
|
|
||||||
|
def remove_filter(self, subscription_id: str):
|
||||||
|
self.filters = [f for f in self.filters if f.subscription_id != subscription_id]
|
||||||
6
config.json
Normal file
6
config.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "Nostr Relay",
|
||||||
|
"short_description": "One click launch your own relay!",
|
||||||
|
"tile": "/nostrrelay/static/image/nostrrelay.png",
|
||||||
|
"contributors": ["arcbtc", "DCs"]
|
||||||
|
}
|
||||||
79
crud.py
Normal file
79
crud.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
from typing import Any, List
|
||||||
|
|
||||||
|
from . import db
|
||||||
|
from .models import NostrEvent, NostrFilter
|
||||||
|
|
||||||
|
|
||||||
|
async def create_event(relay_id: str, e: NostrEvent):
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO nostrrelay.events (
|
||||||
|
relay_id,
|
||||||
|
id,
|
||||||
|
pubkey,
|
||||||
|
created_at,
|
||||||
|
kind,
|
||||||
|
content,
|
||||||
|
sig
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(relay_id, e.id, e.pubkey, e.created_at, e.kind, e.content, e.sig),
|
||||||
|
)
|
||||||
|
|
||||||
|
# todo: optimize with bulk insert
|
||||||
|
for tag in e.tags:
|
||||||
|
await create_event_tags(relay_id, e.id, tag[0], tag[1])
|
||||||
|
|
||||||
|
|
||||||
|
async def create_event_tags(
|
||||||
|
relay_id: str, event_id: str, tag_name: str, tag_value: str
|
||||||
|
):
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO nostrrelay.event_tags (
|
||||||
|
relay_id,
|
||||||
|
event_id,
|
||||||
|
name,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(relay_id, event_id, tag_name, tag_value),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_events(relay_id: str, filter: NostrFilter) -> List[NostrEvent]:
|
||||||
|
query = "SELECT * FROM nostrrelay.events WHERE relay_id = ?"
|
||||||
|
values: List[Any] = [relay_id]
|
||||||
|
if len(filter.ids) != 0:
|
||||||
|
ids = ",".join(["?"] * len(filter.ids))
|
||||||
|
query += f" AND id IN ({ids})"
|
||||||
|
values += filter.ids
|
||||||
|
if len(filter.authors) != 0:
|
||||||
|
authors = ",".join(["?"] * len(filter.authors))
|
||||||
|
query += f" AND pubkey IN ({authors})"
|
||||||
|
values += filter.authors
|
||||||
|
if len(filter.kinds) != 0:
|
||||||
|
kinds = ",".join(["?"] * len(filter.kinds))
|
||||||
|
query += f" AND kind IN ({kinds})"
|
||||||
|
values += filter.kinds
|
||||||
|
if filter.since:
|
||||||
|
query += f" AND created_at >= ?"
|
||||||
|
values += [filter.since]
|
||||||
|
if filter.until:
|
||||||
|
query += f" AND created_at <= ?"
|
||||||
|
values += [filter.until]
|
||||||
|
|
||||||
|
query += " ORDER BY created_at DESC"
|
||||||
|
if filter.limit and type(filter.limit) == int and filter.limit > 0:
|
||||||
|
query += f" LIMIT {filter.limit}"
|
||||||
|
|
||||||
|
# print("### query: ", query)
|
||||||
|
# print("### values: ", tuple(values))
|
||||||
|
rows = await db.fetchall(query, tuple(values))
|
||||||
|
events = [NostrEvent.from_row(row) for row in rows]
|
||||||
|
|
||||||
|
# print("### events: ", len(events))
|
||||||
|
|
||||||
|
return events
|
||||||
38
migrations.py
Normal file
38
migrations.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
async def m001_initial(db):
|
||||||
|
"""
|
||||||
|
Initial nostrrelays tables.
|
||||||
|
"""
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE nostrrelay.relays (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
wallet TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
f"""
|
||||||
|
CREATE TABLE nostrrelay.events (
|
||||||
|
relay_id TEXT NOT NULL,
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
pubkey TEXT NOT NULL,
|
||||||
|
created_at {db.big_int} NOT NULL,
|
||||||
|
kind INT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
sig TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE nostrrelay.event_tags (
|
||||||
|
relay_id TEXT NOT NULL,
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
116
models.py
Normal file
116
models.py
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from enum import Enum
|
||||||
|
from sqlite3 import Row
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from secp256k1 import PublicKey
|
||||||
|
|
||||||
|
|
||||||
|
class NostrRelay(BaseModel):
|
||||||
|
id: str
|
||||||
|
wallet: str
|
||||||
|
name: str
|
||||||
|
currency: str
|
||||||
|
tip_options: Optional[str]
|
||||||
|
tip_wallet: Optional[str]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row) -> "NostrRelay":
|
||||||
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
||||||
|
class NostrEvent(BaseModel):
|
||||||
|
id: str
|
||||||
|
pubkey: str
|
||||||
|
created_at: int
|
||||||
|
kind: int
|
||||||
|
tags: List[List[str]] = []
|
||||||
|
content: str = ""
|
||||||
|
sig: str
|
||||||
|
|
||||||
|
def serialize(self) -> List:
|
||||||
|
return [0, self.pubkey, self.created_at, self.kind, self.tags, self.content]
|
||||||
|
|
||||||
|
def serialize_json(self) -> str:
|
||||||
|
e = self.serialize()
|
||||||
|
return json.dumps(e, separators=(",", ":"))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event_id(self) -> str:
|
||||||
|
data = self.serialize_json()
|
||||||
|
id = hashlib.sha256(data.encode()).hexdigest()
|
||||||
|
return id
|
||||||
|
|
||||||
|
def check_signature(self):
|
||||||
|
event_id = self.event_id
|
||||||
|
if self.id != event_id:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid event id. Expected: '{event_id}' got '{self.id}'"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
pub_key = PublicKey(bytes.fromhex("02" + self.pubkey), True)
|
||||||
|
except Exception:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid public key: '{self.pubkey}' for event '{self.id}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_signature = pub_key.schnorr_verify(
|
||||||
|
bytes.fromhex(event_id), bytes.fromhex(self.sig), None, raw=True
|
||||||
|
)
|
||||||
|
if not valid_signature:
|
||||||
|
raise ValueError(f"Invalid signature: '{self.sig}' for event '{self.id}'")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_row(cls, row: Row) -> "NostrEvent":
|
||||||
|
return cls(**dict(row))
|
||||||
|
|
||||||
|
|
||||||
|
class NostrFilter(BaseModel):
|
||||||
|
subscription_id: Optional[str]
|
||||||
|
|
||||||
|
ids: List[str] = []
|
||||||
|
authors: List[str] = []
|
||||||
|
kinds: List[int] = []
|
||||||
|
e: List[str] = Field([], alias="#e")
|
||||||
|
p: List[str] = Field([], alias="#p")
|
||||||
|
since: Optional[int]
|
||||||
|
until: Optional[int]
|
||||||
|
limit: Optional[int]
|
||||||
|
|
||||||
|
def matches(self, e: NostrEvent) -> bool:
|
||||||
|
# todo: starts with
|
||||||
|
if len(self.ids) != 0 and e.id not in self.ids:
|
||||||
|
return False
|
||||||
|
if len(self.authors) != 0 and e.pubkey not in self.authors:
|
||||||
|
return False
|
||||||
|
if len(self.kinds) != 0 and e.kind not in self.kinds:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.since and e.created_at < self.since:
|
||||||
|
return False
|
||||||
|
if self.until and self.until > 0 and e.created_at > self.until:
|
||||||
|
return False
|
||||||
|
|
||||||
|
found_e_tag = self.tag_in_list(e.tags, "e")
|
||||||
|
found_p_tag = self.tag_in_list(e.tags, "p")
|
||||||
|
if not found_e_tag or not found_p_tag:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def tag_in_list(self, event_tags, tag_name):
|
||||||
|
tag_values = [t[1] for t in event_tags if t[0] == tag_name]
|
||||||
|
if len(tag_values) == 0:
|
||||||
|
return True
|
||||||
|
common_tags = [t for t in tag_values if t in self.e]
|
||||||
|
if len(common_tags) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class NostrEventType(str, Enum):
|
||||||
|
EVENT = "EVENT"
|
||||||
|
REQ = "REQ"
|
||||||
|
CLOSE = "CLOSE"
|
||||||
BIN
static/image/nostrrelay.png
Normal file
BIN
static/image/nostrrelay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
108
templates/nostrrelay/index.html
Normal file
108
templates/nostrrelay/index.html
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
{% 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-8 col-lg-7 q-gutter-y-md">
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<q-btn unelevated color="primary" @click="enableRelay"
|
||||||
|
><div v-if="enabled">Disable relay</div>
|
||||||
|
<div v-else>Enable relay</div></q-btn
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<q-card>
|
||||||
|
<q-card-section>
|
||||||
|
<h6>WebSocket Chat</h6>
|
||||||
|
|
||||||
|
<input type="text" id="messageText" autocomplete="off" />
|
||||||
|
|
||||||
|
<q-btn unelevated color="primary" @click="sendMessage()"
|
||||||
|
><div>Send</div>
|
||||||
|
</q-btn>
|
||||||
|
|
||||||
|
<ul id="messages"></ul>
|
||||||
|
</q-card-section>
|
||||||
|
</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}} NostrRelay extension
|
||||||
|
</h6>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section>
|
||||||
|
<p>
|
||||||
|
Thiago's Point of Sale is a secure, mobile-ready, instant and
|
||||||
|
shareable point of sale terminal (PoS) for merchants. The PoS is
|
||||||
|
linked to your LNbits wallet but completely air-gapped so users can
|
||||||
|
ONLY create invoices. To share the NostrRelay hit the hash on the
|
||||||
|
terminal.
|
||||||
|
</p>
|
||||||
|
<small
|
||||||
|
>Created by
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://pypi.org/user/dcs/"
|
||||||
|
target="_blank"
|
||||||
|
>DCs</a
|
||||||
|
>,
|
||||||
|
<a
|
||||||
|
class="text-secondary"
|
||||||
|
href="https://github.com/benarc"
|
||||||
|
target="_blank"
|
||||||
|
>Ben Arc</a
|
||||||
|
>.</small
|
||||||
|
>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %} {% block scripts %} {{ window_vars(user) }}
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
ws: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
enableRelay: function () {
|
||||||
|
// var self = this
|
||||||
|
// LNbits.api
|
||||||
|
// .request(
|
||||||
|
// 'GET',
|
||||||
|
// '/nostrrelay/api/v1/nostrrelays?all_wallets=true',
|
||||||
|
// this.g.user.wallets[0].inkey
|
||||||
|
// )
|
||||||
|
// .then(function (response) {
|
||||||
|
// self.nostrrelays = response.data.map(function (obj) {
|
||||||
|
// return mapNostrRelay(obj)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
this.enabled = !this.enabled
|
||||||
|
},
|
||||||
|
sendMessage: function (event) {
|
||||||
|
var input = document.getElementById('messageText')
|
||||||
|
this.ws.send(input.value)
|
||||||
|
input.value = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.ws = new WebSocket('ws://localhost:5000/nostrrelay/client')
|
||||||
|
this.ws.onmessage = function (event) {
|
||||||
|
var messages = document.getElementById('messages')
|
||||||
|
var message = document.createElement('li')
|
||||||
|
var content = document.createTextNode(event.data)
|
||||||
|
message.appendChild(content)
|
||||||
|
messages.appendChild(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
29
templates/nostrrelay/public.html
Normal file
29
templates/nostrrelay/public.html
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{% extends "public.html" %} {% block toolbar_title %} {{ nostrrelay.name }}
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
dense
|
||||||
|
size="md"
|
||||||
|
@click.prevent="urlDialog.show = true"
|
||||||
|
icon="share"
|
||||||
|
color="white"
|
||||||
|
></q-btn>
|
||||||
|
{% endblock %} {% block footer %}{% endblock %} {% block page_container %}
|
||||||
|
<q-page-container>
|
||||||
|
<q-page>
|
||||||
|
<h3>Shareable public page on relay to go here!</h3>
|
||||||
|
</q-page>
|
||||||
|
</q-page-container>
|
||||||
|
{% endblock %} {% block scripts %}
|
||||||
|
<script>
|
||||||
|
Vue.component(VueQrcode.name, VueQrcode)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#vue',
|
||||||
|
mixins: [windowMixin],
|
||||||
|
data: function () {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
methods: {}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
32
views.py
Normal file
32
views.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import Depends, Request
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from starlette.responses import HTMLResponse
|
||||||
|
|
||||||
|
from lnbits.core.models import User
|
||||||
|
from lnbits.decorators import check_user_exists
|
||||||
|
|
||||||
|
from . import nostrrelay_ext, nostrrelay_renderer
|
||||||
|
|
||||||
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
|
|
||||||
|
@nostrrelay_ext.get("/", response_class=HTMLResponse)
|
||||||
|
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||||
|
return nostrrelay_renderer().TemplateResponse(
|
||||||
|
"nostrrelay/index.html", {"request": request, "user": user.dict()}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrrelay_ext.get("/public")
|
||||||
|
async def nostrrelay(request: Request, nostrrelay_id):
|
||||||
|
return nostrrelay_renderer().TemplateResponse(
|
||||||
|
"nostrrelay/public.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
# "nostrrelay": relay,
|
||||||
|
"web_manifest": f"/nostrrelay/manifest/{nostrrelay_id}.webmanifest",
|
||||||
|
},
|
||||||
|
)
|
||||||
32
views_api.py
Normal file
32
views_api.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import Depends, Query, WebSocket
|
||||||
|
from loguru import logger
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
|
||||||
|
|
||||||
|
from . import nostrrelay_ext
|
||||||
|
from .client_manager import NostrClientConnection, NostrClientManager
|
||||||
|
|
||||||
|
client_manager = NostrClientManager()
|
||||||
|
|
||||||
|
|
||||||
|
@nostrrelay_ext.websocket("/client")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
|
client = NostrClientConnection(websocket=websocket)
|
||||||
|
client_manager.add_client(client)
|
||||||
|
try:
|
||||||
|
await client.start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
client_manager.remove_client(client)
|
||||||
|
|
||||||
|
|
||||||
|
@nostrrelay_ext.get("/api/v1/enable", status_code=HTTPStatus.OK)
|
||||||
|
async def api_nostrrelay(enable: bool = Query(True)):
|
||||||
|
return await enable_relay(enable)
|
||||||
|
|
||||||
|
|
||||||
|
async def enable_relay(enable: bool):
|
||||||
|
return enable
|
||||||
Loading…
Add table
Add a link
Reference in a new issue