extension manager

This commit is contained in:
dni ⚡
2024-10-17 10:36:45 +02:00
parent 56e348a786
commit 61bf9a6ffb
9 changed files with 157 additions and 158 deletions
+15 -10
View File
@@ -6,7 +6,7 @@ import shutil
import sys import sys
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from typing import Callable, List, Optional from typing import Callable, Optional
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@@ -17,12 +17,10 @@ from slowapi.util import get_remote_address
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from lnbits.core.crud import ( from lnbits.core.crud import (
add_installed_extension,
get_dbversions, get_dbversions,
get_installed_extensions, get_installed_extensions,
update_installed_extension_state, update_installed_extension,
) )
from lnbits.core.extensions.extension_manager import deactivate_extension
from lnbits.core.extensions.helpers import version_parse from lnbits.core.extensions.helpers import version_parse
from lnbits.core.helpers import migrate_extension_database from lnbits.core.helpers import migrate_extension_database
from lnbits.core.tasks import ( # watchdog_task from lnbits.core.tasks import ( # watchdog_task
@@ -47,7 +45,7 @@ from lnbits.wallets import get_funding_source, set_funding_source
from .commands import migrate_databases from .commands import migrate_databases
from .core import init_core_routers from .core import init_core_routers
from .core.db import core_app_extra from .core.db import core_app_extra
from .core.extensions.models import Extension, InstallableExtension from .core.extensions.models import Extension, ExtensionMeta, InstallableExtension
from .core.services import check_admin_settings, check_webpush_settings from .core.services import check_admin_settings, check_webpush_settings
from .middleware import ( from .middleware import (
CustomGZipMiddleware, CustomGZipMiddleware,
@@ -240,7 +238,9 @@ async def check_installed_extensions(app: FastAPI):
) )
except Exception as e: except Exception as e:
logger.warning(e) logger.warning(e)
await deactivate_extension(ext.id) settings.deactivate_extension_paths(ext.id)
ext.active = False
await update_installed_extension(ext)
logger.warning( logger.warning(
f"Failed to re-install extension: {ext.id} ({ext.installed_version})" f"Failed to re-install extension: {ext.id} ({ext.installed_version})"
) )
@@ -252,7 +252,7 @@ async def check_installed_extensions(app: FastAPI):
async def build_all_installed_extensions_list( async def build_all_installed_extensions_list(
include_deactivated: Optional[bool] = True, include_deactivated: Optional[bool] = True,
) -> List[InstallableExtension]: ) -> list[InstallableExtension]:
""" """
Returns a list of all the installed extensions plus the extensions that Returns a list of all the installed extensions plus the extensions that
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL). MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
@@ -272,8 +272,13 @@ async def build_all_installed_extensions_list(
release = next((e for e in ext_releases if e.is_version_compatible), None) release = next((e for e in ext_releases if e.is_version_compatible), None)
if release: if release:
ext_meta = ExtensionMeta(installed_release=release)
ext_info = InstallableExtension( ext_info = InstallableExtension(
id=ext_id, name=ext_id, installed_release=release, icon=release.icon id=ext_id,
name=ext_id,
version=release.version,
icon=release.icon,
meta=ext_meta,
) )
installed_extensions.append(ext_info) installed_extensions.append(ext_info)
@@ -304,8 +309,8 @@ async def check_installed_extension_files(ext: InstallableExtension) -> bool:
async def restore_installed_extension(app: FastAPI, ext: InstallableExtension): async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
await add_installed_extension(ext) ext.active = True
await update_installed_extension_state(ext_id=ext.id, active=True) await update_installed_extension(ext)
extension = Extension.from_installable_ext(ext) extension = Extension.from_installable_ext(ext)
register_ext_routes(app, extension) register_ext_routes(app, extension)
+10 -8
View File
@@ -3,7 +3,7 @@ import importlib
import time import time
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
from typing import List, Optional, Tuple from typing import Optional
import click import click
import httpx import httpx
@@ -231,7 +231,7 @@ async def check_invalid_payments(
click.echo("Funding source: " + str(funding_source)) click.echo("Funding source: " + str(funding_source))
# payments that are settled in the DB, but not at the Funding source level # payments that are settled in the DB, but not at the Funding source level
invalid_payments: List[Payment] = [] invalid_payments: list[Payment] = []
invalid_wallets = {} invalid_wallets = {}
for db_payment in settled_db_payments: for db_payment in settled_db_payments:
if verbose: if verbose:
@@ -277,8 +277,10 @@ async def extensions_list():
from lnbits.app import build_all_installed_extensions_list from lnbits.app import build_all_installed_extensions_list
for ext in await build_all_installed_extensions_list(): for ext in await build_all_installed_extensions_list():
assert ext.installed_release, f"Extension {ext.id} has no installed_release" assert (
click.echo(f" - {ext.id} ({ext.installed_release.version})") ext.meta and ext.meta.installed_release
), f"Extension {ext.id} has no installed_release"
click.echo(f" - {ext.id} ({ext.meta.installed_release.version})")
@extensions.command("update") @extensions.command("update")
@@ -461,7 +463,7 @@ async def install_extension(
source_repo: Optional[str] = None, source_repo: Optional[str] = None,
url: Optional[str] = None, url: Optional[str] = None,
admin_user: Optional[str] = None, admin_user: Optional[str] = None,
) -> Tuple[bool, str]: ) -> tuple[bool, str]:
try: try:
release = await _select_release(extension, repo_index, source_repo) release = await _select_release(extension, repo_index, source_repo)
if not release: if not release:
@@ -490,7 +492,7 @@ async def update_extension(
source_repo: Optional[str] = None, source_repo: Optional[str] = None,
url: Optional[str] = None, url: Optional[str] = None,
admin_user: Optional[str] = None, admin_user: Optional[str] = None,
) -> Tuple[bool, str]: ) -> tuple[bool, str]:
try: try:
click.echo(f"Updating '{extension}' extension.") click.echo(f"Updating '{extension}' extension.")
installed_ext = await get_installed_extension(extension) installed_ext = await get_installed_extension(extension)
@@ -503,7 +505,7 @@ async def update_extension(
click.echo(f"Current '{extension}' version: {installed_ext.installed_version}.") click.echo(f"Current '{extension}' version: {installed_ext.installed_version}.")
assert ( assert (
installed_ext.installed_release installed_ext.meta and installed_ext.meta.installed_release
), "Cannot find previously installed release. Please uninstall first." ), "Cannot find previously installed release. Please uninstall first."
release = await _select_release(extension, repo_index, source_repo) release = await _select_release(extension, repo_index, source_repo)
@@ -511,7 +513,7 @@ async def update_extension(
return False, "No release selected." return False, "No release selected."
if ( if (
release.version == installed_ext.installed_version release.version == installed_ext.installed_version
and release.source_repo == installed_ext.installed_release.source_repo and release.source_repo == installed_ext.meta.installed_release.source_repo
): ):
click.echo(f"Extension '{extension}' already up to date.") click.echo(f"Extension '{extension}' already up to date.")
return False, "Already up to date" return False, "Already up to date"
+7 -43
View File
@@ -9,7 +9,6 @@ import shortuuid
from lnbits.core.db import db from lnbits.core.db import db
from lnbits.core.extensions.models import ( from lnbits.core.extensions.models import (
InstallableExtension, InstallableExtension,
PayToEnableInfo,
UserExtension, UserExtension,
) )
from lnbits.core.models import PaymentState from lnbits.core.models import PaymentState
@@ -242,42 +241,18 @@ async def get_user(account: Account, conn: Optional[Connection] = None) -> User:
# ------- # -------
async def add_installed_extension( async def create_installed_extension(
ext: InstallableExtension, ext: InstallableExtension,
conn: Optional[Connection] = None, conn: Optional[Connection] = None,
) -> None: ) -> None:
meta = { await (conn or db).insert("installed_extensions", ext)
"installed_release": (
dict(ext.installed_release) if ext.installed_release else None
),
"pay_to_enable": (dict(ext.pay_to_enable) if ext.pay_to_enable else None),
"dependencies": ext.dependencies,
"payments": [dict(p) for p in ext.payments] if ext.payments else None,
}
version = ext.installed_release.version if ext.installed_release else ""
await (conn or db).execute( async def update_installed_extension(
""" ext: InstallableExtension,
INSERT INTO installed_extensions conn: Optional[Connection] = None,
(id, version, name, active, short_description, icon, stars, meta) ) -> None:
VALUES await (conn or db).update("installed_extensions", ext)
(:ext, :version, :name, :active, :short_description, :icon, :stars, :meta)
ON CONFLICT (id) DO UPDATE SET
(version, name, active, short_description, icon, stars, meta) =
(:version, :name, :active, :short_description, :icon, :stars, :meta)
""",
{
"ext": ext.id,
"version": version,
"name": ext.name,
"active": ext.active,
"short_description": ext.short_description,
"icon": ext.icon,
"stars": ext.stars,
"meta": json.dumps(meta),
},
)
async def update_installed_extension_state( async def update_installed_extension_state(
@@ -291,17 +266,6 @@ async def update_installed_extension_state(
) )
async def update_extension_pay_to_enable(
ext_id: str, payment_info: PayToEnableInfo, conn: Optional[Connection] = None
) -> None:
ext = await get_installed_extension(ext_id, conn)
if not ext:
return
ext.pay_to_enable = payment_info
await add_installed_extension(ext, conn)
async def delete_installed_extension( async def delete_installed_extension(
*, ext_id: str, conn: Optional[Connection] = None *, ext_id: str, conn: Optional[Connection] = None
) -> None: ) -> None:
+7 -6
View File
@@ -3,14 +3,13 @@ import importlib
from loguru import logger from loguru import logger
from lnbits.core import core_app_extra
from lnbits.core.crud import ( from lnbits.core.crud import (
add_installed_extension,
delete_installed_extension, delete_installed_extension,
get_dbversions, get_dbversions,
get_installed_extension, get_installed_extension,
update_installed_extension_state, update_installed_extension_state,
) )
from lnbits.core.db import core_app_extra
from lnbits.core.helpers import migrate_extension_database from lnbits.core.helpers import migrate_extension_database
from lnbits.settings import settings from lnbits.settings import settings
@@ -18,22 +17,24 @@ from .models import Extension, InstallableExtension
async def install_extension(ext_info: InstallableExtension) -> Extension: async def install_extension(ext_info: InstallableExtension) -> Extension:
ext_id = ext_info.id
extension = Extension.from_installable_ext(ext_info) extension = Extension.from_installable_ext(ext_info)
installed_ext = await get_installed_extension(ext_info.id) installed_ext = await get_installed_extension(ext_id)
ext_info.payments = installed_ext.payments if installed_ext else [] ext_info.payments = installed_ext.payments if installed_ext else []
await ext_info.download_archive() await ext_info.download_archive()
ext_info.extract_archive() ext_info.extract_archive()
db_version = (await get_dbversions()).get(ext_info.id, 0) db_version = (await get_dbversions()).get(ext_id, 0)
await migrate_extension_database(extension, db_version) await migrate_extension_database(extension, db_version)
await add_installed_extension(ext_info) # TODO: think about add installed
# await add_installed_extension(ext_info)
if extension.is_upgrade_extension: if extension.is_upgrade_extension:
# call stop while the old routes are still active # call stop while the old routes are still active
await stop_extension_background_work(ext_info.id) await stop_extension_background_work(ext_id)
return extension return extension
+55 -51
View File
@@ -373,29 +373,37 @@ class ExtensionRelease(BaseModel):
return None return None
class ExtensionMeta(BaseModel):
installed_release: Optional[ExtensionRelease] = None
pay_to_enable: Optional[PayToEnableInfo] = None
payments: list[ReleasePaymentInfo] = []
dependencies: list[str] = []
class InstallableExtension(BaseModel): class InstallableExtension(BaseModel):
id: str id: str
name: str name: str
version: str
active: Optional[bool] = False active: Optional[bool] = False
short_description: Optional[str] = None short_description: Optional[str] = None
icon: Optional[str] = None icon: Optional[str] = None
dependencies: list[str] = []
is_admin_only: bool = False
stars: int = 0 stars: int = 0
featured = False featured = False
latest_release: Optional[ExtensionRelease] = None
installed_release: Optional[ExtensionRelease] = None
payments: list[ReleasePaymentInfo] = []
pay_to_enable: Optional[PayToEnableInfo] = None
archive: Optional[str] = None archive: Optional[str] = None
latest_release: Optional[ExtensionRelease] = None
meta: Optional[ExtensionMeta] = None
@property
def is_admin_only(self) -> bool:
return self.id in settings.lnbits_admin_extensions
@property @property
def hash(self) -> str: def hash(self) -> str:
if self.installed_release: if self.meta and self.meta.installed_release:
if self.installed_release.hash: if self.meta.installed_release.hash:
return self.installed_release.hash return self.meta.installed_release.hash
m = hashlib.sha256() m = hashlib.sha256()
m.update(f"{self.installed_release.archive}".encode()) m.update(f"{self.meta.installed_release.archive}".encode())
return m.hexdigest() return m.hexdigest()
return "not-installed" return "not-installed"
@@ -433,15 +441,15 @@ class InstallableExtension(BaseModel):
@property @property
def installed_version(self) -> str: def installed_version(self) -> str:
if self.installed_release: if self.meta and self.meta.installed_release:
return self.installed_release.version return self.meta.installed_release.version
return "" return ""
@property @property
def requires_payment(self) -> bool: def requires_payment(self) -> bool:
if not self.pay_to_enable: if not self.meta or not self.meta.pay_to_enable:
return False return False
return self.pay_to_enable.required is True return self.meta.pay_to_enable.required is True
async def download_archive(self): async def download_archive(self):
logger.info(f"Downloading extension {self.name} ({self.installed_version}).") logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
@@ -449,12 +457,14 @@ class InstallableExtension(BaseModel):
if ext_zip_file.is_file(): if ext_zip_file.is_file():
os.remove(ext_zip_file) os.remove(ext_zip_file)
try: try:
assert self.installed_release, "installed_release is none." assert (
self.meta and self.meta.installed_release
), "installed_release is none."
self._restore_payment_info() self._restore_payment_info()
await asyncio.to_thread( await asyncio.to_thread(
download_url, self.installed_release.archive_url, ext_zip_file download_url, self.meta.installed_release.archive_url, ext_zip_file
) )
self._remember_payment_info() self._remember_payment_info()
@@ -464,7 +474,11 @@ class InstallableExtension(BaseModel):
raise AssertionError("Cannot fetch extension archive file") from exc raise AssertionError("Cannot fetch extension archive file") from exc
archive_hash = file_hash(ext_zip_file) archive_hash = file_hash(ext_zip_file)
if self.installed_release.hash and self.installed_release.hash != archive_hash: if (
self.meta
and self.meta.installed_release.hash
and self.meta.installed_release.hash != archive_hash
):
# remove downloaded archive # remove downloaded archive
if ext_zip_file.is_file(): if ext_zip_file.is_file():
os.remove(ext_zip_file) os.remove(ext_zip_file)
@@ -498,12 +512,13 @@ class InstallableExtension(BaseModel):
self.short_description = config_json.get("short_description") self.short_description = config_json.get("short_description")
if ( if (
self.installed_release self.meta
and self.installed_release.is_github_release and self.meta.installed_release
and self.meta.installed_release.is_github_release
and config_json.get("tile") and config_json.get("tile")
): ):
self.icon = icon_to_github_url( self.icon = icon_to_github_url(
self.installed_release.source_repo, config_json.get("tile") self.meta.installed_release.source_repo, config_json.get("tile")
) )
shutil.rmtree(self.ext_dir, True) shutil.rmtree(self.ext_dir, True)
@@ -540,48 +555,34 @@ class InstallableExtension(BaseModel):
) )
def _restore_payment_info(self): def _restore_payment_info(self):
if not self.installed_release: if (
not self.meta
or not self.meta.installed_release
or not self.meta.installed_release.pay_link
or not self.meta.installed_release.payment_hash
):
return return
if not self.installed_release.pay_link: payment_info = self.find_existing_payment(self.meta.installed_release.pay_link)
return
if self.installed_release.payment_hash:
return
payment_info = self.find_existing_payment(self.installed_release.pay_link)
if payment_info: if payment_info:
self.installed_release.payment_hash = payment_info.payment_hash self.meta.installed_release.payment_hash = payment_info.payment_hash
def _remember_payment_info(self): def _remember_payment_info(self):
if not self.installed_release or not self.installed_release.pay_link: if (
not self.meta
or not self.meta.installed_release
or not self.meta.installed_release.pay_link
):
return return
payment_info = ReleasePaymentInfo( payment_info = ReleasePaymentInfo(
amount=self.installed_release.cost_sats, amount=self.meta.installed_release.cost_sats,
pay_link=self.installed_release.pay_link, pay_link=self.meta.installed_release.pay_link,
payment_hash=self.installed_release.payment_hash, payment_hash=self.meta.installed_release.payment_hash,
) )
self.payments = [ self.payments = [
p for p in self.payments if p.pay_link != payment_info.pay_link p for p in self.payments if p.pay_link != payment_info.pay_link
] ]
self.payments.append(payment_info) self.payments.append(payment_info)
@classmethod
def from_row(cls, data: dict) -> InstallableExtension:
meta = json.loads(data["meta"])
ext = InstallableExtension(**data)
if "installed_release" in meta:
ext.installed_release = ExtensionRelease(**meta["installed_release"])
if meta.get("pay_to_enable"):
ext.pay_to_enable = PayToEnableInfo(**meta["pay_to_enable"])
if meta.get("payments"):
ext.payments = [ReleasePaymentInfo(**p) for p in meta["payments"]]
return ext
@classmethod
def from_rows(cls, rows: Optional[list[Any]] = None) -> list[InstallableExtension]:
if rows is None:
rows = []
return [InstallableExtension.from_row(row) for row in rows]
@classmethod @classmethod
async def from_github_release( async def from_github_release(
cls, github_release: GitHubRelease cls, github_release: GitHubRelease
@@ -594,6 +595,7 @@ class InstallableExtension(BaseModel):
return InstallableExtension( return InstallableExtension(
id=github_release.id, id=github_release.id,
name=config.name, name=config.name,
version=latest_release.tag_name,
short_description=config.short_description, short_description=config.short_description,
stars=int(repo.stargazers_count), stars=int(repo.stargazers_count),
icon=icon_to_github_url( icon=icon_to_github_url(
@@ -610,13 +612,15 @@ class InstallableExtension(BaseModel):
@classmethod @classmethod
def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension: def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension:
meta = ExtensionMeta(dependencies=e.dependencies)
return InstallableExtension( return InstallableExtension(
id=e.id, id=e.id,
name=e.name, name=e.name,
version=e.version,
archive=e.archive, archive=e.archive,
short_description=e.short_description, short_description=e.short_description,
icon=e.icon, icon=e.icon,
dependencies=e.dependencies, meta=meta,
) )
@classmethod @classmethod
+1 -2
View File
@@ -541,8 +541,6 @@ async def m021_add_success_failed_to_apipayments(db):
GROUP BY apipayments.wallet GROUP BY apipayments.wallet
""" """
) )
# TODO: drop column in next release
# await db.execute("ALTER TABLE apipayments DROP COLUMN pending")
async def m022_add_pubkey_to_accounts(db): async def m022_add_pubkey_to_accounts(db):
@@ -559,6 +557,7 @@ async def m023_add_column_column_to_apipayments(db):
""" """
renames hash to payment_hash and drops unused index renames hash to payment_hash and drops unused index
""" """
await db.execute("ALTER TABLE apipayments DROP COLUMN pending")
await db.execute("DROP INDEX by_hash") await db.execute("DROP INDEX by_hash")
await db.execute("ALTER TABLE apipayments RENAME COLUMN hash TO payment_hash") await db.execute("ALTER TABLE apipayments RENAME COLUMN hash TO payment_hash")
await db.execute("ALTER TABLE apipayments RENAME COLUMN wallet TO wallet_id") await db.execute("ALTER TABLE apipayments RENAME COLUMN wallet TO wallet_id")
+35 -26
View File
@@ -18,6 +18,7 @@ from lnbits.core.extensions.models import (
CreateExtension, CreateExtension,
Extension, Extension,
ExtensionConfig, ExtensionConfig,
ExtensionMeta,
ExtensionRelease, ExtensionRelease,
InstallableExtension, InstallableExtension,
PayToEnableInfo, PayToEnableInfo,
@@ -43,7 +44,7 @@ from ..crud import (
get_installed_extension, get_installed_extension,
get_installed_extensions, get_installed_extensions,
get_user_extension, get_user_extension,
update_extension_pay_to_enable, update_installed_extension,
update_user_extension, update_user_extension,
) )
@@ -69,8 +70,13 @@ async def api_install_extension(data: CreateExtension):
) )
release.payment_hash = data.payment_hash release.payment_hash = data.payment_hash
ext_meta = ExtensionMeta(installed_release=release)
ext_info = InstallableExtension( ext_info = InstallableExtension(
id=data.ext_id, name=data.ext_id, installed_release=release, icon=release.icon id=data.ext_id,
name=data.ext_id,
version=data.version,
meta=ext_meta,
icon=release.icon,
) )
try: try:
@@ -142,22 +148,21 @@ async def api_update_pay_to_enable(
data: PayToEnableInfo, data: PayToEnableInfo,
user: User = Depends(check_admin), user: User = Depends(check_admin),
) -> SimpleStatus: ) -> SimpleStatus:
try: if data.wallet not in user.wallet_ids:
assert (
data.wallet in user.wallet_ids
), "Wallet does not belong to this admin user."
await update_extension_pay_to_enable(ext_id, data)
return SimpleStatus(
success=True, message=f"Payment info updated for '{ext_id}' extension."
)
except AssertionError as exc:
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
except Exception as exc:
logger.warning(exc)
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, HTTPStatus.BAD_REQUEST, "Wallet does not belong to this admin user."
detail=(f"Failed to update pay to install data for extension '{ext_id}' "), )
) from exc extension = await get_installed_extension(ext_id)
if not extension:
raise HTTPException(HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' not found.")
if extension.meta:
extension.meta.pay_to_enable = data
else:
extension.meta = ExtensionMeta(pay_to_enable=data)
await update_installed_extension(extension)
return SimpleStatus(
success=True, message=f"Payment info updated for '{ext_id}' extension."
)
@extension_router.put("/{ext_id}/enable") @extension_router.put("/{ext_id}/enable")
@@ -197,11 +202,11 @@ async def api_enable_extension(
) )
assert ( assert (
ext.pay_to_enable and ext.pay_to_enable.wallet ext.meta and ext.meta.pay_to_enable and ext.meta.pay_to_enable.wallet
), f"Extension '{ext_id}' is missing payment wallet." ), f"Extension '{ext_id}' is missing payment wallet."
payment_status = await check_transaction_status( payment_status = await check_transaction_status(
wallet_id=ext.pay_to_enable.wallet, wallet_id=ext.meta.pay_to_enable.wallet,
payment_hash=user_ext.extra.payment_hash_to_enable, payment_hash=user_ext.extra.payment_hash_to_enable,
) )
@@ -300,7 +305,11 @@ async def api_uninstall_extension(ext_id: str) -> SimpleStatus:
installed_ext = next( installed_ext = next(
(ext for ext in installed_extensions if ext.id == valid_ext_id), None (ext for ext in installed_extensions if ext.id == valid_ext_id), None
) )
if installed_ext and ext_id in installed_ext.dependencies: if (
installed_ext
and installed_ext.meta
and ext_id in installed_ext.meta.dependencies
):
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=( detail=(
@@ -399,35 +408,35 @@ async def get_pay_to_enable_invoice(
status_code=HTTPStatus.NOT_FOUND, detail=f"Extension '{ext_id}' not found." status_code=HTTPStatus.NOT_FOUND, detail=f"Extension '{ext_id}' not found."
) )
if not ext.pay_to_enable: if not ext.meta or not ext.meta.pay_to_enable:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=f"Payment info not found for extension '{ext_id}'.", detail=f"Payment info not found for extension '{ext_id}'.",
) )
if not ext.pay_to_enable.required: if not ext.meta.pay_to_enable.required:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=f"Payment not required for extension '{ext_id}'.", detail=f"Payment not required for extension '{ext_id}'.",
) )
if not ext.pay_to_enable.wallet or not ext.pay_to_enable.amount: if not ext.meta.pay_to_enable.wallet or not ext.meta.pay_to_enable.amount:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=f"Payment wallet or amount missing for extension '{ext_id}'.", detail=f"Payment wallet or amount missing for extension '{ext_id}'.",
) )
if data.amount < ext.pay_to_enable.amount: if data.amount < ext.meta.pay_to_enable.amount:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, status_code=HTTPStatus.BAD_REQUEST,
detail=( detail=(
f"Amount {data.amount} sats is less than required " f"Amount {data.amount} sats is less than required "
f"{ext.pay_to_enable.amount} sats." f"{ext.meta.pay_to_enable.amount} sats."
), ),
) )
payment_hash, payment_request = await create_invoice( payment_hash, payment_request = await create_invoice(
wallet_id=ext.pay_to_enable.wallet, wallet_id=ext.meta.pay_to_enable.wallet,
amount=data.amount, amount=data.amount,
memo=f"Enable '{ext.name}' extension.", memo=f"Enable '{ext.name}' extension.",
) )
+23 -9
View File
@@ -12,7 +12,7 @@ from lnurl import decode as lnurl_decode
from loguru import logger from loguru import logger
from pydantic.types import UUID4 from pydantic.types import UUID4
from lnbits.core.extensions.models import Extension, InstallableExtension from lnbits.core.extensions.models import Extension, ExtensionMeta, InstallableExtension
from lnbits.core.helpers import to_valid_user_id from lnbits.core.helpers import to_valid_user_id
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
@@ -88,13 +88,21 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
for e in installable_exts: for e in installable_exts:
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None) installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
if installed_ext: if installed_ext and installed_ext.meta:
e.installed_release = installed_ext.installed_release installed_release = installed_ext.meta.installed_release
if installed_ext.pay_to_enable and not user.admin: if installed_ext.meta.pay_to_enable and not user.admin:
# not a security leak, but better not to share the wallet id # not a security leak, but better not to share the wallet id
installed_ext.pay_to_enable.wallet = None installed_ext.meta.pay_to_enable.wallet = None
e.pay_to_enable = installed_ext.pay_to_enable pay_to_enable = installed_ext.meta.pay_to_enable
if e.meta:
e.meta.installed_release = installed_release
e.meta.pay_to_enable = pay_to_enable
else:
e.meta = ExtensionMeta(
installed_release=installed_release,
pay_to_enable=pay_to_enable,
)
# use the installed extension values # use the installed extension values
e.name = installed_ext.name e.name = installed_ext.name
e.short_description = installed_ext.short_description e.short_description = installed_ext.short_description
@@ -119,7 +127,7 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
"shortDescription": ext.short_description, "shortDescription": ext.short_description,
"stars": ext.stars, "stars": ext.stars,
"isFeatured": ext.featured, "isFeatured": ext.featured,
"dependencies": ext.dependencies, "dependencies": ext.meta.dependencies if ext.meta else "",
"isInstalled": ext.id in installed_exts_ids, "isInstalled": ext.id in installed_exts_ids,
"hasDatabaseTables": ext.id in db_version, "hasDatabaseTables": ext.id in db_version,
"isAvailable": ext.id in all_ext_ids, "isAvailable": ext.id in all_ext_ids,
@@ -129,9 +137,15 @@ async def extensions(request: Request, user: User = Depends(check_user_exists)):
dict(ext.latest_release) if ext.latest_release else None dict(ext.latest_release) if ext.latest_release else None
), ),
"installedRelease": ( "installedRelease": (
dict(ext.installed_release) if ext.installed_release else None dict(ext.meta.installed_release)
if ext.meta and ext.meta.installed_release
else None
),
"payToEnable": (
dict(ext.meta.pay_to_enable)
if ext.meta and ext.meta.pay_to_enable
else {}
), ),
"payToEnable": (dict(ext.pay_to_enable) if ext.pay_to_enable else {}),
"isPaymentRequired": ext.requires_payment, "isPaymentRequired": ext.requires_payment,
} }
for ext in installable_exts for ext in installable_exts
+4 -3
View File
@@ -611,7 +611,7 @@ def model_to_dict(model: BaseModel) -> dict:
return _dict return _dict
def dict_to_model(_dict: dict, model: type[TModel]) -> TModel: def dict_to_model(_row: dict, model: type[TModel]) -> TModel:
""" """
Convert a dictionary with JSON-encoded nested models to a Pydantic model Convert a dictionary with JSON-encoded nested models to a Pydantic model
:param _dict: Dictionary from database :param _dict: Dictionary from database
@@ -619,9 +619,10 @@ def dict_to_model(_dict: dict, model: type[TModel]) -> TModel:
""" """
# TODO: no recursion, maybe make them recursive? # TODO: no recursion, maybe make them recursive?
# TODO: check why keys are sometimes not in the dict # TODO: check why keys are sometimes not in the dict
for key, value in _dict.items(): _dict = {}
for key, value in _row.items():
if key not in model.__fields__: if key not in model.__fields__:
# logger.warning(f"Converting {key} to model `{model}`.") logger.warning(f"Converting {key} to model `{model}`.")
continue continue
type_ = model.__fields__[key].type_ type_ = model.__fields__[key].type_
if issubclass(type_, BaseModel): if issubclass(type_, BaseModel):