remove nostradmin from this branch

This commit is contained in:
Tiago vasconcelos 2022-08-18 16:56:10 +01:00
parent 627914abda
commit e76c23b096
11 changed files with 0 additions and 14459 deletions

13560
coverage.xml

File diff suppressed because it is too large Load diff

View file

@ -1,3 +0,0 @@
# Nostr
Opens a Nostr daemon

View file

@ -1,37 +0,0 @@
from fastapi import APIRouter, Request
from lnbits.db import Database
from lnbits.helpers import template_renderer
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from http import HTTPStatus
from lnbits.settings import LNBITS_ADMIN_EXTENSIONS
nostradmin_ext: APIRouter = APIRouter(prefix="/nostradmin", tags=["nostradmin"])
db = Database("ext_nostradmin")
if 'nostradmin' not in LNBITS_ADMIN_EXTENSIONS:
@nostradmin_ext.get("/", response_class=HTMLResponse)
async def index(request: Request):
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "NostrAdmin must be added to LNBITS_ADMIN_EXTENSIONS in .env"}
)
else:
def nostr_renderer():
return template_renderer(["lnbits/extensions/nostradmin/templates"])
from .views import * # noqa
from .views_api import * # noqa

View file

@ -1,6 +0,0 @@
{
"name": "NostrAdmin",
"short_description": "Admin daemon for Nostr",
"icon": "swap_horizontal_circle",
"contributors": ["arcbtc"]
}

View file

