black
This commit is contained in:
@@ -82,8 +82,8 @@ def _is_wasm_extension(ext: InstallableExtension) -> bool:
|
|||||||
config_json = json.load(json_file)
|
config_json = json.load(json_file)
|
||||||
if config_json.get("extension_type") == "wasm":
|
if config_json.get("extension_type") == "wasm":
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
logger.debug(f"Failed to load extension config for '{ext.id}': {exc!s}")
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ from lnbits.core.models.extensions import (
|
|||||||
Extension,
|
Extension,
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
ExtensionMeta,
|
ExtensionMeta,
|
||||||
|
ExtensionPermission,
|
||||||
|
ExtensionPermissionsGrant,
|
||||||
ExtensionRelease,
|
ExtensionRelease,
|
||||||
ExtensionReview,
|
ExtensionReview,
|
||||||
ExtensionReviewPaymentRequest,
|
ExtensionReviewPaymentRequest,
|
||||||
ExtensionReviewsStatus,
|
ExtensionReviewsStatus,
|
||||||
ExtensionPermission,
|
|
||||||
ExtensionPermissionsGrant,
|
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
PayToEnableInfo,
|
PayToEnableInfo,
|
||||||
ReleasePaymentInfo,
|
ReleasePaymentInfo,
|
||||||
@@ -182,56 +182,104 @@ async def api_enable_extension(
|
|||||||
account_id: AccountId = Depends(check_account_id_exists),
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
grant: ExtensionPermissionsGrant | None = Body(default=None),
|
grant: ExtensionPermissionsGrant | None = Body(default=None),
|
||||||
) -> SimpleStatus:
|
) -> SimpleStatus:
|
||||||
|
await _ensure_extension_exists(ext_id)
|
||||||
|
logger.info(f"Enabling extension: {ext_id}.")
|
||||||
|
|
||||||
|
ext = await _get_installed_active_extension(ext_id)
|
||||||
|
user_ext = await _get_or_create_user_extension(account_id.id, ext_id)
|
||||||
|
|
||||||
|
required_permissions = _get_required_permissions(ext_id, ext)
|
||||||
|
granted_permissions = _get_granted_permissions(grant, user_ext)
|
||||||
|
_ensure_permissions(required_permissions, granted_permissions)
|
||||||
|
|
||||||
|
if grant and grant.permissions:
|
||||||
|
await _store_granted_permissions(user_ext, granted_permissions)
|
||||||
|
|
||||||
|
if account_id.is_admin_id or not ext.requires_payment:
|
||||||
|
await _activate_user_extension(user_ext)
|
||||||
|
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
||||||
|
|
||||||
|
return await _enable_paid_extension(ext_id, ext, user_ext)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_extension_exists(ext_id: str) -> None:
|
||||||
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"Enabling extension: {ext_id}.")
|
|
||||||
|
async def _get_installed_active_extension(ext_id: str) -> InstallableExtension:
|
||||||
ext = await get_installed_extension(ext_id)
|
ext = await get_installed_extension(ext_id)
|
||||||
if not ext:
|
if not ext:
|
||||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||||
if not ext.active:
|
if not ext.active:
|
||||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||||
|
return ext
|
||||||
|
|
||||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
|
||||||
|
async def _get_or_create_user_extension(user_id: str, ext_id: str) -> UserExtension:
|
||||||
|
user_ext = await get_user_extension(user_id, ext_id)
|
||||||
if not user_ext:
|
if not user_ext:
|
||||||
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
user_ext = UserExtension(user=user_id, extension=ext_id, active=False)
|
||||||
await create_user_extension(user_ext)
|
await create_user_extension(user_ext)
|
||||||
|
return user_ext
|
||||||
|
|
||||||
|
|
||||||
|
def _get_required_permissions(ext_id: str, ext: InstallableExtension) -> list[str]:
|
||||||
permissions_source = (
|
permissions_source = (
|
||||||
ext.meta.permissions
|
ext.meta.permissions
|
||||||
if ext.meta and ext.meta.permissions
|
if ext.meta and ext.meta.permissions
|
||||||
else _load_permissions_from_config(ext_id)
|
else _load_permissions_from_config(ext_id)
|
||||||
)
|
)
|
||||||
required_permissions = [p.id for p in permissions_source] if permissions_source else []
|
return [p.id for p in permissions_source] if permissions_source else []
|
||||||
granted_permissions = []
|
|
||||||
|
|
||||||
|
def _get_granted_permissions(
|
||||||
|
grant: ExtensionPermissionsGrant | None, user_ext: UserExtension
|
||||||
|
) -> list[str]:
|
||||||
if grant and grant.permissions:
|
if grant and grant.permissions:
|
||||||
granted_permissions = grant.permissions
|
return grant.permissions
|
||||||
elif user_ext.extra and user_ext.extra.granted_permissions:
|
if user_ext.extra and user_ext.extra.granted_permissions:
|
||||||
granted_permissions = user_ext.extra.granted_permissions
|
return user_ext.extra.granted_permissions
|
||||||
|
return []
|
||||||
|
|
||||||
if required_permissions:
|
|
||||||
missing = [p for p in required_permissions if p not in granted_permissions]
|
|
||||||
if missing:
|
|
||||||
raise HTTPException(
|
|
||||||
HTTPStatus.BAD_REQUEST,
|
|
||||||
"Missing required permissions to enable this extension.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if account_id.is_admin_id or not ext.requires_payment:
|
def _ensure_permissions(required: list[str], granted: list[str]) -> None:
|
||||||
user_ext.active = True
|
if not required:
|
||||||
await update_user_extension(user_ext)
|
return
|
||||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
missing = [p for p in required if p not in granted]
|
||||||
|
if missing:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"Missing required permissions to enable this extension.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _store_granted_permissions(
|
||||||
|
user_ext: UserExtension, granted_permissions: list[str]
|
||||||
|
) -> None:
|
||||||
|
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||||
|
user_ext_info.granted_permissions = granted_permissions
|
||||||
|
user_ext.extra = user_ext_info
|
||||||
|
await update_user_extension(user_ext)
|
||||||
|
|
||||||
|
|
||||||
|
async def _activate_user_extension(user_ext: UserExtension) -> None:
|
||||||
|
user_ext.active = True
|
||||||
|
await update_user_extension(user_ext)
|
||||||
|
|
||||||
|
|
||||||
|
async def _enable_paid_extension(
|
||||||
|
ext_id: str, ext: InstallableExtension, user_ext: UserExtension
|
||||||
|
) -> SimpleStatus:
|
||||||
if not (user_ext.extra and user_ext.extra.payment_hash_to_enable):
|
if not (user_ext.extra and user_ext.extra.payment_hash_to_enable):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
|
HTTPStatus.PAYMENT_REQUIRED, f"Extension '{ext_id}' requires payment."
|
||||||
)
|
)
|
||||||
|
|
||||||
if user_ext.is_paid:
|
if user_ext.is_paid:
|
||||||
user_ext.active = True
|
await _activate_user_extension(user_ext)
|
||||||
await update_user_extension(user_ext)
|
|
||||||
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
||||||
|
|
||||||
if not ext.meta or not ext.meta.pay_to_enable or not ext.meta.pay_to_enable.wallet:
|
if not ext.meta or not ext.meta.pay_to_enable or not ext.meta.pay_to_enable.wallet:
|
||||||
@@ -248,9 +296,8 @@ async def api_enable_extension(
|
|||||||
f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
|
f"Invoice generated but not paid for enabeling extension '{ext_id}'.",
|
||||||
)
|
)
|
||||||
|
|
||||||
user_ext.active = True
|
|
||||||
user_ext.extra.paid_to_enable = True
|
user_ext.extra.paid_to_enable = True
|
||||||
await update_user_extension(user_ext)
|
await _activate_user_extension(user_ext)
|
||||||
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
return SimpleStatus(success=True, message=f"Paid extension '{ext_id}' enabled.")
|
||||||
|
|
||||||
|
|
||||||
@@ -302,7 +349,9 @@ async def api_update_extension_permissions(
|
|||||||
if ext.meta and ext.meta.permissions
|
if ext.meta and ext.meta.permissions
|
||||||
else _load_permissions_from_config(ext_id)
|
else _load_permissions_from_config(ext_id)
|
||||||
)
|
)
|
||||||
required_permissions = [p.id for p in permissions_source] if permissions_source else []
|
required_permissions = (
|
||||||
|
[p.id for p in permissions_source] if permissions_source else []
|
||||||
|
)
|
||||||
granted_permissions = grant.permissions if grant and grant.permissions else []
|
granted_permissions = grant.permissions if grant and grant.permissions else []
|
||||||
|
|
||||||
if required_permissions:
|
if required_permissions:
|
||||||
@@ -654,6 +703,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
if ext.meta and ext.meta.permissions
|
if ext.meta and ext.meta.permissions
|
||||||
else [dict(p) for p in _load_permissions_from_config(ext.id)]
|
else [dict(p) for p in _load_permissions_from_config(ext.id)]
|
||||||
),
|
),
|
||||||
|
"kvSchema": _load_kv_schema_from_config(ext.id),
|
||||||
"grantedPermissions": (
|
"grantedPermissions": (
|
||||||
user_exts_map.get(ext.id).extra.granted_permissions
|
user_exts_map.get(ext.id).extra.granted_permissions
|
||||||
if user_exts_map.get(ext.id) and user_exts_map.get(ext.id).extra
|
if user_exts_map.get(ext.id) and user_exts_map.get(ext.id).extra
|
||||||
@@ -701,6 +751,21 @@ def _load_permissions_from_config(ext_id: str) -> list[ExtensionPermission]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _load_kv_schema_from_config(ext_id: str) -> dict:
|
||||||
|
try:
|
||||||
|
conf_path = Path(
|
||||||
|
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||||
|
)
|
||||||
|
if not conf_path.is_file():
|
||||||
|
return {}
|
||||||
|
with open(conf_path, "r+") as json_file:
|
||||||
|
config_json = json.load(json_file)
|
||||||
|
schema = config_json.get("kv_schema", {})
|
||||||
|
return schema if isinstance(schema, dict) else {}
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@extension_router.get(
|
@extension_router.get(
|
||||||
"/reviews/tags",
|
"/reviews/tags",
|
||||||
dependencies=[Depends(check_account_exists)],
|
dependencies=[Depends(check_account_exists)],
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import json
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from fastapi.responses import HTMLResponse, Response
|
from fastapi.responses import HTMLResponse, Response
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
@@ -48,6 +47,59 @@ def _ensure_kv_table(db: Database, ext_id: str) -> str:
|
|||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
_kv_schema_cache: dict[str, dict] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_kv_schema(ext_id: str) -> dict:
|
||||||
|
if ext_id in _kv_schema_cache:
|
||||||
|
return _kv_schema_cache[ext_id]
|
||||||
|
try:
|
||||||
|
conf_path = Path(
|
||||||
|
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||||
|
)
|
||||||
|
if not conf_path.is_file():
|
||||||
|
_kv_schema_cache[ext_id] = {}
|
||||||
|
return _kv_schema_cache[ext_id]
|
||||||
|
with open(conf_path, "r+") as json_file:
|
||||||
|
config_json = json.load(json_file)
|
||||||
|
schema = config_json.get("kv_schema", {})
|
||||||
|
if not isinstance(schema, dict):
|
||||||
|
schema = {}
|
||||||
|
_kv_schema_cache[ext_id] = schema
|
||||||
|
return schema
|
||||||
|
except Exception:
|
||||||
|
_kv_schema_cache[ext_id] = {}
|
||||||
|
return _kv_schema_cache[ext_id]
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_for_key(schema: dict, key: str) -> dict | None:
|
||||||
|
if not schema:
|
||||||
|
return None
|
||||||
|
entry = schema.get(key)
|
||||||
|
return entry if isinstance(entry, dict) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_schema_value(schema_entry: dict, value):
|
||||||
|
value_type = schema_entry.get("type", "string")
|
||||||
|
if value_type == "int":
|
||||||
|
return int(value)
|
||||||
|
if value_type == "float":
|
||||||
|
return float(value)
|
||||||
|
if value_type == "bool":
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.lower() in {"true", "1", "yes", "y"}
|
||||||
|
return bool(value)
|
||||||
|
if value_type == "json":
|
||||||
|
if isinstance(value, (dict, list)):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
return json.loads(value)
|
||||||
|
raise ValueError("Invalid json value")
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
async def _kv_get(db: Database, ext_id: str, key: str) -> str | None:
|
async def _kv_get(db: Database, ext_id: str, key: str) -> str | None:
|
||||||
await db.execute(_ensure_kv_table(db, ext_id))
|
await db.execute(_ensure_kv_table(db, ext_id))
|
||||||
table = f"{ext_id}.kv" if db.schema else "kv"
|
table = f"{ext_id}.kv" if db.schema else "kv"
|
||||||
@@ -56,6 +108,17 @@ async def _kv_get(db: Database, ext_id: str, key: str) -> str | None:
|
|||||||
{"key": key},
|
{"key": key},
|
||||||
)
|
)
|
||||||
if not row:
|
if not row:
|
||||||
|
schema = _load_kv_schema(ext_id)
|
||||||
|
entry = _schema_for_key(schema, key)
|
||||||
|
if entry and "default" in entry:
|
||||||
|
default_value = _coerce_schema_value(entry, entry.get("default"))
|
||||||
|
stored = (
|
||||||
|
json.dumps(default_value)
|
||||||
|
if entry.get("type") == "json"
|
||||||
|
else str(default_value)
|
||||||
|
)
|
||||||
|
await _kv_set(db, ext_id, key, stored)
|
||||||
|
return stored
|
||||||
return None
|
return None
|
||||||
return row.get("value")
|
return row.get("value")
|
||||||
|
|
||||||
@@ -88,12 +151,7 @@ async def _require_permission(user_id: str, ext_id: str, permission: str) -> Non
|
|||||||
raise HTTPException(403, f"Missing permission: {permission}")
|
raise HTTPException(403, f"Missing permission: {permission}")
|
||||||
|
|
||||||
|
|
||||||
def register_wasm_ext_routes(app, ext) -> None:
|
def _register_pages_routes(router: APIRouter, ext_id: str) -> None:
|
||||||
ext_id = ext.code
|
|
||||||
db = Database(f"ext_{ext_id}")
|
|
||||||
router = APIRouter(prefix=f"/{ext_id}", tags=[f"{ext_id} (wasm)"])
|
|
||||||
proxy_block = f"/{ext_id}/api/v1/proxy"
|
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
async def index(req: Request, user: User = Depends(check_user_exists)):
|
async def index(req: Request, user: User = Depends(check_user_exists)):
|
||||||
try:
|
try:
|
||||||
@@ -114,6 +172,8 @@ def register_wasm_ext_routes(app, ext) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return HTMLResponse("Public page not found", status_code=404)
|
return HTMLResponse("Public page not found", status_code=404)
|
||||||
|
|
||||||
|
|
||||||
|
def _register_kv_routes(router: APIRouter, ext_id: str, db: Database, ext) -> None:
|
||||||
@router.get("/api/v1/kv/{key}")
|
@router.get("/api/v1/kv/{key}")
|
||||||
async def api_kv_get(key: str, user: User = Depends(check_user_exists)):
|
async def api_kv_get(key: str, user: User = Depends(check_user_exists)):
|
||||||
await _require_permission(user.id, ext_id, "ext.db.read_write")
|
await _require_permission(user.id, ext_id, "ext.db.read_write")
|
||||||
@@ -139,6 +199,16 @@ def register_wasm_ext_routes(app, ext) -> None:
|
|||||||
value = payload.get("value")
|
value = payload.get("value")
|
||||||
if value is None:
|
if value is None:
|
||||||
raise HTTPException(400, "Missing value")
|
raise HTTPException(400, "Missing value")
|
||||||
|
schema = _load_kv_schema(ext_id)
|
||||||
|
entry = _schema_for_key(schema, key)
|
||||||
|
if schema and not entry:
|
||||||
|
raise HTTPException(400, "Key not allowed by schema")
|
||||||
|
if entry:
|
||||||
|
try:
|
||||||
|
coerced = _coerce_schema_value(entry, value)
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(400, "Invalid value for schema") from None
|
||||||
|
value = json.dumps(coerced) if entry.get("type") == "json" else str(coerced)
|
||||||
await _kv_set(db, ext_id, key, str(value))
|
await _kv_set(db, ext_id, key, str(value))
|
||||||
await websocket_updater(f"{ext_id}:{key}", str(value))
|
await websocket_updater(f"{ext_id}:{key}", str(value))
|
||||||
return {"key": key, "value": value}
|
return {"key": key, "value": value}
|
||||||
@@ -159,6 +229,8 @@ def register_wasm_ext_routes(app, ext) -> None:
|
|||||||
await websocket_updater(f"{ext_id}:{key}", str(new_value))
|
await websocket_updater(f"{ext_id}:{key}", str(new_value))
|
||||||
return {"key": key, "value": new_value}
|
return {"key": key, "value": new_value}
|
||||||
|
|
||||||
|
|
||||||
|
def _register_watch_routes(router: APIRouter, ext_id: str, db: Database, ext) -> None:
|
||||||
@router.post("/api/v1/watch")
|
@router.post("/api/v1/watch")
|
||||||
async def api_watch_payment(payload: dict, user: User = Depends(check_user_exists)):
|
async def api_watch_payment(payload: dict, user: User = Depends(check_user_exists)):
|
||||||
payment_hash = payload.get("payment_hash")
|
payment_hash = payload.get("payment_hash")
|
||||||
@@ -199,11 +271,17 @@ def register_wasm_ext_routes(app, ext) -> None:
|
|||||||
finally:
|
finally:
|
||||||
unregister_invoice_listener(queue_name)
|
unregister_invoice_listener(queue_name)
|
||||||
|
|
||||||
asyncio.create_task(_watch())
|
task = asyncio.create_task(_watch())
|
||||||
return {"ok": True}
|
return {"ok": True, "task_id": id(task)}
|
||||||
|
|
||||||
|
|
||||||
|
def _register_proxy_routes(
|
||||||
|
router: APIRouter, app, ext_id: str, proxy_block: str
|
||||||
|
) -> None:
|
||||||
@router.post("/api/v1/proxy")
|
@router.post("/api/v1/proxy")
|
||||||
async def api_proxy(payload: dict, req: Request, user: User = Depends(check_user_exists)):
|
async def api_proxy(
|
||||||
|
payload: dict, req: Request, user: User = Depends(check_user_exists)
|
||||||
|
):
|
||||||
method = str(payload.get("method", "GET")).upper()
|
method = str(payload.get("method", "GET")).upper()
|
||||||
path = str(payload.get("path", "")).strip()
|
path = str(payload.get("path", "")).strip()
|
||||||
if not path.startswith("/") or "://" in path:
|
if not path.startswith("/") or "://" in path:
|
||||||
@@ -233,7 +311,9 @@ def register_wasm_ext_routes(app, ext) -> None:
|
|||||||
media_type=resp.headers.get("content-type"),
|
media_type=resp.headers.get("content-type"),
|
||||||
)
|
)
|
||||||
|
|
||||||
static_dir = _ext_static_dir(ext_id, ext.upgrade_hash)
|
|
||||||
|
def _mount_static(app, ext_id: str, upgrade_hash: str | None) -> None:
|
||||||
|
static_dir = _ext_static_dir(ext_id, upgrade_hash)
|
||||||
if static_dir.is_dir():
|
if static_dir.is_dir():
|
||||||
app.mount(
|
app.mount(
|
||||||
f"/{ext_id}/static",
|
f"/{ext_id}/static",
|
||||||
@@ -241,6 +321,19 @@ def register_wasm_ext_routes(app, ext) -> None:
|
|||||||
name=f"{ext_id}_static",
|
name=f"{ext_id}_static",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_wasm_ext_routes(app, ext) -> None:
|
||||||
|
ext_id = ext.code
|
||||||
|
db = Database(f"ext_{ext_id}")
|
||||||
|
router = APIRouter(prefix=f"/{ext_id}", tags=[f"{ext_id} (wasm)"])
|
||||||
|
proxy_block = f"/{ext_id}/api/v1/proxy"
|
||||||
|
|
||||||
|
_register_pages_routes(router, ext_id)
|
||||||
|
_register_kv_routes(router, ext_id, db, ext)
|
||||||
|
_register_watch_routes(router, ext_id, db, ext)
|
||||||
|
_register_proxy_routes(router, app, ext_id, proxy_block)
|
||||||
|
_mount_static(app, ext_id, ext.upgrade_hash)
|
||||||
|
|
||||||
prefix = f"/upgrades/{ext.upgrade_hash}" if ext.upgrade_hash else ""
|
prefix = f"/upgrades/{ext.upgrade_hash}" if ext.upgrade_hash else ""
|
||||||
app.include_router(router, prefix=prefix)
|
app.include_router(router, prefix=prefix)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,54 @@ from wasmtime import (
|
|||||||
from lnbits.db import Database
|
from lnbits.db import Database
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
|
_kv_schema_cache: dict[str, dict] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_kv_schema(ext_id: str) -> dict:
|
||||||
|
if ext_id in _kv_schema_cache:
|
||||||
|
return _kv_schema_cache[ext_id]
|
||||||
|
try:
|
||||||
|
conf_path = Path(
|
||||||
|
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||||
|
)
|
||||||
|
if not conf_path.is_file():
|
||||||
|
_kv_schema_cache[ext_id] = {}
|
||||||
|
return _kv_schema_cache[ext_id]
|
||||||
|
with open(conf_path, "r+") as json_file:
|
||||||
|
config_json = json.load(json_file)
|
||||||
|
schema = config_json.get("kv_schema", {})
|
||||||
|
if not isinstance(schema, dict):
|
||||||
|
schema = {}
|
||||||
|
_kv_schema_cache[ext_id] = schema
|
||||||
|
return schema
|
||||||
|
except Exception:
|
||||||
|
_kv_schema_cache[ext_id] = {}
|
||||||
|
return _kv_schema_cache[ext_id]
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_for_key(schema: dict, key: str) -> dict | None:
|
||||||
|
if not schema:
|
||||||
|
return None
|
||||||
|
entry = schema.get(key)
|
||||||
|
return entry if isinstance(entry, dict) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_schema_value(schema_entry: dict, value: str):
|
||||||
|
value_type = schema_entry.get("type", "string")
|
||||||
|
if value_type == "int":
|
||||||
|
return int(value)
|
||||||
|
if value_type == "float":
|
||||||
|
return float(value)
|
||||||
|
if value_type == "bool":
|
||||||
|
if value.lower() in {"true", "1", "yes", "y"}:
|
||||||
|
return True
|
||||||
|
if value.lower() in {"false", "0", "no", "n"}:
|
||||||
|
return False
|
||||||
|
raise ValueError("Invalid bool value")
|
||||||
|
if value_type == "json":
|
||||||
|
return json.loads(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _run(coro):
|
def _run(coro):
|
||||||
return asyncio.run(coro)
|
return asyncio.run(coro)
|
||||||
@@ -62,7 +110,11 @@ def _db_get(
|
|||||||
key = _read_bytes(caller, key_ptr, key_len).decode(errors="ignore")
|
key = _read_bytes(caller, key_ptr, key_len).decode(errors="ignore")
|
||||||
_run(db.execute(_ensure_kv_table(db, ext_id)))
|
_run(db.execute(_ensure_kv_table(db, ext_id)))
|
||||||
table = f"{ext_id}.kv" if db.schema else "kv"
|
table = f"{ext_id}.kv" if db.schema else "kv"
|
||||||
row = _run(db.fetchone(f"SELECT value FROM {table} WHERE key = :key", {"key": key}))
|
row = _run(
|
||||||
|
db.fetchone(
|
||||||
|
f"SELECT value FROM {table} WHERE key = :key", {"key": key}
|
||||||
|
)
|
||||||
|
)
|
||||||
if not row:
|
if not row:
|
||||||
return 0
|
return 0
|
||||||
value = str(row.get("value", ""))
|
value = str(row.get("value", ""))
|
||||||
@@ -82,9 +134,23 @@ def _db_set(
|
|||||||
) -> int:
|
) -> int:
|
||||||
key = _read_bytes(caller, key_ptr, key_len).decode(errors="ignore")
|
key = _read_bytes(caller, key_ptr, key_len).decode(errors="ignore")
|
||||||
value = _read_bytes(caller, val_ptr, val_len).decode(errors="ignore")
|
value = _read_bytes(caller, val_ptr, val_len).decode(errors="ignore")
|
||||||
|
schema = _load_kv_schema(ext_id)
|
||||||
|
entry = _schema_for_key(schema, key)
|
||||||
|
if schema and not entry:
|
||||||
|
raise RuntimeError("Key not allowed by schema")
|
||||||
|
if entry:
|
||||||
|
try:
|
||||||
|
coerced = _coerce_schema_value(entry, value)
|
||||||
|
except Exception as exc:
|
||||||
|
raise RuntimeError("Invalid value for schema") from exc
|
||||||
|
value = json.dumps(coerced) if entry.get("type") == "json" else str(coerced)
|
||||||
_run(db.execute(_ensure_kv_table(db, ext_id)))
|
_run(db.execute(_ensure_kv_table(db, ext_id)))
|
||||||
table = f"{ext_id}.kv" if db.schema else "kv"
|
table = f"{ext_id}.kv" if db.schema else "kv"
|
||||||
row = _run(db.fetchone(f"SELECT key FROM {table} WHERE key = :key", {"key": key}))
|
row = _run(
|
||||||
|
db.fetchone(
|
||||||
|
f"SELECT key FROM {table} WHERE key = :key", {"key": key}
|
||||||
|
)
|
||||||
|
)
|
||||||
if row:
|
if row:
|
||||||
_run(
|
_run(
|
||||||
db.execute(
|
db.execute(
|
||||||
|
|||||||
@@ -954,6 +954,25 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
permissionsDialog.extension &&
|
||||||
|
permissionsDialog.extension.kvSchema &&
|
||||||
|
Object.keys(permissionsDialog.extension.kvSchema).length
|
||||||
|
"
|
||||||
|
class="q-mt-md"
|
||||||
|
>
|
||||||
|
<div class="text-caption text-grey">KV keys defined:</div>
|
||||||
|
<q-chip
|
||||||
|
v-for="(value, key) in permissionsDialog.extension.kvSchema"
|
||||||
|
:key="key"
|
||||||
|
color="grey-3"
|
||||||
|
text-color="black"
|
||||||
|
class="q-mr-xs q-mt-xs"
|
||||||
|
>
|
||||||
|
{{ key }}
|
||||||
|
</q-chip>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|||||||
Reference in New Issue
Block a user