Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e71830817 | ||
|
|
0fb2334181 | ||
|
|
94c7796dd5 | ||
|
|
af4ce8da52 | ||
|
|
4e8a5e050b | ||
|
|
3fd7aae10d | ||
|
|
64ad19a65b | ||
|
|
ee6ff7179e | ||
|
|
684dfcc9fb | ||
|
|
a498d4a877 | ||
|
|
ef90c983f2 | ||
|
|
da8a82c78e | ||
|
|
51f98e8130 | ||
|
|
7b4436b288 | ||
|
|
ec88cf483e | ||
|
|
ca215063be | ||
|
|
2674cfb5f1 | ||
|
|
26f0b69085 | ||
|
|
2069e3de8b | ||
|
|
800be6042f | ||
|
|
c65d373f23 | ||
|
|
087e095bcf | ||
|
|
027a161494 | ||
|
|
83ef05696e | ||
|
|
42962308e6 | ||
|
|
783ee256a1 | ||
|
|
4e805fd00e | ||
|
|
f54824cc44 | ||
|
|
768d2cbe2a | ||
|
|
efd36d0221 | ||
|
|
4684939e13 | ||
|
|
193ee20da4 | ||
|
|
2fe7ab4f83 | ||
|
|
10c8ca4ca1 | ||
|
|
f079762b71 | ||
|
|
2e042d2597 | ||
|
|
49b57c9f0b | ||
|
|
656c6cac5b | ||
|
|
45e773b66f | ||
|
|
f692ac6f12 | ||
|
|
78202a027b | ||
|
|
28fa0906a6 | ||
|
|
91354f8be4 | ||
|
|
5f5d45e89f | ||
|
|
52bb125a25 | ||
|
|
2ca1ed897c | ||
|
|
631f4e8455 | ||
|
|
b22f88b264 | ||
|
|
c08bc02930 | ||
|
|
e020a7c5a0 | ||
|
|
d2e8914835 | ||
|
|
5e79ca35ab | ||
|
|
4690d178a2 | ||
|
|
c5825aa4c3 | ||
|
|
ebe0c4e3c3 | ||
|
|
383bdb6312 | ||
|
|
a947686d79 | ||
|
|
28e9d32b44 |
@@ -70,6 +70,7 @@ FORWARDED_ALLOW_IPS="*"
|
|||||||
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
|
# Inside this directory the `extensions` and `upgrades` sub-directories will be created.
|
||||||
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
|
# LNBITS_EXTENSIONS_PATH="/path/to/some/dir"
|
||||||
|
|
||||||
|
|
||||||
# ID of the super user. The user ID must exist.
|
# ID of the super user. The user ID must exist.
|
||||||
# SUPER_USER=""
|
# SUPER_USER=""
|
||||||
|
|
||||||
|
|||||||
+28
-2
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import glob
|
import glob
|
||||||
import importlib
|
import importlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
@@ -35,9 +36,12 @@ from lnbits.core.tasks import (
|
|||||||
purge_audit_data,
|
purge_audit_data,
|
||||||
run_by_the_minute_tasks,
|
run_by_the_minute_tasks,
|
||||||
wait_for_audit_data,
|
wait_for_audit_data,
|
||||||
wait_for_paid_invoices,
|
|
||||||
wait_notification_messages,
|
wait_notification_messages,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.tasks import (
|
||||||
|
wait_for_paid_invoices as wait_for_paid_invoices_core,
|
||||||
|
)
|
||||||
|
from lnbits.core.wasm.extension_host import register_wasm_ext_routes
|
||||||
from lnbits.exceptions import register_exception_handlers
|
from lnbits.exceptions import register_exception_handlers
|
||||||
from lnbits.helpers import version_parse
|
from lnbits.helpers import version_parse
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -419,6 +423,8 @@ def register_new_ratelimiter(app: FastAPI) -> Callable:
|
|||||||
|
|
||||||
def register_ext_tasks(ext: Extension) -> None:
|
def register_ext_tasks(ext: Extension) -> None:
|
||||||
"""Register extension async tasks."""
|
"""Register extension async tasks."""
|
||||||
|
if ext.extension_type == "wasm":
|
||||||
|
return
|
||||||
ext_module = importlib.import_module(ext.module_name)
|
ext_module = importlib.import_module(ext.module_name)
|
||||||
|
|
||||||
if hasattr(ext_module, f"{ext.code}_start"):
|
if hasattr(ext_module, f"{ext.code}_start"):
|
||||||
@@ -428,6 +434,12 @@ def register_ext_tasks(ext: Extension) -> None:
|
|||||||
|
|
||||||
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
||||||
"""Register FastAPI routes for extension."""
|
"""Register FastAPI routes for extension."""
|
||||||
|
if ext.extension_type != "wasm":
|
||||||
|
ext.extension_type = _load_extension_type(ext.code) or ext.extension_type
|
||||||
|
if ext.extension_type == "wasm":
|
||||||
|
settings.activate_extension_paths(ext.code, ext.upgrade_hash, [])
|
||||||
|
register_wasm_ext_routes(app, ext)
|
||||||
|
return
|
||||||
ext_module = importlib.import_module(ext.module_name)
|
ext_module = importlib.import_module(ext.module_name)
|
||||||
|
|
||||||
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
||||||
@@ -453,6 +465,20 @@ def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
|||||||
app.include_router(router=ext_route, prefix=prefix)
|
app.include_router(router=ext_route, prefix=prefix)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_extension_type(ext_id: str) -> str | None:
|
||||||
|
try:
|
||||||
|
conf_path = Path(
|
||||||
|
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||||
|
)
|
||||||
|
if not conf_path.is_file():
|
||||||
|
return None
|
||||||
|
with open(conf_path, "r+") as json_file:
|
||||||
|
config_json = json.load(json_file)
|
||||||
|
return config_json.get("extension_type")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def check_and_register_extensions(app: FastAPI) -> None:
|
async def check_and_register_extensions(app: FastAPI) -> None:
|
||||||
await check_installed_extensions(app)
|
await check_installed_extensions(app)
|
||||||
for ext in await get_valid_extensions(False):
|
for ext in await get_valid_extensions(False):
|
||||||
@@ -476,7 +502,7 @@ def register_async_tasks() -> None:
|
|||||||
# core invoice listener
|
# core invoice listener
|
||||||
invoice_queue: asyncio.Queue = asyncio.Queue()
|
invoice_queue: asyncio.Queue = asyncio.Queue()
|
||||||
register_invoice_listener(invoice_queue, "core")
|
register_invoice_listener(invoice_queue, "core")
|
||||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
create_permanent_task(lambda: wait_for_paid_invoices_core(invoice_queue))
|
||||||
|
|
||||||
create_permanent_task(run_by_the_minute_tasks)
|
create_permanent_task(run_by_the_minute_tasks)
|
||||||
create_permanent_task(purge_audit_data)
|
create_permanent_task(purge_audit_data)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models.extensions import (
|
from lnbits.core.models.extensions import (
|
||||||
InstallableExtension,
|
InstallableExtension,
|
||||||
@@ -49,7 +51,8 @@ async def drop_extension_db(ext_id: str, conn: Connection | None = None) -> None
|
|||||||
)
|
)
|
||||||
# Check that 'ext_id' is a valid extension id and not a malicious string
|
# Check that 'ext_id' is a valid extension id and not a malicious string
|
||||||
if not row:
|
if not row:
|
||||||
raise Exception(f"Extension '{ext_id}' db version cannot be found")
|
if not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*", ext_id):
|
||||||
|
raise Exception(f"Extension '{ext_id}' db version cannot be found")
|
||||||
|
|
||||||
is_file_based_db = await Database.clean_ext_db_files(ext_id)
|
is_file_based_db = await Database.clean_ext_db_files(ext_id)
|
||||||
if is_file_based_db:
|
if is_file_based_db:
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ from lnbits.settings import settings
|
|||||||
async def migrate_extension_database(
|
async def migrate_extension_database(
|
||||||
ext: InstallableExtension, current_version: DbVersion | None = None
|
ext: InstallableExtension, current_version: DbVersion | None = None
|
||||||
):
|
):
|
||||||
|
if _is_wasm_extension(ext):
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
|
ext_migrations = importlib.import_module(f"{ext.module_name}.migrations")
|
||||||
@@ -58,6 +60,34 @@ async def run_migration(
|
|||||||
await update_migration_version(conn, db_name, version)
|
await update_migration_version(conn, db_name, version)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_wasm_extension(ext: InstallableExtension) -> bool:
|
||||||
|
if ext.meta and ext.meta.extension_type == "wasm":
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
candidate_dirs = [
|
||||||
|
Path(ext.ext_dir),
|
||||||
|
Path(settings.lnbits_extensions_path, "extensions", ext.id),
|
||||||
|
Path(settings.lnbits_path, "extensions", ext.id),
|
||||||
|
Path.cwd() / "extensions" / ext.id,
|
||||||
|
]
|
||||||
|
for base in candidate_dirs:
|
||||||
|
conf_path = Path(base, "config.json")
|
||||||
|
if not conf_path.is_file():
|
||||||
|
continue
|
||||||
|
with open(conf_path, "r+") as json_file:
|
||||||
|
config_json = json.load(json_file)
|
||||||
|
if config_json.get("extension_type") == "wasm":
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(f"Failed to load extension config for '{ext.id}': {exc!s}")
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def to_valid_user_id(user_id: str) -> UUID:
|
def to_valid_user_id(user_id: str) -> UUID:
|
||||||
if len(user_id) < 32:
|
if len(user_id) < 32:
|
||||||
raise ValueError("User ID must have at least 128 bits")
|
raise ValueError("User ID must have at least 128 bits")
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ class ExtensionConfig(BaseModel):
|
|||||||
warning: str | None = ""
|
warning: str | None = ""
|
||||||
min_lnbits_version: str | None
|
min_lnbits_version: str | None
|
||||||
max_lnbits_version: str | None
|
max_lnbits_version: str | None
|
||||||
|
permissions: list[ExtensionPermission] = []
|
||||||
|
extension_type: str | None = "python"
|
||||||
|
public_kv_keys: list[str] = []
|
||||||
|
public_wasm_functions: list[str] = []
|
||||||
|
payment_tags: list[str] = []
|
||||||
|
|
||||||
def is_version_compatible(self) -> bool:
|
def is_version_compatible(self) -> bool:
|
||||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||||
@@ -115,6 +120,20 @@ class PayToEnableInfo(BaseModel):
|
|||||||
class UserExtensionInfo(BaseModel):
|
class UserExtensionInfo(BaseModel):
|
||||||
paid_to_enable: bool | None = False
|
paid_to_enable: bool | None = False
|
||||||
payment_hash_to_enable: str | None = None
|
payment_hash_to_enable: str | None = None
|
||||||
|
granted_permissions: list[str] | None = None
|
||||||
|
granted_payment_tags: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionPermission(BaseModel):
|
||||||
|
id: str
|
||||||
|
label: str
|
||||||
|
description: str
|
||||||
|
dangerous: bool | None = False
|
||||||
|
|
||||||
|
|
||||||
|
class ExtensionPermissionsGrant(BaseModel):
|
||||||
|
permissions: list[str] = []
|
||||||
|
payment_tags: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
class UserExtension(BaseModel):
|
class UserExtension(BaseModel):
|
||||||
@@ -147,6 +166,10 @@ class Extension(BaseModel):
|
|||||||
short_description: str | None = None
|
short_description: str | None = None
|
||||||
tile: str | None = None
|
tile: str | None = None
|
||||||
upgrade_hash: str | None = ""
|
upgrade_hash: str | None = ""
|
||||||
|
extension_type: str | None = None
|
||||||
|
public_kv_keys: list[str] = []
|
||||||
|
public_wasm_functions: list[str] = []
|
||||||
|
payment_tags: list[str] = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module_name(self) -> str:
|
def module_name(self) -> str:
|
||||||
@@ -170,6 +193,12 @@ class Extension(BaseModel):
|
|||||||
short_description=ext_info.short_description,
|
short_description=ext_info.short_description,
|
||||||
tile=ext_info.icon,
|
tile=ext_info.icon,
|
||||||
upgrade_hash=ext_info.hash if ext_info.ext_upgrade_dir.is_dir() else "",
|
upgrade_hash=ext_info.hash if ext_info.ext_upgrade_dir.is_dir() else "",
|
||||||
|
extension_type=ext_info.meta.extension_type if ext_info.meta else None,
|
||||||
|
public_kv_keys=ext_info.meta.public_kv_keys if ext_info.meta else [],
|
||||||
|
public_wasm_functions=(
|
||||||
|
ext_info.meta.public_wasm_functions if ext_info.meta else []
|
||||||
|
),
|
||||||
|
payment_tags=ext_info.meta.payment_tags if ext_info.meta else [],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -331,6 +360,11 @@ class ExtensionMeta(BaseModel):
|
|||||||
pay_to_enable: PayToEnableInfo | None = None
|
pay_to_enable: PayToEnableInfo | None = None
|
||||||
payments: list[ReleasePaymentInfo] = []
|
payments: list[ReleasePaymentInfo] = []
|
||||||
dependencies: list[str] = []
|
dependencies: list[str] = []
|
||||||
|
permissions: list[ExtensionPermission] = []
|
||||||
|
extension_type: str | None = "python"
|
||||||
|
public_kv_keys: list[str] = []
|
||||||
|
public_wasm_functions: list[str] = []
|
||||||
|
payment_tags: list[str] = []
|
||||||
archive: str | None = None
|
archive: str | None = None
|
||||||
featured: bool = False
|
featured: bool = False
|
||||||
paid_features: str | None = None
|
paid_features: str | None = None
|
||||||
@@ -454,6 +488,14 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
self.name = config_json.get("name")
|
self.name = config_json.get("name")
|
||||||
self.short_description = config_json.get("short_description")
|
self.short_description = config_json.get("short_description")
|
||||||
|
if self.meta:
|
||||||
|
self.meta.permissions = config_json.get("permissions", [])
|
||||||
|
self.meta.extension_type = config_json.get("extension_type", "python")
|
||||||
|
self.meta.public_kv_keys = config_json.get("public_kv_keys", [])
|
||||||
|
self.meta.public_wasm_functions = config_json.get(
|
||||||
|
"public_wasm_functions", []
|
||||||
|
)
|
||||||
|
self.meta.payment_tags = config_json.get("payment_tags", [])
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.meta
|
self.meta
|
||||||
@@ -572,6 +614,11 @@ class InstallableExtension(BaseModel):
|
|||||||
latest_release=ExtensionRelease.from_github_release(
|
latest_release=ExtensionRelease.from_github_release(
|
||||||
source_repo, latest_release
|
source_repo, latest_release
|
||||||
),
|
),
|
||||||
|
permissions=config.permissions,
|
||||||
|
extension_type=config.extension_type,
|
||||||
|
public_kv_keys=config.public_kv_keys,
|
||||||
|
public_wasm_functions=config.public_wasm_functions,
|
||||||
|
payment_tags=config.payment_tags,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -617,7 +664,14 @@ class InstallableExtension(BaseModel):
|
|||||||
source_repo=f"{conf_path}",
|
source_repo=f"{conf_path}",
|
||||||
min_lnbits_version=config_json.get("min_lnbits_version"),
|
min_lnbits_version=config_json.get("min_lnbits_version"),
|
||||||
max_lnbits_version=config_json.get("max_lnbits_version"),
|
max_lnbits_version=config_json.get("max_lnbits_version"),
|
||||||
)
|
),
|
||||||
|
permissions=config_json.get("permissions", []),
|
||||||
|
extension_type=config_json.get("extension_type", "python"),
|
||||||
|
public_kv_keys=config_json.get("public_kv_keys", []),
|
||||||
|
public_wasm_functions=config_json.get(
|
||||||
|
"public_wasm_functions", []
|
||||||
|
),
|
||||||
|
payment_tags=config_json.get("payment_tags", []),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -841,3 +895,6 @@ def icon_to_github_url(source_repo: str, path: str | None) -> str:
|
|||||||
_, _, *rest = path.split("/")
|
_, _, *rest = path.split("/")
|
||||||
tail = "/".join(rest)
|
tail = "/".join(rest)
|
||||||
return f"https://github.com/{source_repo}/raw/main/{tail}"
|
return f"https://github.com/{source_repo}/raw/main/{tail}"
|
||||||
|
|
||||||
|
|
||||||
|
ExtensionConfig.update_forward_refs(ExtensionPermission=ExtensionPermission)
|
||||||
|
|||||||
@@ -10,13 +10,18 @@ from lnbits.core.crud import (
|
|||||||
get_db_version,
|
get_db_version,
|
||||||
get_installed_extension,
|
get_installed_extension,
|
||||||
update_installed_extension_state,
|
update_installed_extension_state,
|
||||||
|
update_migration_version,
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.extensions import (
|
from lnbits.core.crud.extensions import (
|
||||||
get_installed_extensions,
|
get_installed_extensions,
|
||||||
update_installed_extension,
|
update_installed_extension,
|
||||||
)
|
)
|
||||||
from lnbits.core.helpers import migrate_extension_database
|
from lnbits.core.helpers import migrate_extension_database
|
||||||
from lnbits.db import Connection
|
from lnbits.core.wasm.extension_host import (
|
||||||
|
clear_schedules_for_extension,
|
||||||
|
clear_tag_watches_for_extension,
|
||||||
|
)
|
||||||
|
from lnbits.db import COCKROACH, POSTGRES, Connection, Database
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
from ..models.extensions import Extension, ExtensionMeta, InstallableExtension
|
||||||
@@ -53,6 +58,9 @@ async def install_extension(
|
|||||||
else:
|
else:
|
||||||
await update_installed_extension(ext_info)
|
await update_installed_extension(ext_info)
|
||||||
|
|
||||||
|
if ext_info.meta and ext_info.meta.extension_type == "wasm":
|
||||||
|
await update_migration_version(None, ext_info.id, 0)
|
||||||
|
|
||||||
extension = Extension.from_installable_ext(ext_info)
|
extension = Extension.from_installable_ext(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
|
||||||
@@ -70,10 +78,26 @@ async def uninstall_extension(ext_id: str):
|
|||||||
|
|
||||||
extension = await get_installed_extension(ext_id)
|
extension = await get_installed_extension(ext_id)
|
||||||
if extension:
|
if extension:
|
||||||
|
if extension.meta and extension.meta.extension_type == "wasm":
|
||||||
|
await clear_tag_watches_for_extension(ext_id)
|
||||||
|
await clear_schedules_for_extension(ext_id)
|
||||||
|
await _purge_wasm_extension_db(ext_id)
|
||||||
extension.clean_extension_files()
|
extension.clean_extension_files()
|
||||||
await delete_installed_extension(ext_id=ext_id)
|
await delete_installed_extension(ext_id=ext_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def _purge_wasm_extension_db(ext_id: str) -> None:
|
||||||
|
cleaned = await Database.clean_ext_db_files(ext_id)
|
||||||
|
if cleaned:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
db = Database(f"ext_{ext_id}")
|
||||||
|
if db.type in {POSTGRES, COCKROACH}:
|
||||||
|
await db.execute(f"DROP SCHEMA IF EXISTS {ext_id} CASCADE")
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(f"Failed to drop WASM extension schema for '{ext_id}': {exc}")
|
||||||
|
|
||||||
|
|
||||||
async def activate_extension(ext: Extension):
|
async def activate_extension(ext: Extension):
|
||||||
core_app_extra.register_new_ext_routes(ext)
|
core_app_extra.register_new_ext_routes(ext)
|
||||||
await update_installed_extension_state(ext_id=ext.code, active=True)
|
await update_installed_extension_state(ext_id=ext.code, active=True)
|
||||||
@@ -83,6 +107,10 @@ async def activate_extension(ext: Extension):
|
|||||||
async def deactivate_extension(ext_id: str):
|
async def deactivate_extension(ext_id: str):
|
||||||
settings.deactivate_extension_paths(ext_id)
|
settings.deactivate_extension_paths(ext_id)
|
||||||
await update_installed_extension_state(ext_id=ext_id, active=False)
|
await update_installed_extension_state(ext_id=ext_id, active=False)
|
||||||
|
extension = await get_installed_extension(ext_id)
|
||||||
|
if extension and extension.meta and extension.meta.extension_type == "wasm":
|
||||||
|
await clear_tag_watches_for_extension(ext_id)
|
||||||
|
await clear_schedules_for_extension(ext_id)
|
||||||
await stop_extension_background_work(ext_id)
|
await stop_extension_background_work(ext_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -91,6 +119,9 @@ async def stop_extension_background_work(ext_id: str) -> bool:
|
|||||||
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
Stop background work for extension (like asyncio.Tasks, WebSockets, etc).
|
||||||
Extension must expose a `myextension_stop()` function if it is starting tasks.
|
Extension must expose a `myextension_stop()` function if it is starting tasks.
|
||||||
"""
|
"""
|
||||||
|
installed = await get_installed_extension(ext_id)
|
||||||
|
if installed and installed.meta and installed.meta.extension_type == "wasm":
|
||||||
|
return True
|
||||||
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||||
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||||
|
|
||||||
@@ -123,6 +154,9 @@ async def start_extension_background_work(ext_id: str) -> bool:
|
|||||||
Extension CAN expose a `myextension_start()` function if it is starting tasks.
|
Extension CAN expose a `myextension_start()` function if it is starting tasks.
|
||||||
Extension MUST expose a `myextension_stop()` in that case.
|
Extension MUST expose a `myextension_stop()` in that case.
|
||||||
"""
|
"""
|
||||||
|
installed = await get_installed_extension(ext_id)
|
||||||
|
if installed and installed.meta and installed.meta.extension_type == "wasm":
|
||||||
|
return True
|
||||||
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
upgrade_hash = settings.extension_upgrade_hash(ext_id)
|
||||||
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
ext = Extension(code=ext_id, is_valid=True, upgrade_hash=upgrade_hash)
|
||||||
|
|
||||||
|
|||||||
@@ -313,6 +313,11 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
|||||||
payment.payment_hash,
|
payment.payment_hash,
|
||||||
json.dumps({"pending": payment.pending, "status": payment.status}),
|
json.dumps({"pending": payment.pending, "status": payment.status}),
|
||||||
)
|
)
|
||||||
|
if payment.tag and not payment.is_out:
|
||||||
|
await websocket_manager.send(
|
||||||
|
f"tag:{wallet.id}:{payment.tag}",
|
||||||
|
payment.json(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
async def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from bolt11 import decode as bolt11_decode
|
from bolt11 import decode as bolt11_decode
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||||
from fastapi.requests import Request
|
from fastapi.requests import Request
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from starlette.routing import Match
|
||||||
|
|
||||||
from lnbits.core.crud.extensions import get_user_extensions
|
from lnbits.core.crud.extensions import get_user_extensions
|
||||||
from lnbits.core.crud.wallets import get_wallets_ids
|
from lnbits.core.crud.wallets import get_wallets_ids
|
||||||
@@ -20,6 +23,8 @@ from lnbits.core.models.extensions import (
|
|||||||
Extension,
|
Extension,
|
||||||
ExtensionConfig,
|
ExtensionConfig,
|
||||||
ExtensionMeta,
|
ExtensionMeta,
|
||||||
|
ExtensionPermission,
|
||||||
|
ExtensionPermissionsGrant,
|
||||||
ExtensionRelease,
|
ExtensionRelease,
|
||||||
ExtensionReview,
|
ExtensionReview,
|
||||||
ExtensionReviewPaymentRequest,
|
ExtensionReviewPaymentRequest,
|
||||||
@@ -40,6 +45,11 @@ from lnbits.core.services.extensions import (
|
|||||||
install_extension,
|
install_extension,
|
||||||
uninstall_extension,
|
uninstall_extension,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.wasm import WASM_HOST_MANIFEST
|
||||||
|
from lnbits.core.wasm.extension_host import (
|
||||||
|
clear_schedules_for_user,
|
||||||
|
clear_tag_watches_for_user,
|
||||||
|
)
|
||||||
from lnbits.db import Page
|
from lnbits.db import Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
check_account_exists,
|
check_account_exists,
|
||||||
@@ -174,38 +184,146 @@ async def api_update_pay_to_enable(
|
|||||||
|
|
||||||
@extension_router.put("/{ext_id}/enable")
|
@extension_router.put("/{ext_id}/enable")
|
||||||
async def api_enable_extension(
|
async def api_enable_extension(
|
||||||
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
ext_id: str,
|
||||||
|
request: Request,
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
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)
|
||||||
|
granted_tags = _get_granted_payment_tags(grant, user_ext)
|
||||||
|
required_tags = _get_required_payment_tags(ext_id, ext)
|
||||||
|
if _is_wasm_extension(ext_id, ext):
|
||||||
|
_ensure_api_permissions_available(
|
||||||
|
request, _merge_permissions(required_permissions, granted_permissions)
|
||||||
|
)
|
||||||
|
_ensure_payment_tags_allowed(required_tags, granted_tags)
|
||||||
|
_ensure_permissions(required_permissions, granted_permissions)
|
||||||
|
|
||||||
|
if grant and grant.permissions:
|
||||||
|
await _store_granted_permissions(user_ext, granted_permissions)
|
||||||
|
if grant and grant.payment_tags:
|
||||||
|
await _store_granted_payment_tags(user_ext, granted_tags)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
if account_id.is_admin_id or not ext.requires_payment:
|
|
||||||
user_ext.active = True
|
|
||||||
await update_user_extension(user_ext)
|
|
||||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' enabled.")
|
|
||||||
|
|
||||||
|
def _get_required_permissions(ext_id: str, ext: InstallableExtension) -> list[str]:
|
||||||
|
permissions_source = (
|
||||||
|
ext.meta.permissions
|
||||||
|
if ext.meta and ext.meta.permissions
|
||||||
|
else _load_permissions_from_config(ext_id)
|
||||||
|
)
|
||||||
|
return [p.id for p in permissions_source] if permissions_source else []
|
||||||
|
|
||||||
|
|
||||||
|
def _get_granted_permissions(
|
||||||
|
grant: ExtensionPermissionsGrant | None, user_ext: UserExtension
|
||||||
|
) -> list[str]:
|
||||||
|
if grant and grant.permissions:
|
||||||
|
return grant.permissions
|
||||||
|
if user_ext.extra and user_ext.extra.granted_permissions:
|
||||||
|
return user_ext.extra.granted_permissions
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _get_required_payment_tags(ext_id: str, ext: InstallableExtension) -> list[str]:
|
||||||
|
tags_source = (
|
||||||
|
ext.meta.payment_tags
|
||||||
|
if ext.meta and ext.meta.payment_tags
|
||||||
|
else _load_payment_tags_from_config(ext_id)
|
||||||
|
)
|
||||||
|
return tags_source if tags_source else []
|
||||||
|
|
||||||
|
|
||||||
|
def _get_granted_payment_tags(
|
||||||
|
grant: ExtensionPermissionsGrant | None, user_ext: UserExtension
|
||||||
|
) -> list[str]:
|
||||||
|
if grant and grant.payment_tags:
|
||||||
|
return grant.payment_tags
|
||||||
|
if user_ext.extra and user_ext.extra.granted_payment_tags:
|
||||||
|
return user_ext.extra.granted_payment_tags
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_permissions(required: list[str], granted: list[str]) -> None:
|
||||||
|
if not required:
|
||||||
|
return
|
||||||
|
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 _store_granted_payment_tags(
|
||||||
|
user_ext: UserExtension, granted_tags: list[str]
|
||||||
|
) -> None:
|
||||||
|
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||||
|
user_ext_info.granted_payment_tags = granted_tags
|
||||||
|
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:
|
||||||
@@ -222,9 +340,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.")
|
||||||
|
|
||||||
|
|
||||||
@@ -243,10 +360,117 @@ async def api_disable_extension(
|
|||||||
)
|
)
|
||||||
logger.info(f"Disabling extension: {ext_id}.")
|
logger.info(f"Disabling extension: {ext_id}.")
|
||||||
user_ext.active = False
|
user_ext.active = False
|
||||||
|
if user_ext.extra and user_ext.extra.granted_permissions:
|
||||||
|
user_ext.extra.granted_permissions = []
|
||||||
|
if user_ext.extra and user_ext.extra.granted_payment_tags:
|
||||||
|
user_ext.extra.granted_payment_tags = []
|
||||||
await update_user_extension(user_ext)
|
await update_user_extension(user_ext)
|
||||||
|
ext = await get_installed_extension(ext_id)
|
||||||
|
if ext and ext.meta and ext.meta.extension_type == "wasm":
|
||||||
|
await clear_tag_watches_for_user(ext_id, account_id.id)
|
||||||
|
await clear_schedules_for_user(ext_id, account_id.id)
|
||||||
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
return SimpleStatus(success=True, message=f"Extension '{ext_id}' disabled.")
|
||||||
|
|
||||||
|
|
||||||
|
@extension_router.put("/{ext_id}/permissions")
|
||||||
|
async def api_update_extension_permissions(
|
||||||
|
ext_id: str,
|
||||||
|
request: Request,
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
grant: ExtensionPermissionsGrant | None = Body(default=None),
|
||||||
|
) -> SimpleStatus:
|
||||||
|
if ext_id not in [e.code for e in await get_valid_extensions()]:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.NOT_FOUND, f"Extension '{ext_id}' doesn't exist."
|
||||||
|
)
|
||||||
|
|
||||||
|
ext = await get_installed_extension(ext_id)
|
||||||
|
if not ext:
|
||||||
|
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||||
|
if not ext.active:
|
||||||
|
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||||
|
|
||||||
|
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||||
|
if not user_ext:
|
||||||
|
user_ext = UserExtension(user=account_id.id, extension=ext_id, active=False)
|
||||||
|
await create_user_extension(user_ext)
|
||||||
|
|
||||||
|
permissions_source = (
|
||||||
|
ext.meta.permissions
|
||||||
|
if ext.meta and ext.meta.permissions
|
||||||
|
else _load_permissions_from_config(ext_id)
|
||||||
|
)
|
||||||
|
required_permissions = (
|
||||||
|
[p.id for p in permissions_source] if permissions_source else []
|
||||||
|
)
|
||||||
|
granted_permissions = grant.permissions if grant and grant.permissions else []
|
||||||
|
granted_tags = grant.payment_tags if grant and grant.payment_tags else []
|
||||||
|
if _is_wasm_extension(ext_id, ext):
|
||||||
|
_ensure_api_permissions_available(
|
||||||
|
request, _merge_permissions(required_permissions, granted_permissions)
|
||||||
|
)
|
||||||
|
_ensure_payment_tags_allowed(
|
||||||
|
_get_required_payment_tags(ext_id, ext), granted_tags
|
||||||
|
)
|
||||||
|
|
||||||
|
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 save for this extension.",
|
||||||
|
)
|
||||||
|
|
||||||
|
user_ext_info = user_ext.extra or UserExtensionInfo()
|
||||||
|
user_ext_info.granted_permissions = granted_permissions
|
||||||
|
user_ext_info.granted_payment_tags = granted_tags
|
||||||
|
user_ext.extra = user_ext_info
|
||||||
|
await update_user_extension(user_ext)
|
||||||
|
|
||||||
|
return SimpleStatus(success=True, message=f"Permissions saved for '{ext_id}'.")
|
||||||
|
|
||||||
|
|
||||||
|
@extension_router.get("/{ext_id}/capabilities")
|
||||||
|
async def api_extension_capabilities(
|
||||||
|
ext_id: str,
|
||||||
|
request: Request,
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
) -> dict:
|
||||||
|
await _ensure_extension_exists(ext_id)
|
||||||
|
ext = await get_installed_extension(ext_id)
|
||||||
|
if not ext:
|
||||||
|
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||||
|
|
||||||
|
permissions_source = (
|
||||||
|
ext.meta.permissions
|
||||||
|
if ext.meta and ext.meta.permissions
|
||||||
|
else _load_permissions_from_config(ext_id)
|
||||||
|
)
|
||||||
|
permissions = [p.id for p in permissions_source] if permissions_source else []
|
||||||
|
missing = (
|
||||||
|
_missing_api_permissions(request, permissions)
|
||||||
|
if _is_wasm_extension(ext_id, ext)
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
payment_tags = _get_required_payment_tags(ext_id, ext)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"extension": ext_id,
|
||||||
|
"is_wasm": _is_wasm_extension(ext_id, ext),
|
||||||
|
"permissions": permissions,
|
||||||
|
"missing_permissions": missing,
|
||||||
|
"payment_tags": payment_tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@extension_router.get("/wasm/manifest")
|
||||||
|
async def api_wasm_manifest(
|
||||||
|
account_id: AccountId = Depends(check_account_id_exists),
|
||||||
|
) -> dict:
|
||||||
|
return WASM_HOST_MANIFEST
|
||||||
|
|
||||||
|
|
||||||
@extension_router.put("/{ext_id}/activate", dependencies=[Depends(check_admin)])
|
@extension_router.put("/{ext_id}/activate", dependencies=[Depends(check_admin)])
|
||||||
async def api_activate_extension(ext_id: str) -> SimpleStatus:
|
async def api_activate_extension(ext_id: str) -> SimpleStatus:
|
||||||
try:
|
try:
|
||||||
@@ -519,6 +743,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
installed_exts: list[InstallableExtension] = await get_installed_extensions(
|
installed_exts: list[InstallableExtension] = await get_installed_extensions(
|
||||||
conn=conn
|
conn=conn
|
||||||
)
|
)
|
||||||
|
user_exts = await get_user_extensions(account_id.id, conn=conn)
|
||||||
all_ext_ids = [ext.code for ext in await get_valid_extensions(conn=conn)]
|
all_ext_ids = [ext.code for ext in await get_valid_extensions(conn=conn)]
|
||||||
inactive_extensions = [
|
inactive_extensions = [
|
||||||
e.id for e in await get_installed_extensions(active=False, conn=conn)
|
e.id for e in await get_installed_extensions(active=False, conn=conn)
|
||||||
@@ -526,6 +751,7 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
db_versions = await get_db_versions(conn=conn)
|
db_versions = await get_db_versions(conn=conn)
|
||||||
|
|
||||||
installed_exts_ids = [e.id for e in installed_exts]
|
installed_exts_ids = [e.id for e in installed_exts]
|
||||||
|
user_exts_map = {e.extension: e for e in user_exts}
|
||||||
|
|
||||||
installable_exts = await InstallableExtension.get_installable_extensions(
|
installable_exts = await InstallableExtension.get_installable_extensions(
|
||||||
post_refresh_cache=account_id.is_admin_id
|
post_refresh_cache=account_id.is_admin_id
|
||||||
@@ -554,6 +780,10 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
e.name = installed_ext.name
|
e.name = installed_ext.name
|
||||||
e.short_description = installed_ext.short_description
|
e.short_description = installed_ext.short_description
|
||||||
e.icon = installed_ext.icon
|
e.icon = installed_ext.icon
|
||||||
|
if e.meta and not e.meta.permissions:
|
||||||
|
e.meta.permissions = _load_permissions_from_config(e.id)
|
||||||
|
if e.meta and not e.meta.payment_tags:
|
||||||
|
e.meta.payment_tags = _load_payment_tags_from_config(e.id)
|
||||||
|
|
||||||
extension_data = [
|
extension_data = [
|
||||||
{
|
{
|
||||||
@@ -571,6 +801,27 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
"isAvailable": ext.id in all_ext_ids,
|
"isAvailable": ext.id in all_ext_ids,
|
||||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||||
"isActive": ext.id not in inactive_extensions,
|
"isActive": ext.id not in inactive_extensions,
|
||||||
|
"permissions": (
|
||||||
|
[dict(p) for p in ext.meta.permissions]
|
||||||
|
if ext.meta and ext.meta.permissions
|
||||||
|
else [dict(p) for p in _load_permissions_from_config(ext.id)]
|
||||||
|
),
|
||||||
|
"paymentTags": (
|
||||||
|
ext.meta.payment_tags
|
||||||
|
if ext.meta and ext.meta.payment_tags
|
||||||
|
else _load_payment_tags_from_config(ext.id)
|
||||||
|
),
|
||||||
|
"kvSchema": _load_kv_schema_from_config(ext.id),
|
||||||
|
"grantedPermissions": (
|
||||||
|
user_ext.extra.granted_permissions
|
||||||
|
if (user_ext := user_exts_map.get(ext.id)) and user_ext.extra
|
||||||
|
else []
|
||||||
|
),
|
||||||
|
"grantedPaymentTags": (
|
||||||
|
user_ext.extra.granted_payment_tags
|
||||||
|
if (user_ext := user_exts_map.get(ext.id)) and user_ext.extra
|
||||||
|
else []
|
||||||
|
),
|
||||||
"latestRelease": (
|
"latestRelease": (
|
||||||
dict(ext.meta.latest_release)
|
dict(ext.meta.latest_release)
|
||||||
if ext.meta and ext.meta.latest_release
|
if ext.meta and ext.meta.latest_release
|
||||||
@@ -598,6 +849,162 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
|||||||
return extension_data
|
return extension_data
|
||||||
|
|
||||||
|
|
||||||
|
def _load_permissions_from_config(ext_id: str) -> list[ExtensionPermission]:
|
||||||
|
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)
|
||||||
|
permissions = config_json.get("permissions", [])
|
||||||
|
return [ExtensionPermission(**p) for p in permissions]
|
||||||
|
except Exception:
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_payment_tags_from_config(ext_id: str) -> list[str]:
|
||||||
|
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)
|
||||||
|
tags = config_json.get("payment_tags", [])
|
||||||
|
return tags if isinstance(tags, list) else []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_permissions(required: list[str], granted: list[str]) -> list[str]:
|
||||||
|
merged = set()
|
||||||
|
for perm in required or []:
|
||||||
|
merged.add(perm)
|
||||||
|
for perm in granted or []:
|
||||||
|
merged.add(perm)
|
||||||
|
return list(merged)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_payment_tags_allowed(required: list[str], granted: list[str]) -> None:
|
||||||
|
if not required and granted:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"This extension does not declare any payment tags.",
|
||||||
|
)
|
||||||
|
if required and not granted:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
"Select at least one payment tag before enabling this extension.",
|
||||||
|
)
|
||||||
|
if not required or not granted:
|
||||||
|
return
|
||||||
|
invalid = [t for t in granted if t not in required]
|
||||||
|
if invalid:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
f"Invalid payment tags requested: {', '.join(invalid)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_api_permissions_available(request: Request, permissions: list[str]) -> None:
|
||||||
|
if not permissions:
|
||||||
|
return
|
||||||
|
missing = []
|
||||||
|
for perm in permissions:
|
||||||
|
parsed = _parse_api_permission(perm)
|
||||||
|
if not parsed:
|
||||||
|
continue
|
||||||
|
method, path = parsed
|
||||||
|
if not _route_exists(request, method, path):
|
||||||
|
missing.append(perm)
|
||||||
|
if missing:
|
||||||
|
raise HTTPException(
|
||||||
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
f"Permissions reference missing endpoints: {', '.join(missing)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_api_permission(perm: str) -> tuple[str, str] | None:
|
||||||
|
if not perm.startswith("api."):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
method_part, path = perm.split(":", 1)
|
||||||
|
method = method_part.replace("api.", "").upper()
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if not path.startswith("/"):
|
||||||
|
return None
|
||||||
|
return method, path
|
||||||
|
|
||||||
|
|
||||||
|
def _missing_api_permissions(request: Request, permissions: list[str]) -> list[str]:
|
||||||
|
missing = []
|
||||||
|
for perm in permissions or []:
|
||||||
|
parsed = _parse_api_permission(perm)
|
||||||
|
if not parsed:
|
||||||
|
continue
|
||||||
|
method, path = parsed
|
||||||
|
if not _route_exists(request, method, path):
|
||||||
|
missing.append(perm)
|
||||||
|
return missing
|
||||||
|
|
||||||
|
|
||||||
|
def _route_exists(request: Request, method: str, path: str) -> bool:
|
||||||
|
scope = {
|
||||||
|
"type": "http",
|
||||||
|
"method": method,
|
||||||
|
"path": path,
|
||||||
|
"root_path": "",
|
||||||
|
"headers": [],
|
||||||
|
}
|
||||||
|
for route in request.app.router.routes:
|
||||||
|
methods = getattr(route, "methods", None)
|
||||||
|
if methods and method not in methods:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
match, _ = route.matches(scope)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug(f"Route match failed for {method} {path}: {exc!s}")
|
||||||
|
continue
|
||||||
|
if match == Match.FULL:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_wasm_extension(ext_id: str, ext: InstallableExtension) -> bool:
|
||||||
|
if ext.meta and ext.meta.extension_type == "wasm":
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
conf_path = Path(
|
||||||
|
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||||
|
)
|
||||||
|
if not conf_path.is_file():
|
||||||
|
return False
|
||||||
|
with open(conf_path, "r+") as json_file:
|
||||||
|
config_json = json.load(json_file)
|
||||||
|
return config_json.get("extension_type") == "wasm"
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@extension_router.get(
|
@extension_router.get(
|
||||||
"/reviews/tags",
|
"/reviews/tags",
|
||||||
dependencies=[Depends(check_account_exists)],
|
dependencies=[Depends(check_account_exists)],
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from fastapi import APIRouter, WebSocket
|
from fastapi import APIRouter, WebSocket
|
||||||
|
|
||||||
|
from ..crud.wallets import get_wallet_for_key
|
||||||
|
from ..models import KeyType
|
||||||
from ..services import websocket_manager
|
from ..services import websocket_manager
|
||||||
|
|
||||||
websocket_router = APIRouter(prefix="/api/v1/ws", tags=["Websocket"])
|
websocket_router = APIRouter(prefix="/api/v1/ws", tags=["Websocket"])
|
||||||
@@ -11,6 +13,30 @@ async def websocket_connect(websocket: WebSocket, item_id: str) -> None:
|
|||||||
await websocket_manager.listen(conn)
|
await websocket_manager.listen(conn)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_router.websocket("/tag/{tag}")
|
||||||
|
async def websocket_connect_tag(websocket: WebSocket, tag: str) -> None:
|
||||||
|
api_key = websocket.headers.get("X-API-KEY") or websocket.query_params.get(
|
||||||
|
"api-key"
|
||||||
|
)
|
||||||
|
if not api_key:
|
||||||
|
await websocket.close(code=4401)
|
||||||
|
return
|
||||||
|
|
||||||
|
wallet = await get_wallet_for_key(api_key)
|
||||||
|
if not wallet:
|
||||||
|
await websocket.close(code=4404)
|
||||||
|
return
|
||||||
|
|
||||||
|
key_type = KeyType.admin if wallet.adminkey == api_key else KeyType.invoice
|
||||||
|
if key_type not in {KeyType.admin, KeyType.invoice}:
|
||||||
|
await websocket.close(code=4403)
|
||||||
|
return
|
||||||
|
|
||||||
|
item_id = f"tag:{wallet.id}:{tag}"
|
||||||
|
conn = await websocket_manager.connect(item_id, websocket)
|
||||||
|
await websocket_manager.listen(conn)
|
||||||
|
|
||||||
|
|
||||||
@websocket_router.post("/{item_id}")
|
@websocket_router.post("/{item_id}")
|
||||||
async def websocket_update_post(item_id: str, data: str):
|
async def websocket_update_post(item_id: str, data: str):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
from lnbits.extensions.wasm.wasm_host import * # noqa: F401,F403
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from lnbits.extensions.wasm.wasm_host.extension_host import * # noqa: F401,F403
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from lnbits.extensions.wasm.wasm_host.runner import * # noqa: F401,F403
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from lnbits.extensions.wasm.wasm_host.service import * # noqa: F401,F403
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -28,6 +28,14 @@ window.PageExtensions = {
|
|||||||
paylinkWebsocket: null,
|
paylinkWebsocket: null,
|
||||||
searchToggle: false,
|
searchToggle: false,
|
||||||
reviewsUrl: null,
|
reviewsUrl: null,
|
||||||
|
permissionsDialog: {
|
||||||
|
show: false,
|
||||||
|
extension: null,
|
||||||
|
checked: [],
|
||||||
|
missing: [],
|
||||||
|
tags: [],
|
||||||
|
tagOptions: []
|
||||||
|
},
|
||||||
reviewsDialog: {
|
reviewsDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
extension: null,
|
extension: null,
|
||||||
@@ -92,6 +100,20 @@ window.PageExtensions = {
|
|||||||
this.filterExtensions(this.searchTerm, val)
|
this.filterExtensions(this.searchTerm, val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
permissionsAllChecked() {
|
||||||
|
const ext = this.permissionsDialog.extension
|
||||||
|
if (!ext || !Array.isArray(ext.permissions)) return true
|
||||||
|
const required = ext.permissions.map(p => p.id)
|
||||||
|
return required.every(p => this.permissionsDialog.checked.includes(p))
|
||||||
|
},
|
||||||
|
permissionsHasMissingEndpoints() {
|
||||||
|
return (
|
||||||
|
this.permissionsDialog.missing &&
|
||||||
|
this.permissionsDialog.missing.length > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
filterExtensions(term, tab) {
|
filterExtensions(term, tab) {
|
||||||
// Filter the extensions list
|
// Filter the extensions list
|
||||||
@@ -241,18 +263,49 @@ window.PageExtensions = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async enableExtensionForUser(extension) {
|
async enableExtensionForUser(extension) {
|
||||||
|
if (extension.permissions && extension.permissions.length) {
|
||||||
|
if (!extension._grantedPermissions) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Save permissions before enabling this extension.'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (extension.paymentTags && extension.paymentTags.length) {
|
||||||
|
if (
|
||||||
|
!extension._grantedPaymentTags ||
|
||||||
|
!extension._grantedPaymentTags.length
|
||||||
|
) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'Select payment tags before enabling this extension.'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
if (extension.isPaymentRequired) {
|
if (extension.isPaymentRequired) {
|
||||||
this.showPayToEnable(extension)
|
this.showPayToEnable(extension)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.enableExtension(extension)
|
this.enableExtension(extension)
|
||||||
},
|
},
|
||||||
async enableExtension(extension) {
|
async enableExtension(extension, permissions) {
|
||||||
|
const granted =
|
||||||
|
permissions ||
|
||||||
|
extension._grantedPermissions ||
|
||||||
|
(extension.permissions ? extension.permissions.map(p => p.id) : [])
|
||||||
|
const tags =
|
||||||
|
extension._grantedPaymentTags ||
|
||||||
|
(extension.paymentTags ? extension.paymentTags.slice() : [])
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
'PUT',
|
'PUT',
|
||||||
`/api/v1/extension/${extension.id}/enable`,
|
`/api/v1/extension/${extension.id}/enable`,
|
||||||
this.g.user.wallets[0].adminkey
|
this.g.user.wallets[0].adminkey,
|
||||||
|
granted.length || tags.length
|
||||||
|
? {permissions: granted, payment_tags: tags}
|
||||||
|
: null
|
||||||
)
|
)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.g.user.extensions = this.g.user.extensions.concat([extension.id])
|
this.g.user.extensions = this.g.user.extensions.concat([extension.id])
|
||||||
@@ -287,13 +340,116 @@ window.PageExtensions = {
|
|||||||
LNbits.utils.notifyApiError(err)
|
LNbits.utils.notifyApiError(err)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
showPayToEnable(extension) {
|
showPayToEnable(extension, permissions) {
|
||||||
this.selectedExtension = extension
|
this.selectedExtension = extension
|
||||||
|
if (permissions) {
|
||||||
|
this.selectedExtension._grantedPermissions = permissions
|
||||||
|
}
|
||||||
this.selectedExtension.payToEnable.paidAmount =
|
this.selectedExtension.payToEnable.paidAmount =
|
||||||
extension.payToEnable.amount
|
extension.payToEnable.amount
|
||||||
this.selectedExtension.payToEnable.showQRCode = false
|
this.selectedExtension.payToEnable.showQRCode = false
|
||||||
this.showPayToEnableDialog = true
|
this.showPayToEnableDialog = true
|
||||||
},
|
},
|
||||||
|
openPermissionsDialog(extension) {
|
||||||
|
this.permissionsDialog.extension = extension
|
||||||
|
this.permissionsDialog.checked = extension._grantedPermissions
|
||||||
|
? extension._grantedPermissions.slice()
|
||||||
|
: extension.grantedPermissions
|
||||||
|
? extension.grantedPermissions.slice()
|
||||||
|
: []
|
||||||
|
this.permissionsDialog.missing = []
|
||||||
|
this.permissionsDialog.tags = extension._grantedPaymentTags
|
||||||
|
? extension._grantedPaymentTags.slice()
|
||||||
|
: extension.grantedPaymentTags
|
||||||
|
? extension.grantedPaymentTags.slice()
|
||||||
|
: []
|
||||||
|
this.permissionsDialog.tagOptions = []
|
||||||
|
this.permissionsDialog.show = true
|
||||||
|
},
|
||||||
|
cancelPermissionsDialog() {
|
||||||
|
this.permissionsDialog.show = false
|
||||||
|
this.permissionsDialog.extension = null
|
||||||
|
this.permissionsDialog.checked = []
|
||||||
|
this.permissionsDialog.missing = []
|
||||||
|
this.permissionsDialog.tags = []
|
||||||
|
this.permissionsDialog.tagOptions = []
|
||||||
|
},
|
||||||
|
async openPermissionsForExtension(extension) {
|
||||||
|
if (!extension.permissions || !extension.permissions.length) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'warning',
|
||||||
|
message: 'This extension does not declare permissions.'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.permissionsDialog.extension = extension
|
||||||
|
this.permissionsDialog.checked = extension._grantedPermissions
|
||||||
|
? extension._grantedPermissions.slice()
|
||||||
|
: extension.grantedPermissions
|
||||||
|
? extension.grantedPermissions.slice()
|
||||||
|
: []
|
||||||
|
this.permissionsDialog.missing = []
|
||||||
|
this.permissionsDialog.tags = extension._grantedPaymentTags
|
||||||
|
? extension._grantedPaymentTags.slice()
|
||||||
|
: extension.grantedPaymentTags
|
||||||
|
? extension.grantedPaymentTags.slice()
|
||||||
|
: []
|
||||||
|
this.permissionsDialog.tagOptions = []
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'GET',
|
||||||
|
`/api/v1/extension/${extension.id}/capabilities`,
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
if (data && Array.isArray(data.missing_permissions)) {
|
||||||
|
this.permissionsDialog.missing = data.missing_permissions
|
||||||
|
}
|
||||||
|
if (data && Array.isArray(data.payment_tags)) {
|
||||||
|
this.permissionsDialog.tagOptions = data.payment_tags
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
this.permissionsDialog.show = true
|
||||||
|
},
|
||||||
|
async confirmPermissionsDialog() {
|
||||||
|
const ext = this.permissionsDialog.extension
|
||||||
|
const granted = this.permissionsDialog.checked.slice()
|
||||||
|
const tags = this.permissionsDialog.tags.slice()
|
||||||
|
this.permissionsDialog.show = false
|
||||||
|
this.permissionsDialog.extension = null
|
||||||
|
this.permissionsDialog.checked = []
|
||||||
|
const missing = this.permissionsDialog.missing || []
|
||||||
|
this.permissionsDialog.missing = []
|
||||||
|
this.permissionsDialog.tags = []
|
||||||
|
this.permissionsDialog.tagOptions = []
|
||||||
|
if (missing.length) {
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Missing API endpoints for one or more permissions.'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!ext) return
|
||||||
|
ext._grantedPermissions = granted
|
||||||
|
ext.grantedPermissions = granted
|
||||||
|
ext._grantedPaymentTags = tags
|
||||||
|
ext.grantedPaymentTags = tags
|
||||||
|
try {
|
||||||
|
await LNbits.api.request(
|
||||||
|
'PUT',
|
||||||
|
`/api/v1/extension/${ext.id}/permissions`,
|
||||||
|
this.g.user.wallets[0].adminkey,
|
||||||
|
{permissions: granted, payment_tags: tags}
|
||||||
|
)
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message: 'Permissions saved.'
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
LNbits.utils.notifyApiError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
updatePayToInstallData(extension) {
|
updatePayToInstallData(extension) {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request(
|
.request(
|
||||||
@@ -832,6 +988,16 @@ window.PageExtensions = {
|
|||||||
async fetchAllExtensions() {
|
async fetchAllExtensions() {
|
||||||
try {
|
try {
|
||||||
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
const {data} = await LNbits.api.request('GET', `/api/v1/extension/all`)
|
||||||
|
data.forEach(ext => {
|
||||||
|
if (ext.grantedPermissions && ext.grantedPermissions.length) {
|
||||||
|
ext._grantedPermissions = ext.grantedPermissions
|
||||||
|
}
|
||||||
|
if (ext.grantedPaymentTags && ext.grantedPaymentTags.length) {
|
||||||
|
ext._grantedPaymentTags = ext.grantedPaymentTags
|
||||||
|
}
|
||||||
|
const schema = ext.kvSchema || ext.kv_schema || {}
|
||||||
|
ext.kvSchemaKeys = Object.keys(schema)
|
||||||
|
})
|
||||||
return data
|
return data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(error)
|
console.warn(error)
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ def register_invoice_listener(send_chan: asyncio.Queue, name: str | None = None)
|
|||||||
invoice_listeners[name] = send_chan
|
invoice_listeners[name] = send_chan
|
||||||
|
|
||||||
|
|
||||||
|
def unregister_invoice_listener(name: str) -> None:
|
||||||
|
if name in invoice_listeners:
|
||||||
|
invoice_listeners.pop(name, None)
|
||||||
|
|
||||||
|
|
||||||
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -329,6 +329,19 @@
|
|||||||
<span v-text="$t('enable_extension_details')">
|
<span v-text="$t('enable_extension_details')">
|
||||||
</span> </q-tooltip
|
</span> </q-tooltip
|
||||||
></q-btn>
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
v-if="
|
||||||
|
extension.isInstalled &&
|
||||||
|
extension.isActive &&
|
||||||
|
!g.user.extensions.includes(extension.id) &&
|
||||||
|
extension.permissions &&
|
||||||
|
extension.permissions.length
|
||||||
|
"
|
||||||
|
flat
|
||||||
|
color="grey-5"
|
||||||
|
@click="openPermissionsForExtension(extension)"
|
||||||
|
label="Permissions"
|
||||||
|
></q-btn>
|
||||||
|
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showManageExtension(extension)"
|
@click="showManageExtension(extension)"
|
||||||
@@ -921,6 +934,104 @@
|
|||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<q-dialog v-model="permissionsDialog.show" position="top">
|
||||||
|
<q-card class="q-pa-md" style="min-width: 360px; max-width: 90vw">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">Permissions required</div>
|
||||||
|
<div class="text-caption text-grey">This extension can:</div>
|
||||||
|
<q-list v-if="permissionsDialog.extension">
|
||||||
|
<q-item
|
||||||
|
v-for="perm in permissionsDialog.extension.permissions"
|
||||||
|
:key="perm.id"
|
||||||
|
clickable
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label v-text="perm.label"></q-item-label>
|
||||||
|
<q-item-label caption v-text="perm.description"></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-checkbox v-model="permissionsDialog.checked" :val="perm.id" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
permissionsDialog.tagOptions && permissionsDialog.tagOptions.length
|
||||||
|
"
|
||||||
|
class="q-mt-md"
|
||||||
|
>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Allow this extension to listen for payment tags:
|
||||||
|
</div>
|
||||||
|
<q-list>
|
||||||
|
<q-item
|
||||||
|
v-for="tag in permissionsDialog.tagOptions"
|
||||||
|
:key="tag"
|
||||||
|
clickable
|
||||||
|
>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label v-text="tag"></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-checkbox v-model="permissionsDialog.tags" :val="tag" />
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
permissionsDialog.extension &&
|
||||||
|
permissionsDialog.extension.kvSchemaKeys &&
|
||||||
|
permissionsDialog.extension.kvSchemaKeys.length
|
||||||
|
"
|
||||||
|
class="q-mt-md"
|
||||||
|
>
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
Data this extension stores in its db:
|
||||||
|
</div>
|
||||||
|
<q-chip
|
||||||
|
v-for="kvKey in permissionsDialog.extension.kvSchemaKeys"
|
||||||
|
:key="kvKey"
|
||||||
|
:label="kvKey"
|
||||||
|
color="grey-3"
|
||||||
|
text-color="black"
|
||||||
|
class="q-mr-xs q-mt-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="permissionsDialog.missing && permissionsDialog.missing.length"
|
||||||
|
class="q-mt-md text-negative"
|
||||||
|
>
|
||||||
|
<div class="text-caption">
|
||||||
|
Missing API endpoints required by this extension:
|
||||||
|
</div>
|
||||||
|
<q-chip
|
||||||
|
v-for="perm in permissionsDialog.missing"
|
||||||
|
:key="perm"
|
||||||
|
:label="perm"
|
||||||
|
color="red-2"
|
||||||
|
text-color="black"
|
||||||
|
class="q-mr-xs q-mt-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn
|
||||||
|
flat
|
||||||
|
color="grey"
|
||||||
|
v-text="$t('cancel')"
|
||||||
|
@click="cancelPermissionsDialog"
|
||||||
|
></q-btn>
|
||||||
|
<q-btn
|
||||||
|
color="primary"
|
||||||
|
:disable="!permissionsAllChecked || permissionsHasMissingEndpoints"
|
||||||
|
label="Save"
|
||||||
|
@click="confirmPermissionsDialog"
|
||||||
|
></q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="showExtensionDetailsDialog" position="top">
|
<q-dialog v-model="showExtensionDetailsDialog" position="top">
|
||||||
<q-card
|
<q-card
|
||||||
v-if="selectedExtensionDetails"
|
v-if="selectedExtensionDetails"
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ dependencies = [
|
|||||||
"pillow>=12.1.0",
|
"pillow>=12.1.0",
|
||||||
"python-dotenv>=1.2.1",
|
"python-dotenv>=1.2.1",
|
||||||
"greenlet (>=3.3.0,<4.0.0)",
|
"greenlet (>=3.3.0,<4.0.0)",
|
||||||
|
"wasmtime>=16.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -1290,6 +1290,7 @@ dependencies = [
|
|||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "uvicorn" },
|
{ name = "uvicorn" },
|
||||||
{ name = "uvloop" },
|
{ name = "uvloop" },
|
||||||
|
{ name = "wasmtime" },
|
||||||
{ name = "websocket-client" },
|
{ name = "websocket-client" },
|
||||||
{ name = "websockets" },
|
{ name = "websockets" },
|
||||||
]
|
]
|
||||||
@@ -1371,6 +1372,7 @@ requires-dist = [
|
|||||||
{ name = "sqlalchemy", specifier = "==1.4.54" },
|
{ name = "sqlalchemy", specifier = "==1.4.54" },
|
||||||
{ name = "sse-starlette", specifier = "==2.3.6" },
|
{ name = "sse-starlette", specifier = "==2.3.6" },
|
||||||
{ name = "starlette", specifier = "==0.47.1" },
|
{ name = "starlette", specifier = "==0.47.1" },
|
||||||
|
{ name = "wasmtime", specifier = ">=16.0.0" },
|
||||||
{ name = "typing-extensions", specifier = "==4.15.0" },
|
{ name = "typing-extensions", specifier = "==4.15.0" },
|
||||||
{ name = "uvicorn", specifier = "==0.40.0" },
|
{ name = "uvicorn", specifier = "==0.40.0" },
|
||||||
{ name = "uvloop", specifier = "==0.22.1" },
|
{ name = "uvloop", specifier = "==0.22.1" },
|
||||||
@@ -2710,6 +2712,25 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/49/4b/8e43c5d27753135fa8df3de333c1f1917d6dc315d119ffa60d0587d8d9ff/wallycore-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0b932510c859ac3e1fc57161420ecb2bb9b6562fce7acd1d9351337cc2cc3515", size = 1728013, upload-time = "2025-08-22T04:26:40.774Z" },
|
{ url = "https://files.pythonhosted.org/packages/49/4b/8e43c5d27753135fa8df3de333c1f1917d6dc315d119ffa60d0587d8d9ff/wallycore-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:0b932510c859ac3e1fc57161420ecb2bb9b6562fce7acd1d9351337cc2cc3515", size = 1728013, upload-time = "2025-08-22T04:26:40.774Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmtime"
|
||||||
|
version = "41.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/68/6dc0e7156f883afe0129dd89e4031c8d1163131794ba6ce9e454a09168ad/wasmtime-41.0.0.tar.gz", hash = "sha256:fc2aaacf3ba794eac8baeb739939b2f7903e12d6b78edddc0b7f3ac3a9af6dfc", size = 117354, upload-time = "2026-01-20T18:18:00.565Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/f9/f6aef5de536d12652d97cf162f124cbdd642150c7da61ffa7863272cdab7/wasmtime-41.0.0-py3-none-android_26_arm64_v8a.whl", hash = "sha256:f5a6e237b5b94188ef9867926b447f779f540c729c92e4d91cc946f2bee7c282", size = 6837018, upload-time = "2026-01-20T18:17:41.489Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/b9/42ec977972b2dcc8c61e3a40644d24d229b41fba151410644e44e35e6eb1/wasmtime-41.0.0-py3-none-android_26_x86_64.whl", hash = "sha256:4a3e33d0d3cf49062eaa231f748f54af991e89e9a795c5ab9d4f0eee85736e4c", size = 7654957, upload-time = "2026-01-20T18:17:43.285Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/ca/6cce49b03c35c7fecb4437fd98990c64694a5e0024f9279bef0ddef000f7/wasmtime-41.0.0-py3-none-any.whl", hash = "sha256:5f6721406a6cd186d11f34e6d4991c4d536387b0c577d09a56bd93b8a3cf10c2", size = 6325757, upload-time = "2026-01-20T18:17:44.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/16/d91cb80322cc7ae10bfa5db8cea4e0b9bb112f0c100b4486783ab16c1c22/wasmtime-41.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:2107360212fce33ed2adcfc33b7e75ed7136380a17d3ed598a5bab376dcf9e1b", size = 7471888, upload-time = "2026-01-20T18:17:46.185Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/f0/dcc80973d2ec58a1978b838887ccbd84d56900cf66dec5fb730bec3bd081/wasmtime-41.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f475df32ce9bfec4f6d0e124a49ca4a89e2ee71ccca460677f5237b1c8ee92ae", size = 6507285, upload-time = "2026-01-20T18:17:48.138Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/df/0867edd9ec26eb2e5eee7674a55f82c23ec27dd1d38d2d401f0e308eb920/wasmtime-41.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ad7e866430313eb2ee07c85811e524344884489d00896f3b2246b65553fe322c", size = 7732024, upload-time = "2026-01-20T18:17:50.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/48/b748a2e70478feabc5c876d90e90a39f4aba35378f5ee822f607e8f29c69/wasmtime-41.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:e0ea44584f60dcfa620af82d4fc2589248bcf64a93905b54ac3144242113b48a", size = 6800017, upload-time = "2026-01-20T18:17:51.7Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/29/43656c3a464d437d62421de16f2de2db645647bab0a0153deea30bfdade4/wasmtime-41.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dabb20a2751f01b835095013426a76091bd0bdb36ca9bcfc49c910b78347438", size = 6840763, upload-time = "2026-01-20T18:17:53.125Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/09/4608b65fa35ce5fc1479e138293a1166b4ea817cfa9a79f019ab6d7013d8/wasmtime-41.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d9627dfc5625b4947ea35c819561da358838fe76f65bda8ffe01ce34df8b32b1", size = 7754016, upload-time = "2026-01-20T18:17:55.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/9d/236bb367270579e4f628fb7b04fe93541151df7953006f3766607fc667c9/wasmtime-41.0.0-py3-none-win_amd64.whl", hash = "sha256:4f29171d73b71f232b6fe86cba77526fee84139f1590071af5facba401b0c9eb", size = 6325764, upload-time = "2026-01-20T18:17:57.034Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/4a/bba9c0368c377250ab24fd005a7a1e9076121778c1e83b1bcc092ab84f86/wasmtime-41.0.0-py3-none-win_arm64.whl", hash = "sha256:0c4bcaba055e78fc161f497b85f39f1d35d475f0341b1e0259fa0a4b49e223e8", size = 5392238, upload-time = "2026-01-20T18:17:59.052Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "websocket-client"
|
name = "websocket-client"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user