v1 in the middle (#32)

This commit is contained in:
dni ⚡ 2024-10-31 12:15:31 +01:00 committed by GitHub
parent a8eb139360
commit db20915756
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1259 additions and 1127 deletions

View file

@ -4,7 +4,7 @@ from fastapi import APIRouter
from loguru import logger from loguru import logger
from .crud import db from .crud import db
from .nostr_client import all_routers, nostr_client from .router import all_routers, nostr_client
from .tasks import check_relays, init_relays, subscribe_events from .tasks import check_relays, init_relays, subscribe_events
from .views import nostrclient_generic_router from .views import nostrclient_generic_router
from .views_api import nostrclient_api_router from .views_api import nostrclient_api_router

View file

@ -2,6 +2,6 @@
"name": "Nostr Client", "name": "Nostr Client",
"short_description": "Nostr client for extensions", "short_description": "Nostr client for extensions",
"tile": "/nostrclient/static/images/nostr-bitcoin.png", "tile": "/nostrclient/static/images/nostr-bitcoin.png",
"contributors": ["calle", "motorina0"], "contributors": ["calle", "motorina0", "dni"],
"min_lnbits_version": "0.12.0" "min_lnbits_version": "1.0.0"
} }

73
crud.py
View file

@ -1,59 +1,54 @@
import json
from typing import Optional from typing import Optional
from lnbits.db import Database from lnbits.db import Database
from .models import Config, Relay from .models import Config, Relay, UserConfig
db = Database("ext_nostrclient") db = Database("ext_nostrclient")
async def get_relays() -> list[Relay]: async def get_relays() -> list[Relay]:
rows = await db.fetchall("SELECT * FROM nostrclient.relays") return await db.fetchall(
return [Relay.from_row(r) for r in rows] "SELECT * FROM nostrclient.relays",
model=Relay,
)
async def add_relay(relay: Relay) -> None: async def add_relay(relay: Relay) -> Relay:
await db.execute( await db.insert("nostrclient.relays", relay)
""" return relay
INSERT INTO nostrclient.relays (
id,
url,
active
)
VALUES (?, ?, ?)
""",
(relay.id, relay.url, relay.active),
)
async def delete_relay(relay: Relay) -> None: async def delete_relay(relay: Relay) -> None:
await db.execute("DELETE FROM nostrclient.relays WHERE url = ?", (relay.url,)) if not relay.url:
return
await db.execute(
"DELETE FROM nostrclient.relays WHERE url = :url", {"url": relay.url}
)
######################CONFIG####################### ######################CONFIG#######################
async def create_config() -> Config: async def create_config(owner_id: str) -> Config:
config = Config() admin_config = UserConfig(owner_id=owner_id)
await db.execute( await db.insert("nostrclient.config", admin_config)
return admin_config.extra
async def update_config(owner_id: str, config: Config) -> Config:
user_config = UserConfig(owner_id=owner_id, extra=config)
await db.update("nostrclient.config", user_config, "WHERE owner_id = :owner_id")
return user_config.extra
async def get_config(owner_id: str) -> Optional[Config]:
user_config: UserConfig = await db.fetchone(
""" """
INSERT INTO nostrclient.config (json_data) SELECT * FROM nostrclient.config
VALUES (?) WHERE owner_id = :owner_id
""", """,
(json.dumps(config.dict()),), {"owner_id": owner_id},
model=UserConfig,
) )
row = await db.fetchone("SELECT json_data FROM nostrclient.config", ()) if user_config:
return json.loads(row[0], object_hook=lambda d: Config(**d)) return user_config.extra
return None
async def update_config(config: Config) -> Optional[Config]:
await db.execute(
"""UPDATE nostrclient.config SET json_data = ?""",
(json.dumps(config.dict()),),
)
row = await db.fetchone("SELECT json_data FROM nostrclient.config", ())
return json.loads(row[0], object_hook=lambda d: Config(**d))
async def get_config() -> Optional[Config]:
row = await db.fetchone("SELECT json_data FROM nostrclient.config", ())
return json.loads(row[0], object_hook=lambda d: Config(**d)) if row else None

View file

@ -23,3 +23,10 @@ async def m002_create_config_table(db):
json_data TEXT NOT NULL json_data TEXT NOT NULL
);""" );"""
) )
async def m003_update_config_table(db):
await db.execute("ALTER TABLE nostrclient.config RENAME COLUMN json_data TO extra")
await db.execute(
"ALTER TABLE nostrclient.config ADD COLUMN owner_id TEXT DEFAULT 'admin'"
)

View file

