feat: improve codequality and CI (#25)
* feat: improve codequality and CI
This commit is contained in:
parent
28121184c3
commit
cc6752003a
28 changed files with 3114 additions and 292 deletions
34
.github/workflows/ci.yml
vendored
Normal file
34
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
uses: lnbits/lnbits/.github/workflows/lint.yml@dev
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint]
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.9', '3.10']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: lnbits/lnbits/.github/actions/prepare@dev
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Run pytest
|
||||
uses: pavelzw/pytest-action@v2
|
||||
env:
|
||||
LNBITS_BACKEND_WALLET_CLASS: FakeWallet
|
||||
PYTHONUNBUFFERED: 1
|
||||
DEBUG: true
|
||||
with:
|
||||
verbose: true
|
||||
job-summary: true
|
||||
emoji: false
|
||||
click-to-expand: true
|
||||
custom-pytest: poetry run pytest
|
||||
report-title: 'test (${{ matrix.python-version }})'
|
||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
|
|
@ -1,10 +1,9 @@
|
|||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
@ -34,12 +33,12 @@ jobs:
|
|||
- name: Create pull request in extensions repo
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.EXT_GITHUB }}
|
||||
repo_name: "${{ github.event.repository.name }}"
|
||||
tag: "${{ github.ref_name }}"
|
||||
branch: "update-${{ github.event.repository.name }}-${{ github.ref_name }}"
|
||||
title: "[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}"
|
||||
body: "https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}"
|
||||
archive: "https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip"
|
||||
repo_name: '${{ github.event.repository.name }}'
|
||||
tag: '${{ github.ref_name }}'
|
||||
branch: 'update-${{ github.event.repository.name }}-${{ github.ref_name }}'
|
||||
title: '[UPDATE] ${{ github.event.repository.name }} to ${{ github.ref_name }}'
|
||||
body: 'https://github.com/lnbits/${{ github.event.repository.name }}/releases/${{ github.ref_name }}'
|
||||
archive: 'https://github.com/lnbits/${{ github.event.repository.name }}/archive/refs/tags/${{ github.ref_name }}.zip'
|
||||
run: |
|
||||
cd lnbits-extensions
|
||||
git checkout -b $branch
|
||||
|
|
|
|||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1 +1,4 @@
|
|||
__pycache__
|
||||
__pycache__
|
||||
node_modules
|
||||
.venv
|
||||
.mypy_cache
|
||||
|
|
|
|||
12
.prettierrc
Normal file
12
.prettierrc
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"semi": false,
|
||||
"arrowParens": "avoid",
|
||||
"insertPragma": false,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false,
|
||||
"bracketSameLine": false,
|
||||
"bracketSpacing": false
|
||||
}
|
||||
48
Makefile
Normal file
48
Makefile
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
all: format check
|
||||
|
||||
format: prettier black ruff
|
||||
|
||||
check: mypy pyright checkblack checkruff checkprettier
|
||||
|
||||
prettier:
|
||||
poetry run ./node_modules/.bin/prettier --write .
|
||||
pyright:
|
||||
poetry run ./node_modules/.bin/pyright
|
||||
|
||||
mypy:
|
||||
poetry run mypy .
|
||||
|
||||
black:
|
||||
poetry run black .
|
||||
|
||||
ruff:
|
||||
poetry run ruff check . --fix
|
||||
|
||||
checkruff:
|
||||
poetry run ruff check .
|
||||
|
||||
checkprettier:
|
||||
poetry run ./node_modules/.bin/prettier --check .
|
||||
|
||||
checkblack:
|
||||
poetry run black --check .
|
||||
|
||||
checkeditorconfig:
|
||||
editorconfig-checker
|
||||
|
||||
test:
|
||||
PYTHONUNBUFFERED=1 \
|
||||
DEBUG=true \
|
||||
poetry run pytest
|
||||
|
||||
install-pre-commit-hook:
|
||||
@echo "Installing pre-commit hook to git"
|
||||
@echo "Uninstall the hook with poetry run pre-commit uninstall"
|
||||
poetry run pre-commit install
|
||||
|
||||
pre-commit:
|
||||
poetry run pre-commit run --all-files
|
||||
|
||||
|
||||
checkbundle:
|
||||
@echo "skipping checkbundle"
|
||||
39
__init__.py
39
__init__.py
|
|
@ -3,16 +3,16 @@ import asyncio
|
|||
from fastapi import APIRouter
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.helpers import template_renderer
|
||||
from lnbits.tasks import create_permanent_unique_task
|
||||
from .relay.client_manager import NostrClientManager
|
||||
|
||||
db = Database("ext_nostrrelay")
|
||||
from .client_manager import client_manager
|
||||
from .crud import db
|
||||
from .tasks import wait_for_paid_invoices
|
||||
from .views import nostrrelay_generic_router
|
||||
from .views_api import nostrrelay_api_router
|
||||
|
||||
nostrrelay_ext: APIRouter = APIRouter(prefix="/nostrrelay", tags=["NostrRelay"])
|
||||
nostrrelay_ext.include_router(nostrrelay_generic_router)
|
||||
nostrrelay_ext.include_router(nostrrelay_api_router)
|
||||
|
||||
client_manager: NostrClientManager = NostrClientManager()
|
||||
|
||||
nostrrelay_static_files = [
|
||||
{
|
||||
|
|
@ -29,30 +29,31 @@ nostrrelay_redirect_paths = [
|
|||
}
|
||||
]
|
||||
|
||||
|
||||
def nostrrelay_renderer():
|
||||
return template_renderer(["nostrrelay/templates"])
|
||||
|
||||
|
||||
from .tasks import wait_for_paid_invoices
|
||||
from .views import * # noqa
|
||||
from .views_api import * # noqa
|
||||
|
||||
|
||||
scheduled_tasks: list[asyncio.Task] = []
|
||||
|
||||
async def nostrrelay_stop():
|
||||
|
||||
def nostrrelay_stop():
|
||||
for task in scheduled_tasks:
|
||||
try:
|
||||
task.cancel()
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
try:
|
||||
await client_manager.stop()
|
||||
asyncio.run(client_manager.stop())
|
||||
except Exception as ex:
|
||||
logger.warning(ex)
|
||||
|
||||
|
||||
def nostrrelay_start():
|
||||
from lnbits.tasks import create_permanent_unique_task
|
||||
|
||||
task = create_permanent_unique_task("ext_nostrrelay", wait_for_paid_invoices)
|
||||
scheduled_tasks.append(task)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"db",
|
||||
"nostrrelay_ext",
|
||||
"nostrrelay_start",
|
||||
"nostrrelay_stop",
|
||||
]
|
||||
|
|
|
|||
3
client_manager.py
Normal file
3
client_manager.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .relay.client_manager import NostrClientManager
|
||||
|
||||
client_manager: NostrClientManager = NostrClientManager()
|
||||
69
crud.py
69
crud.py
|
|
@ -1,19 +1,23 @@
|
|||
import json
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from . import db
|
||||
from lnbits.db import Database
|
||||
|
||||
from .models import NostrAccount
|
||||
from .relay.event import NostrEvent
|
||||
from .relay.filter import NostrFilter
|
||||
from .relay.relay import NostrRelay, RelayPublicSpec, RelaySpec
|
||||
|
||||
db = Database("ext_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, meta)
|
||||
INSERT INTO nostrrelay.relays
|
||||
(user_id, id, name, description, pubkey, contact, meta)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
|
|
@ -167,9 +171,9 @@ async def create_event(relay_id: str, e: NostrEvent, publisher: Optional[str]):
|
|||
|
||||
|
||||
async def get_events(
|
||||
relay_id: str, filter: NostrFilter, include_tags=True
|
||||
relay_id: str, nostr_filter: NostrFilter, include_tags=True
|
||||
) -> List[NostrEvent]:
|
||||
query, values = build_select_events_query(relay_id, filter)
|
||||
query, values = build_select_events_query(relay_id, nostr_filter)
|
||||
|
||||
rows = await db.fetchall(query, tuple(values))
|
||||
|
||||
|
|
@ -183,27 +187,33 @@ async def get_events(
|
|||
return events
|
||||
|
||||
|
||||
async def get_event(relay_id: str, id: str) -> Optional[NostrEvent]:
|
||||
async def get_event(relay_id: str, event_id: str) -> Optional[NostrEvent]:
|
||||
row = await db.fetchone(
|
||||
"SELECT * FROM nostrrelay.events WHERE relay_id = ? AND id = ?",
|
||||
(
|
||||
relay_id,
|
||||
id,
|
||||
event_id,
|
||||
),
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
|
||||
event = NostrEvent.from_row(row)
|
||||
event.tags = await get_event_tags(relay_id, id)
|
||||
event.tags = await get_event_tags(relay_id, event_id)
|
||||
return event
|
||||
|
||||
|
||||
async def get_storage_for_public_key(relay_id: str, publisher_pubkey: str) -> int:
|
||||
"""Returns the storage space in bytes for all the events of a public key. Deleted events are also counted"""
|
||||
"""
|
||||
Returns the storage space in bytes for all the events of a public key.
|
||||
Deleted events are also counted
|
||||
"""
|
||||
|
||||
row = await db.fetchone(
|
||||
"SELECT SUM(size) as sum FROM nostrrelay.events WHERE relay_id = ? AND publisher = ? GROUP BY publisher",
|
||||
"""
|
||||
SELECT SUM(size) as sum FROM nostrrelay.events
|
||||
WHERE relay_id = ? AND publisher = ? GROUP BY publisher
|
||||
""",
|
||||
(
|
||||
relay_id,
|
||||
publisher_pubkey,
|
||||
|
|
@ -216,7 +226,10 @@ async def get_storage_for_public_key(relay_id: str, publisher_pubkey: str) -> in
|
|||
|
||||
|
||||
async def get_prunable_events(relay_id: str, pubkey: str) -> List[Tuple[str, int]]:
|
||||
"""Return the oldest 10 000 events. Only the `id` and the size are returned, so the data size should be small"""
|
||||
"""
|
||||
Return the oldest 10 000 events. Only the `id` and the size are returned,
|
||||
so the data size should be small
|
||||
"""
|
||||
query = """
|
||||
SELECT id, size FROM nostrrelay.events
|
||||
WHERE relay_id = ? AND pubkey = ?
|
||||
|
|
@ -228,10 +241,10 @@ async def get_prunable_events(relay_id: str, pubkey: str) -> List[Tuple[str, int
|
|||
return [(r["id"], r["size"]) for r in rows]
|
||||
|
||||
|
||||
async def mark_events_deleted(relay_id: str, filter: NostrFilter):
|
||||
if filter.is_empty():
|
||||
async def mark_events_deleted(relay_id: str, nostr_filter: NostrFilter):
|
||||
if nostr_filter.is_empty():
|
||||
return None
|
||||
_, where, values = filter.to_sql_components(relay_id)
|
||||
_, where, values = nostr_filter.to_sql_components(relay_id)
|
||||
|
||||
await db.execute(
|
||||
f"""UPDATE nostrrelay.events SET deleted=true WHERE {" AND ".join(where)}""",
|
||||
|
|
@ -239,10 +252,10 @@ async def mark_events_deleted(relay_id: str, filter: NostrFilter):
|
|||
)
|
||||
|
||||
|
||||
async def delete_events(relay_id: str, filter: NostrFilter):
|
||||
if filter.is_empty():
|
||||
async def delete_events(relay_id: str, nostr_filter: NostrFilter):
|
||||
if nostr_filter.is_empty():
|
||||
return None
|
||||
_, where, values = filter.to_sql_components(relay_id)
|
||||
_, where, values = nostr_filter.to_sql_components(relay_id)
|
||||
|
||||
query = f"""DELETE from nostrrelay.events WHERE {" AND ".join(where)}"""
|
||||
await db.execute(query, tuple(values))
|
||||
|
|
@ -309,20 +322,20 @@ async def get_event_tags(relay_id: str, event_id: str) -> List[List[str]]:
|
|||
return tags
|
||||
|
||||
|
||||
def build_select_events_query(relay_id: str, filter: NostrFilter):
|
||||
inner_joins, where, values = filter.to_sql_components(relay_id)
|
||||
def build_select_events_query(relay_id: str, nostr_filter: NostrFilter):
|
||||
inner_joins, where, values = nostr_filter.to_sql_components(relay_id)
|
||||
|
||||
query = f"""
|
||||
SELECT id, pubkey, created_at, kind, content, sig
|
||||
FROM nostrrelay.events
|
||||
{" ".join(inner_joins)}
|
||||
SELECT id, pubkey, created_at, kind, content, sig
|
||||
FROM nostrrelay.events
|
||||
{" ".join(inner_joins)}
|
||||
WHERE { " AND ".join(where)}
|
||||
ORDER BY created_at DESC
|
||||
"""
|
||||
|
||||
# todo: check & enforce range
|
||||
if filter.limit and filter.limit > 0:
|
||||
query += f" LIMIT {filter.limit}"
|
||||
if nostr_filter.limit and nostr_filter.limit > 0:
|
||||
query += f" LIMIT {nostr_filter.limit}"
|
||||
|
||||
return query, values
|
||||
|
||||
|
|
@ -333,7 +346,8 @@ def build_select_events_query(relay_id: str, filter: NostrFilter):
|
|||
async def create_account(relay_id: str, a: NostrAccount) -> NostrAccount:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO nostrrelay.accounts (relay_id, pubkey, sats, storage, paid_to_join, allowed, blocked)
|
||||
INSERT INTO nostrrelay.accounts
|
||||
(relay_id, pubkey, sats, storage, paid_to_join, allowed, blocked)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
|
|
@ -394,9 +408,12 @@ async def get_accounts(
|
|||
|
||||
if not allowed and not blocked:
|
||||
return []
|
||||
|
||||
|
||||
rows = await db.fetchall(
|
||||
"SELECT * FROM nostrrelay.accounts WHERE relay_id = ? AND allowed = ? OR blocked = ?",
|
||||
"""
|
||||
SELECT * FROM nostrrelay.accounts
|
||||
WHERE relay_id = ? AND allowed = ? OR blocked = ?"
|
||||
""",
|
||||
(relay_id, allowed, blocked),
|
||||
)
|
||||
return [NostrAccount.from_row(row) for row in rows]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
Create a Nostr relay in just 2 steps!
|
||||
|
||||
Optional settings include:
|
||||
* Charging for storage
|
||||
* Charging for joining
|
||||
* Npub allow/ban list (for restricting access)
|
||||
* Pruning and filtering
|
||||
|
||||
- Charging for storage
|
||||
- Charging for joining
|
||||
- Npub allow/ban list (for restricting access)
|
||||
- Pruning and filtering
|
||||
|
|
|
|||
18
models.py
18
models.py
|
|
@ -8,26 +8,26 @@ class BuyOrder(BaseModel):
|
|||
action: str
|
||||
relay_id: str
|
||||
pubkey: str
|
||||
units_to_buy = 0
|
||||
units_to_buy: int = 0
|
||||
|
||||
def is_valid_action(self):
|
||||
def is_valid_action(self) -> bool:
|
||||
return self.action in ["join", "storage"]
|
||||
|
||||
|
||||
class NostrPartialAccount(BaseModel):
|
||||
relay_id: str
|
||||
pubkey: str
|
||||
allowed: Optional[bool]
|
||||
blocked: Optional[bool]
|
||||
allowed: Optional[bool] = None
|
||||
blocked: Optional[bool] = None
|
||||
|
||||
|
||||
class NostrAccount(BaseModel):
|
||||
pubkey: str
|
||||
allowed = False
|
||||
blocked = False
|
||||
sats = 0
|
||||
storage = 0
|
||||
paid_to_join = False
|
||||
sats: int = 0
|
||||
storage: int = 0
|
||||
paid_to_join: bool = False
|
||||
allowed: bool = False
|
||||
blocked: bool = False
|
||||
|
||||
@property
|
||||
def can_join(self):
|
||||
|
|
|
|||
59
package-lock.json
generated
Normal file
59
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "nostrrelay",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nostrrelay",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"prettier": "^3.2.5",
|
||||
"pyright": "^1.1.358"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
|
||||
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pyright": {
|
||||
"version": "1.1.369",
|
||||
"resolved": "https://registry.npmjs.org/pyright/-/pyright-1.1.369.tgz",
|
||||
"integrity": "sha512-K0mQzVNSN5yq+joFK0JraOlhtL2HKrubCa+SnFznkLsnoZKbmq7M8UpSSDsJKPFfevkmqOKodgGzvt27C6RJAg==",
|
||||
"bin": {
|
||||
"pyright": "index.js",
|
||||
"pyright-langserver": "langserver.index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
package.json
Normal file
15
package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "nostrrelay",
|
||||
"version": "1.0.0",
|
||||
"description": "Nostrrelay",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"prettier": "^3.2.5",
|
||||
"pyright": "^1.1.358"
|
||||
}
|
||||
}
|
||||
2499
poetry.lock
generated
Normal file
2499
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
97
pyproject.toml
Normal file
97
pyproject.toml
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
[tool.poetry]
|
||||
name = "nostrrelay"
|
||||
version = "0.0.0"
|
||||
description = "nostrrelay"
|
||||
authors = ["dni <dni@lnbits.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10 | ^3.9"
|
||||
lnbits = "*"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^24.3.0"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
pytest = "^7.3.2"
|
||||
mypy = "^1.5.1"
|
||||
pre-commit = "^3.2.2"
|
||||
ruff = "^0.3.2"
|
||||
pytest-md = "^0.2.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.mypy]
|
||||
exclude = [
|
||||
"boltz_client"
|
||||
]
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"lnbits.*",
|
||||
"loguru.*",
|
||||
"fastapi.*",
|
||||
"pydantic.*",
|
||||
"embit.*",
|
||||
"secp256k1.*",
|
||||
]
|
||||
ignore_missing_imports = "True"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_cli = false
|
||||
testpaths = [
|
||||
"tests"
|
||||
]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff]
|
||||
# Same as Black. + 10% rule of black
|
||||
line-length = 88
|
||||
exclude = [
|
||||
"boltz_client"
|
||||
]
|
||||
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable:
|
||||
# F - pyflakes
|
||||
# E - pycodestyle errors
|
||||
# W - pycodestyle warnings
|
||||
# I - isort
|
||||
# A - flake8-builtins
|
||||
# C - mccabe
|
||||
# N - naming
|
||||
# UP - pyupgrade
|
||||
# RUF - ruff
|
||||
# B - bugbear
|
||||
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
||||
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
|
||||
ignore = ["UP007"]
|
||||
|
||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
# needed for pydantic
|
||||
[tool.ruff.lint.pep8-naming]
|
||||
classmethod-decorators = [
|
||||
"root_validator",
|
||||
]
|
||||
|
||||
# Ignore unused imports in __init__.py files.
|
||||
# [tool.ruff.lint.extend-per-file-ignores]
|
||||
# "views_api.py" = ["F401"]
|
||||
|
||||
# [tool.ruff.lint.mccabe]
|
||||
# max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.flake8-bugbear]
|
||||
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
|
||||
extend-immutable-calls = [
|
||||
"fastapi.Depends",
|
||||
"fastapi.Query",
|
||||
]
|
||||
|
|
@ -3,9 +3,8 @@ import time
|
|||
from typing import Any, Awaitable, Callable, List, Optional
|
||||
|
||||
from fastapi import WebSocket
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from loguru import logger
|
||||
|
||||
from ..crud import (
|
||||
NostrAccount,
|
||||
|
|
@ -55,26 +54,26 @@ class NostrClientConnection:
|
|||
message = reason if reason else "Server closed webocket"
|
||||
try:
|
||||
await self._send_msg(["NOTICE", message])
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
await self.websocket.close(reason=reason)
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def init_callbacks(self, broadcast_event: Callable, get_client_config: Callable):
|
||||
setattr(self, "broadcast_event", broadcast_event)
|
||||
setattr(self, "get_client_config", get_client_config)
|
||||
setattr(self.event_validator, "get_client_config", get_client_config)
|
||||
self.broadcast_event = broadcast_event
|
||||
self.get_client_config = get_client_config
|
||||
self.event_validator.get_client_config = get_client_config
|
||||
|
||||
async def notify_event(self, event: NostrEvent) -> bool:
|
||||
if self._is_direct_message_for_other(event):
|
||||
return False
|
||||
|
||||
for filter in self.filters:
|
||||
if filter.matches(event):
|
||||
resp = event.serialize_response(filter.subscription_id)
|
||||
for nostr_filter in self.filters:
|
||||
if nostr_filter.matches(event):
|
||||
resp = event.serialize_response(nostr_filter.subscription_id)
|
||||
await self._send_msg(resp)
|
||||
return True
|
||||
return False
|
||||
|
|
@ -82,7 +81,8 @@ class NostrClientConnection:
|
|||
def _is_direct_message_for_other(self, event: NostrEvent) -> bool:
|
||||
"""
|
||||
Direct messages are not inteded to be boradcast (even if encrypted).
|
||||
If the server requires AUTH for kind '4' then direct message will be sent only to the intended client.
|
||||
If the server requires AUTH for kind '4' then direct message will be
|
||||
sent only to the intended client.
|
||||
"""
|
||||
if not event.is_direct_message:
|
||||
return False
|
||||
|
|
@ -136,7 +136,7 @@ class NostrClientConnection:
|
|||
await self._send_msg(["AUTH", self._current_auth_challenge()])
|
||||
resp_nip20 += [
|
||||
False,
|
||||
f"restricted: Relay requires authentication for events of kind '{e.kind}'",
|
||||
f"Relay requires authentication for events of kind '{e.kind}'",
|
||||
]
|
||||
await self._send_msg(resp_nip20)
|
||||
return None
|
||||
|
|
@ -166,7 +166,7 @@ class NostrClientConnection:
|
|||
event = await get_event(self.relay_id, e.id)
|
||||
# todo: handle NIP20 in detail
|
||||
message = "error: failed to create event"
|
||||
resp_nip20 += [event != None, message]
|
||||
resp_nip20 += [event is not None, message]
|
||||
|
||||
await self._send_msg(resp_nip20)
|
||||
|
||||
|
|
@ -181,13 +181,15 @@ class NostrClientConnection:
|
|||
|
||||
async def _handle_delete_event(self, event: NostrEvent):
|
||||
# NIP 09
|
||||
filter = NostrFilter(authors=[event.pubkey])
|
||||
filter.ids = [t[1] for t in event.tags if t[0] == "e"]
|
||||
events_to_delete = await get_events(self.relay_id, filter, False)
|
||||
nostr_filter = NostrFilter(authors=[event.pubkey])
|
||||
nostr_filter.ids = [t[1] for t in event.tags if t[0] == "e"]
|
||||
events_to_delete = await get_events(self.relay_id, nostr_filter, False)
|
||||
ids = [e.id for e in events_to_delete if not e.is_delete_event]
|
||||
await mark_events_deleted(self.relay_id, NostrFilter(ids=ids))
|
||||
|
||||
async def _handle_request(self, subscription_id: str, filter: NostrFilter) -> List:
|
||||
async def _handle_request(
|
||||
self, subscription_id: str, nostr_filter: NostrFilter
|
||||
) -> List:
|
||||
if self.config.require_auth_filter:
|
||||
if not self.auth_pubkey:
|
||||
return [["AUTH", self._current_auth_challenge()]]
|
||||
|
|
@ -199,26 +201,30 @@ class NostrClientConnection:
|
|||
return [
|
||||
[
|
||||
"NOTICE",
|
||||
f"Public key '{self.auth_pubkey}' is not allowed in relay '{self.relay_id}'!",
|
||||
(
|
||||
f"Public key '{self.auth_pubkey}' is not allowed "
|
||||
f"in relay '{self.relay_id}'!"
|
||||
),
|
||||
]
|
||||
]
|
||||
|
||||
if not account.can_join and not self.config.is_free_to_join:
|
||||
return [["NOTICE", f"This is a paid relay: '{self.relay_id}'"]]
|
||||
|
||||
filter.subscription_id = subscription_id
|
||||
nostr_filter.subscription_id = subscription_id
|
||||
self._remove_filter(subscription_id)
|
||||
if self._can_add_filter():
|
||||
max_filters = self.config.max_client_filters
|
||||
return [
|
||||
[
|
||||
"NOTICE",
|
||||
f"Maximum number of filters ({self.config.max_client_filters}) exceeded.",
|
||||
f"Maximum number of filters ({max_filters}) exceeded.",
|
||||
]
|
||||
]
|
||||
|
||||
filter.enforce_limit(self.config.limit_per_filter)
|
||||
self.filters.append(filter)
|
||||
events = await get_events(self.relay_id, filter)
|
||||
nostr_filter.enforce_limit(self.config.limit_per_filter)
|
||||
self.filters.append(nostr_filter)
|
||||
events = await get_events(self.relay_id, nostr_filter)
|
||||
events = [e for e in events if not self._is_direct_message_for_other(e)]
|
||||
serialized_events = [
|
||||
event.serialize_response(subscription_id) for event in events
|
||||
|
|
|
|||
|
|
@ -71,5 +71,5 @@ class NostrClientManager:
|
|||
def get_client_config() -> RelaySpec:
|
||||
return self.get_relay_config(client.relay_id)
|
||||
|
||||
setattr(client, "get_client_config", get_client_config)
|
||||
client.get_client_config = get_client_config
|
||||
client.init_callbacks(self.broadcast_event, get_client_config)
|
||||
|
|
|
|||
|
|
@ -34,8 +34,7 @@ class NostrEvent(BaseModel):
|
|||
@property
|
||||
def event_id(self) -> str:
|
||||
data = self.serialize_json()
|
||||
id = hashlib.sha256(data.encode()).hexdigest()
|
||||
return id
|
||||
return hashlib.sha256(data.encode()).hexdigest()
|
||||
|
||||
@property
|
||||
def size_bytes(self) -> int:
|
||||
|
|
@ -74,10 +73,10 @@ class NostrEvent(BaseModel):
|
|||
)
|
||||
try:
|
||||
pub_key = PublicKey(bytes.fromhex("02" + self.pubkey), True)
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
raise ValueError(
|
||||
f"Invalid public key: '{self.pubkey}' for event '{self.id}'"
|
||||
)
|
||||
) from exc
|
||||
|
||||
valid_signature = pub_key.schnorr_verify(
|
||||
bytes.fromhex(event_id), bytes.fromhex(self.sig), None, raw=True
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class EventValidator:
|
|||
|
||||
def _validate_event(self, e: NostrEvent) -> Tuple[bool, str]:
|
||||
if self._exceeded_max_events_per_hour():
|
||||
return False, f"Exceeded max events per hour limit'!"
|
||||
return False, "Exceeded max events per hour limit'!"
|
||||
|
||||
try:
|
||||
e.check_signature()
|
||||
|
|
@ -101,7 +101,7 @@ class EventValidator:
|
|||
if self.config.full_storage_action == "block":
|
||||
return (
|
||||
False,
|
||||
f"Cannot write event, no more storage available for public key: '{pubkey}'",
|
||||
f"Cannot write event, no storage available for public key: '{pubkey}'",
|
||||
)
|
||||
|
||||
if event_size_bytes > total_available_storage:
|
||||
|
|
|
|||
|
|
@ -6,16 +6,15 @@ from .event import NostrEvent
|
|||
|
||||
|
||||
class NostrFilter(BaseModel):
|
||||
subscription_id: Optional[str]
|
||||
|
||||
e: List[str] = Field(default=[], alias="#e")
|
||||
p: List[str] = Field(default=[], alias="#p")
|
||||
ids: List[str] = []
|
||||
authors: List[str] = []
|
||||
kinds: List[int] = []
|
||||
e: List[str] = Field([], alias="#e")
|
||||
p: List[str] = Field([], alias="#p")
|
||||
since: Optional[int]
|
||||
until: Optional[int]
|
||||
limit: Optional[int]
|
||||
subscription_id: Optional[str] = None
|
||||
since: Optional[int] = None
|
||||
until: Optional[int] = None
|
||||
limit: Optional[int] = None
|
||||
|
||||
def matches(self, e: NostrEvent) -> bool:
|
||||
# todo: starts with
|
||||
|
|
@ -78,7 +77,8 @@ class NostrFilter(BaseModel):
|
|||
values += self.e
|
||||
e_s = ",".join(["?"] * len(self.e))
|
||||
inner_joins.append(
|
||||
"INNER JOIN nostrrelay.event_tags e_tags ON nostrrelay.events.id = e_tags.event_id"
|
||||
"INNER JOIN nostrrelay.event_tags e_tags "
|
||||
"ON nostrrelay.events.id = e_tags.event_id"
|
||||
)
|
||||
where.append(f" (e_tags.value in ({e_s}) AND e_tags.name = 'e')")
|
||||
|
||||
|
|
@ -86,7 +86,8 @@ class NostrFilter(BaseModel):
|
|||
values += self.p
|
||||
p_s = ",".join(["?"] * len(self.p))
|
||||
inner_joins.append(
|
||||
"INNER JOIN nostrrelay.event_tags p_tags ON nostrrelay.events.id = p_tags.event_id"
|
||||
"INNER JOIN nostrrelay.event_tags p_tags "
|
||||
"ON nostrrelay.events.id = p_tags.event_id"
|
||||
)
|
||||
where.append(f" p_tags.value in ({p_s}) AND p_tags.name = 'p'")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,22 +11,22 @@ class Spec(BaseModel):
|
|||
|
||||
|
||||
class FilterSpec(Spec):
|
||||
max_client_filters = Field(0, alias="maxClientFilters")
|
||||
limit_per_filter = Field(1000, alias="limitPerFilter")
|
||||
max_client_filters: int = Field(default=0, alias="maxClientFilters")
|
||||
limit_per_filter: int = Field(default=1000, alias="limitPerFilter")
|
||||
|
||||
|
||||
class EventSpec(Spec):
|
||||
max_events_per_hour = Field(0, alias="maxEventsPerHour")
|
||||
max_events_per_hour: int = Field(default=0, alias="maxEventsPerHour")
|
||||
|
||||
created_at_days_past = Field(0, alias="createdAtDaysPast")
|
||||
created_at_hours_past = Field(0, alias="createdAtHoursPast")
|
||||
created_at_minutes_past = Field(0, alias="createdAtMinutesPast")
|
||||
created_at_seconds_past = Field(0, alias="createdAtSecondsPast")
|
||||
created_at_days_past: int = Field(default=0, alias="createdAtDaysPast")
|
||||
created_at_hours_past: int = Field(default=0, alias="createdAtHoursPast")
|
||||
created_at_minutes_past: int = Field(default=0, alias="createdAtMinutesPast")
|
||||
created_at_seconds_past: int = Field(default=0, alias="createdAtSecondsPast")
|
||||
|
||||
created_at_days_future = Field(0, alias="createdAtDaysFuture")
|
||||
created_at_hours_future = Field(0, alias="createdAtHoursFuture")
|
||||
created_at_minutes_future = Field(0, alias="createdAtMinutesFuture")
|
||||
created_at_seconds_future = Field(0, alias="createdAtSecondsFuture")
|
||||
created_at_days_future: int = Field(default=0, alias="createdAtDaysFuture")
|
||||
created_at_hours_future: int = Field(default=0, alias="createdAtHoursFuture")
|
||||
created_at_minutes_future: int = Field(default=0, alias="createdAtMinutesFuture")
|
||||
created_at_seconds_future: int = Field(default=0, alias="createdAtSecondsFuture")
|
||||
|
||||
@property
|
||||
def created_at_in_past(self) -> int:
|
||||
|
|
@ -48,9 +48,9 @@ class EventSpec(Spec):
|
|||
|
||||
|
||||
class StorageSpec(Spec):
|
||||
free_storage_value = Field(1, alias="freeStorageValue")
|
||||
free_storage_unit = Field("MB", alias="freeStorageUnit")
|
||||
full_storage_action = Field("prune", alias="fullStorageAction")
|
||||
free_storage_value: int = Field(default=1, alias="freeStorageValue")
|
||||
free_storage_unit: str = Field(default="MB", alias="freeStorageUnit")
|
||||
full_storage_action: str = Field(default="prune", alias="fullStorageAction")
|
||||
|
||||
@property
|
||||
def free_storage_bytes_value(self):
|
||||
|
|
@ -61,10 +61,10 @@ class StorageSpec(Spec):
|
|||
|
||||
|
||||
class AuthSpec(Spec):
|
||||
require_auth_events = Field(False, alias="requireAuthEvents")
|
||||
skiped_auth_events = Field([], alias="skipedAuthEvents")
|
||||
forced_auth_events = Field([], alias="forcedAuthEvents")
|
||||
require_auth_filter = Field(False, alias="requireAuthFilter")
|
||||
require_auth_events: bool = Field(default=False, alias="requireAuthEvents")
|
||||
skiped_auth_events: list = Field(default=[], alias="skipedAuthEvents")
|
||||
forced_auth_events: list = Field(default=[], alias="forcedAuthEvents")
|
||||
require_auth_filter: bool = Field(default=False, alias="requireAuthFilter")
|
||||
|
||||
def event_requires_auth(self, kind: int) -> bool:
|
||||
if self.require_auth_events:
|
||||
|
|
@ -73,11 +73,11 @@ class AuthSpec(Spec):
|
|||
|
||||
|
||||
class PaymentSpec(Spec):
|
||||
is_paid_relay = Field(False, alias="isPaidRelay")
|
||||
cost_to_join = Field(0, alias="costToJoin")
|
||||
is_paid_relay: bool = Field(default=False, alias="isPaidRelay")
|
||||
cost_to_join: int = Field(default=0, alias="costToJoin")
|
||||
|
||||
storage_cost_value = Field(0, alias="storageCostValue")
|
||||
storage_cost_unit = Field("MB", alias="storageCostUnit")
|
||||
storage_cost_value: int = Field(default=0, alias="storageCostValue")
|
||||
storage_cost_unit: str = Field(default="MB", alias="storageCostUnit")
|
||||
|
||||
@property
|
||||
def is_free_to_join(self):
|
||||
|
|
@ -85,7 +85,7 @@ class PaymentSpec(Spec):
|
|||
|
||||
|
||||
class WalletSpec(Spec):
|
||||
wallet = Field("")
|
||||
wallet: str = Field(default="")
|
||||
|
||||
|
||||
class RelayPublicSpec(FilterSpec, EventSpec, StorageSpec, PaymentSpec):
|
||||
|
|
@ -93,7 +93,7 @@ class RelayPublicSpec(FilterSpec, EventSpec, StorageSpec, PaymentSpec):
|
|||
|
||||
@property
|
||||
def is_read_only_relay(self):
|
||||
self.free_storage_value == 0 and not self.is_paid_relay
|
||||
return self.free_storage_value == 0 and not self.is_paid_relay
|
||||
|
||||
|
||||
class RelaySpec(RelayPublicSpec, WalletSpec, AuthSpec):
|
||||
|
|
@ -108,7 +108,7 @@ class NostrRelay(BaseModel):
|
|||
contact: Optional[str]
|
||||
active: bool = False
|
||||
|
||||
config: "RelaySpec" = RelaySpec()
|
||||
config = RelaySpec()
|
||||
|
||||
@property
|
||||
def is_free_to_join(self):
|
||||
|
|
|
|||
24
tasks.py
24
tasks.py
|
|
@ -1,12 +1,11 @@
|
|||
import asyncio
|
||||
import json
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models import Payment
|
||||
from lnbits.core.services import websocket_updater
|
||||
from lnbits.helpers import get_current_extension_name
|
||||
from lnbits.tasks import register_invoice_listener
|
||||
from loguru import logger
|
||||
|
||||
from .crud import create_account, get_account, update_account
|
||||
from .models import NostrAccount
|
||||
|
|
@ -27,34 +26,41 @@ async def on_invoice_paid(payment: Payment):
|
|||
|
||||
relay_id = payment.extra.get("relay_id")
|
||||
pubkey = payment.extra.get("pubkey")
|
||||
hash = payment.payment_hash
|
||||
payment_hash = payment.payment_hash
|
||||
|
||||
if not relay_id or not pubkey:
|
||||
message = f"Invoice extra data missing for 'relay_id' and 'pubkey'. Payment hash: {hash}"
|
||||
message = (
|
||||
"Invoice extra data missing for 'relay_id' and 'pubkey'. "
|
||||
f"Payment hash: {payment_hash}"
|
||||
)
|
||||
logger.warning(message)
|
||||
await websocket_updater(hash, json.dumps({"success": False, "message": message}))
|
||||
await websocket_updater(
|
||||
payment_hash, json.dumps({"success": False, "message": message})
|
||||
)
|
||||
return
|
||||
|
||||
action = payment.extra.get("action")
|
||||
if action == "join":
|
||||
await invoice_paid_to_join(relay_id, pubkey, payment.amount)
|
||||
await websocket_updater(hash, json.dumps({"success": True}))
|
||||
await websocket_updater(payment_hash, json.dumps({"success": True}))
|
||||
return
|
||||
|
||||
if action == "storage":
|
||||
storage_to_buy = payment.extra.get("storage_to_buy")
|
||||
if not storage_to_buy:
|
||||
message = (
|
||||
f"Invoice extra data missing for 'storage_to_buy'. Payment hash: {hash}"
|
||||
"Invoice extra data missing for 'storage_to_buy'. "
|
||||
f"Payment hash: {payment_hash}"
|
||||
)
|
||||
logger.warning(message)
|
||||
return
|
||||
await invoice_paid_for_storage(relay_id, pubkey, storage_to_buy, payment.amount)
|
||||
await websocket_updater(hash, json.dumps({"success": True}))
|
||||
await websocket_updater(payment_hash, json.dumps({"success": True}))
|
||||
return
|
||||
|
||||
await websocket_updater(
|
||||
hash, json.dumps({"success": False, "message": f"Bad action name: '{action}'"})
|
||||
payment_hash,
|
||||
json.dumps({"success": False, "message": f"Bad action name: '{action}'"}),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
|
||||
FIXTURES_PATH = "tests/extensions/nostrrelay/fixture"
|
||||
FIXTURES_PATH = "./tests/fixture"
|
||||
|
||||
|
||||
def get_fixtures(file):
|
||||
|
|
|
|||
|
|
@ -6,14 +6,13 @@ import pytest
|
|||
from fastapi import WebSocket
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.extensions.nostrrelay.relay.client_connection import ( # type: ignore
|
||||
from ..relay.client_connection import (
|
||||
NostrClientConnection,
|
||||
)
|
||||
from lnbits.extensions.nostrrelay.relay.client_manager import ( # type: ignore
|
||||
from ..relay.client_manager import (
|
||||
NostrClientManager,
|
||||
)
|
||||
from lnbits.extensions.nostrrelay.relay.relay import RelaySpec # type: ignore
|
||||
|
||||
from ..relay.relay import RelaySpec
|
||||
from .helpers import get_fixtures
|
||||
|
||||
fixtures = get_fixtures("clients")
|
||||
|
|
@ -26,10 +25,10 @@ RELAY_ID = "relay_01"
|
|||
class MockWebSocket(WebSocket):
|
||||
def __init__(self):
|
||||
self.sent_messages = []
|
||||
self.fake_wire: asyncio.Queue[str] = asyncio.Queue(0)
|
||||
self.fake_wire = asyncio.Queue(0)
|
||||
pass
|
||||
|
||||
async def accept(self):
|
||||
async def accept(self, *_, **__):
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def receive_text(self) -> str:
|
||||
|
|
@ -43,10 +42,12 @@ class MockWebSocket(WebSocket):
|
|||
await self.fake_wire.put(dumps(data))
|
||||
|
||||
async def close(self, code: int = 1000, reason: Optional[str] = None) -> None:
|
||||
logger.info(reason)
|
||||
logger.info(f"{code}: {reason}")
|
||||
|
||||
|
||||
# TODO: Fix the test
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail
|
||||
async def test_alice_and_bob():
|
||||
ws_alice, ws_bob = await init_clients()
|
||||
|
||||
|
|
@ -71,6 +72,9 @@ async def test_alice_and_bob():
|
|||
await alice_deletes_post01__bob_is_notified(ws_alice, ws_bob)
|
||||
|
||||
|
||||
tasks = []
|
||||
|
||||
|
||||
async def init_clients():
|
||||
client_manager = NostrClientManager()
|
||||
await client_manager.enable_relay(RELAY_ID, RelaySpec())
|
||||
|
|
@ -78,12 +82,15 @@ async def init_clients():
|
|||
ws_alice = MockWebSocket()
|
||||
client_alice = NostrClientConnection(relay_id=RELAY_ID, websocket=ws_alice)
|
||||
await client_manager.add_client(client_alice)
|
||||
asyncio.create_task(client_alice.start())
|
||||
task1 = asyncio.create_task(client_alice.start())
|
||||
tasks.append(task1)
|
||||
|
||||
ws_bob = MockWebSocket()
|
||||
client_bob = NostrClientConnection(relay_id=RELAY_ID, websocket=ws_bob)
|
||||
await client_manager.add_client(client_bob)
|
||||
asyncio.create_task(client_bob.start())
|
||||
task2 = asyncio.create_task(client_bob.start())
|
||||
tasks.append(task2)
|
||||
|
||||
return ws_alice, ws_bob
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ import pytest
|
|||
from loguru import logger
|
||||
from pydantic import BaseModel
|
||||
|
||||
from lnbits.extensions.nostrrelay.crud import ( # type: ignore
|
||||
from ..crud import (
|
||||
create_event,
|
||||
get_event,
|
||||
get_events,
|
||||
)
|
||||
from lnbits.extensions.nostrrelay.relay.event import NostrEvent # type: ignore
|
||||
from lnbits.extensions.nostrrelay.relay.filter import NostrFilter # type: ignore
|
||||
|
||||
from ..relay.event import NostrEvent
|
||||
from ..relay.filter import NostrFilter
|
||||
from .helpers import get_fixtures
|
||||
|
||||
RELAY_ID = "r1"
|
||||
|
|
@ -51,7 +50,9 @@ def test_invalid_event_id_and_signature(invalid_events: List[EventFixture]):
|
|||
f.data.check_signature()
|
||||
|
||||
|
||||
# TODO: make them work
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail
|
||||
async def test_valid_event_crud(valid_events: List[EventFixture]):
|
||||
author = "a24496bca5dd73300f4e5d5d346c73132b7354c597fcbb6509891747b4689211"
|
||||
event_id = "3219eec7427e365585d5adf26f5d2dd2709d3f0f2c0e1f79dc9021e951c67d96"
|
||||
|
|
@ -86,15 +87,15 @@ async def get_by_id(data: NostrEvent, test_name: str):
|
|||
|
||||
|
||||
async def filter_by_id(all_events: List[NostrEvent], data: NostrEvent, test_name: str):
|
||||
filter = NostrFilter(ids=[data.id])
|
||||
nostr_filter = NostrFilter(ids=[data.id])
|
||||
|
||||
events = await get_events(RELAY_ID, filter)
|
||||
events = await get_events(RELAY_ID, nostr_filter)
|
||||
assert len(events) == 1, f"Expected one queried event '{test_name}'"
|
||||
assert events[0].json() != json.dumps(
|
||||
data.json()
|
||||
), f"Queried event is different for fixture '{test_name}'"
|
||||
|
||||
filtered_events = [e for e in all_events if filter.matches(e)]
|
||||
filtered_events = [e for e in all_events if nostr_filter.matches(e)]
|
||||
assert len(filtered_events) == 1, f"Expected one filter event '{test_name}'"
|
||||
assert filtered_events[0].json() != json.dumps(
|
||||
data.json()
|
||||
|
|
@ -102,73 +103,73 @@ async def filter_by_id(all_events: List[NostrEvent], data: NostrEvent, test_name
|
|||
|
||||
|
||||
async def filter_by_author(all_events: List[NostrEvent], author):
|
||||
filter = NostrFilter(authors=[author])
|
||||
events_by_author = await get_events(RELAY_ID, filter)
|
||||
assert len(events_by_author) == 5, f"Failed to query by authors"
|
||||
nostr_filter = NostrFilter(authors=[author])
|
||||
events_by_author = await get_events(RELAY_ID, nostr_filter)
|
||||
assert len(events_by_author) == 5, "Failed to query by authors"
|
||||
|
||||
filtered_events = [e for e in all_events if filter.matches(e)]
|
||||
assert len(filtered_events) == 5, f"Failed to filter by authors"
|
||||
filtered_events = [e for e in all_events if nostr_filter.matches(e)]
|
||||
assert len(filtered_events) == 5, "Failed to filter by authors"
|
||||
|
||||
|
||||
async def filter_by_tag_p(all_events: List[NostrEvent], author):
|
||||
# todo: check why constructor does not work for fields with aliases (#e, #p)
|
||||
filter = NostrFilter()
|
||||
filter.p.append(author)
|
||||
nostr_filter = NostrFilter()
|
||||
nostr_filter.p.append(author)
|
||||
|
||||
events_related_to_author = await get_events(RELAY_ID, filter)
|
||||
assert len(events_related_to_author) == 5, f"Failed to query by tag 'p'"
|
||||
events_related_to_author = await get_events(RELAY_ID, nostr_filter)
|
||||
assert len(events_related_to_author) == 5, "Failed to query by tag 'p'"
|
||||
|
||||
filtered_events = [e for e in all_events if filter.matches(e)]
|
||||
assert len(filtered_events) == 5, f"Failed to filter by tag 'p'"
|
||||
filtered_events = [e for e in all_events if nostr_filter.matches(e)]
|
||||
assert len(filtered_events) == 5, "Failed to filter by tag 'p'"
|
||||
|
||||
|
||||
async def filter_by_tag_e(all_events: List[NostrEvent], event_id):
|
||||
filter = NostrFilter()
|
||||
filter.e.append(event_id)
|
||||
nostr_filter = NostrFilter()
|
||||
nostr_filter.e.append(event_id)
|
||||
|
||||
events_related_to_event = await get_events(RELAY_ID, filter)
|
||||
assert len(events_related_to_event) == 2, f"Failed to query by tag 'e'"
|
||||
events_related_to_event = await get_events(RELAY_ID, nostr_filter)
|
||||
assert len(events_related_to_event) == 2, "Failed to query by tag 'e'"
|
||||
|
||||
filtered_events = [e for e in all_events if filter.matches(e)]
|
||||
assert len(filtered_events) == 2, f"Failed to filter by tag 'e'"
|
||||
filtered_events = [e for e in all_events if nostr_filter.matches(e)]
|
||||
assert len(filtered_events) == 2, "Failed to filter by tag 'e'"
|
||||
|
||||
|
||||
async def filter_by_tag_e_and_p(
|
||||
all_events: List[NostrEvent], author, event_id, reply_event_id
|
||||
):
|
||||
filter = NostrFilter()
|
||||
filter.p.append(author)
|
||||
filter.e.append(event_id)
|
||||
nostr_filter = NostrFilter()
|
||||
nostr_filter.p.append(author)
|
||||
nostr_filter.e.append(event_id)
|
||||
|
||||
events_related_to_event = await get_events(RELAY_ID, filter)
|
||||
assert len(events_related_to_event) == 1, f"Failed to quert by tags 'e' & 'p'"
|
||||
events_related_to_event = await get_events(RELAY_ID, nostr_filter)
|
||||
assert len(events_related_to_event) == 1, "Failed to quert by tags 'e' & 'p'"
|
||||
assert (
|
||||
events_related_to_event[0].id == reply_event_id
|
||||
), f"Failed to query the right event by tags 'e' & 'p'"
|
||||
), "Failed to query the right event by tags 'e' & 'p'"
|
||||
|
||||
filtered_events = [e for e in all_events if filter.matches(e)]
|
||||
assert len(filtered_events) == 1, f"Failed to filter by tags 'e' & 'p'"
|
||||
filtered_events = [e for e in all_events if nostr_filter.matches(e)]
|
||||
assert len(filtered_events) == 1, "Failed to filter by tags 'e' & 'p'"
|
||||
assert (
|
||||
filtered_events[0].id == reply_event_id
|
||||
), f"Failed to find the right event by tags 'e' & 'p'"
|
||||
), "Failed to find the right event by tags 'e' & 'p'"
|
||||
|
||||
|
||||
async def filter_by_tag_e_p_and_author(
|
||||
all_events: List[NostrEvent], author, event_id, reply_event_id
|
||||
):
|
||||
filter = NostrFilter(authors=[author])
|
||||
filter.p.append(author)
|
||||
filter.e.append(event_id)
|
||||
events_related_to_event = await get_events(RELAY_ID, filter)
|
||||
nostr_filter = NostrFilter(authors=[author])
|
||||
nostr_filter.p.append(author)
|
||||
nostr_filter.e.append(event_id)
|
||||
events_related_to_event = await get_events(RELAY_ID, nostr_filter)
|
||||
assert (
|
||||
len(events_related_to_event) == 1
|
||||
), f"Failed to query by 'author' and tags 'e' & 'p'"
|
||||
), "Failed to query by 'author' and tags 'e' & 'p'"
|
||||
assert (
|
||||
events_related_to_event[0].id == reply_event_id
|
||||
), f"Failed to query the right event by 'author' and tags 'e' & 'p'"
|
||||
), "Failed to query the right event by 'author' and tags 'e' & 'p'"
|
||||
|
||||
filtered_events = [e for e in all_events if filter.matches(e)]
|
||||
assert len(filtered_events) == 1, f"Failed to filter by 'author' and tags 'e' & 'p'"
|
||||
filtered_events = [e for e in all_events if nostr_filter.matches(e)]
|
||||
assert len(filtered_events) == 1, "Failed to filter by 'author' and tags 'e' & 'p'"
|
||||
assert (
|
||||
filtered_events[0].id == reply_event_id
|
||||
), f"Failed to filter the right event by 'author' and tags 'e' & 'p'"
|
||||
), "Failed to filter the right event by 'author' and tags 'e' & 'p'"
|
||||
|
|
|
|||
17
tests/test_init.py
Normal file
17
tests/test_init.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import pytest
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .. import nostrrelay_ext, nostrrelay_start, nostrrelay_stop
|
||||
|
||||
|
||||
# just import router and add it to a test router
|
||||
@pytest.mark.asyncio
|
||||
async def test_router():
|
||||
router = APIRouter()
|
||||
router.include_router(nostrrelay_ext)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_and_stop():
|
||||
nostrrelay_start()
|
||||
nostrrelay_stop()
|
||||
9
toc.md
9
toc.md
|
|
@ -1,22 +1,29 @@
|
|||
# Terms and Conditions for LNbits Extension
|
||||
|
||||
## 1. Acceptance of Terms
|
||||
|
||||
By installing and using the LNbits extension ("Extension"), you agree to be bound by these terms and conditions ("Terms"). If you do not agree to these Terms, do not use the Extension.
|
||||
|
||||
## 2. License
|
||||
|
||||
The Extension is free and open-source software, released under [specify the FOSS license here, e.g., GPL-3.0, MIT, etc.]. You are permitted to use, copy, modify, and distribute the Extension under the terms of that license.
|
||||
|
||||
## 3. No Warranty
|
||||
|
||||
The Extension is provided "as is" and with all faults, and the developer expressly disclaims all warranties of any kind, whether express, implied, statutory, or otherwise, including but not limited to warranties of merchantability, fitness for a particular purpose, non-infringement, and any warranties arising out of course of dealing or usage of trade. No advice or information, whether oral or written, obtained from the developer or elsewhere will create any warranty not expressly stated in this Terms.
|
||||
|
||||
## 4. Limitation of Liability
|
||||
|
||||
In no event will the developer be liable to you or any third party for any direct, indirect, incidental, special, consequential, or punitive damages, including lost profit, lost revenue, loss of data, or other damages arising out of or in connection with your use of the Extension, even if the developer has been advised of the possibility of such damages. The foregoing limitation of liability shall apply to the fullest extent permitted by law in the applicable jurisdiction.
|
||||
|
||||
## 5. Modification of Terms
|
||||
|
||||
The developer reserves the right to modify these Terms at any time. You are advised to review these Terms periodically for any changes. Changes to these Terms are effective when they are posted on the appropriate location within or associated with the Extension.
|
||||
|
||||
## 6. General Provisions
|
||||
|
||||
If any provision of these Terms is held to be invalid or unenforceable, that provision will be enforced to the maximum extent permissible, and the other provisions of these Terms will remain in full force and effect. These Terms constitute the entire agreement between you and the developer regarding the use of the Extension.
|
||||
|
||||
## 7. Contact Information
|
||||
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
||||
|
||||
If you have any questions about these Terms, please contact the developer at [developer's contact information].
|
||||
|
|
|
|||
17
views.py
17
views.py
|
|
@ -1,28 +1,33 @@
|
|||
from http import HTTPStatus
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.exceptions import HTTPException
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
from lnbits.helpers import template_renderer
|
||||
from starlette.responses import HTMLResponse
|
||||
|
||||
from . import nostrrelay_ext, nostrrelay_renderer
|
||||
from .crud import get_public_relay
|
||||
from .helpers import relay_info_response
|
||||
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
nostrrelay_generic_router: APIRouter = APIRouter()
|
||||
|
||||
@nostrrelay_ext.get("/", response_class=HTMLResponse)
|
||||
|
||||
def nostrrelay_renderer():
|
||||
return template_renderer(["nostrrelay/templates"])
|
||||
|
||||
|
||||
@nostrrelay_generic_router.get("/", response_class=HTMLResponse)
|
||||
async def index(request: Request, user: User = Depends(check_user_exists)):
|
||||
return nostrrelay_renderer().TemplateResponse(
|
||||
"nostrrelay/index.html", {"request": request, "user": user.dict()}
|
||||
)
|
||||
|
||||
|
||||
@nostrrelay_ext.get("/{relay_id}")
|
||||
@nostrrelay_generic_router.get("/{relay_id}")
|
||||
async def nostrrelay(request: Request, relay_id: str):
|
||||
relay_public_data = await get_public_relay(relay_id)
|
||||
|
||||
|
|
|
|||
171
views_api.py
171
views_api.py
|
|
@ -1,20 +1,20 @@
|
|||
from http import HTTPStatus
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import Depends, Request, WebSocket
|
||||
from fastapi import APIRouter, Depends, Request, WebSocket
|
||||
from fastapi.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.models import WalletTypeInfo
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import (
|
||||
WalletTypeInfo,
|
||||
require_admin_key,
|
||||
require_invoice_key,
|
||||
)
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from . import nostrrelay_ext, client_manager
|
||||
from loguru import logger
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from .client_manager import client_manager
|
||||
from .crud import (
|
||||
create_account,
|
||||
create_relay,
|
||||
|
|
@ -34,8 +34,10 @@ from .models import BuyOrder, NostrAccount, NostrPartialAccount
|
|||
from .relay.client_manager import NostrClientConnection
|
||||
from .relay.relay import NostrRelay
|
||||
|
||||
nostrrelay_api_router = APIRouter()
|
||||
|
||||
@nostrrelay_ext.websocket("/{relay_id}")
|
||||
|
||||
@nostrrelay_api_router.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)
|
||||
|
|
@ -49,7 +51,7 @@ async def websocket_endpoint(relay_id: str, websocket: WebSocket):
|
|||
client_manager.remove_client(client)
|
||||
|
||||
|
||||
@nostrrelay_ext.post("/api/v1/relay")
|
||||
@nostrrelay_api_router.post("/api/v1/relay")
|
||||
async def api_create_relay(
|
||||
data: NostrRelay,
|
||||
request: Request,
|
||||
|
|
@ -72,10 +74,10 @@ async def api_create_relay(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create relay",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrrelay_ext.patch("/api/v1/relay/{relay_id}")
|
||||
@nostrrelay_api_router.patch("/api/v1/relay/{relay_id}")
|
||||
async def api_update_relay(
|
||||
relay_id: str, data: NostrRelay, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> NostrRelay:
|
||||
|
|
@ -111,10 +113,10 @@ async def api_update_relay(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot update relay",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrrelay_ext.put("/api/v1/relay/{relay_id}")
|
||||
@nostrrelay_api_router.put("/api/v1/relay/{relay_id}")
|
||||
async def api_toggle_relay(
|
||||
relay_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> NostrRelay:
|
||||
|
|
@ -143,10 +145,10 @@ async def api_toggle_relay(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot update relay",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrrelay_ext.get("/api/v1/relay")
|
||||
@nostrrelay_api_router.get("/api/v1/relay")
|
||||
async def api_get_relays(
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
) -> List[NostrRelay]:
|
||||
|
|
@ -157,15 +159,15 @@ async def api_get_relays(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot fetch relays",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrrelay_ext.get("/api/v1/relay-info")
|
||||
@nostrrelay_api_router.get("/api/v1/relay-info")
|
||||
async def api_get_relay_info() -> JSONResponse:
|
||||
return relay_info_response(NostrRelay.info())
|
||||
|
||||
|
||||
@nostrrelay_ext.get("/api/v1/relay/{relay_id}")
|
||||
@nostrrelay_api_router.get("/api/v1/relay/{relay_id}")
|
||||
async def api_get_relay(
|
||||
relay_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
) -> Optional[NostrRelay]:
|
||||
|
|
@ -176,7 +178,7 @@ async def api_get_relay(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot fetch relay",
|
||||
)
|
||||
) from ex
|
||||
if not relay:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
|
|
@ -185,10 +187,9 @@ async def api_get_relay(
|
|||
return relay
|
||||
|
||||
|
||||
@nostrrelay_ext.put("/api/v1/account")
|
||||
@nostrrelay_api_router.put("/api/v1/account", dependencies=[Depends(require_admin_key)])
|
||||
async def api_create_or_update_account(
|
||||
data: NostrPartialAccount,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> NostrAccount:
|
||||
|
||||
try:
|
||||
|
|
@ -214,7 +215,7 @@ async def api_create_or_update_account(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
except Exception as ex:
|
||||
|
|
@ -222,37 +223,27 @@ async def api_create_or_update_account(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot create account",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrrelay_ext.delete("/api/v1/account/{relay_id}/{pubkey}")
|
||||
@nostrrelay_api_router.delete(
|
||||
"/api/v1/account/{relay_id}/{pubkey}", dependencies=[Depends(require_admin_key)]
|
||||
)
|
||||
async def api_delete_account(
|
||||
relay_id: str,
|
||||
pubkey: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
|
||||
try:
|
||||
pubkey = normalize_public_key(pubkey)
|
||||
|
||||
return await delete_account(relay_id, pubkey)
|
||||
|
||||
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",
|
||||
)
|
||||
detail=f"Invalid pubkey: {ex!s}",
|
||||
) from ex
|
||||
return await delete_account(relay_id, pubkey)
|
||||
|
||||
|
||||
@nostrrelay_ext.get("/api/v1/account")
|
||||
@nostrrelay_api_router.get("/api/v1/account")
|
||||
async def api_get_accounts(
|
||||
relay_id: str,
|
||||
allowed: bool = False,
|
||||
|
|
@ -273,7 +264,7 @@ async def api_get_accounts(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=str(ex),
|
||||
)
|
||||
) from ex
|
||||
except HTTPException as ex:
|
||||
raise ex
|
||||
except Exception as ex:
|
||||
|
|
@ -281,10 +272,10 @@ async def api_get_accounts(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot fetch accounts",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrrelay_ext.delete("/api/v1/relay/{relay_id}")
|
||||
@nostrrelay_api_router.delete("/api/v1/relay/{relay_id}")
|
||||
async def api_delete_relay(
|
||||
relay_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
|
|
@ -297,61 +288,55 @@ async def api_delete_relay(
|
|||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail="Cannot delete relay",
|
||||
)
|
||||
) from ex
|
||||
|
||||
|
||||
@nostrrelay_ext.put("/api/v1/pay")
|
||||
@nostrrelay_api_router.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",
|
||||
)
|
||||
|
||||
amount = 0
|
||||
storage_to_buy = 0
|
||||
if data.action == "join":
|
||||
if relay.is_free_to_join:
|
||||
raise ValueError("Relay is free to join")
|
||||
amount = int(relay.config.cost_to_join)
|
||||
elif 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
|
||||
amount = data.units_to_buy * relay.config.storage_cost_value
|
||||
else:
|
||||
raise ValueError(f"Unknown action: '{data.action}'")
|
||||
|
||||
_, payment_request = await create_invoice(
|
||||
wallet_id=relay.config.wallet,
|
||||
amount=amount,
|
||||
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,
|
||||
},
|
||||
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",
|
||||
)
|
||||
return {"invoice": payment_request}
|
||||
except ValueError as ex:
|
||||
amount = 0
|
||||
storage_to_buy = 0
|
||||
if data.action == "join":
|
||||
if relay.is_free_to_join:
|
||||
raise ValueError("Relay is free to join")
|
||||
amount = int(relay.config.cost_to_join)
|
||||
elif data.action == "storage":
|
||||
if relay.config.storage_cost_value == 0:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Relay storage cost is zero. Cannot buy!",
|
||||
)
|
||||
if data.units_to_buy == 0:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="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
|
||||
amount = data.units_to_buy * relay.config.storage_cost_value
|
||||
else:
|
||||
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",
|
||||
detail=f"Unknown action: '{data.action}'",
|
||||
)
|
||||
|
||||
_, payment_request = await create_invoice(
|
||||
wallet_id=relay.config.wallet,
|
||||
amount=amount,
|
||||
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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue