commit 69bf22c9ecb6dce938ff92fe1fe2e5057b75da9e Author: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri Feb 10 14:55:30 2023 +0100 init diff --git a/README.md b/README.md new file mode 100644 index 0000000..596cce9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Nostr + +Opens a Nostr daemon diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..1555eb9 --- /dev/null +++ b/__init__.py @@ -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)) diff --git a/__pycache__/__init__.cpython-39.pyc b/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..5481de0 Binary files /dev/null and b/__pycache__/__init__.cpython-39.pyc differ diff --git a/__pycache__/crud.cpython-39.pyc b/__pycache__/crud.cpython-39.pyc new file mode 100644 index 0000000..9122a6f Binary files /dev/null and b/__pycache__/crud.cpython-39.pyc differ diff --git a/__pycache__/migrations.cpython-39.pyc b/__pycache__/migrations.cpython-39.pyc new file mode 100644 index 0000000..045e1bc Binary files /dev/null and b/__pycache__/migrations.cpython-39.pyc differ diff --git a/__pycache__/models.cpython-39.pyc b/__pycache__/models.cpython-39.pyc new file mode 100644 index 0000000..f27b51e Binary files /dev/null and b/__pycache__/models.cpython-39.pyc differ diff --git a/__pycache__/tasks.cpython-39.pyc b/__pycache__/tasks.cpython-39.pyc new file mode 100644 index 0000000..065ba1e Binary files /dev/null and b/__pycache__/tasks.cpython-39.pyc differ diff --git a/__pycache__/views.cpython-39.pyc b/__pycache__/views.cpython-39.pyc new file mode 100644 index 0000000..1f962bf Binary files /dev/null and b/__pycache__/views.cpython-39.pyc differ diff --git a/__pycache__/views_api.cpython-39.pyc b/__pycache__/views_api.cpython-39.pyc new file mode 100644 index 0000000..1084bda Binary files /dev/null and b/__pycache__/views_api.cpython-39.pyc differ diff --git a/cbc.py b/cbc.py new file mode 100644 index 0000000..0d9e04f --- /dev/null +++ b/cbc.py @@ -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")) diff --git a/config.json b/config.json new file mode 100644 index 0000000..0bfa1bd --- /dev/null +++ b/config.json @@ -0,0 +1,6 @@ +{ + "name": "Nostr Client", + "short_description": "Nostr client for extensions", + "tile": "/nostr-client/static/images/nostr-bitcoin.png", + "contributors": ["calle"] +} diff --git a/crud.py b/crud.py new file mode 100644 index 0000000..61cd865 --- /dev/null +++ b/crud.py @@ -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,)) diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..ed54c1a --- /dev/null +++ b/manifest.json @@ -0,0 +1,9 @@ +{ + "repos": [ + { + "id": "nostr-client", + "organisation": "lnbits", + "repository": "nostr-client-extension" + } + ] +} diff --git a/migrations.py b/migrations.py new file mode 100644 index 0000000..5a30e45 --- /dev/null +++ b/migrations.py @@ -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 + ); + """ + ) diff --git a/models.py b/models.py new file mode 100644 index 0000000..bfbc424 --- /dev/null +++ b/models.py @@ -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] diff --git a/nostr b/nostr new file mode 160000 index 0000000..f598039 --- /dev/null +++ b/nostr @@ -0,0 +1 @@ +Subproject commit f598039e440f1d57c3b5d993ff44473649ffac3d diff --git a/static/images/nostr-bitcoin.png b/static/images/nostr-bitcoin.png new file mode 100644 index 0000000..719feaa Binary files /dev/null and b/static/images/nostr-bitcoin.png differ diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..350418a --- /dev/null +++ b/tasks.py @@ -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() diff --git a/templates/nostr-client/index.html b/templates/nostr-client/index.html new file mode 100644 index 0000000..7b63c07 --- /dev/null +++ b/templates/nostr-client/index.html @@ -0,0 +1,257 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
Only Admin users can manage this extension
+ +