@ -1,34 +1,37 @@
from sqlite3 import Row from typing import Optional
from typing import List, Optional
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from pydantic import BaseModel from pydantic import BaseModel, Field
class RelayStatus(BaseModel): class RelayStatus(BaseModel):
num_sent_events: Optional[int] = 0 num_sent_events: Optional[int] = 0
num_received_events: Optional[int] = 0 num_received_events: Optional[int] = 0
error_counter: Optional[int] = 0 error_counter: Optional[int] = 0
error_list: Optional[List] = [] error_list: Optional[list] = []
notice_list: Optional[List] = [] notice_list: Optional[list] = []
class Relay(BaseModel): class Relay(BaseModel):
id: Optional[str] = None id: Optional[str] = None
url: Optional[str] = None url: Optional[str] = None
connected: Optional[bool] = None
connected_string: Optional[str] = None
status: Optional[RelayStatus] = None
active: Optional[bool] = None active: Optional[bool] = None
ping: Optional[int] = None
connected: Optional[bool] = Field(default=None, no_database=True)
connected_string: Optional[str] = Field(default=None, no_database=True)
status: Optional[RelayStatus] = Field(default=None, no_database=True)
ping: Optional[int] = Field(default=None, no_database=True)
def _init__(self): def _init__(self):
if not self.id: if not self.id:
self.id = urlsafe_short_hash() self.id = urlsafe_short_hash()
@classmethod
def from_row(cls, row: Row) -> "Relay": class RelayDb(BaseModel):
return cls(**dict(row)) id: str
url: str
active: Optional[bool] = True
class TestMessage(BaseModel): class TestMessage(BaseModel):
@ -46,3 +49,8 @@ class TestMessageResponse(BaseModel):
class Config(BaseModel): class Config(BaseModel):
private_ws: bool = True private_ws: bool = True
public_ws: bool = False public_ws: bool = False
class UserConfig(BaseModel):
owner_id: str
extra: Config = Config()

View file

@ -6,10 +6,12 @@ from ..relay_manager import RelayManager
class NostrClient: class NostrClient:
relay_manager = RelayManager() relay_manager: RelayManager
running: bool
def __init__(self): def __init__(self):
self.running = True self.running = True
self.relay_manager = RelayManager()
def connect(self, relays): def connect(self, relays):
for relay in relays: for relay in relays:

View file

@ -1,5 +0,0 @@
from .nostr.client.client import NostrClient
from .router import NostrRouter
nostr_client: NostrClient = NostrClient()
all_routers: list[NostrRouter] = []