@ -1,146 +0,0 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
import shortuuid
from . import db
from .models import (
nostrKeys,
nostrNotes,
nostrCreateRelays,
nostrRelays,
nostrConnections,
nostrCreateConnections,
nostrRelayList,
)
from .models import nostrKeys, nostrCreateRelays, nostrRelaySetList
###############KEYS##################
async def create_nostrkeys(data: nostrKeys) -> nostrKeys:
await db.execute(
"""
INSERT INTO nostradmin.keys (
pubkey,
privkey
)
VALUES (?, ?)
""",
(data.pubkey, data.privkey),
)
return await get_nostrkeys(nostrkey_id)
async def get_nostrkeys(pubkey: str) -> nostrKeys:
row = await db.fetchone("SELECT * FROM nostradmin.keys WHERE pubkey = ?", (pubkey,))
return nostrKeys(**row) if row else None
###############NOTES##################
async def create_nostrnotes(data: nostrNotes) -> nostrNotes:
await db.execute(
"""
INSERT INTO nostradmin.notes (
id,
pubkey,
created_at,
kind,
tags,
content,
sig
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
data.id,
data.pubkey,
data.created_at,
data.kind,
data.tags,
data.content,
data.sig,
),
)
return await get_nostrnotes(data.id)
async def get_nostrnotes(nostrnote_id: str) -> nostrNotes:
row = await db.fetchone("SELECT * FROM nostradmin.notes WHERE id = ?", (nostrnote_id,))
return nostrNotes(**row) if row else None
###############RELAYS##################
async def create_nostrrelays(data: nostrCreateRelays) -> nostrCreateRelays:
nostrrelay_id = shortuuid.uuid(name=data.relay)
if await get_nostrrelay(nostrrelay_id):
return "error"
await db.execute(
"""
INSERT INTO nostradmin.relays (
id,
relay
)
VALUES (?, ?)
""",
(nostrrelay_id, data.relay),
)
return await get_nostrrelay(nostrrelay_id)
async def get_nostrrelays() -> nostrRelays:
rows = await db.fetchall("SELECT * FROM nostradmin.relays")
return [nostrRelays(**row) for row in rows]
async def get_nostrrelay(nostrrelay_id: str) -> nostrRelays:
row = await db.fetchone("SELECT * FROM nostradmin.relays WHERE id = ?", (nostrrelay_id,))
return nostrRelays(**row) if row else None
async def update_nostrrelaysetlist(data: nostrRelaySetList) -> nostrRelayList:
await db.execute(
"""
UPDATE nostradmin.relaylists SET
denylist = ?,
allowlist = ?
WHERE id = ?
""",
(data.denylist, data.allowlist, 1),
)
return await get_nostrrelaylist()
async def get_nostrrelaylist() -> nostrRelayList:
row = await db.fetchone("SELECT * FROM nostradmin.relaylists WHERE id = ?", (1,))
return nostrRelayList(**row) if row else None
###############CONNECTIONS##################
async def create_nostrconnections(
data: nostrCreateConnections
) -> nostrCreateConnections:
nostrrelay_id = shortuuid.uuid(name=data.relayid + data.pubkey)
await db.execute(
"""
INSERT INTO nostradmin.connections (
id,
pubkey,
relayid
)
VALUES (?, ?, ?)
""",
(data.id, data.pubkey, data.relayid),
)
return await get_nostrconnections(data.id)
async def get_nostrconnections(nostrconnections_id: str) -> nostrConnections:
row = await db.fetchone(
"SELECT * FROM nostradmin.connections WHERE id = ?", (nostrconnections_id,)
)
return nostrConnections(**row) if row else None

View file

@ -1,74 +0,0 @@
from lnbits.db import Database
async def m001_initial(db):
"""
Initial nostradmin table.
"""
await db.execute(
f"""
CREATE TABLE nostradmin.keys (
pubkey TEXT NOT NULL PRIMARY KEY,
privkey TEXT NOT NULL
);
"""
)
await db.execute(
f"""
CREATE TABLE nostradmin.notes (
id TEXT NOT NULL PRIMARY KEY,
pubkey TEXT NOT NULL,
created_at TEXT NOT NULL,
kind INT NOT NULL,
tags TEXT NOT NULL,
content TEXT NOT NULL,
sig TEXT NOT NULL
);
"""
)
await db.execute(
f"""
CREATE TABLE nostradmin.relays (
id TEXT NOT NULL PRIMARY KEY,
relay TEXT NOT NULL
);
"""
)
await db.execute(
f"""
CREATE TABLE nostradmin.relaylists (
id TEXT NOT NULL PRIMARY KEY DEFAULT 1,
allowlist TEXT,
denylist TEXT
);
"""
)
await db.execute(
"""
INSERT INTO nostradmin.relaylists (
id,
denylist
)
VALUES (?, ?)
""",
("1", "wss://zucks-meta-relay.com\nwss://nostr.cia.gov",),
)
await db.execute(
f"""
CREATE TABLE nostradmin.connections (
id TEXT NOT NULL PRIMARY KEY,
publickey TEXT NOT NULL,
relayid TEXT NOT NULL
);
"""
)
await db.execute(
f"""
CREATE TABLE nostradmin.subscribed (
id TEXT NOT NULL PRIMARY KEY,
userPubkey TEXT NOT NULL,
subscribedPubkey TEXT NOT NULL
);
"""
)

View file

@ -1,52 +0,0 @@
import json
from sqlite3 import Row
from typing import Optional
from fastapi import Request
from pydantic import BaseModel
from pydantic.main import BaseModel
from fastapi.param_functions import Query
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 nostrRelayList(BaseModel):
id: str
allowlist: Optional[str]
denylist: Optional[str]
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]

View file

@ -1,105 +0,0 @@
import asyncio
import logging
from typing import Any, Dict
import websockets
# Many thanks to Fusion44 for https://gist.github.com/fusion44/9af0b054b4609012752b0c8c1dafbd4a
class Relay:
url: str
read: bool
write: bool
active: bool
_connection: Any
def __init__(self, url: str, read: bool, write: bool, active: bool) -> None:
self.url = url
self.read = read
self.write = write
self.active = active
async def connect(self):
self._connection = await websockets.connect(self.url)
async def send(self, message):
if not self.write or not self.active:
raise RuntimeError("Can't send to a relay that is inactive or not writable")
await self._connection.send(message)
async def listen(self):
if not self.read or not self.active:
raise RuntimeError(
"Can't listen to a relay that is inactive or not readable"
)
while True:
try:
msg = await self._connection.recv()
yield msg
except websockets.ConnectionClosedOK:
break
class RelayManager:
_relays: Dict[str, Relay] = {}
_tasks: Dict[str, asyncio.Task] = {}
msg_channel = asyncio.Queue()
def __init__(self, enable_ws_debugger=False) -> None:
if enable_ws_debugger:
logger = logging.getLogger("websockets")
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler())
pass
async def add_relay(self, relay: Relay):
if relay.url in self._relays.keys():
raise RuntimeError(f"Relay {relay.url} exists")
if relay.active and relay.read:
await relay.connect()
loop = asyncio.get_event_loop()
t = loop.create_task(self._relay_listener(relay, self.msg_channel))
self._tasks[relay.url] = t
self._relays[relay.url] = relay
async def remove_relay(self, url: str):
if url in self._relays.keys():
relay = self._relays[url]
if relay.active:
t = self._tasks[url]
t.cancel()
try:
await t
except asyncio.CancelledError:
print(f"Canceled task {relay.url}")
def get_relay(self, url: str) -> Relay:
if not url in self._relays.keys():
raise RuntimeError("Relay not found")
return self._relays.get(url)
async def update_relay(self, relay: Relay):
if relay.url in self._relays.keys():
# remove the old relay
await self.remove_relay(relay.url)
# add the new relay
await self.add_relay(relay)
else:
raise RuntimeError("Unknown Relay")
async def send_to_all(self, message):
for r in self._relays.values():
if r.write:
await r.send(message=message)
async def _relay_listener(self, relay: Relay, msg_chan: asyncio.Queue):
print(f"listening for {relay.url}")
async for msg in relay.listen():
print(msg)
msg_chan.put_nowait(msg)
print(f"STOP listening for {relay.url}")

View file

@ -1,303 +0,0 @@
{% 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-7 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">NOSTR RELAYS ONLINE</h5>
</div>
<div class="col-auto">
<q-input
borderless
dense
debounce="300"
v-model="filter"
placeholder="Search"
>
<template v-slot:append>
<q-icon name="search"></q-icon>
</template>
</q-input>
<q-btn flat color="grey" @click="exportlnurldeviceCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
flat
dense
:data="nostrrelayLinks"
row-key="id"
:columns="nostrTable.columns"
:pagination.sync="nostrTable.pagination"
:filter="filter"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div>
</q-th>
<!-- <q-th auto-width></q-th> -->
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>
<div v-if="col.value == true" style="background-color: green">
{{ col.value }}
</div>
<div v-else>{{ col.value }}</div>
</div>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-tabs
v-model="listSelection"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
>
<q-tab name="denylist" label="Deny List"></q-tab>
<q-tab name="allowlist" label="Allow List"></q-tab>
</q-tabs>
<q-separator></q-separator>
<q-tab-panels v-model="listSelection" animated>
<q-tab-panel name="denylist">
<div class="text-h6">Relays in this list will NOT be used</div>
<q-form class="q-gutter-md q-y-md" @submit="setRelayList">
<div class="row">
<div class="col q-mx-lg">
<q-input
v-model="setList.denylist"
dense
filled
autogrow
></q-input>
</div>
<div
class="col q-mx-lg items-align flex items-center justify-center"
>
<q-btn unelevated color="primary" type="submit">
Update Deny List
</q-btn>
</div>
</div>
</q-form>
</q-tab-panel>
<q-tab-panel name="allowlist">
<div class="text-h6">ONLY relays in this list will be used</div>
<q-form class="q-gutter-md q-y-md" @submit="setRelayList">
<div class="row">
<div class="col q-mx-lg">
<q-input
v-model="setList.allowlist"
dense
filled
autogrow
></q-input>
</div>
<div
class="col q-mx-lg items-align flex items-center justify-center"
>
<q-btn unelevated color="primary" type="submit">
Update Allow List
</q-btn>
</div>
</div>
</q-form>
</q-tab-panel>
</q-tab-panels>
</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}} Nostr Extension</h6>
<p>Only Admin users can manage this extension</p>
<q-card-section></q-card-section>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
Vue.component(VueQrcode.name, VueQrcode)
var maplrelays = obj => {
obj._data = _.clone(obj)
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
obj.time = obj.time + 'mins'
if (obj.time_elapsed) {
obj.date = 'Time elapsed'
} else {
obj.date = Quasar.utils.date.formatDate(
new Date((obj.theTime - 3600) * 1000),
'HH:mm:ss'
)
}
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
listSelection: 'denylist',
setList: {
allowlist: '',
denylist: ''
},
nostrrelayLinks: [],
filter: '',
nostrTable: {
columns: [
{
name: 'wah',
align: 'left',
label: 'id',
field: 'id'
},
{
name: 'relay',
align: 'left',
label: 'relay',
field: 'relay'
},
{
name: 'status',
align: 'left',
label: 'status',
field: 'status'
}
],
pagination: {
rowsPerPage: 10
}
}
}
},
methods: {
getRelays: function () {
var self = this
LNbits.api
.request(
'GET',
'/nostradmin/api/v1/relays',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
if (response.data) {
console.log(response.data)
response.data.map(maplrelays)
self.nostrrelayLinks = response.data
console.log(self.nostrrelayLinks)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
setAllowList: function () {
var self = this
LNbits.api
.request(
'POST',
'/nostradmin/api/v1/allowlist',
self.g.user.wallets[0].adminkey,
self.allowList
)
.then(function (response) {
if (response.data) {
self.allowList = response.data
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
setRelayList: function () {
var self = this
console.log(self.setList)
LNbits.api
.request(
'POST',
'/nostradmin/api/v1/setlist',
self.g.user.wallets[0].adminkey,
self.setList
)
.then(function (response) {
if (response.data) {
console.log(response.data)
// self.denyList = response.data
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getLists: function () {
var self = this
LNbits.api
.request(
'GET',
'/nostradmin/api/v1/relaylist',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
if (response.data) {
console.log(response.data)
self.setList.denylist = response.data.denylist
self.setList.allowlist = response.data.allowlist
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
exportlnurldeviceCSV: function () {
var self = this
LNbits.utils.exportCSV(self.nostrTable.columns, this.nostrLinks)
}
},
created: function () {
var self = this
this.getRelays()
this.getLists()
}
})
</script>
{% endblock %}

View file

@ -1,110 +0,0 @@
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 nostradmin_ext, nostr_renderer
# FastAPI good for incoming
from fastapi import Request, WebSocket, WebSocketDisconnect
# Websockets needed for outgoing
import websockets
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
from .crud import get_nostrkeys, get_nostrrelay
from .relay_manager import RelayManager, Relay
templates = Jinja2Templates(directory="templates")
nostradmin = True
@nostradmin_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return nostr_renderer().TemplateResponse(
"nostradmin/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)
mgr: RelayManager = RelayManager(enable_ws_debugger=False)
# listen for events coming from relays
async def connectToNostr():
while True:
e = await mgr.msg_channel.get()
print(e)
connectToNostr
#####################################################################
################### 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()
@nostradmin_ext.websocket("/nostradmin/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":
r = Relay(url=relay, read=True, write=True, active=True)
try:
await mgr.add_relay(r)
except:
None
return True
else:
return False

View file

@ -1,63 +0,0 @@
from http import HTTPStatus
import asyncio
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.utils.exchange_rates import currencies
from lnbits.settings import LNBITS_ADMIN_USERS
from . import nostradmin_ext
from .crud import (
create_nostrkeys,
get_nostrkeys,
create_nostrnotes,
get_nostrnotes,
create_nostrrelays,
get_nostrrelays,
get_nostrrelaylist,
update_nostrrelaysetlist,
create_nostrconnections,
get_nostrconnections,
)
from .models import nostrKeys, nostrCreateRelays, nostrRelaySetList
from .views import relay_check
@nostradmin_ext.get("/api/v1/relays")
async def api_relays_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
relays = await get_nostrrelays()
if not relays:
await create_nostrrelays(nostrCreateRelays(relay="wss://relayer.fiatjaf.com"))
await create_nostrrelays(
nostrCreateRelays(relay="wss://nostr-pub.wellorder.net")
)
relays = await get_nostrrelays()
if not relays:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
else:
for relay in relays:
relay.status = await relay_check(relay.relay)
return relays
@nostradmin_ext.get("/api/v1/relaylist")
async def api_relaylist(wallet: WalletTypeInfo = Depends(get_key_type)):
if wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
return await get_nostrrelaylist()
@nostradmin_ext.post("/api/v1/setlist")
async def api_relayssetlist(data: nostrRelaySetList, wallet: WalletTypeInfo = Depends(get_key_type)):
if wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
return await update_nostrrelaysetlist(data)