From 4b82905f7858cb7466411ff675cbe2a901064cd0 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 30 Jan 2023 14:40:55 +0200 Subject: [PATCH] feat: extracted --- README.md | 12 ++++ __init__.py | 27 +++++++ client_manager.py | 94 +++++++++++++++++++++++++ config.json | 6 ++ crud.py | 79 +++++++++++++++++++++ migrations.py | 38 ++++++++++ models.py | 116 +++++++++++++++++++++++++++++++ static/image/nostrrelay.png | Bin 0 -> 20581 bytes templates/nostrrelay/index.html | 108 ++++++++++++++++++++++++++++ templates/nostrrelay/public.html | 29 ++++++++ views.py | 32 +++++++++ views_api.py | 32 +++++++++ 12 files changed, 573 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 client_manager.py create mode 100644 config.json create mode 100644 crud.py create mode 100644 migrations.py create mode 100644 models.py create mode 100644 static/image/nostrrelay.png create mode 100644 templates/nostrrelay/index.html create mode 100644 templates/nostrrelay/public.html create mode 100644 views.py create mode 100644 views_api.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..d98b0e2 --- /dev/null +++ b/README.md @@ -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 nostr_relay. + +UI for diagnostics and management (key alow/ban lists, rate limiting) coming soon! + +### Usage + +1. Enable extension +2. Enable relay diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c95847e --- /dev/null +++ b/__init__.py @@ -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 diff --git a/client_manager.py b/client_manager.py new file mode 100644 index 0000000..514c256 --- /dev/null +++ b/client_manager.py @@ -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] diff --git a/config.json b/config.json new file mode 100644 index 0000000..e3de9fa --- /dev/null +++ b/config.json @@ -0,0 +1,6 @@ +{ + "name": "Nostr Relay", + "short_description": "One click launch your own relay!", + "tile": "/nostrrelay/static/image/nostrrelay.png", + "contributors": ["arcbtc", "DCs"] +} diff --git a/crud.py b/crud.py new file mode 100644 index 0000000..f01bc01 --- /dev/null +++ b/crud.py @@ -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 diff --git a/migrations.py b/migrations.py new file mode 100644 index 0000000..34b4390 --- /dev/null +++ b/migrations.py @@ -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 + ); + """ + ) diff --git a/models.py b/models.py new file mode 100644 index 0000000..435c6e9 --- /dev/null +++ b/models.py @@ -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" diff --git a/static/image/nostrrelay.png b/static/image/nostrrelay.png new file mode 100644 index 0000000000000000000000000000000000000000..8cbcf00f24d0171113d7b64c46677a41b4e7d8eb GIT binary patch literal 20581 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 Gp$P!k6`U#n literal 0 HcmV?d00001 diff --git a/templates/nostrrelay/index.html b/templates/nostrrelay/index.html new file mode 100644 index 0000000..c8145d2 --- /dev/null +++ b/templates/nostrrelay/index.html @@ -0,0 +1,108 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + +
Disable relay
+
Enable relay
+
+
+ + + +
WebSocket Chat
+ + + +
Send
+
+ +
    +
    +
    +
    + +
    + + +
    + {{SITE_TITLE}} NostrRelay extension +
    +
    + +

    + 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. +

    + Created by + DCs, + Ben Arc. +
    +
    +
    +
    +{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/templates/nostrrelay/public.html b/templates/nostrrelay/public.html new file mode 100644 index 0000000..5e349ee --- /dev/null +++ b/templates/nostrrelay/public.html @@ -0,0 +1,29 @@ +{% extends "public.html" %} {% block toolbar_title %} {{ nostrrelay.name }} + +{% endblock %} {% block footer %}{% endblock %} {% block page_container %} + + +

    Shareable public page on relay to go here!

    +
    +
    +{% endblock %} {% block scripts %} + +{% endblock %} diff --git a/views.py b/views.py new file mode 100644 index 0000000..5d66220 --- /dev/null +++ b/views.py @@ -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", + }, + ) diff --git a/views_api.py b/views_api.py new file mode 100644 index 0000000..78bdd46 --- /dev/null +++ b/views_api.py @@ -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