2206
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ authors = ["Alan Bits <alan@lnbits.com>"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.10 | ^3.9" python = "^3.10 | ^3.9"
lnbits = "*" lnbits = {allow-prereleases = true, version = "*"}
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
black = "^24.3.0" black = "^24.3.0"

View file

@ -1,19 +1,26 @@
import asyncio import asyncio
import json import json
from typing import Dict, List from typing import ClassVar, Dict, List
from fastapi import WebSocket, WebSocketDisconnect from fastapi import WebSocket, WebSocketDisconnect
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from loguru import logger from loguru import logger
from . import nostr_client from .nostr.client.client import NostrClient
# from . import nostr_client
from .nostr.message_pool import EndOfStoredEventsMessage, EventMessage, NoticeMessage from .nostr.message_pool import EndOfStoredEventsMessage, EventMessage, NoticeMessage
nostr_client: NostrClient = NostrClient()
all_routers: list["NostrRouter"] = []
class NostrRouter: class NostrRouter:
received_subscription_events: dict[str, List[EventMessage]] received_subscription_events: ClassVar[dict[str, List[EventMessage]]] = {}
received_subscription_notices: list[NoticeMessage] received_subscription_notices: ClassVar[list[NoticeMessage]] = []
received_subscription_eosenotices: dict[str, EndOfStoredEventsMessage] received_subscription_eosenotices: ClassVar[dict[str, EndOfStoredEventsMessage]] = (
{}
)
def __init__(self, websocket: WebSocket): def __init__(self, websocket: WebSocket):
self.connected: bool = True self.connected: bool = True
@ -70,6 +77,7 @@ class NostrRouter:
self._handle_notices() self._handle_notices()
except Exception as e: except Exception as e:
logger.debug(f"Failed to handle response for client: '{e!s}'.") logger.debug(f"Failed to handle response for client: '{e!s}'.")
await asyncio.sleep(1)
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
async def _handle_subscriptions(self): async def _handle_subscriptions(self):

View file

@ -3,10 +3,9 @@ import threading
from loguru import logger from loguru import logger
from . import nostr_client
from .crud import get_relays from .crud import get_relays
from .nostr.message_pool import EndOfStoredEventsMessage, EventMessage, NoticeMessage from .nostr.message_pool import EndOfStoredEventsMessage, EventMessage, NoticeMessage
from .router import NostrRouter from .router import NostrRouter, nostr_client
async def init_relays(): async def init_relays():

View file

@ -71,7 +71,7 @@
<q-table <q-table
flat flat
dense dense
:data="nostrrelayLinks" :rows="nostrrelayLinks"
row-key="id" row-key="id"
:columns="relayTable.columns" :columns="relayTable.columns"
:pagination.sync="relayTable.pagination" :pagination.sync="relayTable.pagination"
@ -372,8 +372,6 @@
{% endraw %} {% endblock %} {% block scripts %} {{ window_vars(user) }} {% endraw %} {% endblock %} {% block scripts %} {{ window_vars(user) }}
<script> <script>
Vue.component(VueQrcode.name, VueQrcode)
var maplrelays = obj => { var maplrelays = obj => {
obj._data = _.clone(obj) obj._data = _.clone(obj)
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp) obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
@ -391,7 +389,7 @@
if (obj.time_elapsed) { if (obj.time_elapsed) {
obj.date = 'Time elapsed' obj.date = 'Time elapsed'
} else { } else {
obj.date = Quasar.utils.date.formatDate( obj.date = Quasar.date.formatDate(
new Date((obj.theTime - 3600) * 1000), new Date((obj.theTime - 3600) * 1000),
'HH:mm:ss' 'HH:mm:ss'
) )
@ -399,7 +397,7 @@
return obj return obj
} }
new Vue({ window.app = Vue.createApp({
el: '#vue', el: '#vue',
mixins: [windowMixin], mixins: [windowMixin],
data: function () { data: function () {
@ -624,7 +622,7 @@
try { try {
const {data} = await LNbits.api.request( const {data} = await LNbits.api.request(
'PUT', 'PUT',
'/nostrclient/api/v1/relay/test?usr=' + this.g.user.id, '/nostrclient/api/v1/relay/test',
this.g.user.wallets[0].adminkey, this.g.user.wallets[0].adminkey,
{ {
sender_private_key: this.testData.senderPrivateKey, sender_private_key: this.testData.senderPrivateKey,

View file

@ -1,12 +1,9 @@
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_admin from lnbits.decorators import check_admin
from lnbits.helpers import template_renderer from lnbits.helpers import template_renderer
templates = Jinja2Templates(directory="templates")
nostrclient_generic_router = APIRouter() nostrclient_generic_router = APIRouter()
@ -17,5 +14,5 @@ def nostr_renderer():
@nostrclient_generic_router.get("/", response_class=HTMLResponse) @nostrclient_generic_router.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_admin)): async def index(request: Request, user: User = Depends(check_admin)):
return nostr_renderer().TemplateResponse( return nostr_renderer().TemplateResponse(
"nostrclient/index.html", {"request": request, "user": user.dict()} "nostrclient/index.html", {"request": request, "user": user.json()}
) )

View file

@ -17,8 +17,7 @@ from .crud import (
from .helpers import normalize_public_key from .helpers import normalize_public_key
from .models import Config, Relay, RelayStatus, TestMessage, TestMessageResponse from .models import Config, Relay, RelayStatus, TestMessage, TestMessageResponse
from .nostr.key import EncryptedDirectMessage, PrivateKey from .nostr.key import EncryptedDirectMessage, PrivateKey
from .nostr_client import all_routers, nostr_client from .router import NostrRouter, all_routers, nostr_client
from .router import NostrRouter
nostrclient_api_router = APIRouter() nostrclient_api_router = APIRouter()
@ -114,13 +113,13 @@ async def api_test_endpoint(data: TestMessage) -> TestMessageResponse:
) from ex ) from ex
@nostrclient_api_router.websocket("/api/v1/{id}") @nostrclient_api_router.websocket("/api/v1/{ws_id}")
async def ws_relay(ws_id: str, websocket: WebSocket) -> None: async def ws_relay(ws_id: str, websocket: WebSocket) -> None:
"""Relay multiplexer: one client (per endpoint) <-> multiple relays""" """Relay multiplexer: one client (per endpoint) <-> multiple relays"""
logger.info("New websocket connection at: '/api/v1/relay'") logger.info("New websocket connection at: '/api/v1/relay'")
try: try:
config = await get_config() config = await get_config(owner_id="admin")
assert config, "Failed to get config" assert config, "Failed to get config"
if not config.private_ws and not config.public_ws: if not config.private_ws and not config.public_ws:
@ -166,15 +165,15 @@ async def ws_relay(ws_id: str, websocket: WebSocket) -> None:
@nostrclient_api_router.get("/api/v1/config", dependencies=[Depends(check_admin)]) @nostrclient_api_router.get("/api/v1/config", dependencies=[Depends(check_admin)])
async def api_get_config() -> Config: async def api_get_config() -> Config:
config = await get_config() config = await get_config(owner_id="admin")
if not config: if not config:
config = await create_config() config = await create_config(owner_id="admin")
assert config, "Failed to create config" assert config, "Failed to create config"
return config return config
@nostrclient_api_router.put("/api/v1/config", dependencies=[Depends(check_admin)]) @nostrclient_api_router.put("/api/v1/config", dependencies=[Depends(check_admin)])
async def api_update_config(data: Config): async def api_update_config(data: Config):
config = await update_config(data) config = await update_config(owner_id="admin", config=data)
assert config assert config
return config.dict() return config.dict()