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",
+ )
+
+
+