301 lines
9 KiB
Python
301 lines
9 KiB
Python
from http import HTTPStatus
|
|
from typing import List, Optional
|
|
|
|
from fastapi import Depends, Request, WebSocket
|
|
from fastapi.exceptions import HTTPException
|
|
from loguru import logger
|
|
from pydantic.types import UUID4
|
|
|
|
from lnbits.core.services import create_invoice
|
|
from lnbits.decorators import (
|
|
WalletTypeInfo,
|
|
check_admin,
|
|
require_admin_key,
|
|
require_invoice_key,
|
|
)
|
|
from lnbits.helpers import urlsafe_short_hash
|
|
|
|
from . import nostrrelay_ext, scheduled_tasks
|
|
from .crud import (
|
|
create_account,
|
|
create_relay,
|
|
delete_all_events,
|
|
delete_relay,
|
|
get_account,
|
|
get_accounts,
|
|
get_relay,
|
|
get_relay_by_id,
|
|
get_relays,
|
|
update_account,
|
|
update_relay,
|
|
)
|
|
from .helpers import extract_domain, normalize_public_key
|
|
from .models import BuyOrder, NostrAccount, NostrPartialAccount
|
|
from .relay.client_manager import NostrClientConnection, NostrClientManager
|
|
from .relay.relay import NostrRelay
|
|
|
|
client_manager = NostrClientManager()
|
|
|
|
|
|
@nostrrelay_ext.websocket("/{relay_id}")
|
|
async def websocket_endpoint(relay_id: str, websocket: WebSocket):
|
|
client = NostrClientConnection(relay_id=relay_id, websocket=websocket)
|
|
client_accepted = await client_manager.add_client(client)
|
|
if not client_accepted:
|
|
return
|
|
|
|
try:
|
|
await client.start()
|
|
except Exception as e:
|
|
logger.warning(e)
|
|
client_manager.remove_client(client)
|
|
|
|
|
|
@nostrrelay_ext.post("/api/v1/relay")
|
|
async def api_create_relay(
|
|
data: NostrRelay,
|
|
request: Request,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> NostrRelay:
|
|
if len(data.id):
|
|
await check_admin(UUID4(wallet.wallet.user))
|
|
else:
|
|
data.id = urlsafe_short_hash()[:8]
|
|
|
|
try:
|
|
data.config.domain = extract_domain(str(request.url))
|
|
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",
|
|
)
|
|
|
|
|
|
@nostrrelay_ext.put("/api/v1/relay/{relay_id}")
|
|
async def api_update_relay(
|
|
relay_id: str, data: NostrRelay, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
) -> NostrRelay:
|
|
if relay_id != data.id:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail="Cannot change the relay id",
|
|
)
|
|
|
|
try:
|
|
relay = await get_relay(wallet.wallet.user, data.id)
|
|
if not relay:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND,
|
|
detail="Relay not found",
|
|
)
|
|
updated_relay = NostrRelay.parse_obj({**dict(relay), **dict(data)})
|
|
updated_relay = await update_relay(wallet.wallet.user, updated_relay)
|
|
|
|
if updated_relay.active:
|
|
await client_manager.enable_relay(relay_id, updated_relay.config)
|
|
else:
|
|
await client_manager.disable_relay(relay_id)
|
|
|
|
return updated_relay
|
|
|
|
except HTTPException as ex:
|
|
raise ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot update relay",
|
|
)
|
|
|
|
|
|
@nostrrelay_ext.get("/api/v1/relay")
|
|
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.put("/api/v1/account")
|
|
async def api_create_or_update_account(
|
|
data: NostrPartialAccount,
|
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
|
) -> NostrAccount:
|
|
|
|
try:
|
|
data.pubkey = normalize_public_key(data.pubkey)
|
|
account = await get_account(data.relay_id, data.pubkey)
|
|
if not account:
|
|
account = NostrAccount(
|
|
pubkey=data.pubkey,
|
|
blocked=data.blocked or False,
|
|
allowed=data.allowed or False,
|
|
)
|
|
return await create_account(data.relay_id, account)
|
|
|
|
if data.blocked is not None:
|
|
account.blocked = data.blocked
|
|
if data.allowed is not None:
|
|
account.allowed = data.allowed
|
|
|
|
return await update_account(data.relay_id, account)
|
|
|
|
except ValueError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
)
|
|
except HTTPException as ex:
|
|
raise ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot create account",
|
|
)
|
|
|
|
|
|
@nostrrelay_ext.get("/api/v1/account")
|
|
async def api_get_accounts(
|
|
relay_id: str,
|
|
allowed=False,
|
|
blocked=True,
|
|
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
|
) -> List[NostrAccount]:
|
|
try:
|
|
# make sure the user has access to the relay
|
|
relay = await get_relay(wallet.wallet.user, relay_id)
|
|
if not relay:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND,
|
|
detail="Relay not found",
|
|
)
|
|
accounts = await get_accounts(relay.id, allowed, blocked)
|
|
return accounts
|
|
except ValueError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
)
|
|
except HTTPException as ex:
|
|
raise ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot fetch accounts",
|
|
)
|
|
|
|
|
|
@nostrrelay_ext.delete("/api/v1/relay/{relay_id}")
|
|
async def api_delete_relay(
|
|
relay_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
|
):
|
|
try:
|
|
await client_manager.disable_relay(relay_id)
|
|
await delete_relay(wallet.wallet.user, relay_id)
|
|
await delete_all_events(relay_id)
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot delete relay",
|
|
)
|
|
|
|
|
|
@nostrrelay_ext.put("/api/v1/pay")
|
|
async def api_pay_to_join(data: BuyOrder):
|
|
try:
|
|
pubkey = normalize_public_key(data.pubkey)
|
|
relay = await get_relay_by_id(data.relay_id)
|
|
if not relay:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.NOT_FOUND,
|
|
detail="Relay not found",
|
|
)
|
|
|
|
if data.action == "join" and relay.is_free_to_join:
|
|
raise ValueError("Relay is free to join")
|
|
|
|
storage_to_buy = 0
|
|
if data.action == "storage":
|
|
if relay.config.storage_cost_value == 0:
|
|
raise ValueError("Relay storage cost is zero. Cannot buy!")
|
|
if data.units_to_buy == 0:
|
|
raise ValueError("Must specify how much storage to buy!")
|
|
storage_to_buy = data.units_to_buy * relay.config.storage_cost_value * 1024
|
|
if relay.config.storage_cost_unit == "MB":
|
|
storage_to_buy *= 1024
|
|
|
|
_, payment_request = await create_invoice(
|
|
wallet_id=relay.config.wallet,
|
|
amount=int(relay.config.cost_to_join),
|
|
memo=f"Pubkey '{data.pubkey}' wants to join {relay.id}",
|
|
extra={
|
|
"tag": "nostrrely",
|
|
"action": data.action,
|
|
"relay_id": relay.id,
|
|
"pubkey": pubkey,
|
|
"storage_to_buy": storage_to_buy,
|
|
},
|
|
)
|
|
return {"invoice": payment_request}
|
|
except ValueError as ex:
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.BAD_REQUEST,
|
|
detail=str(ex),
|
|
)
|
|
except HTTPException as ex:
|
|
raise ex
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
raise HTTPException(
|
|
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
detail="Cannot create invoice for client to join",
|
|
)
|
|
|
|
|
|
@nostrrelay_ext.delete("/api/v1", status_code=HTTPStatus.OK)
|
|
async def api_stop(wallet: WalletTypeInfo = Depends(check_admin)):
|
|
for t in scheduled_tasks:
|
|
try:
|
|
t.cancel()
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
|
|
try:
|
|
await client_manager.stop()
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
|
|
return {"success": True}
|