wY?^*SrVA1RCEqb74U*E=&U2|lU%V0&20!L1Ao_S?e}EYjljVR`1L<=Z;9Sqso8b~x*s^Tw>_Nk
zckPY_r8>r#ZI5^tuo{FtxV!Nsv*H6c?Oki_f8XB7z+lfhFZa`}-)pU`nttzESd&q_
zyF9Jz9owI?i)_!@+z68>tus4+=G@VSr?c$uPw`XjFQ3bhJL%Y@1>MW`#D3ZG;P9k`
z4O8wHGerpTO#AfPiNU++U|`*Sjs#BwiBtLJyjvuMo<=kA2>ePF)MaGi^Jeg7;Pz&i
z%^1NH!Q-OvL*2U};j#BL2C+tk1DnIX98^27_CWuJr>R?igshjpasBguG|{
zZQPZ9WX=&TK1QvM54sQh9^BHie>MAM$ft>n-of`hmQSj+ZnQPXXP7y2t~BdD?mLWc
z-XAlLx>@j&k@MV=;FGI=>3(mlW<7J}%^i^svJbxb2`!v^cbneC1O*ZM4Vej6Q*EaE
zsn+M*IcR&}wd{_!waf-vu3uicglEzx_Df7_jD_4MWGr}wetrIo#7C@?M%{$TlP
z|AVNd!Jc=SnPQIAAOE!YWyq(QYgLXftmJ=i=FOeN2L=TkJNSN7sGnYw%u7L-_-d
u4=z?yYtHK>v#=O%F7BJ!)Y^Kq;K6^XIY#2m;l75TX-rR7KbLh*2~7Y;axIzw
literal 0
HcmV?d00001
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 %}
+
+
+
+
+
+
+
nostr relays
+
+
+
+
+
+
+
+
+
+
+
+ {% raw %}
+
+
+
+
+ {{ col.label }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ col.value }}
+
+
{{ col.value }}
+
+
+
+
+
+
+ {% endraw %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{SITE_TITLE}} Nostr Extension
+ Only Admin users can manage this extension
+
+
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }}
+
+
+{% endblock %}
diff --git a/views.py b/views.py
new file mode 100644
index 0000000..90dd31c
--- /dev/null
+++ b/views.py
@@ -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
diff --git a/views_api.py b/views_api.py
new file mode 100644
index 0000000..8ee004f
--- /dev/null
+++ b/views_api.py
@@ -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",
+ )