Compare commits
32
Commits
v1.0.0-rc8
...
v1.0.0-rc9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
475ae5736e | ||
|
|
d7f0f4da0c | ||
|
|
f7b3444be4 | ||
|
|
28e32a54a4 | ||
|
|
1833adc0a1 | ||
|
|
b759ec7468 | ||
|
|
2661d12ff1 | ||
|
|
ed419f27ac | ||
|
|
c3efd48108 | ||
|
|
4a28c22d79 | ||
|
|
0d0eb36de4 | ||
|
|
2aa73bfe75 | ||
|
|
ebd080192c | ||
|
|
41abe63f18 | ||
|
|
f61471b0f2 | ||
|
|
9bd037b6e7 | ||
|
|
a4ca88b6a4 | ||
|
|
f2b9aafc51 | ||
|
|
ec77a00f49 | ||
|
|
991e0db50b | ||
|
|
fe9b62e8a8 | ||
|
|
c5964436b5 | ||
|
|
4ce14e2312 | ||
|
|
7021add193 | ||
|
|
4be01a405c | ||
|
|
ebe9ff700f | ||
|
|
a3619d40c6 | ||
|
|
048aff3db4 | ||
|
|
89d85a0d7e | ||
|
|
5aa1f9b0f8 | ||
|
|
5dc1705fa6 | ||
|
|
0fc657c55c |
+1
-1
@@ -186,7 +186,7 @@ LNBITS_ADMIN_USERS=""
|
|||||||
# SUPER_USER=""
|
# SUPER_USER=""
|
||||||
|
|
||||||
# Extensions only admin can access
|
# Extensions only admin can access
|
||||||
LNBITS_ADMIN_EXTENSIONS="ngrok, admin"
|
LNBITS_ADMIN_EXTENSIONS="ngrok, nostrclient"
|
||||||
# Extensions enabled by default when a user is created
|
# Extensions enabled by default when a user is created
|
||||||
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
LNBITS_USER_DEFAULT_EXTENSIONS="lnurlp"
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ inputs:
|
|||||||
default: "3.10"
|
default: "3.10"
|
||||||
poetry-version:
|
poetry-version:
|
||||||
description: "Poetry Version"
|
description: "Poetry Version"
|
||||||
default: "1.7.0"
|
default: "1.8.5"
|
||||||
node-version:
|
node-version:
|
||||||
description: "Node Version"
|
description: "Node Version"
|
||||||
default: "20.x"
|
default: "20.x"
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
poetry run pip install pyinstaller
|
poetry run pip install pyinstaller
|
||||||
|
|
||||||
# Build the LNbits binary
|
# Build the LNbits binary
|
||||||
poetry run pyinstaller --onefile --name lnbits --collect-all lnbits --collect-all sqlalchemy --collect-all aiosqlite --hidden-import=passlib.handlers.bcrypt $(poetry run which lnbits)
|
poetry run pyinstaller --onefile --name lnbits --hidden-import=embit --collect-all embit --collect-all lnbits --collect-all sqlalchemy --collect-all aiosqlite --hidden-import=passlib.handlers.bcrypt $(poetry run which lnbits)
|
||||||
cd ../../../../..
|
cd ../../../../..
|
||||||
chmod +x packaging/linux/AppDir/AppRun
|
chmod +x packaging/linux/AppDir/AppRun
|
||||||
chmod +x packaging/linux/AppDir/lnbits.desktop
|
chmod +x packaging/linux/AppDir/lnbits.desktop
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ Problems installing? These commands have helped us install LNbits.
|
|||||||
sudo apt install pkg-config libffi-dev libpq-dev
|
sudo apt install pkg-config libffi-dev libpq-dev
|
||||||
|
|
||||||
# build essentials for debian/ubuntu
|
# build essentials for debian/ubuntu
|
||||||
sudo apt install python3.9-dev gcc build-essential
|
sudo apt install python3.10-dev gcc build-essential
|
||||||
|
|
||||||
# if the secp256k1 build fails:
|
# if the secp256k1 build fails:
|
||||||
# if you used poetry
|
# if you used poetry
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ async def startup(app: FastAPI):
|
|||||||
|
|
||||||
# setup admin settings
|
# setup admin settings
|
||||||
await check_admin_settings()
|
await check_admin_settings()
|
||||||
|
core_app_extra.register_new_ratelimiter()
|
||||||
await check_webpush_settings()
|
await check_webpush_settings()
|
||||||
|
|
||||||
# check extensions after restart
|
# check extensions after restart
|
||||||
|
|||||||
+15
-16
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
@@ -10,16 +9,16 @@ from lnbits.settings import settings
|
|||||||
|
|
||||||
|
|
||||||
class AuditEntry(BaseModel):
|
class AuditEntry(BaseModel):
|
||||||
component: Optional[str] = None
|
component: str | None = None
|
||||||
ip_address: Optional[str] = None
|
ip_address: str | None = None
|
||||||
user_id: Optional[str] = None
|
user_id: str | None = None
|
||||||
path: Optional[str] = None
|
path: str | None = None
|
||||||
request_type: Optional[str] = None
|
request_type: str | None = None
|
||||||
request_method: Optional[str] = None
|
request_method: str | None = None
|
||||||
request_details: Optional[str] = None
|
request_details: str | None = None
|
||||||
response_code: Optional[str] = None
|
response_code: str | None = None
|
||||||
duration: float
|
duration: float
|
||||||
delete_at: Optional[datetime] = None
|
delete_at: datetime | None = None
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
def __init__(self, **data):
|
def __init__(self, **data):
|
||||||
@@ -42,12 +41,12 @@ class AuditFilters(FilterModel):
|
|||||||
"duration",
|
"duration",
|
||||||
]
|
]
|
||||||
|
|
||||||
ip_address: Optional[str] = None
|
ip_address: str | None = None
|
||||||
user_id: Optional[str] = None
|
user_id: str | None = None
|
||||||
path: Optional[str] = None
|
path: str | None = None
|
||||||
request_method: Optional[str] = None
|
request_method: str | None = None
|
||||||
response_code: Optional[str] = None
|
response_code: str | None = None
|
||||||
component: Optional[str] = None
|
component: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AuditCountStat(BaseModel):
|
class AuditCountStat(BaseModel):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import os
|
|||||||
import shutil
|
import shutil
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -29,17 +29,17 @@ class ExplicitRelease(BaseModel):
|
|||||||
archive: str
|
archive: str
|
||||||
hash: str
|
hash: str
|
||||||
dependencies: list[str] = []
|
dependencies: list[str] = []
|
||||||
repo: Optional[str]
|
repo: str | None
|
||||||
icon: Optional[str]
|
icon: str | None
|
||||||
short_description: Optional[str]
|
short_description: str | None
|
||||||
min_lnbits_version: Optional[str]
|
min_lnbits_version: str | None
|
||||||
max_lnbits_version: Optional[str]
|
max_lnbits_version: str | None
|
||||||
html_url: Optional[str] # todo: release_url
|
html_url: str | None # todo: release_url
|
||||||
warning: Optional[str]
|
warning: str | None
|
||||||
info_notification: Optional[str]
|
info_notification: str | None
|
||||||
critical_notification: Optional[str]
|
critical_notification: str | None
|
||||||
details_link: Optional[str]
|
details_link: str | None
|
||||||
pay_link: Optional[str]
|
pay_link: str | None
|
||||||
|
|
||||||
def is_version_compatible(self):
|
def is_version_compatible(self):
|
||||||
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)
|
||||||
@@ -77,9 +77,9 @@ class ExtensionConfig(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
short_description: str
|
short_description: str
|
||||||
tile: str = ""
|
tile: str = ""
|
||||||
warning: Optional[str] = ""
|
warning: str | None = ""
|
||||||
min_lnbits_version: Optional[str]
|
min_lnbits_version: str | None
|
||||||
max_lnbits_version: Optional[str]
|
max_lnbits_version: str | None
|
||||||
|
|
||||||
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)
|
||||||
@@ -87,7 +87,7 @@ class ExtensionConfig(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def fetch_github_release_config(
|
async def fetch_github_release_config(
|
||||||
cls, org: str, repo: str, tag_name: str
|
cls, org: str, repo: str, tag_name: str
|
||||||
) -> Optional[ExtensionConfig]:
|
) -> ExtensionConfig | None:
|
||||||
config_url = (
|
config_url = (
|
||||||
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
|
f"https://raw.githubusercontent.com/{org}/{repo}/{tag_name}/config.json"
|
||||||
)
|
)
|
||||||
@@ -97,28 +97,28 @@ class ExtensionConfig(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ReleasePaymentInfo(BaseModel):
|
class ReleasePaymentInfo(BaseModel):
|
||||||
amount: Optional[int] = None
|
amount: int | None = None
|
||||||
pay_link: Optional[str] = None
|
pay_link: str | None = None
|
||||||
payment_hash: Optional[str] = None
|
payment_hash: str | None = None
|
||||||
payment_request: Optional[str] = None
|
payment_request: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PayToEnableInfo(BaseModel):
|
class PayToEnableInfo(BaseModel):
|
||||||
amount: int = 0
|
amount: int = 0
|
||||||
required: bool = False
|
required: bool = False
|
||||||
wallet: Optional[str] = None
|
wallet: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class UserExtensionInfo(BaseModel):
|
class UserExtensionInfo(BaseModel):
|
||||||
paid_to_enable: Optional[bool] = False
|
paid_to_enable: bool | None = False
|
||||||
payment_hash_to_enable: Optional[str] = None
|
payment_hash_to_enable: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class UserExtension(BaseModel):
|
class UserExtension(BaseModel):
|
||||||
user: str
|
user: str
|
||||||
extension: str
|
extension: str
|
||||||
active: bool
|
active: bool
|
||||||
extra: Optional[UserExtensionInfo] = None
|
extra: UserExtensionInfo | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_paid(self) -> bool:
|
def is_paid(self) -> bool:
|
||||||
@@ -140,10 +140,10 @@ class UserExtension(BaseModel):
|
|||||||
class Extension(BaseModel):
|
class Extension(BaseModel):
|
||||||
code: str
|
code: str
|
||||||
is_valid: bool
|
is_valid: bool
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
short_description: Optional[str] = None
|
short_description: str | None = None
|
||||||
tile: Optional[str] = None
|
tile: str | None = None
|
||||||
upgrade_hash: Optional[str] = ""
|
upgrade_hash: str | None = ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module_name(self) -> str:
|
def module_name(self) -> str:
|
||||||
@@ -176,21 +176,21 @@ class ExtensionRelease(BaseModel):
|
|||||||
archive: str
|
archive: str
|
||||||
source_repo: str
|
source_repo: str
|
||||||
is_github_release: bool = False
|
is_github_release: bool = False
|
||||||
hash: Optional[str] = None
|
hash: str | None = None
|
||||||
min_lnbits_version: Optional[str] = None
|
min_lnbits_version: str | None = None
|
||||||
max_lnbits_version: Optional[str] = None
|
max_lnbits_version: str | None = None
|
||||||
is_version_compatible: Optional[bool] = True
|
is_version_compatible: bool | None = True
|
||||||
html_url: Optional[str] = None
|
html_url: str | None = None
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
warning: Optional[str] = None
|
warning: str | None = None
|
||||||
repo: Optional[str] = None
|
repo: str | None = None
|
||||||
icon: Optional[str] = None
|
icon: str | None = None
|
||||||
details_link: Optional[str] = None
|
details_link: str | None = None
|
||||||
|
|
||||||
pay_link: Optional[str] = None
|
pay_link: str | None = None
|
||||||
cost_sats: Optional[int] = None
|
cost_sats: int | None = None
|
||||||
paid_sats: Optional[int] = 0
|
paid_sats: int | None = 0
|
||||||
payment_hash: Optional[str] = None
|
payment_hash: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def archive_url(self) -> str:
|
def archive_url(self) -> str:
|
||||||
@@ -208,8 +208,8 @@ class ExtensionRelease(BaseModel):
|
|||||||
self.cost_sats = payment_info.amount if payment_info else None
|
self.cost_sats = payment_info.amount if payment_info else None
|
||||||
|
|
||||||
async def fetch_release_payment_info(
|
async def fetch_release_payment_info(
|
||||||
self, amount: Optional[int] = None
|
self, amount: int | None = None
|
||||||
) -> Optional[ReleasePaymentInfo]:
|
) -> ReleasePaymentInfo | None:
|
||||||
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
|
url = f"{self.pay_link}?amount={amount}" if amount else self.pay_link
|
||||||
assert url, "Missing URL for payment info."
|
assert url, "Missing URL for payment info."
|
||||||
try:
|
try:
|
||||||
@@ -281,7 +281,7 @@ class ExtensionRelease(BaseModel):
|
|||||||
return [GitHubRepoRelease.parse_obj(r) for r in releases]
|
return [GitHubRepoRelease.parse_obj(r) for r in releases]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def fetch_release_details(cls, details_link: str) -> Optional[dict]:
|
async def fetch_release_details(cls, details_link: str) -> dict | None:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
@@ -300,12 +300,12 @@ class ExtensionRelease(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ExtensionMeta(BaseModel):
|
class ExtensionMeta(BaseModel):
|
||||||
installed_release: Optional[ExtensionRelease] = None
|
installed_release: ExtensionRelease | None = None
|
||||||
latest_release: Optional[ExtensionRelease] = None
|
latest_release: ExtensionRelease | None = None
|
||||||
pay_to_enable: Optional[PayToEnableInfo] = None
|
pay_to_enable: PayToEnableInfo | None = None
|
||||||
payments: list[ReleasePaymentInfo] = []
|
payments: list[ReleasePaymentInfo] = []
|
||||||
dependencies: list[str] = []
|
dependencies: list[str] = []
|
||||||
archive: Optional[str] = None
|
archive: str | None = None
|
||||||
featured: bool = False
|
featured: bool = False
|
||||||
|
|
||||||
|
|
||||||
@@ -313,11 +313,11 @@ class InstallableExtension(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
version: str
|
version: str
|
||||||
active: Optional[bool] = False
|
active: bool | None = False
|
||||||
short_description: Optional[str] = None
|
short_description: str | None = None
|
||||||
icon: Optional[str] = None
|
icon: str | None = None
|
||||||
stars: int = 0
|
stars: int = 0
|
||||||
meta: Optional[ExtensionMeta] = None
|
meta: ExtensionMeta | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hash(self) -> str:
|
def hash(self) -> str:
|
||||||
@@ -452,7 +452,7 @@ class InstallableExtension(BaseModel):
|
|||||||
|
|
||||||
shutil.rmtree(self.ext_upgrade_dir, True)
|
shutil.rmtree(self.ext_upgrade_dir, True)
|
||||||
|
|
||||||
def check_latest_version(self, release: Optional[ExtensionRelease]):
|
def check_latest_version(self, release: ExtensionRelease | None):
|
||||||
if not release:
|
if not release:
|
||||||
return
|
return
|
||||||
if not self.meta or not self.meta.latest_release:
|
if not self.meta or not self.meta.latest_release:
|
||||||
@@ -465,9 +465,7 @@ class InstallableExtension(BaseModel):
|
|||||||
):
|
):
|
||||||
self.meta.latest_release = release
|
self.meta.latest_release = release
|
||||||
|
|
||||||
def find_existing_payment(
|
def find_existing_payment(self, pay_link: str | None) -> ReleasePaymentInfo | None:
|
||||||
self, pay_link: Optional[str]
|
|
||||||
) -> Optional[ReleasePaymentInfo]:
|
|
||||||
if not pay_link or not self.meta or not self.meta.payments:
|
if not pay_link or not self.meta or not self.meta.payments:
|
||||||
return None
|
return None
|
||||||
return next(
|
return next(
|
||||||
@@ -507,7 +505,7 @@ class InstallableExtension(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def from_github_release(
|
async def from_github_release(
|
||||||
cls, github_release: GitHubRelease
|
cls, github_release: GitHubRelease
|
||||||
) -> Optional[InstallableExtension]:
|
) -> InstallableExtension | None:
|
||||||
try:
|
try:
|
||||||
repo, latest_release, config = await cls.fetch_github_repo_info(
|
repo, latest_release, config = await cls.fetch_github_repo_info(
|
||||||
github_release.organisation, github_release.repository
|
github_release.organisation, github_release.repository
|
||||||
@@ -546,7 +544,7 @@ class InstallableExtension(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_ext_dir(cls, ext_id: str) -> Optional[InstallableExtension]:
|
def from_ext_dir(cls, ext_id: str) -> InstallableExtension | None:
|
||||||
try:
|
try:
|
||||||
conf_path = Path(
|
conf_path = Path(
|
||||||
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
settings.lnbits_extensions_path, "extensions", ext_id, "config.json"
|
||||||
@@ -657,7 +655,7 @@ class InstallableExtension(BaseModel):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def get_extension_release(
|
async def get_extension_release(
|
||||||
cls, ext_id: str, source_repo: str, archive: str, version: str
|
cls, ext_id: str, source_repo: str, archive: str, version: str
|
||||||
) -> Optional[ExtensionRelease]:
|
) -> ExtensionRelease | None:
|
||||||
all_releases: list[ExtensionRelease] = (
|
all_releases: list[ExtensionRelease] = (
|
||||||
await InstallableExtension.get_extension_releases(ext_id)
|
await InstallableExtension.get_extension_releases(ext_id)
|
||||||
)
|
)
|
||||||
@@ -708,8 +706,8 @@ class CreateExtension(BaseModel):
|
|||||||
archive: str
|
archive: str
|
||||||
source_repo: str
|
source_repo: str
|
||||||
version: str
|
version: str
|
||||||
cost_sats: Optional[int] = 0
|
cost_sats: int | None = 0
|
||||||
payment_hash: Optional[str] = None
|
payment_hash: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class ExtensionDetailsRequest(BaseModel):
|
class ExtensionDetailsRequest(BaseModel):
|
||||||
@@ -718,7 +716,7 @@ class ExtensionDetailsRequest(BaseModel):
|
|||||||
version: str
|
version: str
|
||||||
|
|
||||||
|
|
||||||
async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
async def github_api_get(url: str, error_msg: str | None) -> Any:
|
||||||
headers = {"User-Agent": settings.user_agent}
|
headers = {"User-Agent": settings.user_agent}
|
||||||
if settings.lnbits_ext_github_token:
|
if settings.lnbits_ext_github_token:
|
||||||
headers["Authorization"] = f"Bearer {settings.lnbits_ext_github_token}"
|
headers["Authorization"] = f"Bearer {settings.lnbits_ext_github_token}"
|
||||||
@@ -730,7 +728,7 @@ async def github_api_get(url: str, error_msg: Optional[str]) -> Any:
|
|||||||
return resp.json()
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
def icon_to_github_url(source_repo: str, path: Optional[str]) -> str:
|
def icon_to_github_url(source_repo: str, path: str | None) -> str:
|
||||||
if not path:
|
if not path:
|
||||||
return ""
|
return ""
|
||||||
_, _, *rest = path.split("/")
|
_, _, *rest = path.split("/")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Literal, Optional
|
from typing import Literal
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
from pydantic import BaseModel, Field, validator
|
from pydantic import BaseModel, Field, validator
|
||||||
@@ -28,16 +28,16 @@ class PaymentState(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class PaymentExtra(BaseModel):
|
class PaymentExtra(BaseModel):
|
||||||
comment: Optional[str] = None
|
comment: str | None = None
|
||||||
success_action: Optional[str] = None
|
success_action: str | None = None
|
||||||
lnurl_response: Optional[str] = None
|
lnurl_response: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PayInvoice(BaseModel):
|
class PayInvoice(BaseModel):
|
||||||
payment_request: str
|
payment_request: str
|
||||||
description: Optional[str] = None
|
description: str | None = None
|
||||||
max_sat: Optional[int] = None
|
max_sat: int | None = None
|
||||||
extra: Optional[dict] = {}
|
extra: dict | None = {}
|
||||||
|
|
||||||
|
|
||||||
class CreatePayment(BaseModel):
|
class CreatePayment(BaseModel):
|
||||||
@@ -46,10 +46,10 @@ class CreatePayment(BaseModel):
|
|||||||
bolt11: str
|
bolt11: str
|
||||||
amount_msat: int
|
amount_msat: int
|
||||||
memo: str
|
memo: str
|
||||||
extra: Optional[dict] = {}
|
extra: dict | None = {}
|
||||||
preimage: Optional[str] = None
|
preimage: str | None = None
|
||||||
expiry: Optional[datetime] = None
|
expiry: datetime | None = None
|
||||||
webhook: Optional[str] = None
|
webhook: str | None = None
|
||||||
fee: int = 0
|
fee: int = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -61,13 +61,13 @@ class Payment(BaseModel):
|
|||||||
fee: int
|
fee: int
|
||||||
bolt11: str
|
bolt11: str
|
||||||
status: str = PaymentState.PENDING
|
status: str = PaymentState.PENDING
|
||||||
memo: Optional[str] = None
|
memo: str | None = None
|
||||||
expiry: Optional[datetime] = None
|
expiry: datetime | None = None
|
||||||
webhook: Optional[str] = None
|
webhook: str | None = None
|
||||||
webhook_status: Optional[int] = None
|
webhook_status: int | None = None
|
||||||
preimage: Optional[str] = None
|
preimage: str | None = None
|
||||||
tag: Optional[str] = None
|
tag: str | None = None
|
||||||
extension: Optional[str] = None
|
extension: str | None = None
|
||||||
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
@@ -129,16 +129,16 @@ class PaymentFilters(FilterModel):
|
|||||||
|
|
||||||
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
__sort_fields__ = ["created_at", "amount", "fee", "memo", "time", "tag"]
|
||||||
|
|
||||||
status: Optional[str]
|
status: str | None
|
||||||
tag: Optional[str]
|
tag: str | None
|
||||||
checking_id: Optional[str]
|
checking_id: str | None
|
||||||
amount: int
|
amount: int
|
||||||
fee: int
|
fee: int
|
||||||
memo: Optional[str]
|
memo: str | None
|
||||||
time: datetime
|
time: datetime
|
||||||
preimage: Optional[str]
|
preimage: str | None
|
||||||
payment_hash: Optional[str]
|
payment_hash: str | None
|
||||||
wallet_id: Optional[str]
|
wallet_id: str | None
|
||||||
|
|
||||||
|
|
||||||
class PaymentDataPoint(BaseModel):
|
class PaymentDataPoint(BaseModel):
|
||||||
@@ -173,11 +173,11 @@ class PaymentWalletStats(BaseModel):
|
|||||||
class PaymentDailyStats(BaseModel):
|
class PaymentDailyStats(BaseModel):
|
||||||
date: datetime
|
date: datetime
|
||||||
balance: float = 0
|
balance: float = 0
|
||||||
balance_in: Optional[float] = 0
|
balance_in: float | None = 0
|
||||||
balance_out: Optional[float] = 0
|
balance_out: float | None = 0
|
||||||
payments_count: int = 0
|
payments_count: int = 0
|
||||||
count_in: Optional[int] = 0
|
count_in: int | None = 0
|
||||||
count_out: Optional[int] = 0
|
count_out: int | None = 0
|
||||||
fee: float = 0
|
fee: float = 0
|
||||||
|
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ class PaymentHistoryPoint(BaseModel):
|
|||||||
|
|
||||||
class DecodePayment(BaseModel):
|
class DecodePayment(BaseModel):
|
||||||
data: str
|
data: str
|
||||||
filter_fields: Optional[list[str]] = []
|
filter_fields: list[str] | None = []
|
||||||
|
|
||||||
|
|
||||||
class CreateInvoice(BaseModel):
|
class CreateInvoice(BaseModel):
|
||||||
@@ -198,14 +198,14 @@ class CreateInvoice(BaseModel):
|
|||||||
internal: bool = False
|
internal: bool = False
|
||||||
out: bool = True
|
out: bool = True
|
||||||
amount: float = Query(None, ge=0)
|
amount: float = Query(None, ge=0)
|
||||||
memo: Optional[str] = None
|
memo: str | None = None
|
||||||
description_hash: Optional[str] = None
|
description_hash: str | None = None
|
||||||
unhashed_description: Optional[str] = None
|
unhashed_description: str | None = None
|
||||||
expiry: Optional[int] = None
|
expiry: int | None = None
|
||||||
extra: Optional[dict] = None
|
extra: dict | None = None
|
||||||
webhook: Optional[str] = None
|
webhook: str | None = None
|
||||||
bolt11: Optional[str] = None
|
bolt11: str | None = None
|
||||||
lnurl_callback: Optional[str] = None
|
lnurl_callback: str | None = None
|
||||||
|
|
||||||
@validator("unit")
|
@validator("unit")
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
+43
-44
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import Query
|
from fastapi import Query
|
||||||
@@ -17,16 +16,16 @@ from .wallets import Wallet
|
|||||||
|
|
||||||
|
|
||||||
class UserExtra(BaseModel):
|
class UserExtra(BaseModel):
|
||||||
email_verified: Optional[bool] = False
|
email_verified: bool | None = False
|
||||||
first_name: Optional[str] = None
|
first_name: str | None = None
|
||||||
last_name: Optional[str] = None
|
last_name: str | None = None
|
||||||
display_name: Optional[str] = None
|
display_name: str | None = None
|
||||||
picture: Optional[str] = None
|
picture: str | None = None
|
||||||
# Auth provider, possible values:
|
# Auth provider, possible values:
|
||||||
# - "env": the user was created automatically by the system
|
# - "env": the user was created automatically by the system
|
||||||
# - "lnbits": the user was created via register form (username/pass or user_id only)
|
# - "lnbits": the user was created via register form (username/pass or user_id only)
|
||||||
# - "google | github | ...": the user was created using an SSO provider
|
# - "google | github | ...": the user was created using an SSO provider
|
||||||
provider: Optional[str] = "lnbits" # auth provider
|
provider: str | None = "lnbits" # auth provider
|
||||||
|
|
||||||
|
|
||||||
class EndpointAccess(BaseModel):
|
class EndpointAccess(BaseModel):
|
||||||
@@ -50,13 +49,13 @@ class AccessControlList(BaseModel):
|
|||||||
endpoints: list[EndpointAccess] = []
|
endpoints: list[EndpointAccess] = []
|
||||||
token_id_list: list[SimpleItem] = []
|
token_id_list: list[SimpleItem] = []
|
||||||
|
|
||||||
def get_endpoint(self, path: str) -> Optional[EndpointAccess]:
|
def get_endpoint(self, path: str) -> EndpointAccess | None:
|
||||||
for e in self.endpoints:
|
for e in self.endpoints:
|
||||||
if e.path == path:
|
if e.path == path:
|
||||||
return e
|
return e
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_token_by_id(self, token_id: str) -> Optional[SimpleItem]:
|
def get_token_by_id(self, token_id: str) -> SimpleItem | None:
|
||||||
for t in self.token_id_list:
|
for t in self.token_id_list:
|
||||||
if t.id == token_id:
|
if t.id == token_id:
|
||||||
return t
|
return t
|
||||||
@@ -71,7 +70,7 @@ class UserAcls(BaseModel):
|
|||||||
access_control_list: list[AccessControlList] = []
|
access_control_list: list[AccessControlList] = []
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
|
|
||||||
def get_acl_by_id(self, acl_id: str) -> Optional[AccessControlList]:
|
def get_acl_by_id(self, acl_id: str) -> AccessControlList | None:
|
||||||
for acl in self.access_control_list:
|
for acl in self.access_control_list:
|
||||||
if acl.id == acl_id:
|
if acl.id == acl_id:
|
||||||
return acl
|
return acl
|
||||||
@@ -82,7 +81,7 @@ class UserAcls(BaseModel):
|
|||||||
acl for acl in self.access_control_list if acl.id != acl_id
|
acl for acl in self.access_control_list if acl.id != acl_id
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_acl_by_token_id(self, token_id: str) -> Optional[AccessControlList]:
|
def get_acl_by_token_id(self, token_id: str) -> AccessControlList | None:
|
||||||
for acl in self.access_control_list:
|
for acl in self.access_control_list:
|
||||||
if acl.get_token_by_id(token_id):
|
if acl.get_token_by_id(token_id):
|
||||||
return acl
|
return acl
|
||||||
@@ -91,10 +90,10 @@ class UserAcls(BaseModel):
|
|||||||
|
|
||||||
class Account(BaseModel):
|
class Account(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
username: Optional[str] = None
|
username: str | None = None
|
||||||
password_hash: Optional[str] = None
|
password_hash: str | None = None
|
||||||
pubkey: Optional[str] = None
|
pubkey: str | None = None
|
||||||
email: Optional[str] = None
|
email: str | None = None
|
||||||
extra: UserExtra = UserExtra()
|
extra: UserExtra = UserExtra()
|
||||||
|
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
@@ -134,10 +133,10 @@ class Account(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class AccountOverview(Account):
|
class AccountOverview(Account):
|
||||||
transaction_count: Optional[int] = 0
|
transaction_count: int | None = 0
|
||||||
wallet_count: Optional[int] = 0
|
wallet_count: int | None = 0
|
||||||
balance_msat: Optional[int] = 0
|
balance_msat: int | None = 0
|
||||||
last_payment: Optional[datetime] = None
|
last_payment: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class AccountFilters(FilterModel):
|
class AccountFilters(FilterModel):
|
||||||
@@ -151,20 +150,20 @@ class AccountFilters(FilterModel):
|
|||||||
"last_payment",
|
"last_payment",
|
||||||
]
|
]
|
||||||
|
|
||||||
email: Optional[str] = None
|
email: str | None = None
|
||||||
user: Optional[str] = None
|
user: str | None = None
|
||||||
username: Optional[str] = None
|
username: str | None = None
|
||||||
pubkey: Optional[str] = None
|
pubkey: str | None = None
|
||||||
wallet_id: Optional[str] = None
|
wallet_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
email: Optional[str] = None
|
email: str | None = None
|
||||||
username: Optional[str] = None
|
username: str | None = None
|
||||||
pubkey: Optional[str] = None
|
pubkey: str | None = None
|
||||||
extensions: list[str] = []
|
extensions: list[str] = []
|
||||||
wallets: list[Wallet] = []
|
wallets: list[Wallet] = []
|
||||||
admin: bool = False
|
admin: bool = False
|
||||||
@@ -176,7 +175,7 @@ class User(BaseModel):
|
|||||||
def wallet_ids(self) -> list[str]:
|
def wallet_ids(self) -> list[str]:
|
||||||
return [wallet.id for wallet in self.wallets]
|
return [wallet.id for wallet in self.wallets]
|
||||||
|
|
||||||
def get_wallet(self, wallet_id: str) -> Optional[Wallet]:
|
def get_wallet(self, wallet_id: str) -> Wallet | None:
|
||||||
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
w = [wallet for wallet in self.wallets if wallet.id == wallet_id]
|
||||||
return w[0] if w else None
|
return w[0] if w else None
|
||||||
|
|
||||||
@@ -192,33 +191,33 @@ class User(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class RegisterUser(BaseModel):
|
class RegisterUser(BaseModel):
|
||||||
email: Optional[str] = Query(default=None)
|
email: str | None = Query(default=None)
|
||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
|
||||||
|
|
||||||
class CreateUser(BaseModel):
|
class CreateUser(BaseModel):
|
||||||
id: Optional[str] = Query(default=None)
|
id: str | None = Query(default=None)
|
||||||
email: Optional[str] = Query(default=None)
|
email: str | None = Query(default=None)
|
||||||
username: Optional[str] = Query(default=None, min_length=2, max_length=20)
|
username: str | None = Query(default=None, min_length=2, max_length=20)
|
||||||
password: Optional[str] = Query(default=None, min_length=8, max_length=50)
|
password: str | None = Query(default=None, min_length=8, max_length=50)
|
||||||
password_repeat: Optional[str] = Query(default=None, min_length=8, max_length=50)
|
password_repeat: str | None = Query(default=None, min_length=8, max_length=50)
|
||||||
pubkey: str = Query(default=None, max_length=64)
|
pubkey: str = Query(default=None, max_length=64)
|
||||||
extensions: Optional[list[str]] = None
|
extensions: list[str] | None = None
|
||||||
extra: Optional[UserExtra] = None
|
extra: UserExtra | None = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateUser(BaseModel):
|
class UpdateUser(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
email: Optional[str] = Query(default=None)
|
email: str | None = Query(default=None)
|
||||||
username: Optional[str] = Query(default=..., min_length=2, max_length=20)
|
username: str | None = Query(default=..., min_length=2, max_length=20)
|
||||||
extra: Optional[UserExtra] = None
|
extra: UserExtra | None = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserPassword(BaseModel):
|
class UpdateUserPassword(BaseModel):
|
||||||
user_id: str
|
user_id: str
|
||||||
password_old: Optional[str] = None
|
password_old: str | None = None
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
@@ -252,10 +251,10 @@ class LoginUsernamePassword(BaseModel):
|
|||||||
|
|
||||||
class AccessTokenPayload(BaseModel):
|
class AccessTokenPayload(BaseModel):
|
||||||
sub: str
|
sub: str
|
||||||
usr: Optional[str] = None
|
usr: str | None = None
|
||||||
email: Optional[str] = None
|
email: str | None = None
|
||||||
auth_time: Optional[int] = 0
|
auth_time: int | None = 0
|
||||||
api_token_id: Optional[str] = None
|
api_token_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class UpdateBalance(BaseModel):
|
class UpdateBalance(BaseModel):
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import hmac
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from ecdsa import SECP256k1, SigningKey
|
from ecdsa import SECP256k1, SigningKey
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -37,7 +36,7 @@ class Wallet(BaseModel):
|
|||||||
deleted: bool = False
|
deleted: bool = False
|
||||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||||
currency: Optional[str] = None
|
currency: str | None = None
|
||||||
balance_msat: int = Field(default=0, no_database=True)
|
balance_msat: int = Field(default=0, no_database=True)
|
||||||
extra: WalletExtra = WalletExtra()
|
extra: WalletExtra = WalletExtra()
|
||||||
|
|
||||||
@@ -67,7 +66,7 @@ class Wallet(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class CreateWallet(BaseModel):
|
class CreateWallet(BaseModel):
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class KeyType(Enum):
|
class KeyType(Enum):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from .funding_source import (
|
|||||||
switch_to_voidwallet,
|
switch_to_voidwallet,
|
||||||
)
|
)
|
||||||
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
||||||
from .notifications import enqueue_notification
|
from .notifications import enqueue_notification, send_payment_notification
|
||||||
from .payments import (
|
from .payments import (
|
||||||
calculate_fiat_amounts,
|
calculate_fiat_amounts,
|
||||||
check_transaction_status,
|
check_transaction_status,
|
||||||
@@ -12,7 +12,6 @@ from .payments import (
|
|||||||
fee_reserve,
|
fee_reserve,
|
||||||
fee_reserve_total,
|
fee_reserve_total,
|
||||||
pay_invoice,
|
pay_invoice,
|
||||||
send_payment_notification,
|
|
||||||
service_fee,
|
service_fee,
|
||||||
update_pending_payments,
|
update_pending_payments,
|
||||||
update_wallet_balance,
|
update_wallet_balance,
|
||||||
@@ -25,7 +24,6 @@ from .users import (
|
|||||||
check_admin_settings,
|
check_admin_settings,
|
||||||
create_user_account,
|
create_user_account,
|
||||||
create_user_account_no_ckeck,
|
create_user_account_no_ckeck,
|
||||||
init_admin_settings,
|
|
||||||
update_user_account,
|
update_user_account,
|
||||||
update_user_extensions,
|
update_user_extensions,
|
||||||
)
|
)
|
||||||
@@ -40,6 +38,7 @@ __all__ = [
|
|||||||
"perform_lnurlauth",
|
"perform_lnurlauth",
|
||||||
# notifications
|
# notifications
|
||||||
"enqueue_notification",
|
"enqueue_notification",
|
||||||
|
"send_payment_notification",
|
||||||
# payments
|
# payments
|
||||||
"calculate_fiat_amounts",
|
"calculate_fiat_amounts",
|
||||||
"check_transaction_status",
|
"check_transaction_status",
|
||||||
@@ -48,7 +47,6 @@ __all__ = [
|
|||||||
"fee_reserve",
|
"fee_reserve",
|
||||||
"fee_reserve_total",
|
"fee_reserve_total",
|
||||||
"pay_invoice",
|
"pay_invoice",
|
||||||
"send_payment_notification",
|
|
||||||
"service_fee",
|
"service_fee",
|
||||||
"update_pending_payments",
|
"update_pending_payments",
|
||||||
"update_wallet_balance",
|
"update_wallet_balance",
|
||||||
@@ -59,7 +57,6 @@ __all__ = [
|
|||||||
"check_admin_settings",
|
"check_admin_settings",
|
||||||
"create_user_account",
|
"create_user_account",
|
||||||
"create_user_account_no_ckeck",
|
"create_user_account_no_ckeck",
|
||||||
"init_admin_settings",
|
|
||||||
"update_user_account",
|
"update_user_account",
|
||||||
"update_user_extensions",
|
"update_user_extensions",
|
||||||
# websockets
|
# websockets
|
||||||
|
|||||||
@@ -1,15 +1,30 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
|
import smtplib
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from http import HTTPStatus
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
from py_vapid import Vapid
|
||||||
|
from pywebpush import WebPushException, webpush
|
||||||
|
|
||||||
|
from lnbits.core.crud import (
|
||||||
|
delete_webpush_subscriptions,
|
||||||
|
get_webpush_subscriptions_for_user,
|
||||||
|
mark_webhook_sent,
|
||||||
|
)
|
||||||
|
from lnbits.core.models import Payment, Wallet
|
||||||
from lnbits.core.models.notifications import (
|
from lnbits.core.models.notifications import (
|
||||||
NOTIFICATION_TEMPLATES,
|
NOTIFICATION_TEMPLATES,
|
||||||
NotificationMessage,
|
NotificationMessage,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
)
|
)
|
||||||
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
||||||
|
from lnbits.core.services.websockets import websocket_manager
|
||||||
|
from lnbits.helpers import check_callback_url, is_valid_email_address
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.utils.nostr import normalize_private_key
|
from lnbits.utils.nostr import normalize_private_key
|
||||||
|
|
||||||
@@ -50,6 +65,12 @@ async def send_notification(
|
|||||||
logger.debug(f"Sent nostr notification: {message_type}")
|
logger.debug(f"Sent nostr notification: {message_type}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending nostr notification {message_type}: {e}")
|
logger.error(f"Error sending nostr notification {message_type}: {e}")
|
||||||
|
try:
|
||||||
|
if settings.lnbits_email_notifications_enabled:
|
||||||
|
await send_email_notification(message)
|
||||||
|
logger.debug(f"Sent email notification: {message_type}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending email notification {message_type}: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def send_nostr_notification(message: str) -> dict:
|
async def send_nostr_notification(message: str) -> dict:
|
||||||
@@ -90,6 +111,58 @@ async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def send_email_notification(
|
||||||
|
message: str, subject: str = "LNbits Notification"
|
||||||
|
) -> dict:
|
||||||
|
if not settings.lnbits_email_notifications_enabled:
|
||||||
|
return {"status": "error", "message": "Email notifications are disabled"}
|
||||||
|
try:
|
||||||
|
await send_email(
|
||||||
|
settings.lnbits_email_notifications_server,
|
||||||
|
settings.lnbits_email_notifications_port,
|
||||||
|
settings.lnbits_email_notifications_username,
|
||||||
|
settings.lnbits_email_notifications_password,
|
||||||
|
settings.lnbits_email_notifications_email,
|
||||||
|
settings.lnbits_email_notifications_to_emails,
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
return {"status": "ok"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending email notification: {e}")
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
async def send_email(
|
||||||
|
server: str,
|
||||||
|
port: int,
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
from_email: str,
|
||||||
|
to_emails: list[str],
|
||||||
|
subject: str,
|
||||||
|
message: str,
|
||||||
|
) -> bool:
|
||||||
|
if not is_valid_email_address(from_email):
|
||||||
|
raise ValueError(f"Invalid from email address: {from_email}")
|
||||||
|
if len(to_emails) == 0:
|
||||||
|
raise ValueError("No email addresses provided")
|
||||||
|
for email in to_emails:
|
||||||
|
if not is_valid_email_address(email):
|
||||||
|
raise ValueError(f"Invalid email address: {email}")
|
||||||
|
msg = MIMEMultipart()
|
||||||
|
msg["From"] = from_email
|
||||||
|
msg["To"] = ", ".join(to_emails)
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg.attach(MIMEText(message, "plain"))
|
||||||
|
username = username if len(username) > 0 else from_email
|
||||||
|
with smtplib.SMTP(server, port) as smtp_server:
|
||||||
|
smtp_server.starttls()
|
||||||
|
smtp_server.login(username, password)
|
||||||
|
smtp_server.sendmail(from_email, to_emails, msg.as_string())
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
||||||
if message_type == NotificationType.balance_update:
|
if message_type == NotificationType.balance_update:
|
||||||
return settings.lnbits_notification_credit_debit
|
return settings.lnbits_notification_credit_debit
|
||||||
@@ -123,3 +196,135 @@ def _notification_message_to_text(
|
|||||||
text = meesage_value
|
text = meesage_value
|
||||||
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
||||||
return message_type, text
|
return message_type, text
|
||||||
|
|
||||||
|
|
||||||
|
async def dispatch_webhook(payment: Payment):
|
||||||
|
"""
|
||||||
|
Dispatches the webhook to the webhook url.
|
||||||
|
"""
|
||||||
|
logger.debug("sending webhook", payment.webhook)
|
||||||
|
|
||||||
|
if not payment.webhook:
|
||||||
|
return await mark_webhook_sent(payment.payment_hash, -1)
|
||||||
|
|
||||||
|
headers = {"User-Agent": settings.user_agent}
|
||||||
|
async with httpx.AsyncClient(headers=headers) as client:
|
||||||
|
data = payment.dict()
|
||||||
|
try:
|
||||||
|
check_callback_url(payment.webhook)
|
||||||
|
r = await client.post(payment.webhook, json=data, timeout=40)
|
||||||
|
r.raise_for_status()
|
||||||
|
await mark_webhook_sent(payment.payment_hash, r.status_code)
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
await mark_webhook_sent(payment.payment_hash, exc.response.status_code)
|
||||||
|
logger.warning(
|
||||||
|
f"webhook returned a bad status_code: {exc.response.status_code} "
|
||||||
|
f"while requesting {exc.request.url!r}."
|
||||||
|
)
|
||||||
|
except httpx.RequestError:
|
||||||
|
await mark_webhook_sent(payment.payment_hash, -1)
|
||||||
|
logger.warning(f"Could not send webhook to {payment.webhook}")
|
||||||
|
|
||||||
|
|
||||||
|
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
|
try:
|
||||||
|
await send_ws_payment_notification(wallet, payment)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error sending websocket payment notification", e)
|
||||||
|
try:
|
||||||
|
send_chat_payment_notification(wallet, payment)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error sending chat payment notification", e)
|
||||||
|
try:
|
||||||
|
await send_payment_push_notification(wallet, payment)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Error sending push payment notification", e)
|
||||||
|
|
||||||
|
if payment.webhook and not payment.webhook_status:
|
||||||
|
await dispatch_webhook(payment)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
|
# TODO: websocket message should be a clean payment model
|
||||||
|
# await websocket_manager.send_data(payment.json(), wallet.inkey)
|
||||||
|
# TODO: figure out why we send the balance with the payment here.
|
||||||
|
# cleaner would be to have a separate message for the balance
|
||||||
|
# and send it with the id of the wallet so wallets can subscribe to it
|
||||||
|
payment_notification = json.dumps(
|
||||||
|
{
|
||||||
|
"wallet_balance": wallet.balance,
|
||||||
|
# use pydantic json serialization to get the correct datetime format
|
||||||
|
"payment": json.loads(payment.json()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await websocket_manager.send_data(payment_notification, wallet.inkey)
|
||||||
|
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
||||||
|
|
||||||
|
await websocket_manager.send_data(
|
||||||
|
json.dumps({"pending": payment.pending, "status": payment.status}),
|
||||||
|
payment.payment_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
||||||
|
amount_sats = abs(payment.sat)
|
||||||
|
values: dict = {
|
||||||
|
"wallet_id": wallet.id,
|
||||||
|
"wallet_name": wallet.name,
|
||||||
|
"amount_sats": amount_sats,
|
||||||
|
"fiat_value_fmt": "",
|
||||||
|
}
|
||||||
|
if payment.extra.get("wallet_fiat_currency", None):
|
||||||
|
amount_fiat = payment.extra.get("wallet_fiat_amount", None)
|
||||||
|
currency = payment.extra.get("wallet_fiat_currency", None)
|
||||||
|
values["fiat_value_fmt"] = f"`{amount_fiat}`*{currency}* / "
|
||||||
|
|
||||||
|
if payment.is_out:
|
||||||
|
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
|
||||||
|
enqueue_notification(NotificationType.outgoing_payment, values)
|
||||||
|
else:
|
||||||
|
if amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
|
||||||
|
enqueue_notification(NotificationType.incoming_payment, values)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_payment_push_notification(wallet: Wallet, payment: Payment):
|
||||||
|
subscriptions = await get_webpush_subscriptions_for_user(wallet.user)
|
||||||
|
|
||||||
|
amount = int(payment.amount / 1000)
|
||||||
|
|
||||||
|
title = f"LNbits: {wallet.name}"
|
||||||
|
body = f"You just received {amount} sat{'s'[:amount^1]}!"
|
||||||
|
|
||||||
|
if payment.memo:
|
||||||
|
body += f"\r\n{payment.memo}"
|
||||||
|
|
||||||
|
for subscription in subscriptions:
|
||||||
|
# todo: review permissions when user-id-only not allowed
|
||||||
|
# todo: replace all this logic with websockets?
|
||||||
|
url = f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}"
|
||||||
|
await send_push_notification(subscription, title, body, url)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_push_notification(subscription, title, body, url=""):
|
||||||
|
vapid = Vapid()
|
||||||
|
try:
|
||||||
|
logger.debug("sending push notification")
|
||||||
|
webpush(
|
||||||
|
json.loads(subscription.data),
|
||||||
|
json.dumps({"title": title, "body": body, "url": url}),
|
||||||
|
(
|
||||||
|
vapid.from_pem(bytes(settings.lnbits_webpush_privkey, "utf-8"))
|
||||||
|
if settings.lnbits_webpush_privkey
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
{"aud": "", "sub": "mailto:alan@lnbits.com"},
|
||||||
|
)
|
||||||
|
except WebPushException as e:
|
||||||
|
if e.response and e.response.status_code == HTTPStatus.GONE:
|
||||||
|
# cleanup unsubscribed or expired push subscriptions
|
||||||
|
await delete_webpush_subscriptions(subscription.endpoint)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"failed sending push notification: "
|
||||||
|
f"{e.response.text if e.response else e}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -12,8 +11,6 @@ from loguru import logger
|
|||||||
from lnbits.core.crud.payments import get_daily_stats
|
from lnbits.core.crud.payments import get_daily_stats
|
||||||
from lnbits.core.db import db
|
from lnbits.core.db import db
|
||||||
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
from lnbits.core.models import PaymentDailyStats, PaymentFilters
|
||||||
from lnbits.core.models.notifications import NotificationType
|
|
||||||
from lnbits.core.services.notifications import enqueue_notification
|
|
||||||
from lnbits.db import Connection, Filters
|
from lnbits.db import Connection, Filters
|
||||||
from lnbits.decorators import check_user_extension_access
|
from lnbits.decorators import check_user_extension_access
|
||||||
from lnbits.exceptions import InvoiceError, PaymentError
|
from lnbits.exceptions import InvoiceError, PaymentError
|
||||||
@@ -45,7 +42,7 @@ from ..models import (
|
|||||||
PaymentState,
|
PaymentState,
|
||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from .websockets import websocket_manager
|
from .notifications import send_payment_notification
|
||||||
|
|
||||||
|
|
||||||
async def pay_invoice(
|
async def pay_invoice(
|
||||||
@@ -181,13 +178,20 @@ async def update_pending_payments(wallet_id: str):
|
|||||||
exclude_uncheckable=True,
|
exclude_uncheckable=True,
|
||||||
)
|
)
|
||||||
for payment in pending_payments:
|
for payment in pending_payments:
|
||||||
status = await payment.check_status()
|
await update_pending_payment(payment)
|
||||||
if status.failed:
|
|
||||||
payment.status = PaymentState.FAILED
|
|
||||||
await update_payment(payment)
|
async def update_pending_payment(payment: Payment) -> bool:
|
||||||
elif status.success:
|
status = await payment.check_status()
|
||||||
payment.status = PaymentState.SUCCESS
|
if status.failed:
|
||||||
await update_payment(payment)
|
payment.status = PaymentState.FAILED
|
||||||
|
await update_payment(payment)
|
||||||
|
return True
|
||||||
|
if status.success:
|
||||||
|
payment.status = PaymentState.SUCCESS
|
||||||
|
await update_payment(payment)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
def fee_reserve_total(amount_msat: int, internal: bool = False) -> int:
|
||||||
@@ -278,60 +282,6 @@ async def update_wallet_balance(
|
|||||||
await internal_invoice_queue.put(payment.checking_id)
|
await internal_invoice_queue.put(payment.checking_id)
|
||||||
|
|
||||||
|
|
||||||
async def send_payment_notification(wallet: Wallet, payment: Payment):
|
|
||||||
try:
|
|
||||||
await send_ws_payment_notification(wallet, payment)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error sending websocket payment notification", e)
|
|
||||||
|
|
||||||
try:
|
|
||||||
send_chat_payment_notification(wallet, payment)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error sending chat payment notification", e)
|
|
||||||
|
|
||||||
|
|
||||||
async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
|
|
||||||
# TODO: websocket message should be a clean payment model
|
|
||||||
# await websocket_manager.send_data(payment.json(), wallet.inkey)
|
|
||||||
# TODO: figure out why we send the balance with the payment here.
|
|
||||||
# cleaner would be to have a separate message for the balance
|
|
||||||
# and send it with the id of the wallet so wallets can subscribe to it
|
|
||||||
payment_notification = json.dumps(
|
|
||||||
{
|
|
||||||
"wallet_balance": wallet.balance,
|
|
||||||
# use pydantic json serialization to get the correct datetime format
|
|
||||||
"payment": json.loads(payment.json()),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await websocket_manager.send_data(payment_notification, wallet.inkey)
|
|
||||||
await websocket_manager.send_data(payment_notification, wallet.adminkey)
|
|
||||||
|
|
||||||
await websocket_manager.send_data(
|
|
||||||
json.dumps({"pending": payment.pending}), payment.payment_hash
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_chat_payment_notification(wallet: Wallet, payment: Payment):
|
|
||||||
amount_sats = abs(payment.sat)
|
|
||||||
values: dict = {
|
|
||||||
"wallet_id": wallet.id,
|
|
||||||
"wallet_name": wallet.name,
|
|
||||||
"amount_sats": amount_sats,
|
|
||||||
"fiat_value_fmt": "",
|
|
||||||
}
|
|
||||||
if payment.extra.get("wallet_fiat_currency", None):
|
|
||||||
amount_fiat = payment.extra.get("wallet_fiat_amount", None)
|
|
||||||
currency = payment.extra.get("wallet_fiat_currency", None)
|
|
||||||
values["fiat_value_fmt"] = f"`{amount_fiat}`*{currency}* / "
|
|
||||||
|
|
||||||
if payment.is_out:
|
|
||||||
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
|
|
||||||
enqueue_notification(NotificationType.outgoing_payment, values)
|
|
||||||
else:
|
|
||||||
if amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
|
|
||||||
enqueue_notification(NotificationType.incoming_payment, values)
|
|
||||||
|
|
||||||
|
|
||||||
async def check_wallet_limits(
|
async def check_wallet_limits(
|
||||||
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
|
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from lnbits.core.models.extensions import UserExtension
|
|||||||
from lnbits.settings import (
|
from lnbits.settings import (
|
||||||
EditableSettings,
|
EditableSettings,
|
||||||
SuperSettings,
|
SuperSettings,
|
||||||
send_admin_user_to_saas,
|
|
||||||
settings,
|
settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,14 +153,6 @@ async def check_admin_settings():
|
|||||||
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
with open(Path(settings.lnbits_data_folder) / ".super_user", "w") as file:
|
||||||
file.write(settings.super_user)
|
file.write(settings.super_user)
|
||||||
|
|
||||||
# callback for saas
|
|
||||||
if (
|
|
||||||
settings.lnbits_saas_callback
|
|
||||||
and settings.lnbits_saas_secret
|
|
||||||
and settings.lnbits_saas_instance_id
|
|
||||||
):
|
|
||||||
send_admin_user_to_saas()
|
|
||||||
|
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if account and account.extra and account.extra.provider == "env":
|
if account and account.extra and account.extra.provider == "env":
|
||||||
settings.first_install = True
|
settings.first_install = True
|
||||||
|
|||||||
+3
-65
@@ -2,25 +2,19 @@ import asyncio
|
|||||||
import traceback
|
import traceback
|
||||||
from typing import Callable, Coroutine
|
from typing import Callable, Coroutine
|
||||||
|
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
create_audit_entry,
|
create_audit_entry,
|
||||||
get_wallet,
|
get_wallet,
|
||||||
get_webpush_subscriptions_for_user,
|
|
||||||
mark_webhook_sent,
|
|
||||||
)
|
)
|
||||||
from lnbits.core.crud.audit import delete_expired_audit_entries
|
from lnbits.core.crud.audit import delete_expired_audit_entries
|
||||||
from lnbits.core.crud.payments import get_payments_status_count
|
from lnbits.core.crud.payments import get_payments_status_count
|
||||||
from lnbits.core.crud.users import get_accounts
|
from lnbits.core.crud.users import get_accounts
|
||||||
from lnbits.core.crud.wallets import get_wallets_count
|
from lnbits.core.crud.wallets import get_wallets_count
|
||||||
from lnbits.core.models import AuditEntry, Payment
|
from lnbits.core.models import AuditEntry
|
||||||
from lnbits.core.models.extensions import InstallableExtension
|
from lnbits.core.models.extensions import InstallableExtension
|
||||||
from lnbits.core.models.notifications import NotificationType
|
from lnbits.core.models.notifications import NotificationType
|
||||||
from lnbits.core.services import (
|
|
||||||
send_payment_notification,
|
|
||||||
)
|
|
||||||
from lnbits.core.services.funding_source import (
|
from lnbits.core.services.funding_source import (
|
||||||
check_balance_delta_changed,
|
check_balance_delta_changed,
|
||||||
check_server_balance_against_node,
|
check_server_balance_against_node,
|
||||||
@@ -29,11 +23,11 @@ from lnbits.core.services.funding_source import (
|
|||||||
from lnbits.core.services.notifications import (
|
from lnbits.core.services.notifications import (
|
||||||
enqueue_notification,
|
enqueue_notification,
|
||||||
process_next_notification,
|
process_next_notification,
|
||||||
|
send_payment_notification,
|
||||||
)
|
)
|
||||||
from lnbits.db import Filters
|
from lnbits.db import Filters
|
||||||
from lnbits.helpers import check_callback_url
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
from lnbits.tasks import create_unique_task, send_push_notification
|
from lnbits.tasks import create_unique_task
|
||||||
from lnbits.utils.exchange_rates import btc_rates
|
from lnbits.utils.exchange_rates import btc_rates
|
||||||
|
|
||||||
audit_queue: asyncio.Queue = asyncio.Queue()
|
audit_queue: asyncio.Queue = asyncio.Queue()
|
||||||
@@ -106,62 +100,6 @@ async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
|||||||
wallet = await get_wallet(payment.wallet_id)
|
wallet = await get_wallet(payment.wallet_id)
|
||||||
if wallet:
|
if wallet:
|
||||||
await send_payment_notification(wallet, payment)
|
await send_payment_notification(wallet, payment)
|
||||||
# dispatch webhook
|
|
||||||
if payment.webhook and not payment.webhook_status:
|
|
||||||
await dispatch_webhook(payment)
|
|
||||||
# dispatch push notification
|
|
||||||
await send_payment_push_notification(payment)
|
|
||||||
|
|
||||||
|
|
||||||
async def dispatch_webhook(payment: Payment):
|
|
||||||
"""
|
|
||||||
Dispatches the webhook to the webhook url.
|
|
||||||
"""
|
|
||||||
logger.debug("sending webhook", payment.webhook)
|
|
||||||
|
|
||||||
if not payment.webhook:
|
|
||||||
return await mark_webhook_sent(payment.payment_hash, -1)
|
|
||||||
|
|
||||||
headers = {"User-Agent": settings.user_agent}
|
|
||||||
async with httpx.AsyncClient(headers=headers) as client:
|
|
||||||
data = payment.dict()
|
|
||||||
try:
|
|
||||||
check_callback_url(payment.webhook)
|
|
||||||
r = await client.post(payment.webhook, json=data, timeout=40)
|
|
||||||
r.raise_for_status()
|
|
||||||
await mark_webhook_sent(payment.payment_hash, r.status_code)
|
|
||||||
except httpx.HTTPStatusError as exc:
|
|
||||||
await mark_webhook_sent(payment.payment_hash, exc.response.status_code)
|
|
||||||
logger.warning(
|
|
||||||
f"webhook returned a bad status_code: {exc.response.status_code} "
|
|
||||||
f"while requesting {exc.request.url!r}."
|
|
||||||
)
|
|
||||||
except httpx.RequestError:
|
|
||||||
await mark_webhook_sent(payment.payment_hash, -1)
|
|
||||||
logger.warning(f"Could not send webhook to {payment.webhook}")
|
|
||||||
|
|
||||||
|
|
||||||
async def send_payment_push_notification(payment: Payment):
|
|
||||||
wallet = await get_wallet(payment.wallet_id)
|
|
||||||
|
|
||||||
if wallet:
|
|
||||||
subscriptions = await get_webpush_subscriptions_for_user(wallet.user)
|
|
||||||
|
|
||||||
amount = int(payment.amount / 1000)
|
|
||||||
|
|
||||||
title = f"LNbits: {wallet.name}"
|
|
||||||
body = f"You just received {amount} sat{'s'[:amount^1]}!"
|
|
||||||
|
|
||||||
if payment.memo:
|
|
||||||
body += f"\r\n{payment.memo}"
|
|
||||||
|
|
||||||
for subscription in subscriptions:
|
|
||||||
# todo: review permissions when user-id-only not allowed
|
|
||||||
# todo: replace all this logic with websockets?
|
|
||||||
url = (
|
|
||||||
f"https://{subscription.host}/wallet?usr={wallet.user}&wal={wallet.id}"
|
|
||||||
)
|
|
||||||
await send_push_notification(subscription, title, body, url)
|
|
||||||
|
|
||||||
|
|
||||||
async def wait_for_audit_data():
|
async def wait_for_audit_data():
|
||||||
|
|||||||
@@ -2,7 +2,14 @@
|
|||||||
<q-card-section class="q-pa-none">
|
<q-card-section class="q-pa-none">
|
||||||
<h6 class="q-my-none q-mb-sm">
|
<h6 class="q-my-none q-mb-sm">
|
||||||
<span v-text="$t('notifications_configure')"></span>
|
<span v-text="$t('notifications_configure')"></span>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click="hideInputsToggle()"
|
||||||
|
:icon="hideInputToggle ? 'visibility_off' : 'visibility'"
|
||||||
|
></q-btn>
|
||||||
</h6>
|
</h6>
|
||||||
|
|
||||||
<q-separator class="q-mt-md q-mb-sm"></q-separator>
|
<q-separator class="q-mt-md q-mb-sm"></q-separator>
|
||||||
<div class="row q-col-gutter-md">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-sm-12 col-md-6">
|
<div class="col-sm-12 col-md-6">
|
||||||
@@ -39,12 +46,13 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-input
|
<q-input
|
||||||
type="password"
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_nostr_notifications_private_key"
|
v-model="formData.lnbits_nostr_notifications_private_key"
|
||||||
/>
|
/>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
|
||||||
<q-item tag="label" v-ripple>
|
<q-item tag="label" v-ripple>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label
|
||||||
@@ -117,7 +125,7 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-input
|
<q-input
|
||||||
type="password"
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
filled
|
filled
|
||||||
v-model="formData.lnbits_telegram_notifications_access_token"
|
v-model="formData.lnbits_telegram_notifications_access_token"
|
||||||
/>
|
/>
|
||||||
@@ -139,6 +147,174 @@
|
|||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<q-separator></q-separator>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<strong v-text="$t('notifications_email_config')"></strong>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('notifications_enable_email')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('notifications_enable_email_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-toggle
|
||||||
|
size="md"
|
||||||
|
v-model="formData.lnbits_email_notifications_enabled"
|
||||||
|
checked-icon="check"
|
||||||
|
color="green"
|
||||||
|
unchecked-icon="clear"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
|
||||||
|
<div v-if="formData.lnbits_email_notifications_enabled" class="row">
|
||||||
|
<div class="col">
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('notifications_send_email')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('notifications_send_email_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_email_notifications_email"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('notifications_send_email_username')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('notifications_send_email_username_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_email_notifications_username"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('notifications_send_email_password')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('notifications_send_email_password_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_email_notifications_password"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item>
|
||||||
|
<q-btn
|
||||||
|
@click="sendTestEmail()"
|
||||||
|
:label="$t('notifications_send_test_email')"
|
||||||
|
color="primary"
|
||||||
|
class="q-mt-md"
|
||||||
|
></q-btn>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('notifications_send_to_emails')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('notifications_send_to_emails_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model="emailNotificationAddress"
|
||||||
|
@keydown.enter="addEmailNotificationAddress"
|
||||||
|
>
|
||||||
|
<q-btn
|
||||||
|
@click="addEmailNotificationAddress()"
|
||||||
|
dense
|
||||||
|
flat
|
||||||
|
icon="add"
|
||||||
|
></q-btn>
|
||||||
|
</q-input>
|
||||||
|
<div>
|
||||||
|
<q-chip
|
||||||
|
v-for="to_email in formData.lnbits_email_notifications_to_emails"
|
||||||
|
:key="to_email"
|
||||||
|
removable
|
||||||
|
@remove="removeEmailNotificationAddress(to_email)"
|
||||||
|
color="primary"
|
||||||
|
text-color="white"
|
||||||
|
><span class="ellipsis" v-text="to_email"></span
|
||||||
|
></q-chip>
|
||||||
|
</div>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('notifications_send_email_server_port')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('notifications_send_email_server_port_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_email_notifications_port"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item tag="label" v-ripple>
|
||||||
|
<q-item-section>
|
||||||
|
<q-item-label
|
||||||
|
v-text="$t('notifications_send_email_server')"
|
||||||
|
></q-item-label>
|
||||||
|
<q-item-label
|
||||||
|
caption
|
||||||
|
v-text="$t('notifications_send_email_server_desc')"
|
||||||
|
></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>
|
||||||
|
<q-input
|
||||||
|
:type="hideInputToggle ? 'password' : 'text'"
|
||||||
|
filled
|
||||||
|
v-model="formData.lnbits_email_notifications_server"
|
||||||
|
/>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-separator> </q-separator>
|
<q-separator> </q-separator>
|
||||||
<h6 class="q-mb-sm">
|
<h6 class="q-mb-sm">
|
||||||
|
|||||||
@@ -53,16 +53,6 @@
|
|||||||
:label="$t('lnbits_wallet')"
|
:label="$t('lnbits_wallet')"
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<p><span v-text="$t('denomination')"></span></p>
|
|
||||||
<q-input
|
|
||||||
filled
|
|
||||||
type="text"
|
|
||||||
v-model="formData.lnbits_denomination"
|
|
||||||
label="sats"
|
|
||||||
:hint="$t('denomination_hint')"
|
|
||||||
></q-input>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
<p><span v-text="$t('ui_qr_code_logo')"></span></p>
|
||||||
<q-input
|
<q-input
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
<span v-text="$t('password_config')"></span>
|
<span v-text="$t('password')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
unelevated
|
unelevated
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
:label="$t('change_password')"
|
:label="$t('update_password')"
|
||||||
>
|
>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="col q-mb-sm">
|
<div class="col q-mb-sm">
|
||||||
<h4 class="q-my-none">
|
<h4 class="q-my-none">
|
||||||
<span v-text="$t('pubkey')"></span>
|
Nostr <span v-text="$t('pubkey')"></span>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<q-input
|
<q-input
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="showUpdateCredentials()"
|
@click="showUpdateCredentials()"
|
||||||
:label="$t('update_credentials')"
|
:label="$t('change_password')"
|
||||||
filled
|
filled
|
||||||
color="primary"
|
color="primary"
|
||||||
class="float-right"
|
class="float-right"
|
||||||
@@ -356,7 +356,7 @@
|
|||||||
flat
|
flat
|
||||||
@click="themeChoiceFunc('bitcoin')"
|
@click="themeChoiceFunc('bitcoin')"
|
||||||
icon="circle"
|
icon="circle"
|
||||||
color="orange"
|
color="deep-orange"
|
||||||
size="md"
|
size="md"
|
||||||
><q-tooltip>bitcoin</q-tooltip>
|
><q-tooltip>bitcoin</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
|||||||
@@ -59,6 +59,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script>
|
||||||
|
const ALLOWED_REGISTER = {{ LNBITS_NEW_ACCOUNTS_ALLOWED | tojson }};
|
||||||
|
</script>
|
||||||
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
<script src="{{ static_url_for('static', 'js/index.js') }}"></script>
|
||||||
{% endblock %} {% block page_container %}
|
{% endblock %} {% block page_container %}
|
||||||
<q-page-container>
|
<q-page-container>
|
||||||
@@ -84,18 +87,20 @@
|
|||||||
>
|
>
|
||||||
{{SITE_TITLE}}
|
{{SITE_TITLE}}
|
||||||
</h5>
|
</h5>
|
||||||
<h6
|
<template v-if="$q.screen.gt.sm">
|
||||||
class="q-my-sm"
|
<h6
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
class="q-my-sm"
|
||||||
>
|
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||||
{{SITE_TAGLINE}}
|
>
|
||||||
</h6>
|
{{SITE_TAGLINE}}
|
||||||
<p
|
</h6>
|
||||||
class="q-my-sm"
|
<p
|
||||||
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
class="q-my-sm"
|
||||||
>
|
v-if="'{{LNBITS_SHOW_HOME_PAGE_ELEMENTS}}' == 'True'"
|
||||||
{{SITE_DESCRIPTION}}
|
>
|
||||||
</p>
|
{{SITE_DESCRIPTION}}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
<!-- <div
|
<!-- <div
|
||||||
class="gt-sm"
|
class="gt-sm"
|
||||||
v-html="formatDescription"
|
v-html="formatDescription"
|
||||||
@@ -145,7 +150,7 @@
|
|||||||
{%else%}
|
{%else%}
|
||||||
<username-password
|
<username-password
|
||||||
v-if="authMethod != 'user-id-only'"
|
v-if="authMethod != 'user-id-only'"
|
||||||
:allowed_new_users="{{ LNBITS_NEW_ACCOUNTS_ALLOWED | tojson }}"
|
:allowed_new_users="allowedRegister"
|
||||||
:auth-methods="{{ LNBITS_AUTH_METHODS }}"
|
:auth-methods="{{ LNBITS_AUTH_METHODS }}"
|
||||||
:auth-action="authAction"
|
:auth-action="authAction"
|
||||||
v-model:user-name="username"
|
v-model:user-name="username"
|
||||||
@@ -160,7 +165,10 @@
|
|||||||
class="text-center text-grey-6"
|
class="text-center text-grey-6"
|
||||||
v-if="authAction !== 'reset'"
|
v-if="authAction !== 'reset'"
|
||||||
>
|
>
|
||||||
<p v-if="authAction === 'login'" class="q-mb-none">
|
<p
|
||||||
|
v-if="authAction === 'login' && allowedRegister"
|
||||||
|
class="q-mb-none"
|
||||||
|
>
|
||||||
Not registered? Create an
|
Not registered? Create an
|
||||||
<span
|
<span
|
||||||
class="text-secondary cursor-pointer"
|
class="text-secondary cursor-pointer"
|
||||||
@@ -168,7 +176,14 @@
|
|||||||
>Account</span
|
>Account</span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="q-mb-none">
|
<p
|
||||||
|
v-else-if="authAction === 'login' && !allowedRegister"
|
||||||
|
class="q-mb-none"
|
||||||
|
>
|
||||||
|
<span v-text="$t('new_user_not_allowed')"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-else-if="authAction === 'register'" class="q-mb-none">
|
||||||
Aready have an account?
|
Aready have an account?
|
||||||
<span
|
<span
|
||||||
class="text-secondary cursor-pointer"
|
class="text-secondary cursor-pointer"
|
||||||
@@ -180,7 +195,7 @@
|
|||||||
</username-password>
|
</username-password>
|
||||||
{% if "user-id-only" in LNBITS_AUTH_METHODS %}
|
{% if "user-id-only" in LNBITS_AUTH_METHODS %}
|
||||||
<user-id-only
|
<user-id-only
|
||||||
:allowed_new_users="{{ LNBITS_NEW_ACCOUNTS_ALLOWED | tojson }}"
|
:allowed_new_users="allowedRegister"
|
||||||
v-model:usr="usr"
|
v-model:usr="usr"
|
||||||
v-model:wallet="walletName"
|
v-model:wallet="walletName"
|
||||||
:auth-action="authAction"
|
:auth-action="authAction"
|
||||||
|
|||||||
@@ -726,6 +726,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="g.fiatTracking">
|
<div v-if="g.fiatTracking">
|
||||||
<div v-if="isFiatPriority">
|
<div v-if="isFiatPriority">
|
||||||
|
<h5 class="q-my-none text-bold">
|
||||||
|
<span
|
||||||
|
v-text="walletFormatBalance(parse.invoice.sat)"
|
||||||
|
></span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div v-else style="opacity: 0.75">
|
||||||
<div class="text-h5 text-italic">
|
<div class="text-h5 text-italic">
|
||||||
<span
|
<span
|
||||||
v-text="parse.invoice.fiatAmount"
|
v-text="parse.invoice.fiatAmount"
|
||||||
@@ -733,13 +740,6 @@
|
|||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="opacity: 0.75">
|
|
||||||
<h5 class="q-my-none text-bold">
|
|
||||||
<span
|
|
||||||
v-text="walletFormatBalance(parse.invoice.sat)"
|
|
||||||
></span>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<q-separator></q-separator>
|
<q-separator></q-separator>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
<q-table :rows="wallets" :columns="walletTable.columns">
|
<q-table :rows="wallets" :columns="walletTable.columns">
|
||||||
<template v-slot:header="props">
|
<template v-slot:header="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
|
<q-th auto-width v-if="g.user.super_user"></q-th>
|
||||||
<q-th auto-width></q-th>
|
<q-th auto-width></q-th>
|
||||||
<q-th
|
<q-th
|
||||||
auto-width
|
auto-width
|
||||||
@@ -47,12 +48,14 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-slot:body="props">
|
<template v-slot:body="props">
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<q-td auto-width>
|
<q-td auto-width v-if="g.user.super_user">
|
||||||
<lnbits-update-balance
|
<lnbits-update-balance
|
||||||
:wallet_id="props.row.id"
|
:wallet_id="props.row.id"
|
||||||
@credit-value="handleBalanceUpdate"
|
@credit-value="handleBalanceUpdate"
|
||||||
class="q-mr-md"
|
class="q-mr-md"
|
||||||
></lnbits-update-balance>
|
></lnbits-update-balance>
|
||||||
|
</q-td>
|
||||||
|
<q-td auto-width>
|
||||||
<q-btn
|
<q-btn
|
||||||
round
|
round
|
||||||
icon="menu"
|
icon="menu"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from lnbits.core.services import (
|
|||||||
get_balance_delta,
|
get_balance_delta,
|
||||||
update_cached_settings,
|
update_cached_settings,
|
||||||
)
|
)
|
||||||
|
from lnbits.core.services.notifications import send_email_notification
|
||||||
from lnbits.core.services.settings import dict_to_settings
|
from lnbits.core.services.settings import dict_to_settings
|
||||||
from lnbits.decorators import check_admin, check_super_user
|
from lnbits.decorators import check_admin, check_super_user
|
||||||
from lnbits.server import server_restart
|
from lnbits.server import server_restart
|
||||||
@@ -50,6 +51,18 @@ async def api_monitor():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@admin_router.get(
|
||||||
|
"/api/v1/testemail",
|
||||||
|
name="TestEmail",
|
||||||
|
description="send a test email to the admin",
|
||||||
|
dependencies=[Depends(check_admin)],
|
||||||
|
)
|
||||||
|
async def api_test_email():
|
||||||
|
return await send_email_notification(
|
||||||
|
"This is a LNbits test email.", "LNbits Test Email"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
@admin_router.get("/api/v1/settings", response_model=Optional[AdminSettings])
|
||||||
async def api_get_settings(
|
async def api_get_settings(
|
||||||
user: User = Depends(check_admin),
|
user: User = Depends(check_admin),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
|
||||||
from typing import Annotated, List, Optional, Union
|
from typing import Annotated, List, Optional, Union
|
||||||
from urllib.parse import urlencode, urlparse
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ generic_router = APIRouter(
|
|||||||
|
|
||||||
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
@generic_router.get("/favicon.ico", response_class=FileResponse)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return FileResponse(Path("lnbits", "static", "favicon.ico"))
|
return RedirectResponse(settings.lnbits_qr_logo)
|
||||||
|
|
||||||
|
|
||||||
@generic_router.get("/", response_class=HTMLResponse)
|
@generic_router.get("/", response_class=HTMLResponse)
|
||||||
|
|||||||
@@ -37,7 +37,10 @@ from lnbits.core.models import (
|
|||||||
Wallet,
|
Wallet,
|
||||||
)
|
)
|
||||||
from lnbits.core.models.users import User
|
from lnbits.core.models.users import User
|
||||||
from lnbits.core.services.payments import get_payments_daily_stats
|
from lnbits.core.services.payments import (
|
||||||
|
get_payments_daily_stats,
|
||||||
|
update_pending_payment,
|
||||||
|
)
|
||||||
from lnbits.db import Filters, Page
|
from lnbits.db import Filters, Page
|
||||||
from lnbits.decorators import (
|
from lnbits.decorators import (
|
||||||
WalletTypeInfo,
|
WalletTypeInfo,
|
||||||
@@ -180,11 +183,14 @@ async def api_payments_paginated(
|
|||||||
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
key_info: WalletTypeInfo = Depends(require_invoice_key),
|
||||||
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
filters: Filters = Depends(parse_filters(PaymentFilters)),
|
||||||
):
|
):
|
||||||
await update_pending_payments(key_info.wallet.id)
|
|
||||||
page = await get_payments_paginated(
|
page = await get_payments_paginated(
|
||||||
wallet_id=key_info.wallet.id,
|
wallet_id=key_info.wallet.id,
|
||||||
filters=filters,
|
filters=filters,
|
||||||
)
|
)
|
||||||
|
for payment in page.data:
|
||||||
|
if payment.pending:
|
||||||
|
await update_pending_payment(payment)
|
||||||
|
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+35
-35
@@ -8,7 +8,7 @@ import time
|
|||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Generic, Literal, Optional, TypeVar, Union, get_origin
|
from typing import Any, Generic, Literal, TypeVar, get_origin
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, ValidationError, root_validator
|
from pydantic import BaseModel, ValidationError, root_validator
|
||||||
@@ -65,8 +65,8 @@ def get_placeholder(model: Any, field: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
class Compat:
|
class Compat:
|
||||||
type: Optional[str] = "<inherited>"
|
type: str | None = "<inherited>"
|
||||||
schema: Optional[str] = "<inherited>"
|
schema: str | None = "<inherited>"
|
||||||
|
|
||||||
def interval_seconds(self, seconds: int) -> str:
|
def interval_seconds(self, seconds: int) -> str:
|
||||||
if self.type in {POSTGRES, COCKROACH}:
|
if self.type in {POSTGRES, COCKROACH}:
|
||||||
@@ -167,8 +167,8 @@ class Connection(Compat):
|
|||||||
async def fetchall(
|
async def fetchall(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
) -> list[TModel]:
|
) -> list[TModel]:
|
||||||
params = self.rewrite_values(values) if values else {}
|
params = self.rewrite_values(values) if values else {}
|
||||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||||
@@ -183,8 +183,8 @@ class Connection(Compat):
|
|||||||
async def fetchone(
|
async def fetchone(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
) -> TModel:
|
) -> TModel:
|
||||||
params = self.rewrite_values(values) if values else {}
|
params = self.rewrite_values(values) if values else {}
|
||||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||||
@@ -211,11 +211,11 @@ class Connection(Compat):
|
|||||||
async def fetch_page(
|
async def fetch_page(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
where: Optional[list[str]] = None,
|
where: list[str] | None = None,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
filters: Optional[Filters] = None,
|
filters: Filters | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
group_by: Optional[list[str]] = None,
|
group_by: list[str] | None = None,
|
||||||
) -> Page[TModel]:
|
) -> Page[TModel]:
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = Filters()
|
filters = Filters()
|
||||||
@@ -268,7 +268,7 @@ class Connection(Compat):
|
|||||||
total=count,
|
total=count,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def execute(self, query: str, values: Optional[dict] = None):
|
async def execute(self, query: str, values: dict | None = None):
|
||||||
params = self.rewrite_values(values) if values else {}
|
params = self.rewrite_values(values) if values else {}
|
||||||
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
result = await self.conn.execute(text(self.rewrite_query(query)), params)
|
||||||
await self.conn.commit()
|
await self.conn.commit()
|
||||||
@@ -350,8 +350,8 @@ class Database(Compat):
|
|||||||
async def fetchall(
|
async def fetchall(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
) -> list[TModel]:
|
) -> list[TModel]:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetchall(query, values, model)
|
return await conn.fetchall(query, values, model)
|
||||||
@@ -359,8 +359,8 @@ class Database(Compat):
|
|||||||
async def fetchone(
|
async def fetchone(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
) -> TModel:
|
) -> TModel:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetchone(query, values, model)
|
return await conn.fetchone(query, values, model)
|
||||||
@@ -378,16 +378,16 @@ class Database(Compat):
|
|||||||
async def fetch_page(
|
async def fetch_page(
|
||||||
self,
|
self,
|
||||||
query: str,
|
query: str,
|
||||||
where: Optional[list[str]] = None,
|
where: list[str] | None = None,
|
||||||
values: Optional[dict] = None,
|
values: dict | None = None,
|
||||||
filters: Optional[Filters] = None,
|
filters: Filters | None = None,
|
||||||
model: Optional[type[TModel]] = None,
|
model: type[TModel] | None = None,
|
||||||
group_by: Optional[list[str]] = None,
|
group_by: list[str] | None = None,
|
||||||
) -> Page[TModel]:
|
) -> Page[TModel]:
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
return await conn.fetch_page(query, where, values, filters, model, group_by)
|
||||||
|
|
||||||
async def execute(self, query: str, values: Optional[dict] = None):
|
async def execute(self, query: str, values: dict | None = None):
|
||||||
async with self.connect() as conn:
|
async with self.connect() as conn:
|
||||||
return await conn.execute(query, values)
|
return await conn.execute(query, values)
|
||||||
|
|
||||||
@@ -445,7 +445,7 @@ class Operator(Enum):
|
|||||||
|
|
||||||
class FilterModel(BaseModel):
|
class FilterModel(BaseModel):
|
||||||
__search_fields__: list[str] = []
|
__search_fields__: list[str] = []
|
||||||
__sort_fields__: Optional[list[str]] = None
|
__sort_fields__: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
@@ -461,8 +461,8 @@ class Page(BaseModel, Generic[T]):
|
|||||||
class Filter(BaseModel, Generic[TFilterModel]):
|
class Filter(BaseModel, Generic[TFilterModel]):
|
||||||
field: str
|
field: str
|
||||||
op: Operator = Operator.EQ
|
op: Operator = Operator.EQ
|
||||||
model: Optional[type[TFilterModel]]
|
model: type[TFilterModel] | None
|
||||||
values: Optional[dict] = None
|
values: dict | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_query(
|
def parse_query(
|
||||||
@@ -517,15 +517,15 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
filters: list[Filter[TFilterModel]] = []
|
filters: list[Filter[TFilterModel]] = []
|
||||||
search: Optional[str] = None
|
search: str | None = None
|
||||||
|
|
||||||
offset: Optional[int] = None
|
offset: int | None = None
|
||||||
limit: Optional[int] = None
|
limit: int | None = None
|
||||||
|
|
||||||
sortby: Optional[str] = None
|
sortby: str | None = None
|
||||||
direction: Optional[Literal["asc", "desc"]] = None
|
direction: Literal["asc", "desc"] | None = None
|
||||||
|
|
||||||
model: Optional[type[TFilterModel]] = None
|
model: type[TFilterModel] | None = None
|
||||||
|
|
||||||
@root_validator(pre=True)
|
@root_validator(pre=True)
|
||||||
def validate_sortby(cls, values):
|
def validate_sortby(cls, values):
|
||||||
@@ -547,7 +547,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
stmt += f"OFFSET {self.offset}"
|
stmt += f"OFFSET {self.offset}"
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
def where(self, where_stmts: Optional[list[str]] = None) -> str:
|
def where(self, where_stmts: list[str] | None = None) -> str:
|
||||||
if not where_stmts:
|
if not where_stmts:
|
||||||
where_stmts = []
|
where_stmts = []
|
||||||
if self.filters:
|
if self.filters:
|
||||||
@@ -567,7 +567,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
|||||||
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
return f"ORDER BY {self.sortby} {self.direction or 'asc'}"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def values(self, values: Optional[dict] = None) -> dict:
|
def values(self, values: dict | None = None) -> dict:
|
||||||
if not values:
|
if not values:
|
||||||
values = {}
|
values = {}
|
||||||
if self.filters:
|
if self.filters:
|
||||||
@@ -641,7 +641,7 @@ def model_to_dict(model: BaseModel) -> dict:
|
|||||||
return _dict
|
return _dict
|
||||||
|
|
||||||
|
|
||||||
def dict_to_submodel(model: type[TModel], value: Union[dict, str]) -> Optional[TModel]:
|
def dict_to_submodel(model: type[TModel], value: dict | str) -> TModel | None:
|
||||||
"""convert a dictionary or JSON string to a Pydantic model"""
|
"""convert a dictionary or JSON string to a Pydantic model"""
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
if value == "null":
|
if value == "null":
|
||||||
|
|||||||
+22
-11
@@ -10,7 +10,6 @@ from urllib.parse import urlparse
|
|||||||
import jinja2
|
import jinja2
|
||||||
import jwt
|
import jwt
|
||||||
import shortuuid
|
import shortuuid
|
||||||
from fastapi import Request
|
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from packaging import version
|
from packaging import version
|
||||||
from pydantic.schema import field_schema
|
from pydantic.schema import field_schema
|
||||||
@@ -225,18 +224,34 @@ def create_access_token(data: dict, token_expire_minutes: Optional[int] = None)
|
|||||||
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
return jwt.encode(to_encode, settings.auth_secret_key, "HS256")
|
||||||
|
|
||||||
|
|
||||||
def encrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
|
def encrypt_internal_message(
|
||||||
"""Encrypt message with the internal secret key"""
|
m: Optional[str] = None, urlsafe: bool = False
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Encrypt message with the internal secret key
|
||||||
|
|
||||||
|
Args:
|
||||||
|
m: Message to encrypt
|
||||||
|
urlsafe: Whether to use URL-safe base64 encoding
|
||||||
|
"""
|
||||||
if not m:
|
if not m:
|
||||||
return None
|
return None
|
||||||
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode())
|
return AESCipher(key=settings.auth_secret_key).encrypt(m.encode(), urlsafe=urlsafe)
|
||||||
|
|
||||||
|
|
||||||
def decrypt_internal_message(m: Optional[str] = None) -> Optional[str]:
|
def decrypt_internal_message(
|
||||||
"""Decrypt message with the internal secret key"""
|
m: Optional[str] = None, urlsafe: bool = False
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Decrypt message with the internal secret key
|
||||||
|
|
||||||
|
Args:
|
||||||
|
m: Message to decrypt
|
||||||
|
urlsafe: Whether the message uses URL-safe base64 encoding
|
||||||
|
"""
|
||||||
if not m:
|
if not m:
|
||||||
return None
|
return None
|
||||||
return AESCipher(key=settings.auth_secret_key).decrypt(m)
|
return AESCipher(key=settings.auth_secret_key).decrypt(m, urlsafe=urlsafe)
|
||||||
|
|
||||||
|
|
||||||
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict:
|
def filter_dict_keys(data: dict, filter_keys: Optional[list[str]]) -> dict:
|
||||||
@@ -328,7 +343,3 @@ def path_segments(path: str) -> list[str]:
|
|||||||
def normalize_path(path: Optional[str]) -> str:
|
def normalize_path(path: Optional[str]) -> str:
|
||||||
path = path or ""
|
path = path or ""
|
||||||
return "/" + "/".join(path_segments(path))
|
return "/" + "/".join(path_segments(path))
|
||||||
|
|
||||||
|
|
||||||
def normalized_path(request: Request) -> str:
|
|
||||||
return "/" + "/".join(path_segments(request.url.path))
|
|
||||||
|
|||||||
@@ -134,8 +134,9 @@ class AuditMiddleware(BaseHTTPMiddleware):
|
|||||||
assert response
|
assert response
|
||||||
return response
|
return response
|
||||||
finally:
|
finally:
|
||||||
duration = (datetime.now(timezone.utc) - start_time).total_seconds()
|
if request_details:
|
||||||
await self._log_audit(request, response, duration, request_details)
|
duration = (datetime.now(timezone.utc) - start_time).total_seconds()
|
||||||
|
await self._log_audit(request, response, duration, request_details)
|
||||||
|
|
||||||
async def _log_audit(
|
async def _log_audit(
|
||||||
self,
|
self,
|
||||||
|
|||||||
+33
-33
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
@@ -15,10 +15,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class NodePeerInfo(BaseModel):
|
class NodePeerInfo(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
alias: Optional[str] = None
|
alias: str | None = None
|
||||||
color: Optional[str] = None
|
color: str | None = None
|
||||||
last_timestamp: Optional[int] = None
|
last_timestamp: int | None = None
|
||||||
addresses: Optional[list[str]] = None
|
addresses: list[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
class ChannelState(Enum):
|
class ChannelState(Enum):
|
||||||
@@ -47,20 +47,20 @@ class NodeChannel(BaseModel):
|
|||||||
balance: ChannelBalance
|
balance: ChannelBalance
|
||||||
state: ChannelState
|
state: ChannelState
|
||||||
# could be optional for closing/pending channels on lndrest
|
# could be optional for closing/pending channels on lndrest
|
||||||
id: Optional[str] = None
|
id: str | None = None
|
||||||
short_id: Optional[str] = None
|
short_id: str | None = None
|
||||||
point: Optional[ChannelPoint] = None
|
point: ChannelPoint | None = None
|
||||||
name: Optional[str] = None
|
name: str | None = None
|
||||||
color: Optional[str] = None
|
color: str | None = None
|
||||||
fee_ppm: Optional[int] = None
|
fee_ppm: int | None = None
|
||||||
fee_base_msat: Optional[int] = None
|
fee_base_msat: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class ChannelStats(BaseModel):
|
class ChannelStats(BaseModel):
|
||||||
counts: dict[ChannelState, int]
|
counts: dict[ChannelState, int]
|
||||||
avg_size: int
|
avg_size: int
|
||||||
biggest_size: Optional[int]
|
biggest_size: int | None
|
||||||
smallest_size: Optional[int]
|
smallest_size: int | None
|
||||||
total_capacity: int
|
total_capacity: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -95,9 +95,9 @@ class ChannelStats(BaseModel):
|
|||||||
|
|
||||||
class NodeFees(BaseModel):
|
class NodeFees(BaseModel):
|
||||||
total_msat: int
|
total_msat: int
|
||||||
daily_msat: Optional[int] = None
|
daily_msat: int | None = None
|
||||||
weekly_msat: Optional[int] = None
|
weekly_msat: int | None = None
|
||||||
monthly_msat: Optional[int] = None
|
monthly_msat: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class PublicNodeInfo(BaseModel):
|
class PublicNodeInfo(BaseModel):
|
||||||
@@ -121,25 +121,25 @@ class NodeInfoResponse(PublicNodeInfo):
|
|||||||
class NodePayment(BaseModel):
|
class NodePayment(BaseModel):
|
||||||
pending: bool
|
pending: bool
|
||||||
amount: int
|
amount: int
|
||||||
fee: Optional[int] = None
|
fee: int | None = None
|
||||||
memo: Optional[str] = None
|
memo: str | None = None
|
||||||
time: int
|
time: int
|
||||||
bolt11: Optional[str] = None
|
bolt11: str | None = None
|
||||||
preimage: Optional[str]
|
preimage: str | None
|
||||||
payment_hash: str
|
payment_hash: str
|
||||||
expiry: Optional[float] = None
|
expiry: float | None = None
|
||||||
destination: Optional[NodePeerInfo] = None
|
destination: NodePeerInfo | None = None
|
||||||
|
|
||||||
|
|
||||||
class NodeInvoice(BaseModel):
|
class NodeInvoice(BaseModel):
|
||||||
pending: bool
|
pending: bool
|
||||||
amount: int
|
amount: int
|
||||||
memo: Optional[str]
|
memo: str | None
|
||||||
bolt11: str
|
bolt11: str
|
||||||
preimage: Optional[str]
|
preimage: str | None
|
||||||
payment_hash: str
|
payment_hash: str
|
||||||
paid_at: Optional[int] = None
|
paid_at: int | None = None
|
||||||
expiry: Optional[int] = None
|
expiry: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class NodeInvoiceFilters(FilterModel):
|
class NodeInvoiceFilters(FilterModel):
|
||||||
@@ -154,7 +154,7 @@ class Node(ABC):
|
|||||||
|
|
||||||
def __init__(self, wallet: Wallet):
|
def __init__(self, wallet: Wallet):
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
self.id: Optional[str] = None
|
self.id: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@@ -203,22 +203,22 @@ class Node(ABC):
|
|||||||
self,
|
self,
|
||||||
peer_id: str,
|
peer_id: str,
|
||||||
local_amount: int,
|
local_amount: int,
|
||||||
push_amount: Optional[int] = None,
|
push_amount: int | None = None,
|
||||||
fee_rate: Optional[int] = None,
|
fee_rate: int | None = None,
|
||||||
) -> ChannelPoint:
|
) -> ChannelPoint:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def close_channel(
|
async def close_channel(
|
||||||
self,
|
self,
|
||||||
short_id: Optional[str] = None,
|
short_id: str | None = None,
|
||||||
point: Optional[ChannelPoint] = None,
|
point: ChannelPoint | None = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
):
|
):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
|
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
+6
-6
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
@@ -119,8 +119,8 @@ class CoreLightningNode(Node):
|
|||||||
self,
|
self,
|
||||||
peer_id: str,
|
peer_id: str,
|
||||||
local_amount: int,
|
local_amount: int,
|
||||||
push_amount: Optional[int] = None,
|
push_amount: int | None = None,
|
||||||
fee_rate: Optional[int] = None,
|
fee_rate: int | None = None,
|
||||||
) -> ChannelPoint:
|
) -> ChannelPoint:
|
||||||
try:
|
try:
|
||||||
result = await self.ln_rpc(
|
result = await self.ln_rpc(
|
||||||
@@ -173,8 +173,8 @@ class CoreLightningNode(Node):
|
|||||||
@catch_rpc_errors
|
@catch_rpc_errors
|
||||||
async def close_channel(
|
async def close_channel(
|
||||||
self,
|
self,
|
||||||
short_id: Optional[str] = None,
|
short_id: str | None = None,
|
||||||
point: Optional[ChannelPoint] = None,
|
point: ChannelPoint | None = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
):
|
):
|
||||||
if not short_id:
|
if not short_id:
|
||||||
@@ -229,7 +229,7 @@ class CoreLightningNode(Node):
|
|||||||
await self.ln_rpc("setchannel", channel_id, feebase=base_msat, feeppm=ppm)
|
await self.ln_rpc("setchannel", channel_id, feebase=base_msat, feeppm=ppm)
|
||||||
|
|
||||||
@catch_rpc_errors
|
@catch_rpc_errors
|
||||||
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
|
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
||||||
channels = await self.get_channels()
|
channels = await self.get_channels()
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
if channel.id == channel_id:
|
if channel.id == channel_id:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from httpx import HTTPStatusError
|
from httpx import HTTPStatusError
|
||||||
@@ -60,9 +60,7 @@ def _parse_channel_point(raw: str) -> ChannelPoint:
|
|||||||
class LndRestNode(Node):
|
class LndRestNode(Node):
|
||||||
wallet: LndRestWallet
|
wallet: LndRestWallet
|
||||||
|
|
||||||
async def request(
|
async def request(self, method: str, path: str, json: dict | None = None, **kwargs):
|
||||||
self, method: str, path: str, json: Optional[dict] = None, **kwargs
|
|
||||||
):
|
|
||||||
response = await self.wallet.client.request(
|
response = await self.wallet.client.request(
|
||||||
method, f"{self.wallet.endpoint}{path}", json=json, **kwargs
|
method, f"{self.wallet.endpoint}{path}", json=json, **kwargs
|
||||||
)
|
)
|
||||||
@@ -131,8 +129,8 @@ class LndRestNode(Node):
|
|||||||
self,
|
self,
|
||||||
peer_id: str,
|
peer_id: str,
|
||||||
local_amount: int,
|
local_amount: int,
|
||||||
push_amount: Optional[int] = None,
|
push_amount: int | None = None,
|
||||||
fee_rate: Optional[int] = None,
|
fee_rate: int | None = None,
|
||||||
) -> ChannelPoint:
|
) -> ChannelPoint:
|
||||||
response = await self.request(
|
response = await self.request(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -176,8 +174,8 @@ class LndRestNode(Node):
|
|||||||
|
|
||||||
async def close_channel(
|
async def close_channel(
|
||||||
self,
|
self,
|
||||||
short_id: Optional[str] = None,
|
short_id: str | None = None,
|
||||||
point: Optional[ChannelPoint] = None,
|
point: ChannelPoint | None = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
):
|
):
|
||||||
if short_id:
|
if short_id:
|
||||||
@@ -218,7 +216,7 @@ class LndRestNode(Node):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
|
async def get_channel(self, channel_id: str) -> NodeChannel | None:
|
||||||
channel_info = await self.get(f"/v1/graph/edge/{channel_id}")
|
channel_info = await self.get(f"/v1/graph/edge/{channel_id}")
|
||||||
info = await self.get("/v1/getinfo")
|
info = await self.get("/v1/getinfo")
|
||||||
if info["identity_pubkey"] == channel_info["node1_pub"]:
|
if info["identity_pubkey"] == channel_info["node1_pub"]:
|
||||||
|
|||||||
+152
-160
@@ -4,16 +4,16 @@ import importlib
|
|||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from hashlib import sha256
|
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import gmtime, strftime, time
|
from time import gmtime, strftime, time
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import httpx
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
from pydantic import BaseModel, BaseSettings, Extra, Field, validator
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class RedirectPath(BaseModel):
|
|||||||
other.from_path, list(other.header_filters.items())
|
other.from_path, list(other.header_filters.items())
|
||||||
) or other.redirect_matches(self.from_path, list(self.header_filters.items()))
|
) or other.redirect_matches(self.from_path, list(self.header_filters.items()))
|
||||||
|
|
||||||
def find_in_conflict(self, others: list[RedirectPath]) -> Optional[RedirectPath]:
|
def find_in_conflict(self, others: list[RedirectPath]) -> RedirectPath | None:
|
||||||
for other in others:
|
for other in others:
|
||||||
if self.in_conflict(other):
|
if self.in_conflict(other):
|
||||||
return other
|
return other
|
||||||
@@ -153,7 +153,7 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
|||||||
|
|
||||||
def find_extension_redirect(
|
def find_extension_redirect(
|
||||||
self, path: str, req_headers: list[tuple[bytes, bytes]]
|
self, path: str, req_headers: list[tuple[bytes, bytes]]
|
||||||
) -> Optional[RedirectPath]:
|
) -> RedirectPath | None:
|
||||||
headers = [(k.decode(), v.decode()) for k, v in req_headers]
|
headers = [(k.decode(), v.decode()) for k, v in req_headers]
|
||||||
return next(
|
return next(
|
||||||
(
|
(
|
||||||
@@ -167,8 +167,8 @@ class InstalledExtensionsSettings(LNbitsSettings):
|
|||||||
def activate_extension_paths(
|
def activate_extension_paths(
|
||||||
self,
|
self,
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
upgrade_hash: Optional[str] = None,
|
upgrade_hash: str | None = None,
|
||||||
ext_redirects: Optional[list[dict]] = None,
|
ext_redirects: list[dict] | None = None,
|
||||||
):
|
):
|
||||||
self.lnbits_deactivated_extensions.discard(ext_id)
|
self.lnbits_deactivated_extensions.discard(ext_id)
|
||||||
|
|
||||||
@@ -231,12 +231,12 @@ class ExchangeHistorySettings(LNbitsSettings):
|
|||||||
class ThemesSettings(LNbitsSettings):
|
class ThemesSettings(LNbitsSettings):
|
||||||
lnbits_site_title: str = Field(default="LNbits")
|
lnbits_site_title: str = Field(default="LNbits")
|
||||||
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
lnbits_site_tagline: str = Field(default="free and open-source lightning wallet")
|
||||||
lnbits_site_description: Optional[str] = Field(
|
lnbits_site_description: str | None = Field(
|
||||||
default="The world's most powerful suite of bitcoin tools."
|
default="The world's most powerful suite of bitcoin tools."
|
||||||
)
|
)
|
||||||
lnbits_show_home_page_elements: bool = Field(default=True)
|
lnbits_show_home_page_elements: bool = Field(default=True)
|
||||||
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
lnbits_default_wallet_name: str = Field(default="LNbits wallet")
|
||||||
lnbits_custom_badge: Optional[str] = Field(default=None)
|
lnbits_custom_badge: str | None = Field(default=None)
|
||||||
lnbits_custom_badge_color: str = Field(default="warning")
|
lnbits_custom_badge_color: str = Field(default="warning")
|
||||||
lnbits_theme_options: list[str] = Field(
|
lnbits_theme_options: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -251,18 +251,16 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
"bitcoin",
|
"bitcoin",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
lnbits_custom_logo: Optional[str] = Field(default=None)
|
lnbits_custom_logo: str | None = Field(default=None)
|
||||||
lnbits_custom_image: Optional[str] = Field(
|
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
|
||||||
default="/static/images/logos/lnbits.svg"
|
|
||||||
)
|
|
||||||
lnbits_ad_space_title: str = Field(default="Supported by")
|
lnbits_ad_space_title: str = Field(default="Supported by")
|
||||||
lnbits_ad_space: str = Field(
|
lnbits_ad_space: str = Field(
|
||||||
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
|
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
|
||||||
) # sneaky sneaky
|
) # sneaky sneaky
|
||||||
lnbits_ad_space_enabled: bool = Field(default=False)
|
lnbits_ad_space_enabled: bool = Field(default=False)
|
||||||
lnbits_allowed_currencies: list[str] = Field(default=[])
|
lnbits_allowed_currencies: list[str] = Field(default=[])
|
||||||
lnbits_default_accounting_currency: Optional[str] = Field(default=None)
|
lnbits_default_accounting_currency: str | None = Field(default=None)
|
||||||
lnbits_qr_logo: str = Field(default="/static/images/logos/lnbits.png")
|
lnbits_qr_logo: str = Field(default="/static/favicon.ico")
|
||||||
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
lnbits_default_reaction: str = Field(default="confettiBothSides")
|
||||||
lnbits_default_theme: str = Field(default="salvador")
|
lnbits_default_theme: str = Field(default="salvador")
|
||||||
lnbits_default_border: str = Field(default="hard-border")
|
lnbits_default_border: str = Field(default="hard-border")
|
||||||
@@ -273,16 +271,15 @@ class ThemesSettings(LNbitsSettings):
|
|||||||
class OpsSettings(LNbitsSettings):
|
class OpsSettings(LNbitsSettings):
|
||||||
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
lnbits_baseurl: str = Field(default="http://127.0.0.1:5000/")
|
||||||
lnbits_hide_api: bool = Field(default=False)
|
lnbits_hide_api: bool = Field(default=False)
|
||||||
lnbits_denomination: str = Field(default="sats")
|
|
||||||
|
|
||||||
|
|
||||||
class FeeSettings(LNbitsSettings):
|
class FeeSettings(LNbitsSettings):
|
||||||
lnbits_reserve_fee_min: int = Field(default=2000)
|
lnbits_reserve_fee_min: int = Field(default=2000, ge=0)
|
||||||
lnbits_reserve_fee_percent: float = Field(default=1.0)
|
lnbits_reserve_fee_percent: float = Field(default=1.0, ge=0)
|
||||||
lnbits_service_fee: float = Field(default=0)
|
lnbits_service_fee: float = Field(default=0, ge=0)
|
||||||
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
lnbits_service_fee_ignore_internal: bool = Field(default=True)
|
||||||
lnbits_service_fee_max: int = Field(default=0)
|
lnbits_service_fee_max: int = Field(default=0)
|
||||||
lnbits_service_fee_wallet: Optional[str] = Field(default=None)
|
lnbits_service_fee_wallet: str | None = Field(default=None)
|
||||||
|
|
||||||
# WARN: this same value must be used for balance check and passed to
|
# WARN: this same value must be used for balance check and passed to
|
||||||
# funding_source.pay_invoice(), it may cause a vulnerability if the values differ
|
# funding_source.pay_invoice(), it may cause a vulnerability if the values differ
|
||||||
@@ -295,9 +292,9 @@ class FeeSettings(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class ExchangeProvidersSettings(LNbitsSettings):
|
class ExchangeProvidersSettings(LNbitsSettings):
|
||||||
lnbits_exchange_rate_cache_seconds: int = Field(default=30)
|
lnbits_exchange_rate_cache_seconds: int = Field(default=30, ge=0)
|
||||||
lnbits_exchange_history_size: int = Field(default=60)
|
lnbits_exchange_history_size: int = Field(default=60, ge=0)
|
||||||
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300)
|
lnbits_exchange_history_refresh_interval_seconds: int = Field(default=300, ge=0)
|
||||||
|
|
||||||
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
lnbits_exchange_rate_providers: list[ExchangeRateProvider] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -362,7 +359,7 @@ class ExchangeProvidersSettings(LNbitsSettings):
|
|||||||
|
|
||||||
|
|
||||||
class SecuritySettings(LNbitsSettings):
|
class SecuritySettings(LNbitsSettings):
|
||||||
lnbits_rate_limit_no: str = Field(default="200")
|
lnbits_rate_limit_no: int = Field(default=200, ge=0)
|
||||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||||
lnbits_allowed_ips: list[str] = Field(default=[])
|
lnbits_allowed_ips: list[str] = Field(default=[])
|
||||||
lnbits_blocked_ips: list[str] = Field(default=[])
|
lnbits_blocked_ips: list[str] = Field(default=[])
|
||||||
@@ -370,16 +367,16 @@ class SecuritySettings(LNbitsSettings):
|
|||||||
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
|
default=["^(?!\\d+\\.\\d+\\.\\d+\\.\\d+$)(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"]
|
||||||
)
|
)
|
||||||
|
|
||||||
lnbits_wallet_limit_max_balance: int = Field(default=0)
|
lnbits_wallet_limit_max_balance: int = Field(default=0, ge=0)
|
||||||
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
|
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0, ge=0)
|
||||||
lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
|
lnbits_wallet_limit_secs_between_trans: int = Field(default=0, ge=0)
|
||||||
lnbits_only_allow_incoming_payments: bool = Field(default=False)
|
lnbits_only_allow_incoming_payments: bool = Field(default=False)
|
||||||
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
|
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
|
||||||
lnbits_watchdog_interval_minutes: int = Field(default=60)
|
lnbits_watchdog_interval_minutes: int = Field(default=60, gt=0)
|
||||||
lnbits_watchdog_delta: int = Field(default=1_000_000)
|
lnbits_watchdog_delta: int = Field(default=1_000_000, gt=0)
|
||||||
|
|
||||||
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000)
|
lnbits_max_outgoing_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000)
|
lnbits_max_incoming_payment_amount_sats: int = Field(default=10_000_000, ge=0)
|
||||||
|
|
||||||
def is_wallet_max_balance_exceeded(self, amount):
|
def is_wallet_max_balance_exceeded(self, amount):
|
||||||
return (
|
return (
|
||||||
@@ -396,142 +393,154 @@ class NotificationsSettings(LNbitsSettings):
|
|||||||
lnbits_telegram_notifications_enabled: bool = Field(default=False)
|
lnbits_telegram_notifications_enabled: bool = Field(default=False)
|
||||||
lnbits_telegram_notifications_access_token: str = Field(default="")
|
lnbits_telegram_notifications_access_token: str = Field(default="")
|
||||||
lnbits_telegram_notifications_chat_id: str = Field(default="")
|
lnbits_telegram_notifications_chat_id: str = Field(default="")
|
||||||
|
lnbits_email_notifications_enabled: bool = Field(default=False)
|
||||||
|
lnbits_email_notifications_email: str = Field(default="")
|
||||||
|
lnbits_email_notifications_username: str = Field(default="")
|
||||||
|
lnbits_email_notifications_password: str = Field(default="")
|
||||||
|
lnbits_email_notifications_server: str = Field(default="smtp.protonmail.ch")
|
||||||
|
lnbits_email_notifications_port: int = Field(default=587)
|
||||||
|
lnbits_email_notifications_to_emails: list[str] = Field(default=[])
|
||||||
|
|
||||||
lnbits_notification_settings_update: bool = Field(default=True)
|
lnbits_notification_settings_update: bool = Field(default=True)
|
||||||
lnbits_notification_credit_debit: bool = Field(default=True)
|
lnbits_notification_credit_debit: bool = Field(default=True)
|
||||||
notification_balance_delta_changed: bool = Field(default=True)
|
notification_balance_delta_changed: bool = Field(default=True)
|
||||||
lnbits_notification_server_start_stop: bool = Field(default=True)
|
lnbits_notification_server_start_stop: bool = Field(default=True)
|
||||||
lnbits_notification_watchdog: bool = Field(default=False)
|
lnbits_notification_watchdog: bool = Field(default=False)
|
||||||
lnbits_notification_server_status_hours: int = Field(default=24)
|
lnbits_notification_server_status_hours: int = Field(default=24, gt=0)
|
||||||
lnbits_notification_incoming_payment_amount_sats: int = Field(default=1_000_000)
|
lnbits_notification_incoming_payment_amount_sats: int = Field(
|
||||||
lnbits_notification_outgoing_payment_amount_sats: int = Field(default=1_000_000)
|
default=1_000_000, ge=0
|
||||||
|
)
|
||||||
|
lnbits_notification_outgoing_payment_amount_sats: int = Field(
|
||||||
|
default=1_000_000, ge=0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeWalletFundingSource(LNbitsSettings):
|
class FakeWalletFundingSource(LNbitsSettings):
|
||||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||||
|
lnbits_denomination: str = Field(default="sats")
|
||||||
|
|
||||||
|
|
||||||
class LNbitsFundingSource(LNbitsSettings):
|
class LNbitsFundingSource(LNbitsSettings):
|
||||||
lnbits_endpoint: str = Field(default="https://demo.lnbits.com")
|
lnbits_endpoint: str = Field(default="https://demo.lnbits.com")
|
||||||
lnbits_key: Optional[str] = Field(default=None)
|
lnbits_key: str | None = Field(default=None)
|
||||||
lnbits_admin_key: Optional[str] = Field(default=None)
|
lnbits_admin_key: str | None = Field(default=None)
|
||||||
lnbits_invoice_key: Optional[str] = Field(default=None)
|
lnbits_invoice_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class ClicheFundingSource(LNbitsSettings):
|
class ClicheFundingSource(LNbitsSettings):
|
||||||
cliche_endpoint: Optional[str] = Field(default=None)
|
cliche_endpoint: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class CoreLightningFundingSource(LNbitsSettings):
|
class CoreLightningFundingSource(LNbitsSettings):
|
||||||
corelightning_rpc: Optional[str] = Field(default=None)
|
corelightning_rpc: str | None = Field(default=None)
|
||||||
corelightning_pay_command: str = Field(default="pay")
|
corelightning_pay_command: str = Field(default="pay")
|
||||||
clightning_rpc: Optional[str] = Field(default=None)
|
clightning_rpc: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class CoreLightningRestFundingSource(LNbitsSettings):
|
class CoreLightningRestFundingSource(LNbitsSettings):
|
||||||
corelightning_rest_url: Optional[str] = Field(default=None)
|
corelightning_rest_url: str | None = Field(default=None)
|
||||||
corelightning_rest_macaroon: Optional[str] = Field(default=None)
|
corelightning_rest_macaroon: str | None = Field(default=None)
|
||||||
corelightning_rest_cert: Optional[str] = Field(default=None)
|
corelightning_rest_cert: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class EclairFundingSource(LNbitsSettings):
|
class EclairFundingSource(LNbitsSettings):
|
||||||
eclair_url: Optional[str] = Field(default=None)
|
eclair_url: str | None = Field(default=None)
|
||||||
eclair_pass: Optional[str] = Field(default=None)
|
eclair_pass: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LndRestFundingSource(LNbitsSettings):
|
class LndRestFundingSource(LNbitsSettings):
|
||||||
lnd_rest_endpoint: Optional[str] = Field(default=None)
|
lnd_rest_endpoint: str | None = Field(default=None)
|
||||||
lnd_rest_cert: Optional[str] = Field(default=None)
|
lnd_rest_cert: str | None = Field(default=None)
|
||||||
lnd_rest_macaroon: Optional[str] = Field(default=None)
|
lnd_rest_macaroon: str | None = Field(default=None)
|
||||||
lnd_rest_macaroon_encrypted: Optional[str] = Field(default=None)
|
lnd_rest_macaroon_encrypted: str | None = Field(default=None)
|
||||||
lnd_rest_route_hints: bool = Field(default=True)
|
lnd_rest_route_hints: bool = Field(default=True)
|
||||||
lnd_rest_allow_self_payment: bool = Field(default=False)
|
lnd_rest_allow_self_payment: bool = Field(default=False)
|
||||||
lnd_cert: Optional[str] = Field(default=None)
|
lnd_cert: str | None = Field(default=None)
|
||||||
lnd_admin_macaroon: Optional[str] = Field(default=None)
|
lnd_admin_macaroon: str | None = Field(default=None)
|
||||||
lnd_invoice_macaroon: Optional[str] = Field(default=None)
|
lnd_invoice_macaroon: str | None = Field(default=None)
|
||||||
lnd_rest_admin_macaroon: Optional[str] = Field(default=None)
|
lnd_rest_admin_macaroon: str | None = Field(default=None)
|
||||||
lnd_rest_invoice_macaroon: Optional[str] = Field(default=None)
|
lnd_rest_invoice_macaroon: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LndGrpcFundingSource(LNbitsSettings):
|
class LndGrpcFundingSource(LNbitsSettings):
|
||||||
lnd_grpc_endpoint: Optional[str] = Field(default=None)
|
lnd_grpc_endpoint: str | None = Field(default=None)
|
||||||
lnd_grpc_cert: Optional[str] = Field(default=None)
|
lnd_grpc_cert: str | None = Field(default=None)
|
||||||
lnd_grpc_port: Optional[int] = Field(default=None)
|
lnd_grpc_port: int | None = Field(default=None)
|
||||||
lnd_grpc_admin_macaroon: Optional[str] = Field(default=None)
|
lnd_grpc_admin_macaroon: str | None = Field(default=None)
|
||||||
lnd_grpc_invoice_macaroon: Optional[str] = Field(default=None)
|
lnd_grpc_invoice_macaroon: str | None = Field(default=None)
|
||||||
lnd_grpc_macaroon: Optional[str] = Field(default=None)
|
lnd_grpc_macaroon: str | None = Field(default=None)
|
||||||
lnd_grpc_macaroon_encrypted: Optional[str] = Field(default=None)
|
lnd_grpc_macaroon_encrypted: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LnPayFundingSource(LNbitsSettings):
|
class LnPayFundingSource(LNbitsSettings):
|
||||||
lnpay_api_endpoint: Optional[str] = Field(default=None)
|
lnpay_api_endpoint: str | None = Field(default=None)
|
||||||
lnpay_api_key: Optional[str] = Field(default=None)
|
lnpay_api_key: str | None = Field(default=None)
|
||||||
lnpay_wallet_key: Optional[str] = Field(default=None)
|
lnpay_wallet_key: str | None = Field(default=None)
|
||||||
lnpay_admin_key: Optional[str] = Field(default=None)
|
lnpay_admin_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class BlinkFundingSource(LNbitsSettings):
|
class BlinkFundingSource(LNbitsSettings):
|
||||||
blink_api_endpoint: Optional[str] = Field(default="https://api.blink.sv/graphql")
|
blink_api_endpoint: str | None = Field(default="https://api.blink.sv/graphql")
|
||||||
blink_ws_endpoint: Optional[str] = Field(default="wss://ws.blink.sv/graphql")
|
blink_ws_endpoint: str | None = Field(default="wss://ws.blink.sv/graphql")
|
||||||
blink_token: Optional[str] = Field(default=None)
|
blink_token: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class ZBDFundingSource(LNbitsSettings):
|
class ZBDFundingSource(LNbitsSettings):
|
||||||
zbd_api_endpoint: Optional[str] = Field(default="https://api.zebedee.io/v0/")
|
zbd_api_endpoint: str | None = Field(default="https://api.zebedee.io/v0/")
|
||||||
zbd_api_key: Optional[str] = Field(default=None)
|
zbd_api_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class PhoenixdFundingSource(LNbitsSettings):
|
class PhoenixdFundingSource(LNbitsSettings):
|
||||||
phoenixd_api_endpoint: Optional[str] = Field(default="http://localhost:9740/")
|
phoenixd_api_endpoint: str | None = Field(default="http://localhost:9740/")
|
||||||
phoenixd_api_password: Optional[str] = Field(default=None)
|
phoenixd_api_password: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class AlbyFundingSource(LNbitsSettings):
|
class AlbyFundingSource(LNbitsSettings):
|
||||||
alby_api_endpoint: Optional[str] = Field(default="https://api.getalby.com/")
|
alby_api_endpoint: str | None = Field(default="https://api.getalby.com/")
|
||||||
alby_access_token: Optional[str] = Field(default=None)
|
alby_access_token: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class OpenNodeFundingSource(LNbitsSettings):
|
class OpenNodeFundingSource(LNbitsSettings):
|
||||||
opennode_api_endpoint: Optional[str] = Field(default=None)
|
opennode_api_endpoint: str | None = Field(default=None)
|
||||||
opennode_key: Optional[str] = Field(default=None)
|
opennode_key: str | None = Field(default=None)
|
||||||
opennode_admin_key: Optional[str] = Field(default=None)
|
opennode_admin_key: str | None = Field(default=None)
|
||||||
opennode_invoice_key: Optional[str] = Field(default=None)
|
opennode_invoice_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class SparkFundingSource(LNbitsSettings):
|
class SparkFundingSource(LNbitsSettings):
|
||||||
spark_url: Optional[str] = Field(default=None)
|
spark_url: str | None = Field(default=None)
|
||||||
spark_token: Optional[str] = Field(default=None)
|
spark_token: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LnTipsFundingSource(LNbitsSettings):
|
class LnTipsFundingSource(LNbitsSettings):
|
||||||
lntips_api_endpoint: Optional[str] = Field(default=None)
|
lntips_api_endpoint: str | None = Field(default=None)
|
||||||
lntips_api_key: Optional[str] = Field(default=None)
|
lntips_api_key: str | None = Field(default=None)
|
||||||
lntips_admin_key: Optional[str] = Field(default=None)
|
lntips_admin_key: str | None = Field(default=None)
|
||||||
lntips_invoice_key: Optional[str] = Field(default=None)
|
lntips_invoice_key: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class NWCFundingSource(LNbitsSettings):
|
class NWCFundingSource(LNbitsSettings):
|
||||||
nwc_pairing_url: Optional[str] = Field(default=None)
|
nwc_pairing_url: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class BreezSdkFundingSource(LNbitsSettings):
|
class BreezSdkFundingSource(LNbitsSettings):
|
||||||
breez_api_key: Optional[str] = Field(default=None)
|
breez_api_key: str | None = Field(default=None)
|
||||||
breez_greenlight_seed: Optional[str] = Field(default=None)
|
breez_greenlight_seed: str | None = Field(default=None)
|
||||||
breez_greenlight_invite_code: Optional[str] = Field(default=None)
|
breez_greenlight_invite_code: str | None = Field(default=None)
|
||||||
breez_greenlight_device_key: Optional[str] = Field(default=None)
|
breez_greenlight_device_key: str | None = Field(default=None)
|
||||||
breez_greenlight_device_cert: Optional[str] = Field(default=None)
|
breez_greenlight_device_cert: str | None = Field(default=None)
|
||||||
breez_use_trampoline: bool = Field(default=True)
|
breez_use_trampoline: bool = Field(default=True)
|
||||||
|
|
||||||
|
|
||||||
class BoltzFundingSource(LNbitsSettings):
|
class BoltzFundingSource(LNbitsSettings):
|
||||||
boltz_client_endpoint: Optional[str] = Field(default="127.0.0.1:9002")
|
boltz_client_endpoint: str | None = Field(default="127.0.0.1:9002")
|
||||||
boltz_client_macaroon: Optional[str] = Field(default=None)
|
boltz_client_macaroon: str | None = Field(default=None)
|
||||||
boltz_client_wallet: Optional[str] = Field(default="lnbits")
|
boltz_client_wallet: str | None = Field(default="lnbits")
|
||||||
boltz_client_cert: Optional[str] = Field(default=None)
|
boltz_client_cert: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class LightningSettings(LNbitsSettings):
|
class LightningSettings(LNbitsSettings):
|
||||||
lightning_invoice_expiry: int = Field(default=3600)
|
lightning_invoice_expiry: int = Field(default=3600, gt=0)
|
||||||
|
|
||||||
|
|
||||||
class FundingSourcesSettings(
|
class FundingSourcesSettings(
|
||||||
@@ -558,12 +567,12 @@ class FundingSourcesSettings(
|
|||||||
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
lnbits_backend_wallet_class: str = Field(default="VoidWallet")
|
||||||
# How long to wait for the payment to be confirmed before returning a pending status
|
# How long to wait for the payment to be confirmed before returning a pending status
|
||||||
# It will not fail the payment, it will make it return pending after the timeout
|
# It will not fail the payment, it will make it return pending after the timeout
|
||||||
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5)
|
lnbits_funding_source_pay_invoice_wait_seconds: int = Field(default=5, ge=0)
|
||||||
|
|
||||||
|
|
||||||
class WebPushSettings(LNbitsSettings):
|
class WebPushSettings(LNbitsSettings):
|
||||||
lnbits_webpush_pubkey: Optional[str] = Field(default=None)
|
lnbits_webpush_pubkey: str | None = Field(default=None)
|
||||||
lnbits_webpush_privkey: Optional[str] = Field(default=None)
|
lnbits_webpush_privkey: str | None = Field(default=None)
|
||||||
|
|
||||||
|
|
||||||
class NodeUISettings(LNbitsSettings):
|
class NodeUISettings(LNbitsSettings):
|
||||||
@@ -597,7 +606,7 @@ class AuthMethods(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class AuthSettings(LNbitsSettings):
|
class AuthSettings(LNbitsSettings):
|
||||||
auth_token_expire_minutes: int = Field(default=525600)
|
auth_token_expire_minutes: int = Field(default=525600, gt=0)
|
||||||
auth_all_methods = [a.value for a in AuthMethods]
|
auth_all_methods = [a.value for a in AuthMethods]
|
||||||
auth_allowed_methods: list[str] = Field(
|
auth_allowed_methods: list[str] = Field(
|
||||||
default=[
|
default=[
|
||||||
@@ -607,7 +616,7 @@ class AuthSettings(LNbitsSettings):
|
|||||||
)
|
)
|
||||||
# How many seconds after login the user is allowed to update its credentials.
|
# How many seconds after login the user is allowed to update its credentials.
|
||||||
# A fresh login is required afterwards.
|
# A fresh login is required afterwards.
|
||||||
auth_credetials_update_threshold: int = Field(default=120)
|
auth_credetials_update_threshold: int = Field(default=120, gt=0)
|
||||||
|
|
||||||
def is_auth_method_allowed(self, method: AuthMethods):
|
def is_auth_method_allowed(self, method: AuthMethods):
|
||||||
return method.value in self.auth_allowed_methods
|
return method.value in self.auth_allowed_methods
|
||||||
@@ -639,7 +648,7 @@ class AuditSettings(LNbitsSettings):
|
|||||||
lnbits_audit_enabled: bool = Field(default=True)
|
lnbits_audit_enabled: bool = Field(default=True)
|
||||||
|
|
||||||
# number of days to keep the audit entry
|
# number of days to keep the audit entry
|
||||||
lnbits_audit_retention_days: int = Field(default=7)
|
lnbits_audit_retention_days: int = Field(default=7, ge=0)
|
||||||
|
|
||||||
lnbits_audit_log_ip_address: bool = Field(default=False)
|
lnbits_audit_log_ip_address: bool = Field(default=False)
|
||||||
lnbits_audit_log_path_params: bool = Field(default=True)
|
lnbits_audit_log_path_params: bool = Field(default=True)
|
||||||
@@ -669,9 +678,9 @@ class AuditSettings(LNbitsSettings):
|
|||||||
|
|
||||||
def audit_http_request(
|
def audit_http_request(
|
||||||
self,
|
self,
|
||||||
http_method: Optional[str] = None,
|
http_method: str | None = None,
|
||||||
path: Optional[str] = None,
|
path: str | None = None,
|
||||||
http_response_code: Optional[str] = None,
|
http_response_code: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if not self.lnbits_audit_enabled:
|
if not self.lnbits_audit_enabled:
|
||||||
return False
|
return False
|
||||||
@@ -689,24 +698,24 @@ class AuditSettings(LNbitsSettings):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _is_http_request_path_auditable(self, path: Optional[str]):
|
def _is_http_request_path_auditable(self, path: str | None):
|
||||||
if len(self.lnbits_audit_exclude_paths) != 0 and path:
|
if len(self.lnbits_audit_exclude_paths) != 0 and path:
|
||||||
for exclude_path in self.lnbits_audit_exclude_paths:
|
for exclude_path in self.lnbits_audit_exclude_paths:
|
||||||
if _re_fullmatch_safe(exclude_path, path):
|
if _re_fullmatch_safe(exclude_path, path):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if len(self.lnbits_audit_include_paths) != 0:
|
if len(self.lnbits_audit_include_paths) == 0:
|
||||||
if not path:
|
return True
|
||||||
return False
|
|
||||||
for include_path in self.lnbits_audit_include_paths:
|
if not path:
|
||||||
if _re_fullmatch_safe(include_path, path):
|
return False
|
||||||
return True
|
for include_path in self.lnbits_audit_include_paths:
|
||||||
|
if _re_fullmatch_safe(include_path, path):
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _is_http_response_code_auditable(
|
def _is_http_response_code_auditable(self, http_response_code: str | None) -> bool:
|
||||||
self, http_response_code: Optional[str]
|
|
||||||
) -> bool:
|
|
||||||
if not http_response_code:
|
if not http_response_code:
|
||||||
# No response code means only request filters should apply
|
# No response code means only request filters should apply
|
||||||
return True
|
return True
|
||||||
@@ -776,7 +785,7 @@ class EnvSettings(LNbitsSettings):
|
|||||||
debug_database: bool = Field(default=False)
|
debug_database: bool = Field(default=False)
|
||||||
bundle_assets: bool = Field(default=True)
|
bundle_assets: bool = Field(default=True)
|
||||||
host: str = Field(default="127.0.0.1")
|
host: str = Field(default="127.0.0.1")
|
||||||
port: int = Field(default=5000)
|
port: int = Field(default=5000, gt=0)
|
||||||
forwarded_allow_ips: str = Field(default="*")
|
forwarded_allow_ips: str = Field(default="*")
|
||||||
lnbits_title: str = Field(default="LNbits API")
|
lnbits_title: str = Field(default="LNbits API")
|
||||||
lnbits_path: str = Field(default=".")
|
lnbits_path: str = Field(default=".")
|
||||||
@@ -788,24 +797,27 @@ class EnvSettings(LNbitsSettings):
|
|||||||
enable_log_to_file: bool = Field(default=True)
|
enable_log_to_file: bool = Field(default=True)
|
||||||
log_rotation: str = Field(default="100 MB")
|
log_rotation: str = Field(default="100 MB")
|
||||||
log_retention: str = Field(default="3 months")
|
log_retention: str = Field(default="3 months")
|
||||||
server_startup_time: int = Field(default=time())
|
|
||||||
cleanup_wallets_days: int = Field(default=90)
|
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_default_extension_path(self) -> bool:
|
def has_default_extension_path(self) -> bool:
|
||||||
return self.lnbits_extensions_path == "lnbits"
|
return self.lnbits_extensions_path == "lnbits"
|
||||||
|
|
||||||
@property
|
def check_auth_secret_key(self):
|
||||||
def lnbits_server_up_time(self) -> str:
|
if self.auth_secret_key:
|
||||||
up_time = int(time() - self.server_startup_time)
|
return
|
||||||
return strftime("%H:%M:%S", gmtime(up_time))
|
if not os.path.isdir(settings.lnbits_data_folder):
|
||||||
|
os.mkdir(settings.lnbits_data_folder)
|
||||||
|
auth_key_file = Path(settings.lnbits_data_folder, ".lnbits_auth_key")
|
||||||
class SaaSSettings(LNbitsSettings):
|
if auth_key_file.is_file():
|
||||||
lnbits_saas_callback: Optional[str] = Field(default=None)
|
with open(auth_key_file) as file:
|
||||||
lnbits_saas_secret: Optional[str] = Field(default=None)
|
self.auth_secret_key = file.readline()
|
||||||
lnbits_saas_instance_id: Optional[str] = Field(default=None)
|
return
|
||||||
|
self.auth_secret_key = uuid4().hex
|
||||||
|
with open(auth_key_file, "w+") as file:
|
||||||
|
file.write(self.auth_secret_key)
|
||||||
|
|
||||||
|
|
||||||
class PersistenceSettings(LNbitsSettings):
|
class PersistenceSettings(LNbitsSettings):
|
||||||
@@ -857,6 +869,13 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
|
|
||||||
lnbits_all_extensions_ids: set[str] = Field(default=[])
|
lnbits_all_extensions_ids: set[str] = Field(default=[])
|
||||||
|
|
||||||
|
server_startup_time: int = Field(default=time())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lnbits_server_up_time(self) -> str:
|
||||||
|
up_time = int(time() - self.server_startup_time)
|
||||||
|
return strftime("%H:%M:%S", gmtime(up_time))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def readonly_fields(cls):
|
def readonly_fields(cls):
|
||||||
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
||||||
@@ -865,7 +884,6 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
|||||||
class ReadOnlySettings(
|
class ReadOnlySettings(
|
||||||
EnvSettings,
|
EnvSettings,
|
||||||
ExtensionsInstallSettings,
|
ExtensionsInstallSettings,
|
||||||
SaaSSettings,
|
|
||||||
PersistenceSettings,
|
PersistenceSettings,
|
||||||
SuperUserSettings,
|
SuperUserSettings,
|
||||||
):
|
):
|
||||||
@@ -903,7 +921,7 @@ class Settings(EditableSettings, ReadOnlySettings, TransientSettings, BaseSettin
|
|||||||
or user_id == self.super_user
|
or user_id == self.super_user
|
||||||
)
|
)
|
||||||
|
|
||||||
def is_super_user(self, user_id: Optional[str] = None) -> bool:
|
def is_super_user(self, user_id: str | None = None) -> bool:
|
||||||
return user_id == self.super_user
|
return user_id == self.super_user
|
||||||
|
|
||||||
def is_admin_user(self, user_id: str) -> bool:
|
def is_admin_user(self, user_id: str) -> bool:
|
||||||
@@ -922,12 +940,12 @@ class SuperSettings(EditableSettings):
|
|||||||
|
|
||||||
class AdminSettings(EditableSettings):
|
class AdminSettings(EditableSettings):
|
||||||
is_super_user: bool
|
is_super_user: bool
|
||||||
lnbits_allowed_funding_sources: Optional[list[str]]
|
lnbits_allowed_funding_sources: list[str] | None
|
||||||
|
|
||||||
|
|
||||||
class SettingsField(BaseModel):
|
class SettingsField(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
value: Optional[Any]
|
value: Any | None
|
||||||
tag: str = "core"
|
tag: str = "core"
|
||||||
|
|
||||||
|
|
||||||
@@ -944,31 +962,6 @@ def set_cli_settings(**kwargs):
|
|||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
|
|
||||||
|
|
||||||
def send_admin_user_to_saas():
|
|
||||||
if settings.lnbits_saas_callback:
|
|
||||||
with httpx.Client() as client:
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
"X-API-KEY": settings.lnbits_saas_secret,
|
|
||||||
}
|
|
||||||
payload = {
|
|
||||||
"instance_id": settings.lnbits_saas_instance_id,
|
|
||||||
"adminuser": settings.super_user,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
client.post(
|
|
||||||
settings.lnbits_saas_callback,
|
|
||||||
headers=headers,
|
|
||||||
json=payload,
|
|
||||||
)
|
|
||||||
logger.success("sent super_user to saas application")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(
|
|
||||||
"error sending super_user to saas:"
|
|
||||||
f" {settings.lnbits_saas_callback}. Error: {e!s}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
readonly_variables = ReadOnlySettings.readonly_fields()
|
readonly_variables = ReadOnlySettings.readonly_fields()
|
||||||
transient_variables = TransientSettings.readonly_fields()
|
transient_variables = TransientSettings.readonly_fields()
|
||||||
|
|
||||||
@@ -977,9 +970,8 @@ settings = Settings()
|
|||||||
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
settings.lnbits_path = str(path.dirname(path.realpath(__file__)))
|
||||||
|
|
||||||
settings.version = importlib.metadata.version("lnbits")
|
settings.version = importlib.metadata.version("lnbits")
|
||||||
settings.auth_secret_key = (
|
|
||||||
settings.auth_secret_key or sha256(settings.super_user.encode("utf-8")).hexdigest()
|
settings.check_auth_secret_key()
|
||||||
)
|
|
||||||
|
|
||||||
if not settings.user_agent:
|
if not settings.user_agent:
|
||||||
settings.user_agent = f"LNbits/{settings.version}"
|
settings.user_agent = f"LNbits/{settings.version}"
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+7
-5
File diff suppressed because one or more lines are too long
@@ -110,16 +110,16 @@ body[data-theme=bitcoin].body--light {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-theme=bitcoin] .bg-primary {
|
[data-theme=bitcoin] .bg-primary {
|
||||||
background: #ff9853 !important;
|
background: #ea611d !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-primary {
|
[data-theme=bitcoin] .text-primary {
|
||||||
color: #ff9853 !important;
|
color: #ea611d !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-secondary {
|
[data-theme=bitcoin] .bg-secondary {
|
||||||
background: #ff7353 !important;
|
background: #e56f35 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-secondary {
|
[data-theme=bitcoin] .text-secondary {
|
||||||
color: #ff7353 !important;
|
color: #e56f35 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-dark {
|
[data-theme=bitcoin] .bg-dark {
|
||||||
background: #2d293b !important;
|
background: #2d293b !important;
|
||||||
@@ -134,10 +134,10 @@ body[data-theme=bitcoin].body--light {
|
|||||||
color: #333646 !important;
|
color: #333646 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-marginal-bg {
|
[data-theme=bitcoin] .bg-marginal-bg {
|
||||||
background: #2d293b !important;
|
background: #000000 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .text-marginal-bg {
|
[data-theme=bitcoin] .text-marginal-bg {
|
||||||
color: #2d293b !important;
|
color: #000000 !important;
|
||||||
}
|
}
|
||||||
[data-theme=bitcoin] .bg-marginal-text {
|
[data-theme=bitcoin] .bg-marginal-text {
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ window.localisation.en = {
|
|||||||
view_github: 'View on GitHub',
|
view_github: 'View on GitHub',
|
||||||
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
voidwallet_active: 'VoidWallet is active! Payments disabled',
|
||||||
use_with_caution: 'USE WITH CAUTION - {name} wallet is still in BETA',
|
use_with_caution: 'USE WITH CAUTION - {name} wallet is still in BETA',
|
||||||
service_fee: 'Service fee: {amount} % per transaction',
|
service_fee_badge: 'Service fee: {amount} % per transaction',
|
||||||
service_fee_max: 'Service fee: {amount} % per transaction (max {max} sats)',
|
service_fee_max_badge:
|
||||||
|
'Service fee: {amount} % per transaction (max {max} {denom})',
|
||||||
service_fee_tooltip:
|
service_fee_tooltip:
|
||||||
'Service fee charged by the LNbits server admin per outgoing transaction',
|
'Service fee charged by the LNbits server admin per outgoing transaction',
|
||||||
toggle_darkmode: 'Toggle Dark Mode',
|
toggle_darkmode: 'Toggle Dark Mode',
|
||||||
@@ -82,6 +83,7 @@ window.localisation.en = {
|
|||||||
create_invoice: 'Create Invoice',
|
create_invoice: 'Create Invoice',
|
||||||
camera_tooltip: 'Use camera to scan an invoice/QR',
|
camera_tooltip: 'Use camera to scan an invoice/QR',
|
||||||
export_csv: 'Export to CSV',
|
export_csv: 'Export to CSV',
|
||||||
|
export_csv_details: 'Export to CSV with details',
|
||||||
chart_tooltip: 'Show chart',
|
chart_tooltip: 'Show chart',
|
||||||
pending: 'Pending',
|
pending: 'Pending',
|
||||||
copy_invoice: 'Copy invoice',
|
copy_invoice: 'Copy invoice',
|
||||||
@@ -174,6 +176,7 @@ window.localisation.en = {
|
|||||||
payment_proof: 'Payment Proof',
|
payment_proof: 'Payment Proof',
|
||||||
update: 'Update',
|
update: 'Update',
|
||||||
update_available: 'Update {version} available!',
|
update_available: 'Update {version} available!',
|
||||||
|
funding_sources: 'Funding Sources',
|
||||||
latest_update: 'You are on the latest version {version}.',
|
latest_update: 'You are on the latest version {version}.',
|
||||||
notifications: 'Notifications',
|
notifications: 'Notifications',
|
||||||
notifications_configure: 'Configure Notifications',
|
notifications_configure: 'Configure Notifications',
|
||||||
@@ -195,6 +198,26 @@ window.localisation.en = {
|
|||||||
notifications_chat_id: 'Chat ID',
|
notifications_chat_id: 'Chat ID',
|
||||||
notifications_chat_id_desc: 'Chat ID to send the notifications to',
|
notifications_chat_id_desc: 'Chat ID to send the notifications to',
|
||||||
|
|
||||||
|
notifications_email_config: 'Email Configuration',
|
||||||
|
notifications_enable_email: 'Enable Email',
|
||||||
|
notifications_enable_email_desc: 'Send notfications over email',
|
||||||
|
notifications_send_test_email: 'Send test email',
|
||||||
|
notifications_send_email: 'Send email',
|
||||||
|
notifications_send_email_desc: 'Email you will send from',
|
||||||
|
notifications_send_email_username: 'Username',
|
||||||
|
notifications_send_email_username_desc:
|
||||||
|
'Username, will use the email if not set',
|
||||||
|
notifications_send_email_password: 'Send email password',
|
||||||
|
notifications_send_email_password_desc:
|
||||||
|
'Password for the email you will send from',
|
||||||
|
notifications_send_email_server_port: 'Send email SMTP port',
|
||||||
|
notifications_send_email_server_port_desc: 'Port for the SMTP server',
|
||||||
|
notifications_send_email_server: 'Send email SMTP server',
|
||||||
|
notifications_send_email_server_desc:
|
||||||
|
'SMTP server for the email you will send from',
|
||||||
|
notifications_send_to_emails: 'Emails to send to',
|
||||||
|
notifications_send_to_emails_desc: 'Emails notifications will be sent to',
|
||||||
|
|
||||||
notification_settings_update: 'Settings updated',
|
notification_settings_update: 'Settings updated',
|
||||||
notification_settings_update_desc:
|
notification_settings_update_desc:
|
||||||
'Notify when server settings have been updated',
|
'Notify when server settings have been updated',
|
||||||
@@ -293,6 +316,7 @@ window.localisation.en = {
|
|||||||
password: 'Password',
|
password: 'Password',
|
||||||
password_config: 'Password Config',
|
password_config: 'Password Config',
|
||||||
password_repeat: 'Password repeat',
|
password_repeat: 'Password repeat',
|
||||||
|
update_password: 'Update Password',
|
||||||
change_password: 'Change Password',
|
change_password: 'Change Password',
|
||||||
update_credentials: 'Update Credentials',
|
update_credentials: 'Update Credentials',
|
||||||
update_pubkey: 'Update Public Key',
|
update_pubkey: 'Update Public Key',
|
||||||
@@ -482,8 +506,9 @@ window.localisation.en = {
|
|||||||
lnbits_wallet: 'LNbits wallet',
|
lnbits_wallet: 'LNbits wallet',
|
||||||
denomination: 'Denomination',
|
denomination: 'Denomination',
|
||||||
denomination_hint: 'The name for the FakeWallet token',
|
denomination_hint: 'The name for the FakeWallet token',
|
||||||
ui_qr_code_logo: 'QR Code Logo',
|
denomination_error: 'Denomination must be 3 characters, or `sats`',
|
||||||
ui_qr_code_logo_hint: 'URL to logo image in QR code',
|
ui_qr_code_logo: 'QR Code/Favicon Logo',
|
||||||
|
ui_qr_code_logo_hint: 'QR code and favicon logo url',
|
||||||
ui_custom_image: 'Custom Image',
|
ui_custom_image: 'Custom Image',
|
||||||
ui_custom_image_label: 'URL to custom image',
|
ui_custom_image_label: 'URL to custom image',
|
||||||
ui_custom_image_hint: 'Image showed at homepage/login',
|
ui_custom_image_hint: 'Image showed at homepage/login',
|
||||||
@@ -512,6 +537,7 @@ window.localisation.en = {
|
|||||||
allowed_users_label: 'User ID',
|
allowed_users_label: 'User ID',
|
||||||
allow_creation_user: 'Allow creation of new users',
|
allow_creation_user: 'Allow creation of new users',
|
||||||
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
allow_creation_user_desc: 'Allow creation of new users on the index page',
|
||||||
|
new_user_not_allowed: 'Registration is disabled.',
|
||||||
components: 'Components',
|
components: 'Components',
|
||||||
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
long_running_endpoints: 'Top 5 Long Running Endpoints',
|
||||||
http_request_methods: 'HTTP Request Methods',
|
http_request_methods: 'HTTP Request Methods',
|
||||||
@@ -539,5 +565,7 @@ window.localisation.en = {
|
|||||||
reset_wallet_keys_desc:
|
reset_wallet_keys_desc:
|
||||||
'Reset the API keys for this wallet. This will invalidate the current keys and generate new ones.',
|
'Reset the API keys for this wallet. This will invalidate the current keys and generate new ones.',
|
||||||
view_list: 'View wallets as list',
|
view_list: 'View wallets as list',
|
||||||
view_column: 'View wallets as rows'
|
view_column: 'View wallets as rows',
|
||||||
|
filter_payments: 'Filter payments',
|
||||||
|
filter_date: 'Filter by date'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,8 +53,10 @@ window.AdminPageLogic = {
|
|||||||
chartReady: false,
|
chartReady: false,
|
||||||
formAddAdmin: '',
|
formAddAdmin: '',
|
||||||
formAddUser: '',
|
formAddUser: '',
|
||||||
|
hideInputToggle: true,
|
||||||
formAddExtensionsManifest: '',
|
formAddExtensionsManifest: '',
|
||||||
nostrNotificationIdentifier: '',
|
nostrNotificationIdentifier: '',
|
||||||
|
emailNotificationAddress: '',
|
||||||
formAllowedIPs: '',
|
formAllowedIPs: '',
|
||||||
formCallbackUrlRule: '',
|
formCallbackUrlRule: '',
|
||||||
formBlockedIPs: '',
|
formBlockedIPs: '',
|
||||||
@@ -270,6 +272,23 @@ window.AdminPageLogic = {
|
|||||||
m => m !== identifer
|
m => m !== identifer
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
addEmailNotificationAddress() {
|
||||||
|
const email = this.emailNotificationAddress.trim()
|
||||||
|
const emails = this.formData.lnbits_email_notifications_to_emails
|
||||||
|
if (email && email.length && !emails.includes(email)) {
|
||||||
|
this.formData.lnbits_email_notifications_to_emails = [...emails, email]
|
||||||
|
this.emailNotificationAddress = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeEmailNotificationAddress(email) {
|
||||||
|
const emails = this.formData.lnbits_email_notifications_to_emails
|
||||||
|
this.formData.lnbits_email_notifications_to_emails = emails.filter(
|
||||||
|
m => m !== email
|
||||||
|
)
|
||||||
|
},
|
||||||
|
hideInputsToggle() {
|
||||||
|
this.hideInputToggle = !this.hideInputToggle
|
||||||
|
},
|
||||||
async toggleServerLog() {
|
async toggleServerLog() {
|
||||||
this.serverlogEnabled = !this.serverlogEnabled
|
this.serverlogEnabled = !this.serverlogEnabled
|
||||||
if (this.serverlogEnabled) {
|
if (this.serverlogEnabled) {
|
||||||
@@ -419,7 +438,7 @@ window.AdminPageLogic = {
|
|||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/restart/')
|
.request('GET', '/admin/api/v1/restart/')
|
||||||
.then(response => {
|
.then(response => {
|
||||||
Quasar.Notify.create({
|
this.$q.notify({
|
||||||
type: 'positive',
|
type: 'positive',
|
||||||
message: 'Success! Restarted Server',
|
message: 'Success! Restarted Server',
|
||||||
icon: null
|
icon: null
|
||||||
@@ -431,7 +450,29 @@ window.AdminPageLogic = {
|
|||||||
formatDate(date) {
|
formatDate(date) {
|
||||||
return moment(date * 1000).fromNow()
|
return moment(date * 1000).fromNow()
|
||||||
},
|
},
|
||||||
|
sendTestEmail() {
|
||||||
|
LNbits.api
|
||||||
|
.request(
|
||||||
|
'GET',
|
||||||
|
'/admin/api/v1/testemail',
|
||||||
|
this.g.user.wallets[0].adminkey
|
||||||
|
)
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.status === 'error') {
|
||||||
|
throw new Error(response.data.message)
|
||||||
|
}
|
||||||
|
this.$q.notify({
|
||||||
|
message: 'Test email sent!',
|
||||||
|
color: 'positive'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$q.notify({
|
||||||
|
message: error.message,
|
||||||
|
color: 'negative'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
getAudit() {
|
getAudit() {
|
||||||
LNbits.api
|
LNbits.api
|
||||||
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ window.LNbits = {
|
|||||||
})
|
})
|
||||||
obj.walletOptions = obj.wallets.map(obj => {
|
obj.walletOptions = obj.wallets.map(obj => {
|
||||||
return {
|
return {
|
||||||
label: [obj.name, ' - ', obj.id].join(''),
|
label: [obj.name, ' - ', obj.id.substring(0, 5), '...'].join(''),
|
||||||
value: obj.id
|
value: obj.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -357,9 +357,10 @@ window.LNbits = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
prepareFilterQuery(tableConfig, props) {
|
prepareFilterQuery(tableConfig, props) {
|
||||||
|
tableConfig.filter = tableConfig.filter || {}
|
||||||
if (props) {
|
if (props) {
|
||||||
tableConfig.pagination = props.pagination
|
tableConfig.pagination = props.pagination
|
||||||
tableConfig.filter = {...tableConfig.filter, ...props.filter}
|
Object.assign(tableConfig.filter, props.filter)
|
||||||
}
|
}
|
||||||
const pagination = tableConfig.pagination
|
const pagination = tableConfig.pagination
|
||||||
tableConfig.loading = true
|
tableConfig.loading = true
|
||||||
|
|||||||
@@ -583,7 +583,7 @@ window.app.component('username-password', {
|
|||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.$emit('update:resetKey', this.reset_key)
|
this.$emit('update:resetKey', this.reset_key)
|
||||||
this.$emit('update:passeord_1', this.password)
|
this.$emit('update:password_1', this.password)
|
||||||
this.$emit('update:password_2', this.passwordRepeat)
|
this.$emit('update:password_2', this.passwordRepeat)
|
||||||
this.$emit('reset')
|
this.$emit('reset')
|
||||||
},
|
},
|
||||||
@@ -595,7 +595,6 @@ window.app.component('username-password', {
|
|||||||
},
|
},
|
||||||
async signInWithNostr() {
|
async signInWithNostr() {
|
||||||
try {
|
try {
|
||||||
console.log('### signInWithNostr')
|
|
||||||
const nostrToken = await this.createNostrToken()
|
const nostrToken = await this.createNostrToken()
|
||||||
if (!nostrToken) {
|
if (!nostrToken) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ window.app.component('lnbits-funding-sources', {
|
|||||||
'FakeWallet',
|
'FakeWallet',
|
||||||
'Fake Wallet',
|
'Fake Wallet',
|
||||||
{
|
{
|
||||||
fake_wallet_secret: 'Secret'
|
fake_wallet_secret: 'Secret',
|
||||||
|
lnbits_denomination: '"sats" or 3 Letter Custom Denomination'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ window.app.component('payment-list', {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
denomination: LNBITS_DENOMINATION,
|
denomination: LNBITS_DENOMINATION,
|
||||||
failedPaymentsToggle: false,
|
|
||||||
payments: [],
|
payments: [],
|
||||||
paymentsTable: {
|
paymentsTable: {
|
||||||
columns: [
|
columns: [
|
||||||
@@ -39,6 +38,13 @@ window.app.component('payment-list', {
|
|||||||
loading: false
|
loading: false
|
||||||
},
|
},
|
||||||
searchDate: {from: null, to: null},
|
searchDate: {from: null, to: null},
|
||||||
|
searchStatus: {
|
||||||
|
success: true,
|
||||||
|
pending: true,
|
||||||
|
failed: false,
|
||||||
|
incoming: true,
|
||||||
|
outgoing: true
|
||||||
|
},
|
||||||
exportTagName: '',
|
exportTagName: '',
|
||||||
exportPaymentTagList: [],
|
exportPaymentTagList: [],
|
||||||
paymentsCSV: {
|
paymentsCSV: {
|
||||||
@@ -263,18 +269,40 @@ window.app.component('payment-list', {
|
|||||||
console.error(e)
|
console.error(e)
|
||||||
return `${amount} ???`
|
return `${amount} ???`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
handleFilterChanged() {
|
||||||
|
const {success, pending, failed, incoming, outgoing} = this.searchStatus
|
||||||
|
|
||||||
|
delete this.paymentsTable.filter['status[ne]']
|
||||||
|
delete this.paymentsTable.filter['status[eq]']
|
||||||
|
if (success && pending && failed) {
|
||||||
|
// No status filter
|
||||||
|
} else if (success && pending) {
|
||||||
|
this.paymentsTable.filter['status[ne]'] = 'failed'
|
||||||
|
} else if (success && failed) {
|
||||||
|
this.paymentsTable.filter['status[ne]'] = 'pending'
|
||||||
|
} else if (failed && pending) {
|
||||||
|
this.paymentsTable.filter['status[ne]'] = 'success'
|
||||||
|
} else if (success) {
|
||||||
|
this.paymentsTable.filter['status[eq]'] = 'success'
|
||||||
|
} else if (pending) {
|
||||||
|
this.paymentsTable.filter['status[eq]'] = 'pending'
|
||||||
|
} else if (failed) {
|
||||||
|
this.paymentsTable.filter['status[eq]'] = 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.paymentsTable.filter['amount[ge]']
|
||||||
|
delete this.paymentsTable.filter['amount[le]']
|
||||||
|
if (incoming && outgoing) {
|
||||||
|
// do nothing
|
||||||
|
} else if (incoming) {
|
||||||
|
this.paymentsTable.filter['amount[ge]'] = 0
|
||||||
|
} else if (outgoing) {
|
||||||
|
this.paymentsTable.filter['amount[le]'] = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
failedPaymentsToggle(newVal) {
|
|
||||||
if (newVal === false) {
|
|
||||||
this.paymentsTable.filter['status[ne]'] = 'failed'
|
|
||||||
} else {
|
|
||||||
delete this.paymentsTable.filter['status[ne]']
|
|
||||||
}
|
|
||||||
this.paymentsTable.pagination.page = 1
|
|
||||||
this.fetchPayments()
|
|
||||||
},
|
|
||||||
'paymentsTable.search': {
|
'paymentsTable.search': {
|
||||||
handler() {
|
handler() {
|
||||||
const props = {}
|
const props = {}
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ window.app = Vue.createApp({
|
|||||||
description: ''
|
description: ''
|
||||||
},
|
},
|
||||||
isUserAuthorized: false,
|
isUserAuthorized: false,
|
||||||
authAction: Quasar.LocalStorage.getItem('lnbits.disclaimerShown')
|
authAction: 'login',
|
||||||
? 'login'
|
|
||||||
: 'register',
|
|
||||||
authMethod: 'username-password',
|
authMethod: 'username-password',
|
||||||
usr: '',
|
usr: '',
|
||||||
username: '',
|
username: '',
|
||||||
@@ -104,6 +102,12 @@ window.app = Vue.createApp({
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.description = SITE_DESCRIPTION
|
this.description = SITE_DESCRIPTION
|
||||||
|
this.allowedRegister = ALLOWED_REGISTER
|
||||||
|
this.authAction =
|
||||||
|
!this.allowedRegister ||
|
||||||
|
Quasar.LocalStorage.getItem('lnbits.disclaimerShown')
|
||||||
|
? 'login'
|
||||||
|
: 'register'
|
||||||
this.isUserAuthorized = !!this.$q.cookies.get('is_lnbits_user_authorized')
|
this.isUserAuthorized = !!this.$q.cookies.get('is_lnbits_user_authorized')
|
||||||
const _acccess_cookies_for_safari_refresh_do_not_delete = document.cookie
|
const _acccess_cookies_for_safari_refresh_do_not_delete = document.cookie
|
||||||
if (this.isUserAuthorized) {
|
if (this.isUserAuthorized) {
|
||||||
|
|||||||
@@ -352,9 +352,6 @@ window.UsersPageLogic = {
|
|||||||
})
|
})
|
||||||
.catch(LNbits.utils.notifyApiError)
|
.catch(LNbits.utils.notifyApiError)
|
||||||
},
|
},
|
||||||
exportUsers() {
|
|
||||||
console.log('export users')
|
|
||||||
},
|
|
||||||
async showAccountPage(user_id) {
|
async showAccountPage(user_id) {
|
||||||
this.activeUser.showPassword = false
|
this.activeUser.showPassword = false
|
||||||
this.activeUser.showUserId = false
|
this.activeUser.showUserId = false
|
||||||
|
|||||||
@@ -854,7 +854,9 @@ window.WalletPageLogic = {
|
|||||||
handleFilterChange(value = {}) {
|
handleFilterChange(value = {}) {
|
||||||
if (
|
if (
|
||||||
this.paymentsFilter['time[ge]'] !== value['time[ge]'] ||
|
this.paymentsFilter['time[ge]'] !== value['time[ge]'] ||
|
||||||
this.paymentsFilter['time[le]'] !== value['time[le]']
|
this.paymentsFilter['time[le]'] !== value['time[le]'] ||
|
||||||
|
this.paymentsFilter['amount[ge]'] !== value['amount[ge]'] ||
|
||||||
|
this.paymentsFilter['amount[le]'] !== value['amount[le]']
|
||||||
) {
|
) {
|
||||||
this.refreshCharts()
|
this.refreshCharts()
|
||||||
}
|
}
|
||||||
@@ -884,6 +886,19 @@ window.WalletPageLogic = {
|
|||||||
filterChartData(data) {
|
filterChartData(data) {
|
||||||
const timeFrom = this.paymentsFilter['time[ge]'] + 'T00:00:00'
|
const timeFrom = this.paymentsFilter['time[ge]'] + 'T00:00:00'
|
||||||
const timeTo = this.paymentsFilter['time[le]'] + 'T23:59:59'
|
const timeTo = this.paymentsFilter['time[le]'] + 'T23:59:59'
|
||||||
|
|
||||||
|
let totalBalance = 0
|
||||||
|
data = data.map(p => {
|
||||||
|
if (this.paymentsFilter['amount[ge]'] !== undefined) {
|
||||||
|
totalBalance += p.balance_in
|
||||||
|
return {...p, balance: totalBalance, balance_out: 0, count_out: 0}
|
||||||
|
}
|
||||||
|
if (this.paymentsFilter['amount[le]'] !== undefined) {
|
||||||
|
totalBalance -= p.balance_out
|
||||||
|
return {...p, balance: totalBalance, balance_in: 0, count_in: 0}
|
||||||
|
}
|
||||||
|
return {...p}
|
||||||
|
})
|
||||||
data = data.filter(p => {
|
data = data.filter(p => {
|
||||||
if (
|
if (
|
||||||
this.paymentsFilter['time[ge]'] &&
|
this.paymentsFilter['time[ge]'] &&
|
||||||
@@ -899,6 +914,7 @@ window.WalletPageLogic = {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
const labels = data.map(s =>
|
const labels = data.map(s =>
|
||||||
new Date(s.date).toLocaleString('default', {
|
new Date(s.date).toLocaleString('default', {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ $themes: (
|
|||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
'bitcoin': (
|
'bitcoin': (
|
||||||
primary: #ff9853,
|
primary: #ea611d,
|
||||||
secondary: #ff7353,
|
secondary: #e56f35,
|
||||||
dark: #2d293b,
|
dark: #2d293b,
|
||||||
info: #333646,
|
info: #333646,
|
||||||
marginal-bg: #2d293b,
|
marginal-bg: #000000,
|
||||||
marginal-text: #fff
|
marginal-text: #fff
|
||||||
),
|
),
|
||||||
'freedom': (
|
'freedom': (
|
||||||
|
|||||||
Vendored
+45
-85
@@ -1,4 +1,4 @@
|
|||||||
// Axios v1.7.7 Copyright (c) 2024 Matt Zabriskie and contributors
|
/*! Axios v1.8.2 Copyright (c) 2025 Matt Zabriskie and contributors */
|
||||||
(function (global, factory) {
|
(function (global, factory) {
|
||||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||||
typeof define === 'function' && define.amd ? define(factory) :
|
typeof define === 'function' && define.amd ? define(factory) :
|
||||||
@@ -1266,23 +1266,6 @@
|
|||||||
var toFiniteNumber = function toFiniteNumber(value, defaultValue) {
|
var toFiniteNumber = function toFiniteNumber(value, defaultValue) {
|
||||||
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
|
return value != null && Number.isFinite(value = +value) ? value : defaultValue;
|
||||||
};
|
};
|
||||||
var ALPHA = 'abcdefghijklmnopqrstuvwxyz';
|
|
||||||
var DIGIT = '0123456789';
|
|
||||||
var ALPHABET = {
|
|
||||||
DIGIT: DIGIT,
|
|
||||||
ALPHA: ALPHA,
|
|
||||||
ALPHA_DIGIT: ALPHA + ALPHA.toUpperCase() + DIGIT
|
|
||||||
};
|
|
||||||
var generateString = function generateString() {
|
|
||||||
var size = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 16;
|
|
||||||
var alphabet = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ALPHABET.ALPHA_DIGIT;
|
|
||||||
var str = '';
|
|
||||||
var length = alphabet.length;
|
|
||||||
while (size--) {
|
|
||||||
str += alphabet[Math.random() * length | 0];
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the thing is a FormData object, return true, otherwise return false.
|
* If the thing is a FormData object, return true, otherwise return false.
|
||||||
@@ -1399,8 +1382,6 @@
|
|||||||
findKey: findKey,
|
findKey: findKey,
|
||||||
global: _global,
|
global: _global,
|
||||||
isContextDefined: isContextDefined,
|
isContextDefined: isContextDefined,
|
||||||
ALPHABET: ALPHABET,
|
|
||||||
generateString: generateString,
|
|
||||||
isSpecCompliantForm: isSpecCompliantForm,
|
isSpecCompliantForm: isSpecCompliantForm,
|
||||||
toJSONObject: toJSONObject,
|
toJSONObject: toJSONObject,
|
||||||
isAsyncFn: isAsyncFn,
|
isAsyncFn: isAsyncFn,
|
||||||
@@ -1735,7 +1716,7 @@
|
|||||||
*
|
*
|
||||||
* @param {string} url The base of the url (e.g., http://www.google.com)
|
* @param {string} url The base of the url (e.g., http://www.google.com)
|
||||||
* @param {object} [params] The params to be appended
|
* @param {object} [params] The params to be appended
|
||||||
* @param {?object} options
|
* @param {?(object|Function)} options
|
||||||
*
|
*
|
||||||
* @returns {string} The formatted url
|
* @returns {string} The formatted url
|
||||||
*/
|
*/
|
||||||
@@ -1745,6 +1726,11 @@
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
var _encode = options && options.encode || encode;
|
var _encode = options && options.encode || encode;
|
||||||
|
if (utils$1.isFunction(options)) {
|
||||||
|
options = {
|
||||||
|
serialize: options
|
||||||
|
};
|
||||||
|
}
|
||||||
var serializeFn = options && options.serialize;
|
var serializeFn = options && options.serialize;
|
||||||
var serializedParams;
|
var serializedParams;
|
||||||
if (serializeFn) {
|
if (serializeFn) {
|
||||||
@@ -2642,60 +2628,14 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
var isURLSameOrigin = platform.hasStandardBrowserEnv ?
|
var isURLSameOrigin = platform.hasStandardBrowserEnv ? function (origin, isMSIE) {
|
||||||
// Standard browser envs have full support of the APIs needed to test
|
return function (url) {
|
||||||
// whether the request URL is of the same origin as current location.
|
url = new URL(url, platform.origin);
|
||||||
function standardBrowserEnv() {
|
return origin.protocol === url.protocol && origin.host === url.host && (isMSIE || origin.port === url.port);
|
||||||
var msie = platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent);
|
|
||||||
var urlParsingNode = document.createElement('a');
|
|
||||||
var originURL;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a URL to discover its components
|
|
||||||
*
|
|
||||||
* @param {String} url The URL to be parsed
|
|
||||||
* @returns {Object}
|
|
||||||
*/
|
|
||||||
function resolveURL(url) {
|
|
||||||
var href = url;
|
|
||||||
if (msie) {
|
|
||||||
// IE needs attribute set twice to normalize properties
|
|
||||||
urlParsingNode.setAttribute('href', href);
|
|
||||||
href = urlParsingNode.href;
|
|
||||||
}
|
|
||||||
urlParsingNode.setAttribute('href', href);
|
|
||||||
|
|
||||||
// urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
|
|
||||||
return {
|
|
||||||
href: urlParsingNode.href,
|
|
||||||
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
|
|
||||||
host: urlParsingNode.host,
|
|
||||||
search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
|
|
||||||
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
|
|
||||||
hostname: urlParsingNode.hostname,
|
|
||||||
port: urlParsingNode.port,
|
|
||||||
pathname: urlParsingNode.pathname.charAt(0) === '/' ? urlParsingNode.pathname : '/' + urlParsingNode.pathname
|
|
||||||
};
|
|
||||||
}
|
|
||||||
originURL = resolveURL(window.location.href);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if a URL shares the same origin as the current location
|
|
||||||
*
|
|
||||||
* @param {String} requestURL The URL to test
|
|
||||||
* @returns {boolean} True if URL shares the same origin, otherwise false
|
|
||||||
*/
|
|
||||||
return function isURLSameOrigin(requestURL) {
|
|
||||||
var parsed = utils$1.isString(requestURL) ? resolveURL(requestURL) : requestURL;
|
|
||||||
return parsed.protocol === originURL.protocol && parsed.host === originURL.host;
|
|
||||||
};
|
};
|
||||||
}() :
|
}(new URL(platform.origin), platform.navigator && /(msie|trident)/i.test(platform.navigator.userAgent)) : function () {
|
||||||
// Non standard browser envs (web workers, react-native) lack needed support.
|
return true;
|
||||||
function nonStandardBrowserEnv() {
|
};
|
||||||
return function isURLSameOrigin() {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
}();
|
|
||||||
|
|
||||||
var cookies = platform.hasStandardBrowserEnv ?
|
var cookies = platform.hasStandardBrowserEnv ?
|
||||||
// Standard browser envs support document.cookie
|
// Standard browser envs support document.cookie
|
||||||
@@ -2761,8 +2701,9 @@
|
|||||||
*
|
*
|
||||||
* @returns {string} The combined full path
|
* @returns {string} The combined full path
|
||||||
*/
|
*/
|
||||||
function buildFullPath(baseURL, requestedURL) {
|
function buildFullPath(baseURL, requestedURL, allowAbsoluteUrls) {
|
||||||
if (baseURL && !isAbsoluteURL(requestedURL)) {
|
var isRelativeUrl = !isAbsoluteURL(requestedURL);
|
||||||
|
if (baseURL && isRelativeUrl || allowAbsoluteUrls == false) {
|
||||||
return combineURLs(baseURL, requestedURL);
|
return combineURLs(baseURL, requestedURL);
|
||||||
}
|
}
|
||||||
return requestedURL;
|
return requestedURL;
|
||||||
@@ -2785,7 +2726,7 @@
|
|||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
config2 = config2 || {};
|
config2 = config2 || {};
|
||||||
var config = {};
|
var config = {};
|
||||||
function getMergedValue(target, source, caseless) {
|
function getMergedValue(target, source, prop, caseless) {
|
||||||
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
|
if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
|
||||||
return utils$1.merge.call({
|
return utils$1.merge.call({
|
||||||
caseless: caseless
|
caseless: caseless
|
||||||
@@ -2799,11 +2740,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
function mergeDeepProperties(a, b, caseless) {
|
function mergeDeepProperties(a, b, prop, caseless) {
|
||||||
if (!utils$1.isUndefined(b)) {
|
if (!utils$1.isUndefined(b)) {
|
||||||
return getMergedValue(a, b, caseless);
|
return getMergedValue(a, b, prop, caseless);
|
||||||
} else if (!utils$1.isUndefined(a)) {
|
} else if (!utils$1.isUndefined(a)) {
|
||||||
return getMergedValue(undefined, a, caseless);
|
return getMergedValue(undefined, a, prop, caseless);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2860,8 +2801,8 @@
|
|||||||
socketPath: defaultToConfig2,
|
socketPath: defaultToConfig2,
|
||||||
responseEncoding: defaultToConfig2,
|
responseEncoding: defaultToConfig2,
|
||||||
validateStatus: mergeDirectKeys,
|
validateStatus: mergeDirectKeys,
|
||||||
headers: function headers(a, b) {
|
headers: function headers(a, b, prop) {
|
||||||
return mergeDeepProperties(headersToObject(a), headersToObject(b), true);
|
return mergeDeepProperties(headersToObject(a), headersToObject(b), prop, true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
|
utils$1.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
|
||||||
@@ -3717,7 +3658,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var VERSION = "1.7.7";
|
var VERSION = "1.8.2";
|
||||||
|
|
||||||
var validators$1 = {};
|
var validators$1 = {};
|
||||||
|
|
||||||
@@ -3756,6 +3697,13 @@
|
|||||||
return validator ? validator(value, opt, opts) : true;
|
return validator ? validator(value, opt, opts) : true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
validators$1.spelling = function spelling(correctSpelling) {
|
||||||
|
return function (value, opt) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn("".concat(opt, " is likely a misspelling of ").concat(correctSpelling));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assert object's properties type
|
* Assert object's properties type
|
||||||
@@ -3838,7 +3786,8 @@
|
|||||||
_context.prev = 6;
|
_context.prev = 6;
|
||||||
_context.t0 = _context["catch"](0);
|
_context.t0 = _context["catch"](0);
|
||||||
if (_context.t0 instanceof Error) {
|
if (_context.t0 instanceof Error) {
|
||||||
Error.captureStackTrace ? Error.captureStackTrace(dummy = {}) : dummy = new Error();
|
dummy = {};
|
||||||
|
Error.captureStackTrace ? Error.captureStackTrace(dummy) : dummy = new Error();
|
||||||
|
|
||||||
// slice off the Error: ... line
|
// slice off the Error: ... line
|
||||||
stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
|
stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
|
||||||
@@ -3901,6 +3850,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set config.allowAbsoluteUrls
|
||||||
|
if (config.allowAbsoluteUrls !== undefined) ; else if (this.defaults.allowAbsoluteUrls !== undefined) {
|
||||||
|
config.allowAbsoluteUrls = this.defaults.allowAbsoluteUrls;
|
||||||
|
} else {
|
||||||
|
config.allowAbsoluteUrls = true;
|
||||||
|
}
|
||||||
|
validator.assertOptions(config, {
|
||||||
|
baseUrl: validators.spelling('baseURL'),
|
||||||
|
withXsrfToken: validators.spelling('withXSRFToken')
|
||||||
|
}, true);
|
||||||
|
|
||||||
// Set config.method
|
// Set config.method
|
||||||
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
|
config.method = (config.method || this.defaults.method || 'get').toLowerCase();
|
||||||
|
|
||||||
@@ -3968,7 +3928,7 @@
|
|||||||
key: "getUri",
|
key: "getUri",
|
||||||
value: function getUri(config) {
|
value: function getUri(config) {
|
||||||
config = mergeConfig(this.defaults, config);
|
config = mergeConfig(this.defaults, config);
|
||||||
var fullPath = buildFullPath(config.baseURL, config.url);
|
var fullPath = buildFullPath(config.baseURL, config.url, config.allowAbsoluteUrls);
|
||||||
return buildURL(fullPath, config.params, config.paramsSerializer);
|
return buildURL(fullPath, config.params, config.paramsSerializer);
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
+3
-3
File diff suppressed because one or more lines are too long
@@ -1,9 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
from http import HTTPStatus
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
Coroutine,
|
||||||
@@ -13,11 +11,8 @@ from typing import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from py_vapid import Vapid
|
|
||||||
from pywebpush import WebPushException, webpush
|
|
||||||
|
|
||||||
from lnbits.core.crud import (
|
from lnbits.core.crud import (
|
||||||
delete_webpush_subscriptions,
|
|
||||||
get_payments,
|
get_payments,
|
||||||
get_standalone_payment,
|
get_standalone_payment,
|
||||||
update_payment,
|
update_payment,
|
||||||
@@ -216,28 +211,3 @@ async def invoice_callback_dispatcher(checking_id: str, is_internal: bool = Fals
|
|||||||
for name, send_chan in invoice_listeners.items():
|
for name, send_chan in invoice_listeners.items():
|
||||||
logger.trace(f"invoice listeners: sending to `{name}`")
|
logger.trace(f"invoice listeners: sending to `{name}`")
|
||||||
await send_chan.put(payment)
|
await send_chan.put(payment)
|
||||||
|
|
||||||
|
|
||||||
async def send_push_notification(subscription, title, body, url=""):
|
|
||||||
vapid = Vapid()
|
|
||||||
try:
|
|
||||||
logger.debug("sending push notification")
|
|
||||||
webpush(
|
|
||||||
json.loads(subscription.data),
|
|
||||||
json.dumps({"title": title, "body": body, "url": url}),
|
|
||||||
(
|
|
||||||
vapid.from_pem(bytes(settings.lnbits_webpush_privkey, "utf-8"))
|
|
||||||
if settings.lnbits_webpush_privkey
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
{"aud": "", "sub": "mailto:alan@lnbits.com"},
|
|
||||||
)
|
|
||||||
except WebPushException as e:
|
|
||||||
if e.response and e.response.status_code == HTTPStatus.GONE:
|
|
||||||
# cleanup unsubscribed or expired push subscriptions
|
|
||||||
await delete_webpush_subscriptions(subscription.endpoint)
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
f"failed sending push notification: "
|
|
||||||
f"{e.response.text if e.response else e}"
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -89,11 +89,11 @@
|
|||||||
>
|
>
|
||||||
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
|
{% if LNBITS_SERVICE_FEE_MAX > 0 %}
|
||||||
<span
|
<span
|
||||||
v-text='$t("service_fee_max", { amount: "{{ LNBITS_SERVICE_FEE }}", max: "{{ LNBITS_SERVICE_FEE_MAX }}"})'
|
v-text='$t("service_fee_max_badge", { amount: "{{ LNBITS_SERVICE_FEE }}", max: "{{ LNBITS_SERVICE_FEE_MAX }}", denom: "{{ LNBITS_DENOMINATION }}"})'
|
||||||
></span>
|
></span>
|
||||||
{%else%}
|
{%else%}
|
||||||
<span
|
<span
|
||||||
v-text='$t("service_fee", { amount: "{{ LNBITS_SERVICE_FEE }}" })'
|
v-text='$t("service_fee_badge", { amount: "{{ LNBITS_SERVICE_FEE}}"})'
|
||||||
></span>
|
></span>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
<q-tooltip
|
<q-tooltip
|
||||||
|
|||||||
+102
-61
@@ -419,7 +419,6 @@
|
|||||||
dense
|
dense
|
||||||
use-input
|
use-input
|
||||||
use-chips
|
use-chips
|
||||||
multiple
|
|
||||||
hide-dropdown-icon
|
hide-dropdown-icon
|
||||||
></q-select>
|
></q-select>
|
||||||
<div v-else-if="o.type === 'bool'">
|
<div v-else-if="o.type === 'bool'">
|
||||||
@@ -639,16 +638,100 @@
|
|||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gt-sm col-auto">
|
<div class="gt-sm col-auto">
|
||||||
|
<q-btn icon="event" flat color="grey">
|
||||||
|
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
||||||
|
<q-date v-model="searchDate" mask="YYYY-MM-DD" range />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
label="Search"
|
||||||
|
@click="searchByDate()"
|
||||||
|
color="primary"
|
||||||
|
flat
|
||||||
|
class="float-left"
|
||||||
|
v-close-popup
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<q-btn
|
||||||
|
v-close-popup
|
||||||
|
@click="clearDateSeach()"
|
||||||
|
label="Clear"
|
||||||
|
class="float-right"
|
||||||
|
color="grey"
|
||||||
|
flat
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-popup-proxy>
|
||||||
|
<q-badge
|
||||||
|
v-if="searchDate?.to || searchDate?.from"
|
||||||
|
class="q-mt-lg q-mr-md"
|
||||||
|
color="primary"
|
||||||
|
rounded
|
||||||
|
floating
|
||||||
|
style="border-radius: 6px"
|
||||||
|
></q-badge>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('filter_date')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn color="grey" icon="filter_alt" flat>
|
||||||
|
<q-menu>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.success"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Success Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.pending"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Pending Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.failed"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Failed Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-separator></q-separator>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.incoming"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Incoming Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
<q-item dense>
|
||||||
|
<q-checkbox
|
||||||
|
v-model="searchStatus.outgoing"
|
||||||
|
@click="handleFilterChanged"
|
||||||
|
label="Outgoing Payments"
|
||||||
|
></q-checkbox>
|
||||||
|
</q-item>
|
||||||
|
</q-menu>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('filter_payments')"></span>
|
||||||
|
</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
<q-btn-dropdown
|
<q-btn-dropdown
|
||||||
|
dense
|
||||||
outline
|
outline
|
||||||
persistent
|
persistent
|
||||||
dense
|
icon="archive"
|
||||||
|
split
|
||||||
class="q-mr-sm"
|
class="q-mr-sm"
|
||||||
color="grey"
|
color="grey"
|
||||||
label="Export"
|
|
||||||
split
|
|
||||||
@click="exportCSV(false)"
|
@click="exportCSV(false)"
|
||||||
>
|
>
|
||||||
|
<q-tooltip>
|
||||||
|
<span v-text="$t('export_csv')"></span>
|
||||||
|
</q-tooltip>
|
||||||
<q-list>
|
<q-list>
|
||||||
<q-item>
|
<q-item>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
@@ -688,59 +771,12 @@
|
|||||||
outline
|
outline
|
||||||
color="grey"
|
color="grey"
|
||||||
@click="exportCSV(true)"
|
@click="exportCSV(true)"
|
||||||
label="Export to CSV with details"
|
:label="$t('export_csv_details')"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
</q-btn-dropdown>
|
</q-btn-dropdown>
|
||||||
<q-btn icon="event" outline flat color="grey">
|
|
||||||
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
|
|
||||||
<q-date v-model="searchDate" mask="YYYY-MM-DD" range />
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn
|
|
||||||
label="Search"
|
|
||||||
@click="searchByDate()"
|
|
||||||
color="primary"
|
|
||||||
flat
|
|
||||||
class="float-left"
|
|
||||||
v-close-popup
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<q-btn
|
|
||||||
v-close-popup
|
|
||||||
@click="clearDateSeach()"
|
|
||||||
label="Clear"
|
|
||||||
class="float-right"
|
|
||||||
color="grey"
|
|
||||||
flat
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</q-popup-proxy>
|
|
||||||
<q-badge
|
|
||||||
v-if="searchDate?.to || searchDate?.from"
|
|
||||||
class="q-mt-lg q-mr-md"
|
|
||||||
color="primary"
|
|
||||||
rounded
|
|
||||||
floating
|
|
||||||
style="border-radius: 6px"
|
|
||||||
/>
|
|
||||||
</q-btn>
|
|
||||||
|
|
||||||
<q-checkbox
|
|
||||||
v-model="failedPaymentsToggle"
|
|
||||||
checked-icon="warning"
|
|
||||||
unchecked-icon="warning_off"
|
|
||||||
:color="failedPaymentsToggle ? 'yellow' : 'grey'"
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
<q-tooltip>
|
|
||||||
<span v-text="`Include failed payments`"></span>
|
|
||||||
</q-tooltip>
|
|
||||||
</q-checkbox>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-my-md"></div>
|
<div class="row q-my-md"></div>
|
||||||
@@ -1021,7 +1057,16 @@
|
|||||||
|
|
||||||
<template id="lnbits-funding-sources">
|
<template id="lnbits-funding-sources">
|
||||||
<div class="funding-sources">
|
<div class="funding-sources">
|
||||||
<h6 class="q-mt-xl q-mb-md">Funding Sources</h6>
|
<h6 class="q-my-none q-mb-sm">
|
||||||
|
<span v-text="$t('funding_sources')"></span>
|
||||||
|
<q-btn
|
||||||
|
round
|
||||||
|
flat
|
||||||
|
@click="this.hideInput = !this.hideInput"
|
||||||
|
:icon="this.hideInput ? 'visibility_off' : 'visibility'"
|
||||||
|
></q-btn>
|
||||||
|
</h6>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<p>Active Funding<small> (Requires server restart)</small></p>
|
<p>Active Funding<small> (Requires server restart)</small></p>
|
||||||
@@ -1059,13 +1104,6 @@
|
|||||||
:label="prop.label"
|
:label="prop.label"
|
||||||
:hint="prop.hint"
|
:hint="prop.hint"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
|
||||||
<q-icon
|
|
||||||
:name="hideInput ? 'visibility_off' : 'visibility'"
|
|
||||||
class="cursor-pointer"
|
|
||||||
@click="this.hideInput = !this.hideInput"
|
|
||||||
></q-icon>
|
|
||||||
</template>
|
|
||||||
</q-input>
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1223,7 +1261,7 @@
|
|||||||
</q-form>
|
</q-form>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<!-- REGISTER -->
|
<!-- REGISTER -->
|
||||||
<q-card-section v-if="authAction === 'register'">
|
<q-card-section v-if="allowed_new_users && authAction === 'register'">
|
||||||
<q-form @submit="register" class="q-gutter-sm">
|
<q-form @submit="register" class="q-gutter-sm">
|
||||||
<q-input
|
<q-input
|
||||||
dense
|
dense
|
||||||
@@ -1266,6 +1304,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</q-form>
|
</q-form>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
<q-card-section v-else-if="!allowed_new_users && authAction === 'register'">
|
||||||
|
<h5 class="text-center" v-text="$t('new_user_not_allowed')"></h5>
|
||||||
|
</q-card-section>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<!-- RESET -->
|
<!-- RESET -->
|
||||||
<q-card-section v-if="authAction === 'reset'">
|
<q-card-section v-if="authAction === 'reset'">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any, NamedTuple, Optional
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class Cache:
|
|||||||
self.interval = interval
|
self.interval = interval
|
||||||
self._values: dict[Any, Cached] = {}
|
self._values: dict[Any, Cached] = {}
|
||||||
|
|
||||||
def get(self, key: str, default=None) -> Optional[Any]:
|
def get(self, key: str, default=None) -> Any | None:
|
||||||
cached = self._values.get(key)
|
cached = self._values.get(key)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
if cached.expiry > time():
|
if cached.expiry > time():
|
||||||
@@ -35,7 +35,7 @@ class Cache:
|
|||||||
def set(self, key: str, value: Any, expiry: float = 10):
|
def set(self, key: str, value: Any, expiry: float = 10):
|
||||||
self._values[key] = Cached(value, time() + expiry)
|
self._values[key] = Cached(value, time() + expiry)
|
||||||
|
|
||||||
def pop(self, key: str, default=None) -> Optional[Any]:
|
def pop(self, key: str, default=None) -> Any | None:
|
||||||
cached = self._values.pop(key, None)
|
cached = self._values.pop(key, None)
|
||||||
if cached and cached.expiry > time():
|
if cached and cached.expiry > time():
|
||||||
return cached.value
|
return cached.value
|
||||||
|
|||||||
+11
-5
@@ -63,10 +63,15 @@ class AESCipher:
|
|||||||
final_key += key
|
final_key += key
|
||||||
return final_key[:output]
|
return final_key[:output]
|
||||||
|
|
||||||
def decrypt(self, encrypted: str) -> str:
|
def decrypt(self, encrypted: str, urlsafe: bool = False) -> str:
|
||||||
"""Decrypts a string using AES-256-CBC."""
|
"""Decrypts a string using AES-256-CBC."""
|
||||||
passphrase = self.passphrase
|
passphrase = self.passphrase
|
||||||
encrypted_bytes = base64.b64decode(encrypted)
|
|
||||||
|
if urlsafe:
|
||||||
|
encrypted_bytes = base64.urlsafe_b64decode(encrypted)
|
||||||
|
else:
|
||||||
|
encrypted_bytes = base64.b64decode(encrypted)
|
||||||
|
|
||||||
assert encrypted_bytes[0:8] == b"Salted__"
|
assert encrypted_bytes[0:8] == b"Salted__"
|
||||||
salt = encrypted_bytes[8:16]
|
salt = encrypted_bytes[8:16]
|
||||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||||
@@ -78,13 +83,14 @@ class AESCipher:
|
|||||||
except UnicodeDecodeError as exc:
|
except UnicodeDecodeError as exc:
|
||||||
raise ValueError("Wrong passphrase") from exc
|
raise ValueError("Wrong passphrase") from exc
|
||||||
|
|
||||||
def encrypt(self, message: bytes) -> str:
|
def encrypt(self, message: bytes, urlsafe: bool = False) -> str:
|
||||||
passphrase = self.passphrase
|
passphrase = self.passphrase
|
||||||
salt = Random.new().read(8)
|
salt = Random.new().read(8)
|
||||||
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16)
|
||||||
key = key_iv[:32]
|
key = key_iv[:32]
|
||||||
iv = key_iv[32:]
|
iv = key_iv[32:]
|
||||||
aes = AES.new(key, AES.MODE_CBC, iv)
|
aes = AES.new(key, AES.MODE_CBC, iv)
|
||||||
return base64.b64encode(
|
encoded = b"Salted__" + salt + aes.encrypt(self.pad(message))
|
||||||
b"Salted__" + salt + aes.encrypt(self.pad(message))
|
return (
|
||||||
|
base64.urlsafe_b64encode(encoded) if urlsafe else base64.b64encode(encoded)
|
||||||
).decode()
|
).decode()
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ currencies = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def allowed_currencies():
|
def allowed_currencies() -> list[str]:
|
||||||
if len(settings.lnbits_allowed_currencies) > 0:
|
if len(settings.lnbits_allowed_currencies) > 0:
|
||||||
return [
|
return [
|
||||||
item
|
item
|
||||||
@@ -187,6 +187,9 @@ def allowed_currencies():
|
|||||||
|
|
||||||
|
|
||||||
async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
async def btc_rates(currency: str) -> list[tuple[str, float]]:
|
||||||
|
if currency.upper() not in allowed_currencies():
|
||||||
|
raise ValueError(f"Currency '{currency}' not allowed.")
|
||||||
|
|
||||||
def replacements(ticker: str):
|
def replacements(ticker: str):
|
||||||
return {
|
return {
|
||||||
"FROM": "BTC",
|
"FROM": "BTC",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from lnbits.nodes import set_node_class
|
from lnbits.nodes import set_node_class
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
@@ -33,7 +32,7 @@ from .void import VoidWallet
|
|||||||
from .zbd import ZBDWallet
|
from .zbd import ZBDWallet
|
||||||
|
|
||||||
|
|
||||||
def set_funding_source(class_name: Optional[str] = None):
|
def set_funding_source(class_name: str | None = None):
|
||||||
backend_wallet_class = class_name or settings.lnbits_backend_wallet_class
|
backend_wallet_class = class_name or settings.lnbits_backend_wallet_class
|
||||||
funding_source_constructor = getattr(wallets_module, backend_wallet_class)
|
funding_source_constructor = getattr(wallets_module, backend_wallet_class)
|
||||||
global funding_source
|
global funding_source
|
||||||
|
|||||||
+17
-17
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING, AsyncGenerator, Coroutine, NamedTuple, Optional
|
from typing import TYPE_CHECKING, AsyncGenerator, Coroutine, NamedTuple
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
@@ -11,15 +11,15 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class StatusResponse(NamedTuple):
|
class StatusResponse(NamedTuple):
|
||||||
error_message: Optional[str]
|
error_message: str | None
|
||||||
balance_msat: int
|
balance_msat: int
|
||||||
|
|
||||||
|
|
||||||
class InvoiceResponse(NamedTuple):
|
class InvoiceResponse(NamedTuple):
|
||||||
ok: bool
|
ok: bool
|
||||||
checking_id: Optional[str] = None # payment_hash, rpc_id
|
checking_id: str | None = None # payment_hash, rpc_id
|
||||||
payment_request: Optional[str] = None
|
payment_request: str | None = None
|
||||||
error_message: Optional[str] = None
|
error_message: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self) -> bool:
|
def success(self) -> bool:
|
||||||
@@ -36,11 +36,11 @@ class InvoiceResponse(NamedTuple):
|
|||||||
|
|
||||||
class PaymentResponse(NamedTuple):
|
class PaymentResponse(NamedTuple):
|
||||||
# when ok is None it means we don't know if this succeeded
|
# when ok is None it means we don't know if this succeeded
|
||||||
ok: Optional[bool] = None
|
ok: bool | None = None
|
||||||
checking_id: Optional[str] = None # payment_hash, rcp_id
|
checking_id: str | None = None # payment_hash, rcp_id
|
||||||
fee_msat: Optional[int] = None
|
fee_msat: int | None = None
|
||||||
preimage: Optional[str] = None
|
preimage: str | None = None
|
||||||
error_message: Optional[str] = None
|
error_message: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self) -> bool:
|
def success(self) -> bool:
|
||||||
@@ -56,9 +56,9 @@ class PaymentResponse(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class PaymentStatus(NamedTuple):
|
class PaymentStatus(NamedTuple):
|
||||||
paid: Optional[bool] = None
|
paid: bool | None = None
|
||||||
fee_msat: Optional[int] = None
|
fee_msat: int | None = None
|
||||||
preimage: Optional[str] = None
|
preimage: str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def success(self) -> bool:
|
def success(self) -> bool:
|
||||||
@@ -94,7 +94,7 @@ class PaymentPendingStatus(PaymentStatus):
|
|||||||
|
|
||||||
class Wallet(ABC):
|
class Wallet(ABC):
|
||||||
|
|
||||||
__node_cls__: Optional[type[Node]] = None
|
__node_cls__: type[Node] | None = None
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.pending_invoices: list[str] = []
|
self.pending_invoices: list[str] = []
|
||||||
@@ -111,9 +111,9 @@ class Wallet(ABC):
|
|||||||
def create_invoice(
|
def create_invoice(
|
||||||
self,
|
self,
|
||||||
amount: int,
|
amount: int,
|
||||||
memo: Optional[str] = None,
|
memo: str | None = None,
|
||||||
description_hash: Optional[bytes] = None,
|
description_hash: bytes | None = None,
|
||||||
unhashed_description: Optional[bytes] = None,
|
unhashed_description: bytes | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Coroutine[None, None, InvoiceResponse]:
|
) -> Coroutine[None, None, InvoiceResponse]:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -87,13 +87,15 @@ class LNbitsWallet(Wallet):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
data = r.json()
|
data = r.json()
|
||||||
|
|
||||||
if r.is_error or "bolt11" not in data:
|
# Backwards compatibility for pre-v1 which used the key "payment_request"
|
||||||
|
payment_str = data.get("bolt11") or data.get("payment_request")
|
||||||
|
if r.is_error or not payment_str:
|
||||||
error_message = data["detail"] if "detail" in data else r.text
|
error_message = data["detail"] if "detail" in data else r.text
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, f"Server error: '{error_message}'"
|
False, None, None, f"Server error: '{error_message}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
return InvoiceResponse(True, data["checking_id"], data["bolt11"], None)
|
return InvoiceResponse(True, data["checking_id"], payment_str, None)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return InvoiceResponse(
|
return InvoiceResponse(
|
||||||
False, None, None, "Server error: 'invalid json response'"
|
False, None, None, "Server error: 'invalid json response'"
|
||||||
|
|||||||
Generated
+27
-22
@@ -6,7 +6,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "lnbits",
|
"name": "lnbits",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.8.2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"showdown": "^2.1.0",
|
"showdown": "^2.1.0",
|
||||||
"underscore": "^1.13.7",
|
"underscore": "^1.13.7",
|
||||||
"vue": "3.5.8",
|
"vue": "3.5.8",
|
||||||
"vue-i18n": "^10.0.5",
|
"vue-i18n": "^10.0.6",
|
||||||
"vue-qrcode-reader": "^5.5.10",
|
"vue-qrcode-reader": "^5.5.10",
|
||||||
"vue-router": "4.4.5",
|
"vue-router": "4.4.5",
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
@@ -72,12 +72,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/core-base": {
|
"node_modules/@intlify/core-base": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.6.tgz",
|
||||||
"integrity": "sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==",
|
"integrity": "sha512-/NINGvy7t8qSCyyuqMIPmHS6CBQjqPIPVOps0Rb7xWrwwkwHJKtahiFnW1HC4iQVhzoYwEW6Js0923zTScLDiA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/message-compiler": "10.0.5",
|
"@intlify/message-compiler": "10.0.6",
|
||||||
"@intlify/shared": "10.0.5"
|
"@intlify/shared": "10.0.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
@@ -87,11 +88,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/message-compiler": {
|
"node_modules/@intlify/message-compiler": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.6.tgz",
|
||||||
"integrity": "sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==",
|
"integrity": "sha512-QcUYprK+e4X2lU6eJDxLuf/mUtCuVPj2RFBoFRlJJxK3wskBejzlRvh1Q0lQCi9tDOnD4iUK1ftcGylE3X3idA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/shared": "10.0.5",
|
"@intlify/shared": "10.0.6",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -102,9 +104,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/shared": {
|
"node_modules/@intlify/shared": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.6.tgz",
|
||||||
"integrity": "sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==",
|
"integrity": "sha512-2xqwm05YPpo7TM//+v0bzS0FWiTzsjpSMnWdt7ZXs5/ZfQIedSuBXIrskd8HZ7c/cZzo1G9ALHTksnv/74vk/Q==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
},
|
},
|
||||||
@@ -399,9 +402,10 @@
|
|||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.7.7",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz",
|
||||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
"integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.6",
|
||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
@@ -1301,12 +1305,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-i18n": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "10.0.5",
|
"version": "10.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.6.tgz",
|
||||||
"integrity": "sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==",
|
"integrity": "sha512-pQPspK5H4srzlu+47+HEY2tmiY3GyYIvSPgSBdQaYVWv7t1zj1t9p1FvHlxBXyJ17t9stG/Vxj+pykrvPWBLeQ==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "10.0.5",
|
"@intlify/core-base": "10.0.6",
|
||||||
"@intlify/shared": "10.0.5",
|
"@intlify/shared": "10.0.6",
|
||||||
"@vue/devtools-api": "^6.5.0"
|
"@vue/devtools-api": "^6.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@
|
|||||||
"sass": "^1.78.0"
|
"sass": "^1.78.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.8.2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"qrcode.vue": "^3.4.1",
|
"qrcode.vue": "^3.4.1",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"nostr-tools": "^2.7.2",
|
"nostr-tools": "^2.7.2",
|
||||||
"underscore": "^1.13.7",
|
"underscore": "^1.13.7",
|
||||||
"vue": "3.5.8",
|
"vue": "3.5.8",
|
||||||
"vue-i18n": "^10.0.5",
|
"vue-i18n": "^10.0.6",
|
||||||
"vue-qrcode-reader": "^5.5.10",
|
"vue-qrcode-reader": "^5.5.10",
|
||||||
"vue-router": "4.4.5",
|
"vue-router": "4.4.5",
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
|
|||||||
Generated
+2
-21
@@ -1395,9 +1395,6 @@ files = [
|
|||||||
{file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"},
|
{file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||||
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"]
|
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"]
|
||||||
@@ -2916,7 +2913,6 @@ files = [
|
|||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
anyio = ">=3.4.0,<5"
|
anyio = ">=3.4.0,<5"
|
||||||
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
|
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
||||||
@@ -3400,26 +3396,11 @@ files = [
|
|||||||
{file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"},
|
{file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "zipp"
|
|
||||||
version = "3.19.2"
|
|
||||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.8"
|
|
||||||
files = [
|
|
||||||
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
|
|
||||||
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
|
||||||
test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
|
||||||
|
|
||||||
[extras]
|
[extras]
|
||||||
breez = ["breez-sdk"]
|
breez = ["breez-sdk"]
|
||||||
liquid = ["wallycore"]
|
liquid = ["wallycore"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.13 | ^3.12 | ^3.11 | ^3.10 | ^3.9"
|
python-versions = "~3.12 | ~3.11 | ~3.10"
|
||||||
content-hash = "e263865649975ea7e977b3cbf6cb453c3653de115523d026e3863605ab48a463"
|
content-hash = "96dd180aaa4fbfeb34fa6f9647c8684fce183a72b1b41d22101a9dd4b962fa2e"
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "lnbits"
|
name = "lnbits"
|
||||||
version = "1.0.0-rc8"
|
version = "1.0.0-rc9"
|
||||||
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
description = "LNbits, free and open-source Lightning wallet and accounts system."
|
||||||
authors = ["Alan Bits <alan@lnbits.com>"]
|
authors = ["Alan Bits <alan@lnbits.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -12,7 +12,7 @@ packages = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.13 | ^3.12 | ^3.11 | ^3.10 | ^3.9"
|
python = "~3.12 | ~3.11 | ~3.10"
|
||||||
bech32 = "1.2.0"
|
bech32 = "1.2.0"
|
||||||
click = "8.1.7"
|
click = "8.1.7"
|
||||||
ecdsa = "0.19.0"
|
ecdsa = "0.19.0"
|
||||||
@@ -192,7 +192,7 @@ extend-exclude = [
|
|||||||
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
select = ["F", "E", "W", "I", "A", "C", "N", "UP", "RUF", "B"]
|
||||||
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
|
# UP007: pyupgrade: use X | Y instead of Optional. (python3.10)
|
||||||
# RUF012: mutable-class-default
|
# RUF012: mutable-class-default
|
||||||
ignore = ["UP007", "RUF012"]
|
ignore = ["RUF012"]
|
||||||
|
|
||||||
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
# Allow autofix for all enabled rules (when `--fix`) is provided.
|
||||||
fixable = ["ALL"]
|
fixable = ["ALL"]
|
||||||
|
|||||||
Reference in New Issue
Block a user