feat: add UI for basic relay operations

This commit is contained in:
Vlad Stan 2023-02-06 15:30:05 +02:00
parent 0db0d9c351
commit a849dea99f
6 changed files with 152 additions and 62 deletions

53
crud.py
View file

@ -1,10 +1,59 @@
import json import json
from typing import Any, List, Optional from typing import Any, List, Optional
from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import NostrEvent, NostrFilter from .models import NostrEvent, NostrFilter, NostrRelay
########################## RELAYS ####################
async def create_relay(user_id: str, r: NostrRelay) -> NostrRelay:
await db.execute(
"""
INSERT INTO nostrrelay.relays (user_id, id, name, description, pubkey, contact)
VALUES (?, ?, ?, ?, ?, ?)
""",
(user_id, r.id, r.name, r.description, r.pubkey, r.contact,),
)
relay = await get_relay(user_id, r.id)
assert relay, "Created relay cannot be retrieved"
return relay
async def get_relay(user_id: str, relay_id: str) -> Optional[NostrRelay]:
row = await db.fetchone("""SELECT * FROM nostrrelay.relays WHERE user_id = ? AND id = ?""", (user_id, relay_id,))
return NostrRelay.from_row(row) if row else None
async def get_relays(user_id: str) -> List[NostrRelay]:
rows = await db.fetchall("""SELECT * FROM nostrrelay.relays WHERE user_id = ?""", (user_id,))
return [NostrRelay.from_row(row) for row in rows]
async def get_public_relay(relay_id: str) -> Optional[dict]:
row = await db.fetchone("""SELECT * FROM nostrrelay.relays WHERE id = ?""", (relay_id,))
if row:
relay = NostrRelay.parse_obj({"id": row["id"], **json.loads(row["meta"])})
return {
"id": relay.id,
"name": relay.name,
"description":relay.description,
"pubkey":relay.pubkey,
"contact":relay.contact,
"supported_nips":relay.supported_nips,
}
return None
async def delete_relay(user_id: str, relay_id: str):
await db.execute("""DELETE FROM nostrrelay.relays WHERE user_id = ? AND id = ?""", (user_id, relay_id,))
########################## EVENTS ####################
async def create_event(relay_id: str, e: NostrEvent): async def create_event(relay_id: str, e: NostrEvent):
await db.execute( await db.execute(
""" """
@ -28,7 +77,6 @@ async def create_event(relay_id: str, e: NostrEvent):
extra = json.dumps(rest) if rest else None extra = json.dumps(rest) if rest else None
await create_event_tags(relay_id, e.id, name, value, extra) await create_event_tags(relay_id, e.id, name, value, extra)
async def get_events(relay_id: str, filter: NostrFilter, include_tags = True) -> List[NostrEvent]: async def get_events(relay_id: str, filter: NostrFilter, include_tags = True) -> List[NostrEvent]:
values, query = build_select_events_query(relay_id, filter) values, query = build_select_events_query(relay_id, filter)
@ -43,7 +91,6 @@ async def get_events(relay_id: str, filter: NostrFilter, include_tags = True) ->
return events return events
async def get_event(relay_id: str, id: str) -> Optional[NostrEvent]: async def get_event(relay_id: str, id: str) -> Optional[NostrEvent]:
row = await db.fetchone("SELECT * FROM nostrrelay.events WHERE relay_id = ? AND id = ?", (relay_id, id,)) row = await db.fetchone("SELECT * FROM nostrrelay.events WHERE relay_id = ? AND id = ?", (relay_id, id,))
if not row: if not row:

View file

@ -2,17 +2,17 @@ async def m001_initial(db):
""" """
Initial nostrrelays tables. Initial nostrrelays tables.
""" """
await db.execute( await db.execute(
""" """
CREATE TABLE nostrrelay.relays ( CREATE TABLE nostrrelay.relays (
user_id TEXT NOT NULL,
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT, description TEXT,
pubkey TEXT, pubkey TEXT,
contact TEXT, contact TEXT,
supported_nips TEXT, active BOOLEAN DEFAULT false,
software TEXT,
version TEXT,
meta TEXT NOT NULL DEFAULT '{}' meta TEXT NOT NULL DEFAULT '{}'
); );
""" """

View file

@ -10,25 +10,19 @@ from secp256k1 import PublicKey
class NostrRelay(BaseModel): class NostrRelay(BaseModel):
id: str id: str
wallet: str
name: 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 NostrRelayInfo(BaseModel):
name: Optional[str]
description: Optional[str] description: Optional[str]
pubkey: Optional[str] pubkey: Optional[str]
contact: Optional[str] = "https://t.me/lnbits" contact: Optional[str] = "https://t.me/lnbits"
supported_nips: List[str] = ["NIP01", "NIP09", "NIP11", "NIP15", "NIP20"] supported_nips: List[str] = ["NIP01", "NIP09", "NIP11", "NIP15", "NIP20"]
software: Optional[str] = "LNbist" software: Optional[str] = "LNbist"
version: Optional[str] version: Optional[str]
# meta: Optional[str]
@classmethod
def from_row(cls, row: Row) -> "NostrRelay":
return cls(**dict(row))
class NostrEventType(str, Enum): class NostrEventType(str, Enum):

View file

@ -22,29 +22,22 @@ const relays = async () => {
wallet: '' wallet: ''
} }
}, },
relayTypes: [
{
id: 'rating',
label: 'Rating (rate one item from a list)'
},
{
id: 'poll',
label: 'Poll (choose one item from a list)'
},
{
id: 'likes',
label: 'Likes (like or dislike an item)'
}
],
relaysTable: { relaysTable: {
columns: [ columns: [
{
name: 'id',
align: 'left',
label: 'ID',
field: 'id'
},
{ {
name: '', name: '',
align: 'left', align: 'left',
label: '', label: '',
field: '' field: ''
}, },
{ {
name: 'name', name: 'name',
align: 'left', align: 'left',
@ -58,16 +51,16 @@ const relays = async () => {
field: 'description' field: 'description'
}, },
{ {
name: 'type', name: 'pubkey',
align: 'left', align: 'left',
label: 'Type', label: 'Public Key',
field: 'type' field: 'pubkey'
}, },
{ {
name: 'amount', name: 'contact',
align: 'left', align: 'left',
label: 'Amount', label: 'Contact',
field: 'amount' field: 'contact'
} }
], ],
pagination: { pagination: {
@ -79,17 +72,14 @@ const relays = async () => {
methods: { methods: {
getDefaultRelayData: function () { getDefaultRelayData: function () {
return { return {
id: '',
name: '', name: '',
description: '', description: '',
type: this.relayTypes[0], pubkey: '',
amount: '100', contact: ''
wallet: ''
} }
}, },
getRelayTypeLabel: function (relayType) {
const type = this.relayTypes.find(s => (s.id = relayType))
return type ? type.label : '?'
},
openCreateRelayDialog: function () { openCreateRelayDialog: function () {
this.formDialogRelay.data = this.getDefaultRelayData() this.formDialogRelay.data = this.getDefaultRelayData()
this.formDialogRelay.show = true this.formDialogRelay.show = true
@ -98,7 +88,7 @@ const relays = async () => {
try { try {
const {data} = await LNbits.api.request( const {data} = await LNbits.api.request(
'GET', 'GET',
'/reviews/api/v1/survey', '/nostrrelay/api/v1/relay',
this.g.user.wallets[0].inkey this.g.user.wallets[0].inkey
) )
this.relayLinks = data.map(c => this.relayLinks = data.map(c =>
@ -116,10 +106,10 @@ const relays = async () => {
createRelay: async function (data) { createRelay: async function (data) {
try { try {
data.type = data.type.id console.log('### createRelay', data)
const resp = await LNbits.api.request( const resp = await LNbits.api.request(
'POST', 'POST',
'/reviews/api/v1/survey', '/nostrrelay/api/v1/relay',
this.g.user.wallets[0].adminkey, this.g.user.wallets[0].adminkey,
data data
) )
@ -138,7 +128,7 @@ const relays = async () => {
try { try {
const response = await LNbits.api.request( const response = await LNbits.api.request(
'DELETE', 'DELETE',
'/reviews/api/v1/survey/' + relayId, '/nostrrelay/api/v1/relay/' + relayId,
this.g.user.wallets[0].adminkey this.g.user.wallets[0].adminkey
) )

View file

@ -69,14 +69,15 @@
</q-td> </q-td>
<q-td auto-width> {{props.row.name}} </q-td> <q-td auto-width> {{props.row.name}} </q-td>
<q-td key="description" :props="props" :class=""> <q-td key="id" :props="props"> {{props.row.id}} </q-td>
<q-td key="description" :props="props">
{{props.row.description}} {{props.row.description}}
</q-td> </q-td>
<q-td key="type" :props="props" :class=""> <q-td key="contact" :props="props">
<div>{{getRelayTypeLabel(props.row.type)}}</div> <div>{{props.row.contact}}</div>
</q-td> </q-td>
<q-td key="amount" :props="props" :class=""> <q-td key="pubkey" :props="props">
<div>{{props.row.amount}}</div> <div>{{props.row.pubkey}}</div>
</q-td> </q-td>
</q-tr> </q-tr>
<q-tr v-if="props.row.expanded" :props="props"> <q-tr v-if="props.row.expanded" :props="props">

View file

@ -1,12 +1,23 @@
from http import HTTPStatus from http import HTTPStatus
from typing import List, Optional
from fastapi import Query, WebSocket from fastapi import Depends, Query, WebSocket
from fastapi.exceptions import HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from loguru import logger from loguru import logger
from lnbits.decorators import (
WalletTypeInfo,
check_admin,
require_admin_key,
require_invoice_key,
)
from lnbits.helpers import urlsafe_short_hash
from . import nostrrelay_ext from . import nostrrelay_ext
from .client_manager import NostrClientConnection, NostrClientManager from .client_manager import NostrClientConnection, NostrClientManager
from .models import NostrRelayInfo from .crud import create_relay, delete_relay, get_relay, get_relays
from .models import NostrRelay
client_manager = NostrClientManager() client_manager = NostrClientManager()
@ -29,14 +40,61 @@ async def api_nostrrelay_info():
"Access-Control-Allow-Headers": "*", "Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "GET" "Access-Control-Allow-Methods": "GET"
} }
info = NostrRelayInfo() info = NostrRelay()
return JSONResponse(content=dict(info), headers=headers) return JSONResponse(content=dict(info), headers=headers)
@nostrrelay_ext.get("/api/v1/enable", status_code=HTTPStatus.OK)
async def api_nostrrelay(enable: bool = Query(True)): @nostrrelay_ext.post("/api/v1/relay")
return await enable_relay(enable) async def api_create_survey(data: NostrRelay, wallet: WalletTypeInfo = Depends(require_admin_key)) -> NostrRelay:
try:
relay = await create_relay(wallet.wallet.user, data)
return relay
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot create relay",
)
async def enable_relay(enable: bool): @nostrrelay_ext.get("/api/v1/relay")
return enable async def api_get_relays(wallet: WalletTypeInfo = Depends(require_invoice_key)) -> List[NostrRelay]:
try:
return await get_relays(wallet.wallet.user)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot fetch relays",
)
@nostrrelay_ext.get("/api/v1/relay/{relay_id}")
async def api_get_relay(relay_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)) -> Optional[NostrRelay]:
try:
relay = await get_relay(wallet.wallet.user, relay_id)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot fetch relay",
)
if not relay:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail="Cannot find relay",
)
return relay
@nostrrelay_ext.delete("/api/v1/relay/{relay_id}")
async def api_delete_relay(relay_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)):
try:
await delete_relay(wallet.wallet.user, relay_id)
except Exception as ex:
logger.warning(ex)
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Cannot delete relay",
)