From 8678090e7b492dd4571095af0bf02e4831e62d08 Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 10 Feb 2023 17:20:55 +0200 Subject: [PATCH] feat: create invoice to join --- crud.py | 13 ++++- helpers.py | 19 +++++++ models.py | 9 ++++ templates/nostrrelay/public.html | 86 ++++++++++++++++++++++++++++++-- views_api.py | 57 +++++++++++++++++++-- 5 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 helpers.py diff --git a/crud.py b/crud.py index b945537..cf5b786 100644 --- a/crud.py +++ b/crud.py @@ -61,6 +61,17 @@ async def get_relay(user_id: str, relay_id: str) -> Optional[NostrRelay]: return NostrRelay.from_row(row) if row else None +async def get_relay_by_id(relay_id: str) -> Optional[NostrRelay]: + """Note: it does not require `user_id`. Can read any relay. Use it with care.""" + row = await db.fetchone( + """SELECT * FROM nostrrelay.relays WHERE 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( @@ -100,7 +111,7 @@ async def get_public_relay(relay_id: str) -> Optional[dict]: "description": relay.description, "pubkey": relay.pubkey, "contact": relay.contact, - "config": dict(RelayPublicSpec(**dict(relay.config))) + "config": RelayPublicSpec(**dict(relay.config)).dict(by_alias=True) } diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..fdff734 --- /dev/null +++ b/helpers.py @@ -0,0 +1,19 @@ +from bech32 import bech32_decode, convertbits + + +def normalize_public_key(pubkey: str) -> str: + if pubkey.startswith('npub1'): + _, decoded_data = bech32_decode(pubkey) + if not decoded_data: + raise ValueError("Public Key is not valid npub") + + decoded_data_bits = convertbits(decoded_data, 5, 8, False) + if not decoded_data_bits: + raise ValueError("Public Key is not valid npub") + return bytes(decoded_data_bits).hex() + + #check if valid hex + if len(pubkey) != 64: + raise ValueError("Public Key is not valid hex") + int(pubkey, 16) + return pubkey \ No newline at end of file diff --git a/models.py b/models.py index 44647d5..d9151fa 100644 --- a/models.py +++ b/models.py @@ -97,6 +97,10 @@ class NostrRelay(BaseModel): config: "RelaySpec" = RelaySpec() + @property + def is_free_to_join(self): + return not self.config.is_paid_relay or self.config.cost_to_join == 0 + @classmethod def from_row(cls, row: Row) -> "NostrRelay": relay = cls(**dict(row)) @@ -290,3 +294,8 @@ class NostrFilter(BaseModel): values += [self.until] return inner_joins, where, values + + +class RelayJoin(BaseModel): + relay_id: str + pubkey: str \ No newline at end of file diff --git a/templates/nostrrelay/public.html b/templates/nostrrelay/public.html index ec65d42..ffaf409 100644 --- a/templates/nostrrelay/public.html +++ b/templates/nostrrelay/public.html @@ -8,7 +8,55 @@
- + +

+
+
+ + + Public Key: + + + + Pay to join + Cost to join: + + + + sats + + + + + This is a free relay + + + + +
+ + + +
+
+
@@ -17,7 +65,7 @@ @@ -94,10 +142,40 @@ mixins: [windowMixin], data: function () { return { - relay: JSON.parse('{{relay | tojson | safe}}') + relay: JSON.parse('{{relay | tojson | safe}}'), + pubkey: '', + joinInvoice: '' } }, - methods: {} + methods: { + payToJoin: async function () { + if (!this.pubkey) { + this.$q.notify({ + timeout: 5000, + type: 'warning', + message: 'Public key is missing' + }) + return + } + try { + const {data} = await LNbits.api.request( + 'PUT', + '/nostrrelay/api/v1/join', + '', + { + relay_id: this.relay.id, + pubkey: this.pubkey + } + ) + this.joinInvoice = data.invoice + } catch (error) { + LNbits.utils.notifyApiError(error) + } + } + }, + created: function () { + console.log('### created', this.relay) + } }) {% endblock %} diff --git a/views_api.py b/views_api.py index f883b77..cb8c646 100644 --- a/views_api.py +++ b/views_api.py @@ -3,10 +3,10 @@ from typing import List, Optional from fastapi import Depends, WebSocket from fastapi.exceptions import HTTPException -from fastapi.responses import JSONResponse from loguru import logger from pydantic.types import UUID4 +from lnbits.core.services import create_invoice from lnbits.decorators import ( WalletTypeInfo, check_admin, @@ -21,12 +21,13 @@ from .crud import ( create_relay, delete_all_events, delete_relay, - get_public_relay, get_relay, + get_relay_by_id, get_relays, update_relay, ) -from .models import NostrRelay +from .helpers import normalize_public_key +from .models import NostrRelay, RelayJoin client_manager = NostrClientManager() @@ -151,3 +152,53 @@ async def api_delete_relay( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Cannot delete relay", ) + + +@nostrrelay_ext.put("/api/v1/join") +async def api_pay_to_join( + data: RelayJoin +): + + 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 relay.is_free_to_join: + raise ValueError("Relay is free to join") + + _, 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": "join", + "relay": relay.id, + "pubkey": pubkey + }, + ) + print("### payment_request", payment_request) + 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", + ) + + +