mirror of
https://github.com/lnbits/lnbits.git
synced 2025-04-09 12:30:13 +02:00
[feat] Watchdog and notifications (#2895)
This commit is contained in:
parent
56a4b702f3
commit
b6bdf50ed7
@ -24,14 +24,17 @@ from lnbits.core.crud import (
|
||||
)
|
||||
from lnbits.core.crud.extensions import create_installed_extension
|
||||
from lnbits.core.helpers import migrate_extension_database
|
||||
from lnbits.core.models.notifications import NotificationType
|
||||
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
|
||||
from lnbits.core.tasks import ( # watchdog_task
|
||||
from lnbits.core.services.notifications import enqueue_notification
|
||||
from lnbits.core.tasks import (
|
||||
audit_queue,
|
||||
collect_exchange_rates_data,
|
||||
killswitch_task,
|
||||
purge_audit_data,
|
||||
run_by_the_minute_tasks,
|
||||
wait_for_audit_data,
|
||||
wait_for_paid_invoices,
|
||||
wait_notification_messages,
|
||||
)
|
||||
from lnbits.exceptions import register_exception_handlers
|
||||
from lnbits.helpers import version_parse
|
||||
@ -102,9 +105,24 @@ async def startup(app: FastAPI):
|
||||
# initialize tasks
|
||||
register_async_tasks()
|
||||
|
||||
enqueue_notification(
|
||||
NotificationType.server_start_stop,
|
||||
{
|
||||
"message": "LNbits server started.",
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def shutdown():
|
||||
logger.warning("LNbits shutting down...")
|
||||
enqueue_notification(
|
||||
NotificationType.server_start_stop,
|
||||
{
|
||||
"message": "LNbits server shutting down...",
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
},
|
||||
)
|
||||
settings.lnbits_running = False
|
||||
|
||||
# shutdown event
|
||||
@ -444,6 +462,8 @@ async def check_and_register_extensions(app: FastAPI):
|
||||
def register_async_tasks():
|
||||
|
||||
create_permanent_task(wait_for_audit_data)
|
||||
create_permanent_task(wait_notification_messages)
|
||||
|
||||
create_permanent_task(check_pending_payments)
|
||||
create_permanent_task(invoice_listener)
|
||||
create_permanent_task(internal_invoice_listener)
|
||||
@ -454,9 +474,7 @@ def register_async_tasks():
|
||||
register_invoice_listener(invoice_queue, "core")
|
||||
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
||||
|
||||
# TODO: implement watchdog properly
|
||||
# create_permanent_task(watchdog_task)
|
||||
create_permanent_task(killswitch_task)
|
||||
create_permanent_task(run_by_the_minute_tasks)
|
||||
create_permanent_task(purge_audit_data)
|
||||
create_permanent_task(collect_exchange_rates_data)
|
||||
|
||||
|
@ -4,6 +4,7 @@ from typing import Literal, Optional
|
||||
from lnbits.core.crud.wallets import get_total_balance, get_wallet
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models import PaymentState
|
||||
from lnbits.core.models.payments import PaymentsStatusCount
|
||||
from lnbits.db import DB_TYPE, SQLITE, Connection, Filters, Page
|
||||
|
||||
from ..models import (
|
||||
@ -99,6 +100,7 @@ async def get_payments_paginated(
|
||||
wallet_id: Optional[str] = None,
|
||||
complete: bool = False,
|
||||
pending: bool = False,
|
||||
failed: bool = False,
|
||||
outgoing: bool = False,
|
||||
incoming: bool = False,
|
||||
since: Optional[int] = None,
|
||||
@ -107,7 +109,8 @@ async def get_payments_paginated(
|
||||
conn: Optional[Connection] = None,
|
||||
) -> Page[Payment]:
|
||||
"""
|
||||
Filters payments to be returned by complete | pending | outgoing | incoming.
|
||||
Filters payments to be returned by:
|
||||
- complete | pending | failed | outgoing | incoming.
|
||||
"""
|
||||
|
||||
values: dict = {
|
||||
@ -137,6 +140,8 @@ async def get_payments_paginated(
|
||||
)
|
||||
elif pending:
|
||||
clause.append(f"status = '{PaymentState.PENDING}'")
|
||||
elif failed:
|
||||
clause.append(f"status = '{PaymentState.FAILED}'")
|
||||
|
||||
if outgoing and incoming:
|
||||
pass
|
||||
@ -200,6 +205,21 @@ async def get_payments(
|
||||
return page.data
|
||||
|
||||
|
||||
async def get_payments_status_count() -> PaymentsStatusCount:
|
||||
empty_page: Filters = Filters(limit=0)
|
||||
in_payments = await get_payments_paginated(incoming=True, filters=empty_page)
|
||||
out_payments = await get_payments_paginated(outgoing=True, filters=empty_page)
|
||||
pending_payments = await get_payments_paginated(pending=True, filters=empty_page)
|
||||
failed_payments = await get_payments_paginated(failed=True, filters=empty_page)
|
||||
|
||||
return PaymentsStatusCount(
|
||||
incoming=in_payments.total,
|
||||
outgoing=out_payments.total,
|
||||
pending=pending_payments.total,
|
||||
failed=failed_payments.total,
|
||||
)
|
||||
|
||||
|
||||
async def delete_expired_invoices(
|
||||
conn: Optional[Connection] = None,
|
||||
) -> None:
|
||||
|
@ -135,6 +135,12 @@ async def get_wallets(
|
||||
)
|
||||
|
||||
|
||||
async def get_wallets_count():
|
||||
result = await db.execute("SELECT COUNT(*) as count FROM wallets")
|
||||
row = result.mappings().first()
|
||||
return row.get("count", 0)
|
||||
|
||||
|
||||
async def get_wallet_for_key(
|
||||
key: str,
|
||||
conn: Optional[Connection] = None,
|
||||
@ -153,6 +159,6 @@ async def get_wallet_for_key(
|
||||
|
||||
|
||||
async def get_total_balance(conn: Optional[Connection] = None):
|
||||
result = await (conn or db).execute("SELECT SUM(balance) FROM balances")
|
||||
result = await (conn or db).execute("SELECT SUM(balance) as balance FROM balances")
|
||||
row = result.mappings().first()
|
||||
return row.get("balance", 0)
|
||||
|
@ -25,12 +25,12 @@ class Callback(BaseModel):
|
||||
|
||||
|
||||
class BalanceDelta(BaseModel):
|
||||
lnbits_balance_msats: int
|
||||
node_balance_msats: int
|
||||
lnbits_balance_sats: int
|
||||
node_balance_sats: int
|
||||
|
||||
@property
|
||||
def delta_msats(self):
|
||||
return self.node_balance_msats - self.lnbits_balance_msats
|
||||
def delta_sats(self) -> int:
|
||||
return int(self.lnbits_balance_sats - self.node_balance_sats)
|
||||
|
||||
|
||||
class SimpleStatus(BaseModel):
|
||||
|
64
lnbits/core/models/notifications.py
Normal file
64
lnbits/core/models/notifications.py
Normal file
@ -0,0 +1,64 @@
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class NotificationType(Enum):
|
||||
server_status = "server_status"
|
||||
settings_update = "settings_update"
|
||||
balance_update = "balance_update"
|
||||
watchdog_check = "watchdog_check"
|
||||
balance_delta = "balance_delta"
|
||||
server_start_stop = "server_start_stop"
|
||||
incoming_payment = "incoming_payment"
|
||||
outgoing_payment = "outgoing_payment"
|
||||
text_message = "text_message"
|
||||
|
||||
|
||||
class NotificationMessage(BaseModel):
|
||||
message_type: NotificationType
|
||||
values: dict
|
||||
|
||||
|
||||
NOTIFICATION_TEMPLATES = {
|
||||
"text_message": "{message}",
|
||||
"server_status": """*SERVER STATUS*
|
||||
*Up time*: `{up_time}`.
|
||||
*Accounts*: `{accounts_count}`.
|
||||
*Wallets*: `{wallets_count}`.
|
||||
*In/Out payments*: `{in_payments_count}`/`{out_payments_count}`.
|
||||
*Pending payments*: `{pending_payments_count}`.
|
||||
*Failed payments*: `{failed_payments_count}`.
|
||||
*LNbits balance*: `{lnbits_balance_sats}` sats.""",
|
||||
"server_start_stop": """*SERVER*
|
||||
{message}
|
||||
*Time*: `{up_time}` seconds.
|
||||
""",
|
||||
"settings_update": """*SETTINGS UPDATED*
|
||||
User: `{username}`.
|
||||
""",
|
||||
"balance_update": """*WALLET CREDIT/DEBIT*
|
||||
Wallet `{wallet_name}` balance updated with `{amount}` sats.
|
||||
*Current balance*: `{balance}` sats.
|
||||
*Wallet ID*: `{wallet_id}`
|
||||
""",
|
||||
"watchdog_check": """*WATCHDOG BALANCE CHECK*
|
||||
*Delta*: `{delta_sats}` sats.
|
||||
*LNbits balance*: `{lnbits_balance_sats}` sats.
|
||||
*Node balance*: `{node_balance_sats}` sats.
|
||||
*Switching to Void Wallet*: `{switch_to_void_wallet}`.
|
||||
""",
|
||||
"balance_delta": """*BALANCE DELTA CHANGED*
|
||||
*New delta*: `{delta_sats}` sats.
|
||||
*Old delta*: `{old_delta_sats}` sats.
|
||||
*LNbits balance*: `{lnbits_balance_sats}` sats.
|
||||
*Node balance*: `{node_balance_sats}` sats.
|
||||
""",
|
||||
"outgoing_payment": """*PAYMENT SENT*
|
||||
*Amount*: {fiat_value_fmt}`{amount_sats}`*sats*.
|
||||
*Wallet*: `{wallet_name}` ({wallet_id}).""",
|
||||
"incoming_payment": """*PAYMENT RECEIVED*
|
||||
*Amount*: {fiat_value_fmt}`{amount_sats}`*sats*.
|
||||
*Wallet*: `{wallet_name}` ({wallet_id}).
|
||||
""",
|
||||
}
|
@ -175,3 +175,10 @@ class CreateInvoice(BaseModel):
|
||||
if v != "sat" and v not in allowed_currencies():
|
||||
raise ValueError("The provided unit is not supported")
|
||||
return v
|
||||
|
||||
|
||||
class PaymentsStatusCount(BaseModel):
|
||||
incoming: int = 0
|
||||
outgoing: int = 0
|
||||
failed: int = 0
|
||||
pending: int = 0
|
||||
|
@ -3,6 +3,7 @@ from .funding_source import (
|
||||
switch_to_voidwallet,
|
||||
)
|
||||
from .lnurl import perform_lnurlauth, redeem_lnurl_withdraw
|
||||
from .notifications import enqueue_notification
|
||||
from .payments import (
|
||||
calculate_fiat_amounts,
|
||||
check_transaction_status,
|
||||
@ -37,6 +38,8 @@ __all__ = [
|
||||
# lnurl
|
||||
"redeem_lnurl_withdraw",
|
||||
"perform_lnurlauth",
|
||||
# notifications
|
||||
"enqueue_notification",
|
||||
# payments
|
||||
"calculate_fiat_amounts",
|
||||
"check_transaction_status",
|
||||
|
@ -1,3 +1,7 @@
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models.notifications import NotificationType
|
||||
from lnbits.core.services.notifications import enqueue_notification
|
||||
from lnbits.settings import settings
|
||||
from lnbits.wallets import get_funding_source, set_funding_source
|
||||
|
||||
@ -18,6 +22,62 @@ async def get_balance_delta() -> BalanceDelta:
|
||||
status = await funding_source.status()
|
||||
lnbits_balance = await get_total_balance()
|
||||
return BalanceDelta(
|
||||
lnbits_balance_msats=lnbits_balance,
|
||||
node_balance_msats=status.balance_msat,
|
||||
lnbits_balance_sats=lnbits_balance // 1000,
|
||||
node_balance_sats=status.balance_msat // 1000,
|
||||
)
|
||||
|
||||
|
||||
async def check_server_balance_against_node():
|
||||
"""
|
||||
Watchdog will check lnbits balance and nodebalance
|
||||
and will switch to VoidWallet if the watchdog delta is reached.
|
||||
"""
|
||||
if (
|
||||
not settings.lnbits_watchdog_switch_to_voidwallet
|
||||
and not settings.lnbits_notification_watchdog
|
||||
):
|
||||
return
|
||||
|
||||
funding_source = get_funding_source()
|
||||
if funding_source.__class__.__name__ == "VoidWallet":
|
||||
return
|
||||
|
||||
status = await get_balance_delta()
|
||||
if status.delta_sats < settings.lnbits_watchdog_delta:
|
||||
return
|
||||
|
||||
use_voidwallet = settings.lnbits_watchdog_switch_to_voidwallet
|
||||
logger.warning(
|
||||
f"Balance delta reached: {status.delta_sats} sats."
|
||||
f" Switch to void wallet: {use_voidwallet}."
|
||||
)
|
||||
enqueue_notification(
|
||||
NotificationType.watchdog_check,
|
||||
{
|
||||
"delta_sats": status.delta_sats,
|
||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
||||
"node_balance_sats": status.node_balance_sats,
|
||||
"switch_to_void_wallet": use_voidwallet,
|
||||
},
|
||||
)
|
||||
if use_voidwallet:
|
||||
logger.error(f"Switching to VoidWallet. Delta: {status.delta_sats} sats.")
|
||||
await switch_to_voidwallet()
|
||||
|
||||
|
||||
async def check_balance_delta_changed():
|
||||
status = await get_balance_delta()
|
||||
if settings.latest_balance_delta_sats is None:
|
||||
settings.latest_balance_delta_sats = status.delta_sats
|
||||
return
|
||||
if status.delta_sats != settings.latest_balance_delta_sats:
|
||||
enqueue_notification(
|
||||
NotificationType.balance_delta,
|
||||
{
|
||||
"delta_sats": status.delta_sats,
|
||||
"old_delta_sats": settings.latest_balance_delta_sats,
|
||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
||||
"node_balance_sats": status.node_balance_sats,
|
||||
},
|
||||
)
|
||||
settings.latest_balance_delta_sats = status.delta_sats
|
||||
|
73
lnbits/core/services/nostr.py
Normal file
73
lnbits/core/services/nostr.py
Normal file
@ -0,0 +1,73 @@
|
||||
import asyncio
|
||||
from typing import Tuple
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from pynostr.encrypted_dm import EncryptedDirectMessage
|
||||
from websocket import WebSocket, create_connection
|
||||
|
||||
from lnbits.core.helpers import is_valid_url
|
||||
from lnbits.utils.nostr import (
|
||||
validate_identifier,
|
||||
validate_pub_key,
|
||||
)
|
||||
|
||||
|
||||
async def send_nostr_dm(
|
||||
from_private_key_hex: str,
|
||||
to_pubkey_hex: str,
|
||||
message: str,
|
||||
relays: list[str],
|
||||
) -> dict:
|
||||
dm = EncryptedDirectMessage()
|
||||
dm.encrypt(
|
||||
private_key_hex=from_private_key_hex,
|
||||
recipient_pubkey=to_pubkey_hex,
|
||||
cleartext_content=message,
|
||||
)
|
||||
|
||||
dm_event = dm.to_event()
|
||||
dm_event.sign(private_key_hex=from_private_key_hex)
|
||||
notification = dm_event.to_message()
|
||||
|
||||
ws_connections: list[WebSocket] = []
|
||||
for relay in relays:
|
||||
try:
|
||||
ws = create_connection(relay, timeout=2)
|
||||
ws.send(notification)
|
||||
ws_connections.append(ws)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error sending notification to relay {relay}: {e}")
|
||||
await asyncio.sleep(1)
|
||||
for ws in ws_connections:
|
||||
try:
|
||||
ws.close()
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to close websocket connection: {e}")
|
||||
|
||||
return dm_event.to_dict()
|
||||
|
||||
|
||||
async def fetch_nip5_details(identifier: str) -> Tuple[str, list[str]]:
|
||||
identifier, domain = identifier.split("@")
|
||||
if not identifier or not domain:
|
||||
raise ValueError("Invalid NIP5 identifier")
|
||||
|
||||
if not is_valid_url(f"https://{domain}"):
|
||||
raise ValueError("Invalid NIP5 domain")
|
||||
|
||||
validate_identifier(identifier)
|
||||
|
||||
url = f"https://{domain}/.well-known/nostr.json?name={identifier}"
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if "names" not in data or identifier not in data["names"]:
|
||||
raise ValueError("NIP5 not name found")
|
||||
pubkey = data["names"][identifier]
|
||||
validate_pub_key(pubkey)
|
||||
|
||||
relays = data["relays"].get(pubkey, []) if "relays" in data else []
|
||||
|
||||
return pubkey, relays
|
125
lnbits/core/services/notifications.py
Normal file
125
lnbits/core/services/notifications.py
Normal file
@ -0,0 +1,125 @@
|
||||
import asyncio
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.models.notifications import (
|
||||
NOTIFICATION_TEMPLATES,
|
||||
NotificationMessage,
|
||||
NotificationType,
|
||||
)
|
||||
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
|
||||
from lnbits.settings import settings
|
||||
from lnbits.utils.nostr import normalize_private_key
|
||||
|
||||
notifications_queue: asyncio.Queue = asyncio.Queue()
|
||||
|
||||
|
||||
def enqueue_notification(message_type: NotificationType, values: dict) -> None:
|
||||
if not is_message_type_enabled(message_type):
|
||||
return
|
||||
try:
|
||||
notifications_queue.put_nowait(
|
||||
NotificationMessage(message_type=message_type, values=values)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error enqueuing notification: {e}")
|
||||
|
||||
|
||||
async def process_next_notification():
|
||||
notification_message: NotificationMessage = await notifications_queue.get()
|
||||
message_type, text = _notification_message_to_text(notification_message)
|
||||
await send_notification(text, message_type)
|
||||
|
||||
|
||||
async def send_notification(
|
||||
message: str,
|
||||
message_type: Optional[str] = None,
|
||||
) -> None:
|
||||
try:
|
||||
if settings.lnbits_telegram_notifications_enabled:
|
||||
await send_telegram_notification(message)
|
||||
logger.debug(f"Sent telegram notification: {message_type}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending telegram notification {message_type}: {e}")
|
||||
|
||||
try:
|
||||
if settings.lnbits_nostr_notifications_enabled:
|
||||
await send_nostr_notification(message)
|
||||
logger.debug(f"Sent nostr notification: {message_type}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending nostr notification {message_type}: {e}")
|
||||
|
||||
|
||||
async def send_nostr_notification(message: str) -> dict:
|
||||
for i in settings.lnbits_nostr_notifications_identifiers:
|
||||
try:
|
||||
identifier = await fetch_nip5_details(i)
|
||||
user_pubkey = identifier[0]
|
||||
relays = identifier[1]
|
||||
server_private_key = normalize_private_key(
|
||||
settings.lnbits_nostr_notifications_private_key
|
||||
)
|
||||
await send_nostr_dm(
|
||||
server_private_key,
|
||||
user_pubkey,
|
||||
message,
|
||||
relays,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error notifying identifier {i}: {e}")
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
async def send_telegram_notification(message: str) -> dict:
|
||||
return await send_telegram_message(
|
||||
settings.lnbits_telegram_notifications_access_token,
|
||||
settings.lnbits_telegram_notifications_chat_id,
|
||||
message,
|
||||
)
|
||||
|
||||
|
||||
async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
|
||||
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
||||
payload = {"chat_id": chat_id, "text": message, "parse_mode": "markdown"}
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, data=payload)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def is_message_type_enabled(message_type: NotificationType) -> bool:
|
||||
if message_type == NotificationType.balance_update:
|
||||
return settings.lnbits_notification_credit_debit
|
||||
if message_type == NotificationType.settings_update:
|
||||
return settings.lnbits_notification_settings_update
|
||||
if message_type == NotificationType.watchdog_check:
|
||||
return settings.lnbits_notification_watchdog
|
||||
if message_type == NotificationType.balance_delta:
|
||||
return settings.notification_balance_delta_changed
|
||||
if message_type == NotificationType.server_start_stop:
|
||||
return settings.lnbits_notification_server_start_stop
|
||||
if message_type == NotificationType.server_status:
|
||||
return settings.lnbits_notification_server_status_hours > 0
|
||||
if message_type == NotificationType.incoming_payment:
|
||||
return settings.lnbits_notification_incoming_payment_amount_sats > 0
|
||||
if message_type == NotificationType.outgoing_payment:
|
||||
return settings.lnbits_notification_outgoing_payment_amount_sats > 0
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _notification_message_to_text(
|
||||
notification_message: NotificationMessage,
|
||||
) -> Tuple[str, str]:
|
||||
message_type = notification_message.message_type.value
|
||||
meesage_value = NOTIFICATION_TEMPLATES.get(message_type, message_type)
|
||||
try:
|
||||
text = meesage_value.format(**notification_message.values)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error formatting notification message: {e}")
|
||||
text = meesage_value
|
||||
text = f"""[{settings.lnbits_site_title}]\n{text}"""
|
||||
return message_type, text
|
@ -8,6 +8,8 @@ from bolt11 import encode as bolt11_encode
|
||||
from loguru import logger
|
||||
|
||||
from lnbits.core.db import db
|
||||
from lnbits.core.models.notifications import NotificationType
|
||||
from lnbits.core.services.notifications import enqueue_notification
|
||||
from lnbits.db import Connection
|
||||
from lnbits.decorators import check_user_extension_access
|
||||
from lnbits.exceptions import InvoiceError, PaymentError
|
||||
@ -262,6 +264,18 @@ async def update_wallet_balance(
|
||||
|
||||
|
||||
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.
|
||||
@ -282,6 +296,27 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
|
||||
)
|
||||
|
||||
|
||||
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(
|
||||
wallet_id: str, amount_msat: int, conn: Optional[Connection] = None
|
||||
):
|
||||
@ -354,6 +389,7 @@ async def calculate_fiat_amounts(
|
||||
fiat_amounts["fiat_currency"] = currency
|
||||
fiat_amounts["fiat_amount"] = round(amount, ndigits=3)
|
||||
fiat_amounts["fiat_rate"] = amount_sat / amount
|
||||
fiat_amounts["btc_rate"] = (amount / amount_sat) * 100_000_000
|
||||
else:
|
||||
amount_sat = int(amount)
|
||||
|
||||
@ -365,6 +401,7 @@ async def calculate_fiat_amounts(
|
||||
fiat_amounts["wallet_fiat_currency"] = wallet_currency
|
||||
fiat_amounts["wallet_fiat_amount"] = round(fiat_amount, ndigits=3)
|
||||
fiat_amounts["wallet_fiat_rate"] = amount_sat / fiat_amount
|
||||
fiat_amounts["wallet_btc_rate"] = (fiat_amount / amount_sat) * 100_000_000
|
||||
|
||||
logger.debug(
|
||||
f"Calculated fiat amounts {wallet.id=} {amount=} {currency=}: {fiat_amounts=}"
|
||||
|
@ -1,4 +1,6 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
from typing import Callable, Coroutine
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
@ -10,70 +12,78 @@ from lnbits.core.crud import (
|
||||
mark_webhook_sent,
|
||||
)
|
||||
from lnbits.core.crud.audit import delete_expired_audit_entries
|
||||
from lnbits.core.crud.payments import get_payments_status_count
|
||||
from lnbits.core.crud.users import get_accounts
|
||||
from lnbits.core.crud.wallets import get_wallets_count
|
||||
from lnbits.core.models import AuditEntry, Payment
|
||||
from lnbits.core.models.notifications import NotificationType
|
||||
from lnbits.core.services import (
|
||||
get_balance_delta,
|
||||
send_payment_notification,
|
||||
switch_to_voidwallet,
|
||||
)
|
||||
from lnbits.settings import get_funding_source, settings
|
||||
from lnbits.tasks import send_push_notification
|
||||
from lnbits.core.services.funding_source import (
|
||||
check_balance_delta_changed,
|
||||
check_server_balance_against_node,
|
||||
get_balance_delta,
|
||||
)
|
||||
from lnbits.core.services.notifications import (
|
||||
enqueue_notification,
|
||||
process_next_notification,
|
||||
)
|
||||
from lnbits.db import Filters
|
||||
from lnbits.settings import settings
|
||||
from lnbits.tasks import create_unique_task, send_push_notification
|
||||
from lnbits.utils.exchange_rates import btc_rates
|
||||
|
||||
audit_queue: asyncio.Queue = asyncio.Queue()
|
||||
|
||||
|
||||
async def killswitch_task():
|
||||
"""
|
||||
killswitch will check lnbits-status repository for a signal from
|
||||
LNbits and will switch to VoidWallet if the killswitch is triggered.
|
||||
"""
|
||||
async def run_by_the_minute_tasks():
|
||||
minute_counter = 0
|
||||
while settings.lnbits_running:
|
||||
funding_source = get_funding_source()
|
||||
if (
|
||||
settings.lnbits_killswitch
|
||||
and funding_source.__class__.__name__ != "VoidWallet"
|
||||
):
|
||||
with httpx.Client() as client:
|
||||
try:
|
||||
r = client.get(settings.lnbits_status_manifest, timeout=4)
|
||||
r.raise_for_status()
|
||||
if r.status_code == 200:
|
||||
ks = r.json().get("killswitch")
|
||||
if ks and ks == 1:
|
||||
logger.error(
|
||||
"Switching to VoidWallet. Killswitch triggered."
|
||||
)
|
||||
await switch_to_voidwallet()
|
||||
except (httpx.RequestError, httpx.HTTPStatusError):
|
||||
logger.error(
|
||||
"Cannot fetch lnbits status manifest."
|
||||
f" {settings.lnbits_status_manifest}"
|
||||
)
|
||||
await asyncio.sleep(settings.lnbits_killswitch_interval * 60)
|
||||
status_minutes = settings.lnbits_notification_server_status_hours * 60
|
||||
|
||||
|
||||
async def watchdog_task():
|
||||
"""
|
||||
Registers a watchdog which will check lnbits balance and nodebalance
|
||||
and will switch to VoidWallet if the watchdog delta is reached.
|
||||
"""
|
||||
while settings.lnbits_running:
|
||||
funding_source = get_funding_source()
|
||||
if (
|
||||
settings.lnbits_watchdog
|
||||
and funding_source.__class__.__name__ != "VoidWallet"
|
||||
):
|
||||
if settings.notification_balance_delta_changed:
|
||||
try:
|
||||
balance = await get_balance_delta()
|
||||
delta = balance.delta_msats
|
||||
logger.debug(f"Running watchdog task. current delta: {delta}")
|
||||
if delta + settings.lnbits_watchdog_delta <= 0:
|
||||
logger.error(f"Switching to VoidWallet. current delta: {delta}")
|
||||
await switch_to_voidwallet()
|
||||
except Exception as e:
|
||||
logger.error("Error in watchdog task", e)
|
||||
await asyncio.sleep(settings.lnbits_watchdog_interval * 60)
|
||||
# runs by default every minute, the delta should not change that often
|
||||
await check_balance_delta_changed()
|
||||
except Exception as ex:
|
||||
logger.error(ex)
|
||||
|
||||
if minute_counter % settings.lnbits_watchdog_interval_minutes == 0:
|
||||
try:
|
||||
await check_server_balance_against_node()
|
||||
except Exception as ex:
|
||||
logger.error(ex)
|
||||
|
||||
if minute_counter % status_minutes == 0:
|
||||
try:
|
||||
await _notify_server_status()
|
||||
except Exception as ex:
|
||||
logger.error(ex)
|
||||
|
||||
minute_counter += 1
|
||||
await asyncio.sleep(60)
|
||||
|
||||
|
||||
async def _notify_server_status():
|
||||
accounts = await get_accounts(filters=Filters(limit=0))
|
||||
wallets_count = await get_wallets_count()
|
||||
payments = await get_payments_status_count()
|
||||
|
||||
status = await get_balance_delta()
|
||||
values = {
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
"accounts_count": accounts.total,
|
||||
"wallets_count": wallets_count,
|
||||
"in_payments_count": payments.incoming,
|
||||
"out_payments_count": payments.outgoing,
|
||||
"pending_payments_count": payments.pending,
|
||||
"failed_payments_count": payments.failed,
|
||||
"delta_sats": status.delta_sats,
|
||||
"lnbits_balance_sats": status.lnbits_balance_sats,
|
||||
"node_balance_sats": status.node_balance_sats,
|
||||
}
|
||||
enqueue_notification(NotificationType.server_status, values)
|
||||
|
||||
|
||||
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue):
|
||||
@ -158,6 +168,16 @@ async def wait_for_audit_data():
|
||||
await asyncio.sleep(3)
|
||||
|
||||
|
||||
async def wait_notification_messages():
|
||||
|
||||
while settings.lnbits_running:
|
||||
try:
|
||||
await process_next_notification()
|
||||
except Exception as ex:
|
||||
logger.log("error", ex)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
|
||||
async def purge_audit_data():
|
||||
"""
|
||||
Remove audit entries which have passed their retention period.
|
||||
@ -194,3 +214,14 @@ async def collect_exchange_rates_data():
|
||||
else:
|
||||
sleep_time = 60
|
||||
await asyncio.sleep(sleep_time)
|
||||
|
||||
|
||||
def _create_unique_task(name: str, func: Callable):
|
||||
async def _to_coro(func: Callable[[], Coroutine]) -> Coroutine:
|
||||
return await func()
|
||||
|
||||
try:
|
||||
create_unique_task(name, _to_coro(func))
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {name} task", e)
|
||||
logger.error(traceback.format_exc())
|
||||
|
@ -15,16 +15,16 @@
|
||||
v-text="$t('funding_source', {wallet_class: settings.lnbits_backend_wallet_class})"
|
||||
></li>
|
||||
<li
|
||||
v-text="$t('node_balance', {balance: (auditData.node_balance_msats / 1000).toLocaleString()})"
|
||||
v-text="$t('node_balance', {balance: (auditData.node_balance_sats || 0).toLocaleString()})"
|
||||
></li>
|
||||
<li
|
||||
v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_msats / 1000).toLocaleString()})"
|
||||
v-text="$t('lnbits_balance', {balance: (auditData.lnbits_balance_sats || 0).toLocaleString()})"
|
||||
></li>
|
||||
<li
|
||||
v-text="$t('funding_reserve_percent', {
|
||||
percent: auditData.lnbits_balance_msats > 0
|
||||
? (auditData.node_balance_msats / auditData.lnbits_balance_msats * 100).toFixed(2)
|
||||
: 100
|
||||
v-text="$t('funding_reserve_percent', {
|
||||
percent: auditData.lnbits_balance_sats > 0
|
||||
? (auditData.node_balance_sats / auditData.lnbits_balance_sats * 100).toFixed(2)
|
||||
: 100
|
||||
})"
|
||||
></li>
|
||||
</ul>
|
||||
@ -96,6 +96,90 @@
|
||||
:allowed-funding-sources="settings.lnbits_allowed_funding_sources"
|
||||
/>
|
||||
</div>
|
||||
<q-separator></q-separator>
|
||||
<h6 class="q-mt-lg q-mb-sm">
|
||||
<p v-text="$t('watchdog')"></p>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('enable_watchdog_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_watchdog_switch_to_voidwallet"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notification_watchdog_limit')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notification_watchdog_limit_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_notification_watchdog"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('watchdog_interval_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_watchdog_interval_minutes"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('watchdog_delta_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_watchdog_delta"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-tab-panel>
|
||||
|
@ -1,139 +1,328 @@
|
||||
<q-tab-panel name="notifications">
|
||||
<q-card-section class="q-pa-none">
|
||||
<div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12 col-md-6">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('enable_notifications')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('enable_notifications_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_notifications"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
<p
|
||||
v-if="!formData.lnbits_notifications"
|
||||
v-text="$t('notifications_disabled')"
|
||||
></p>
|
||||
<div v-if="formData.lnbits_notifications">
|
||||
{% include "admin/_tab_security_notifications.html" %}
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<p v-text="$t('notification_source')"></p>
|
||||
<h6 class="q-my-none q-mb-sm">
|
||||
<span v-text="$t('notifications_configure')"></span>
|
||||
</h6>
|
||||
<q-separator class="q-mt-md q-mb-sm"></q-separator>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<strong v-text="$t('notifications_nostr_config')"></strong>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notifications_enable_nostr')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notifications_enable_nostr_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_nostr_notifications_enabled"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notifications_nostr_private_key')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notifications_nostr_private_key_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
type="password"
|
||||
filled
|
||||
v-model="formData.lnbits_nostr_notifications_private_key"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notifications_nostr_identifiers')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notifications_nostr_identifiers_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_status_manifest"
|
||||
type="text"
|
||||
:label="$t('notification_source_label')"
|
||||
v-model="nostrNotificationIdentifier"
|
||||
@keydown.enter="addNostrNotificationIdentifier"
|
||||
>
|
||||
<q-btn
|
||||
@click="addNostrNotificationIdentifier()"
|
||||
dense
|
||||
flat
|
||||
icon="add"
|
||||
></q-btn>
|
||||
</q-input>
|
||||
<div>
|
||||
<q-chip
|
||||
v-for="identifier in formData.lnbits_nostr_notifications_identifiers"
|
||||
:key="identifier"
|
||||
removable
|
||||
@remove="removeNostrNotificationIdentifier(identifier)"
|
||||
color="primary"
|
||||
text-color="white"
|
||||
><span class="ellipsis" v-text="identifier"></span
|
||||
></q-chip>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-6">
|
||||
<strong v-text="$t('notifications_telegram_config')"></strong>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notifications_enable_telegram')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notifications_enable_telegram_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_telegram_notifications_enabled"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<p v-text="$t('killswitch')"></p>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('enable_killswitch')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('enable_killswitch_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_killswitch"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('killswitch_interval')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('killswitch_interval_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_killswitch_interval"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
<p v-text="$t('watchdog')"></p>
|
||||
<q-item disabled tag="label" v-ripple>
|
||||
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('enable_watchdog')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('enable_watchdog_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_watchdog"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item disabled tag="label" v-ripple>
|
||||
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('watchdog_interval')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('watchdog_interval_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_watchdog_interval"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item disabled tag="label" v-ripple>
|
||||
<q-tooltip><span v-text="$t('coming_soon')"></span></q-tooltip>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('watchdog_delta')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('watchdog_delta_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_watchdog_delta"
|
||||
type="number"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<br />
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notifications_telegram_access_token')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notifications_telegram_access_token_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
type="password"
|
||||
filled
|
||||
v-model="formData.lnbits_telegram_notifications_access_token"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label v-text="$t('notifications_chat_id')"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notifications_chat_id_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-input
|
||||
filled
|
||||
v-model="formData.lnbits_telegram_notifications_chat_id"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
<q-separator> </q-separator>
|
||||
<h6 class="q-mb-sm">
|
||||
<span v-text="$t('notifications')"></span>
|
||||
</h6>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-12">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notification_settings_update')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notification_settings_update_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_notification_settings_update"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notification_credit_debit')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notification_credit_debit_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_notification_credit_debit"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notification_server_start_stop')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notification_server_start_stop_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_notification_server_start_stop"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notification_balance_delta_changed')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notification_balance_delta_changed_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.notification_balance_delta_changed"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notification_watchdog_limit')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notification_watchdog_limit_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section avatar>
|
||||
<q-toggle
|
||||
size="md"
|
||||
v-model="formData.lnbits_notification_watchdog"
|
||||
checked-icon="check"
|
||||
color="green"
|
||||
unchecked-icon="clear"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notification_server_status')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notification_server_status_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-input
|
||||
class="flow-right"
|
||||
type="number"
|
||||
min="0"
|
||||
filled
|
||||
v-model="formData.lnbits_notification_server_status_hours"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notification_incoming_payment')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notification_incoming_payment_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-input
|
||||
class="flow-right"
|
||||
type="number"
|
||||
min="0"
|
||||
filled
|
||||
v-model="formData.lnbits_notification_incoming_payment_amount_sats"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<q-item tag="label" v-ripple>
|
||||
<q-item-section>
|
||||
<q-item-label
|
||||
v-text="$t('notification_outgoing_payment')"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="$t('notification_outgoing_payment_desc')"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section avatar>
|
||||
<q-input
|
||||
class="flow-right"
|
||||
type="number"
|
||||
min="0"
|
||||
filled
|
||||
v-model="formData.lnbits_notification_outgoing_payment_amount_sats"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
@ -1,77 +0,0 @@
|
||||
<q-banner v-if="updateAvailable" class="bg-primary text-white">
|
||||
<q-icon size="28px" name="update"></q-icon>
|
||||
|
||||
<span v-text="$t('update_available', {version: statusData.version})"></span>
|
||||
<template v-slot:action>
|
||||
<a
|
||||
class="q-btn"
|
||||
color="white"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/lnbits/lnbits/releases"
|
||||
v-text="$t('releases')"
|
||||
></a>
|
||||
</template>
|
||||
</q-banner>
|
||||
<q-banner v-if="!updateAvailable" class="bg-green text-white">
|
||||
<q-icon size="28px" name="checkmark"></q-icon>
|
||||
<span v-text="$t('latest_update', {version: statusData.version})"></span>
|
||||
</q-banner>
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<q-table
|
||||
dense
|
||||
flat
|
||||
:rows="statusData.notifications"
|
||||
:columns="statusDataTable.columns"
|
||||
:no-data-label="$t('no_notifications')"
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th auto-width> </q-th>
|
||||
<q-th
|
||||
v-for="col in props.cols"
|
||||
:key="col.name"
|
||||
:props="props"
|
||||
v-text="col.label"
|
||||
></q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td auto-width class="text-center">
|
||||
<q-icon
|
||||
v-if="props.row.type === 'update'"
|
||||
size="18px"
|
||||
name="update"
|
||||
color="green"
|
||||
></q-icon>
|
||||
<q-icon
|
||||
v-if="props.row.type === 'warning'"
|
||||
size="18px"
|
||||
name="warning"
|
||||
color="red"
|
||||
></q-icon>
|
||||
</q-td>
|
||||
<q-td
|
||||
auto-width
|
||||
key="date"
|
||||
:props="props"
|
||||
v-text="formatDate(props.row.date)"
|
||||
>
|
||||
</q-td>
|
||||
<q-td key="message" :props="props"
|
||||
><span v-text="props.row.message"></span
|
||||
><a
|
||||
v-if="props.row.link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:href="props.row.link"
|
||||
v-text="$t('more')"
|
||||
></a
|
||||
></q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
</q-card-section>
|
||||
</q-card>
|
@ -10,7 +10,9 @@ from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.models.notifications import NotificationType
|
||||
from lnbits.core.services import (
|
||||
enqueue_notification,
|
||||
get_balance_delta,
|
||||
update_cached_settings,
|
||||
)
|
||||
@ -60,6 +62,7 @@ async def api_get_settings(
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
|
||||
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
||||
await update_admin_settings(data)
|
||||
admin_settings = await get_admin_settings(user.super_user)
|
||||
assert admin_settings, "Updated admin settings not found."
|
||||
@ -78,12 +81,9 @@ async def api_reset_settings(field_name: str):
|
||||
return {"default_value": getattr(default_settings, field_name)}
|
||||
|
||||
|
||||
@admin_router.delete(
|
||||
"/api/v1/settings",
|
||||
status_code=HTTPStatus.OK,
|
||||
dependencies=[Depends(check_super_user)],
|
||||
)
|
||||
async def api_delete_settings() -> None:
|
||||
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
|
||||
async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
|
||||
enqueue_notification(NotificationType.settings_update, {"username": user.username})
|
||||
await delete_admin_settings()
|
||||
server_restart.set()
|
||||
|
||||
|
@ -49,7 +49,7 @@ api_router = APIRouter(tags=["Core"])
|
||||
async def health() -> dict:
|
||||
return {
|
||||
"server_time": int(time()),
|
||||
"up_time": int(time() - settings.server_startup_time),
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
}
|
||||
|
||||
|
||||
@ -57,7 +57,8 @@ async def health() -> dict:
|
||||
async def health_check(user: User = Depends(check_user_exists)) -> dict:
|
||||
stat: dict[str, Any] = {
|
||||
"server_time": int(time()),
|
||||
"up_time": int(time() - settings.server_startup_time),
|
||||
"up_time": settings.lnbits_server_up_time,
|
||||
"up_time_seconds": int(time() - settings.server_startup_time),
|
||||
}
|
||||
|
||||
stat["version"] = settings.version
|
||||
|
@ -31,9 +31,11 @@ from lnbits.core.models import (
|
||||
UserExtra,
|
||||
Wallet,
|
||||
)
|
||||
from lnbits.core.models.notifications import NotificationType
|
||||
from lnbits.core.models.users import Account
|
||||
from lnbits.core.services import (
|
||||
create_user_account_no_ckeck,
|
||||
enqueue_notification,
|
||||
update_user_account,
|
||||
update_user_extensions,
|
||||
update_wallet_balance,
|
||||
@ -277,4 +279,14 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
|
||||
if not wallet:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
|
||||
await update_wallet_balance(wallet=wallet, amount=int(data.amount))
|
||||
enqueue_notification(
|
||||
NotificationType.balance_update,
|
||||
{
|
||||
"amount": int(data.amount),
|
||||
"wallet_id": wallet.id,
|
||||
"wallet_name": wallet.name,
|
||||
"balance": wallet.balance,
|
||||
},
|
||||
)
|
||||
|
||||
return SimpleStatus(success=True, message="Balance updated.")
|
||||
|
@ -9,7 +9,7 @@ from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from hashlib import sha256
|
||||
from os import path
|
||||
from time import time
|
||||
from time import gmtime, strftime, time
|
||||
from typing import Any, Optional
|
||||
|
||||
import httpx
|
||||
@ -364,20 +364,13 @@ class SecuritySettings(LNbitsSettings):
|
||||
lnbits_rate_limit_unit: str = Field(default="minute")
|
||||
lnbits_allowed_ips: list[str] = Field(default=[])
|
||||
lnbits_blocked_ips: list[str] = Field(default=[])
|
||||
lnbits_notifications: bool = Field(default=False)
|
||||
lnbits_killswitch: bool = Field(default=False)
|
||||
lnbits_killswitch_interval: int = Field(default=60)
|
||||
|
||||
lnbits_wallet_limit_max_balance: int = Field(default=0)
|
||||
lnbits_wallet_limit_daily_max_withdraw: int = Field(default=0)
|
||||
lnbits_wallet_limit_secs_between_trans: int = Field(default=0)
|
||||
lnbits_watchdog: bool = Field(default=False)
|
||||
lnbits_watchdog_interval: int = Field(default=60)
|
||||
lnbits_watchdog_switch_to_voidwallet: bool = Field(default=False)
|
||||
lnbits_watchdog_interval_minutes: int = Field(default=60)
|
||||
lnbits_watchdog_delta: int = Field(default=1_000_000)
|
||||
lnbits_status_manifest: str = Field(
|
||||
default=(
|
||||
"https://raw.githubusercontent.com/lnbits/lnbits-status/main/manifest.json"
|
||||
)
|
||||
)
|
||||
|
||||
def is_wallet_max_balance_exceeded(self, amount):
|
||||
return (
|
||||
@ -387,6 +380,24 @@ class SecuritySettings(LNbitsSettings):
|
||||
)
|
||||
|
||||
|
||||
class NotificationsSettings(LNbitsSettings):
|
||||
lnbits_nostr_notifications_enabled: bool = Field(default=False)
|
||||
lnbits_nostr_notifications_private_key: str = Field(default="")
|
||||
lnbits_nostr_notifications_identifiers: list[str] = Field(default=[])
|
||||
lnbits_telegram_notifications_enabled: bool = Field(default=False)
|
||||
lnbits_telegram_notifications_access_token: str = Field(default="")
|
||||
lnbits_telegram_notifications_chat_id: str = Field(default="")
|
||||
|
||||
lnbits_notification_settings_update: bool = Field(default=True)
|
||||
lnbits_notification_credit_debit: bool = Field(default=True)
|
||||
notification_balance_delta_changed: bool = Field(default=True)
|
||||
lnbits_notification_server_start_stop: bool = Field(default=True)
|
||||
lnbits_notification_watchdog: bool = Field(default=False)
|
||||
lnbits_notification_server_status_hours: int = Field(default=24)
|
||||
lnbits_notification_incoming_payment_amount_sats: int = Field(default=1_000_000)
|
||||
lnbits_notification_outgoing_payment_amount_sats: int = Field(default=1_000_000)
|
||||
|
||||
|
||||
class FakeWalletFundingSource(LNbitsSettings):
|
||||
fake_wallet_secret: str = Field(default="ToTheMoon1")
|
||||
|
||||
@ -705,6 +716,7 @@ class EditableSettings(
|
||||
FeeSettings,
|
||||
ExchangeProvidersSettings,
|
||||
SecuritySettings,
|
||||
NotificationsSettings,
|
||||
FundingSourcesSettings,
|
||||
LightningSettings,
|
||||
WebPushSettings,
|
||||
@ -771,6 +783,11 @@ class EnvSettings(LNbitsSettings):
|
||||
def has_default_extension_path(self) -> bool:
|
||||
return self.lnbits_extensions_path == "lnbits"
|
||||
|
||||
@property
|
||||
def lnbits_server_up_time(self) -> str:
|
||||
up_time = int(time() - self.server_startup_time)
|
||||
return strftime("%H:%M:%S", gmtime(up_time))
|
||||
|
||||
|
||||
class SaaSSettings(LNbitsSettings):
|
||||
lnbits_saas_callback: Optional[str] = Field(default=None)
|
||||
@ -822,6 +839,9 @@ class TransientSettings(InstalledExtensionsSettings, ExchangeHistorySettings):
|
||||
# Long running while loops should use this flag instead of `while True:`
|
||||
lnbits_running: bool = Field(default=True)
|
||||
|
||||
# Remember the latest balance delta in order to compare with the current one
|
||||
latest_balance_delta_sats: int = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def readonly_fields(cls):
|
||||
return [f for f in inspect.signature(cls).parameters if not f.startswith("_")]
|
||||
|
2
lnbits/static/bundle.min.js
vendored
2
lnbits/static/bundle.min.js
vendored
File diff suppressed because one or more lines are too long
@ -175,12 +175,6 @@ window.localisation.br = {
|
||||
enable_notifications: 'Ativar notificações',
|
||||
enable_notifications_desc:
|
||||
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
|
||||
enable_killswitch: 'Ativar Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Se ativado, mudará sua fonte de fundos para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
|
||||
killswitch_interval: 'Intervalo do Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNbits proveniente da fonte de status (em minutos).',
|
||||
enable_watchdog: 'Ativar Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se ativado, ele mudará automaticamente sua fonte de financiamento para VoidWallet se o seu saldo for inferior ao saldo do LNbits. Você precisará ativar manualmente após uma atualização.',
|
||||
@ -197,7 +191,6 @@ window.localisation.br = {
|
||||
more: 'mais',
|
||||
less: 'menos',
|
||||
releases: 'Lançamentos',
|
||||
killswitch: 'Dispositivo de desativação',
|
||||
watchdog: 'Cão de guarda',
|
||||
server_logs: 'Registros do Servidor',
|
||||
ip_blocker: 'Bloqueador de IP',
|
||||
|
@ -166,12 +166,7 @@ window.localisation.cn = {
|
||||
enable_notifications: '启用通知',
|
||||
enable_notifications_desc:
|
||||
'如果启用,它将获取最新的LNbits状态更新,如安全事件和更新。',
|
||||
enable_killswitch: '启用紧急停止开关',
|
||||
enable_killswitch_desc:
|
||||
'如果启用,当LNbits发送终止信号时,系统将自动将您的资金来源更改为VoidWallet。更新后,您将需要手动启用。',
|
||||
killswitch_interval: 'Killswitch 间隔',
|
||||
killswitch_interval_desc:
|
||||
'后台任务应该多久检查一次来自状态源的LNbits断路信号(以分钟为单位)。',
|
||||
|
||||
enable_watchdog: '启用看门狗',
|
||||
enable_watchdog_desc:
|
||||
'如果启用,当您的余额低于LNbits余额时,系统将自动将您的资金来源更改为VoidWallet。更新后您将需要手动启用。',
|
||||
@ -187,7 +182,6 @@ window.localisation.cn = {
|
||||
more: '更多',
|
||||
less: '少',
|
||||
releases: '版本',
|
||||
killswitch: '杀手锏',
|
||||
watchdog: '监控程序',
|
||||
server_logs: '服务器日志',
|
||||
ip_blocker: 'IP 阻止器',
|
||||
|
@ -174,15 +174,6 @@ window.localisation.cs = {
|
||||
enable_notifications: 'Povolit notifikace',
|
||||
enable_notifications_desc:
|
||||
'Pokud je povoleno, bude stahovat nejnovější aktualizace stavu LNbits, jako jsou bezpečnostní incidenty a aktualizace.',
|
||||
enable_killswitch: 'Povolit Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud LNbits odešle signál killswitch. Po aktualizaci budete muset povolit ručně.',
|
||||
killswitch_interval: 'Interval Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Jak často by měl úkol na pozadí kontrolovat signál killswitch od LNbits ze zdroje stavu (v minutách).',
|
||||
enable_watchdog: 'Povolit Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Pokud je povoleno, automaticky změní zdroj financování na VoidWallet pokud je váš zůstatek nižší než zůstatek LNbits. Po aktualizaci budete muset povolit ručně.',
|
||||
watchdog_interval: 'Interval Watchdog',
|
||||
watchdog_interval_desc:
|
||||
'Jak často by měl úkol na pozadí kontrolovat signál killswitch v watchdog delta [node_balance - lnbits_balance] (v minutách).',
|
||||
@ -196,7 +187,6 @@ window.localisation.cs = {
|
||||
more: 'více',
|
||||
less: 'méně',
|
||||
releases: 'Vydání',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Logy serveru',
|
||||
ip_blocker: 'Blokování IP',
|
||||
|
@ -177,12 +177,6 @@ window.localisation.de = {
|
||||
enable_notifications: 'Aktiviere Benachrichtigungen',
|
||||
enable_notifications_desc:
|
||||
'Wenn aktiviert, werden die neuesten LNbits-Statusaktualisierungen, wie Sicherheitsvorfälle und Updates, abgerufen.',
|
||||
enable_killswitch: 'Aktivieren Sie den Notausschalter',
|
||||
enable_killswitch_desc:
|
||||
'Falls aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn LNbits ein Killswitch-Signal sendet. Nach einem Update müssen Sie dies manuell wieder aktivieren.',
|
||||
killswitch_interval: 'Intervall für den Notausschalter',
|
||||
killswitch_interval_desc:
|
||||
'Wie oft die Hintergrundaufgabe nach dem LNbits-Killswitch-Signal aus der Statusquelle suchen soll (in Minuten).',
|
||||
enable_watchdog: 'Aktiviere Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Wenn aktiviert, wird Ihre Zahlungsquelle automatisch auf VoidWallet umgestellt, wenn Ihr Guthaben niedriger als das LNbits-Guthaben ist. Nach einem Update müssen Sie dies manuell aktivieren.',
|
||||
@ -199,7 +193,7 @@ window.localisation.de = {
|
||||
more: 'mehr',
|
||||
less: 'weniger',
|
||||
releases: 'Veröffentlichungen',
|
||||
killswitch: 'Killswitch',
|
||||
|
||||
watchdog: 'Wachhund',
|
||||
server_logs: 'Serverprotokolle',
|
||||
ip_blocker: 'IP-Sperre',
|
||||
|
@ -168,18 +168,58 @@ window.localisation.en = {
|
||||
update_available: 'Update {version} available!',
|
||||
latest_update: 'You are on the latest version {version}.',
|
||||
notifications: 'Notifications',
|
||||
no_notifications: 'No notifications',
|
||||
notifications_disabled: 'LNbits status notifications are disabled.',
|
||||
enable_notifications: 'Enable Notifications',
|
||||
enable_notifications_desc:
|
||||
'If enabled it will fetch the latest LNbits Status updates, like security incidents and updates.',
|
||||
enable_killswitch: 'Enable Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'If enabled it will change your funding source to VoidWallet automatically if LNbits sends out a killswitch signal. You will need to enable manually after an update.',
|
||||
killswitch_interval: 'Killswitch Interval',
|
||||
killswitch_interval_desc:
|
||||
'How often the background task should check for the LNbits killswitch signal from the status source (in minutes).',
|
||||
enable_watchdog: 'Enable Watchdog',
|
||||
notifications_configure: 'Configure Notifications',
|
||||
notifications_nostr_config: 'Nostr Configuration',
|
||||
notifications_enable_nostr: 'Enable Nostr',
|
||||
notifications_enable_nostr_desc: 'Send notfications over Nostr',
|
||||
notifications_nostr_private_key: 'Nostr Private Key',
|
||||
notifications_nostr_private_key_desc:
|
||||
'Private key (hex or nsec) to sign the messages sent to Nostr',
|
||||
notifications_nostr_identifiers: 'Nostr Identifiers',
|
||||
notifications_nostr_identifiers_desc:
|
||||
'List of identifiers to send notifications to',
|
||||
|
||||
notifications_telegram_config: 'Telegram Configuration',
|
||||
notifications_enable_telegram: 'Enable Telegram',
|
||||
notifications_enable_telegram_desc: 'Send notfications over Telegram',
|
||||
notifications_telegram_access_token: 'Access Token',
|
||||
notifications_telegram_access_token_desc: 'Access token for the bot',
|
||||
notifications_chat_id: 'Chat ID',
|
||||
notifications_chat_id_desc: 'Chat ID to send the notifications to',
|
||||
|
||||
notification_settings_update: 'Settings updated',
|
||||
notification_settings_update_desc:
|
||||
'Notify when server settings have been updated',
|
||||
|
||||
notification_server_start_stop: 'Server Start/Stop',
|
||||
notification_server_start_stop_desc:
|
||||
'Notify when the server has been started/stopped',
|
||||
|
||||
notification_watchdog_limit: 'Watchdog Limit Notification',
|
||||
notification_watchdog_limit_desc:
|
||||
'Notify when the watchdog limit has been reached (does not affect the funding source)',
|
||||
|
||||
notification_server_status: 'Server Status',
|
||||
notification_server_status_desc:
|
||||
'Send regular notifications about the server status (interval value in hours)',
|
||||
|
||||
notification_incoming_payment: 'Incoming Payments',
|
||||
notification_incoming_payment_desc:
|
||||
'Notify when a wallet has received a payment above the specified amount (sats)',
|
||||
|
||||
notification_outgoing_payment: 'Outgoing Payments',
|
||||
notification_outgoing_payment_desc:
|
||||
'Notify when a wallet has sent a payment above the specified amount (sats)',
|
||||
|
||||
notification_credit_debit: 'Credit / Debit',
|
||||
notification_credit_debit_desc:
|
||||
'Notify when a wallet has been credited/debited by the superuser',
|
||||
|
||||
notification_balance_delta_changed: 'Balance Delta Changed',
|
||||
notification_balance_delta_changed_desc:
|
||||
'Notify when the diference between the node balance and the LNbits balance has changed even by 1 sat. This runs every minute.',
|
||||
|
||||
enable_watchdog: 'Enable Watchdog Switch',
|
||||
enable_watchdog_desc:
|
||||
'If enabled it will change your funding source to VoidWallet automatically if your balance is lower than the LNbits balance. You will need to enable manually after an update.',
|
||||
watchdog_interval: 'Watchdog Interval',
|
||||
@ -195,7 +235,6 @@ window.localisation.en = {
|
||||
more: 'more',
|
||||
less: 'less',
|
||||
releases: 'Releases',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Server Logs',
|
||||
ip_blocker: 'IP Blocker',
|
||||
|
@ -177,13 +177,7 @@ window.localisation.es = {
|
||||
enable_notifications: 'Activar notificaciones',
|
||||
enable_notifications_desc:
|
||||
'Si está activado, buscará las últimas actualizaciones del estado de LNbits, como incidentes de seguridad y actualizaciones.',
|
||||
enable_killswitch: 'Activar Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si LNbits envía una señal de parada de emergencia. Necesitará activarlo manualmente después de una actualización.',
|
||||
killswitch_interval: 'Intervalo de Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Con qué frecuencia la tarea en segundo plano debe verificar la señal de interruptor de emergencia de LNbits desde la fuente de estado (en minutos).',
|
||||
enable_watchdog: 'Activar Watchdog',
|
||||
|
||||
enable_watchdog_desc:
|
||||
'Si está activado, cambiará automáticamente su fuente de financiamiento a VoidWallet si su saldo es inferior al saldo de LNbits. Tendrá que activarlo manualmente después de una actualización.',
|
||||
watchdog_interval: 'Intervalo de vigilancia',
|
||||
@ -199,7 +193,6 @@ window.localisation.es = {
|
||||
more: 'más',
|
||||
less: 'menos',
|
||||
releases: 'Lanzamientos',
|
||||
killswitch: 'Interruptor de apagado',
|
||||
watchdog: 'Perro guardián',
|
||||
server_logs: 'Registros del Servidor',
|
||||
ip_blocker: 'Bloqueador de IP',
|
||||
|
@ -176,12 +176,7 @@ window.localisation.fi = {
|
||||
enable_notifications: 'Ota tiedotteet käyttöön',
|
||||
enable_notifications_desc:
|
||||
'Tämän ollessa valittuna, noudetaan LNbits-tilatiedotteet. Niitä ovat esimerkiksi turvallisuuteen liittyvät tapahtumatiedotteet ja tiedot tämän ohjelmiston päivityksistä.',
|
||||
enable_killswitch: 'Ota Killswitch käyttöön',
|
||||
enable_killswitch_desc:
|
||||
'Jos LNbits antaa killswitch-komennon, niin rahoituslähteeksi valitaan automaattisesti heti VoidWallet. Päivityksen jälkeen tämä asetus pitää tarkastaa uudelleen.',
|
||||
killswitch_interval: 'Killswitch-aikaväli',
|
||||
killswitch_interval_desc:
|
||||
'Tällä määritetään kuinka usein taustatoiminto tarkistaa killswitch-signaalin tilatiedotteiden lähteestä. Hakujen väli ilmoitetaan minuutteina.',
|
||||
|
||||
enable_watchdog: 'Ota Watchdog käyttöön',
|
||||
enable_watchdog_desc:
|
||||
'Tämän ollessa käytössä, ja solmun varojen laskiessa alle LNbits-varojen määrän, otetaan automaattisesti käyttöön VoidWallet. Päivityksen jälkeen tämä asetus pitää tarkastaa uudelleen.',
|
||||
@ -198,7 +193,7 @@ window.localisation.fi = {
|
||||
more: 'enemmän',
|
||||
less: 'vähemmän',
|
||||
releases: 'Julkaisut',
|
||||
killswitch: 'Killswitch',
|
||||
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Palvelimen lokit',
|
||||
ip_blocker: 'IP-suodatin',
|
||||
|
@ -180,12 +180,7 @@ window.localisation.fr = {
|
||||
enable_notifications: 'Activer les notifications',
|
||||
enable_notifications_desc:
|
||||
'Si activé, il récupérera les dernières mises à jour du statut LNbits, telles que les incidents de sécurité et les mises à jour.',
|
||||
enable_killswitch: 'Activer le Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Si activé, il changera automatiquement votre source de financement en VoidWallet si LNbits envoie un signal de coupure. Vous devrez activer manuellement après une mise à jour.',
|
||||
killswitch_interval: 'Intervalle du Killswitch',
|
||||
killswitch_interval_desc:
|
||||
"À quelle fréquence la tâche de fond doit-elle vérifier le signal d'arrêt d'urgence LNbits provenant de la source de statut (en minutes).",
|
||||
|
||||
enable_watchdog: 'Activer le Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Si elle est activée, elle changera automatiquement votre source de financement en VoidWallet si votre solde est inférieur au solde LNbits. Vous devrez activer manuellement après une mise à jour.',
|
||||
@ -202,7 +197,6 @@ window.localisation.fr = {
|
||||
more: 'plus',
|
||||
less: 'moins',
|
||||
releases: 'Versions',
|
||||
killswitch: "Interrupteur d'arrêt",
|
||||
watchdog: 'Chien de garde',
|
||||
server_logs: 'Journaux du serveur',
|
||||
ip_blocker: "Bloqueur d'IP",
|
||||
|
@ -176,12 +176,6 @@ window.localisation.it = {
|
||||
enable_notifications: 'Attiva le notifiche',
|
||||
enable_notifications_desc:
|
||||
'Se attivato, recupererà gli ultimi aggiornamenti sullo stato di LNbits, come incidenti di sicurezza e aggiornamenti.',
|
||||
enable_killswitch: 'Attiva Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Se attivato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se LNbits invia un segnale di killswitch. Dovrai attivare manualmente dopo un aggiornamento.',
|
||||
killswitch_interval: 'Intervallo Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Quanto spesso il compito in background dovrebbe controllare il segnale di killswitch LNbits dalla fonte di stato (in minuti).',
|
||||
enable_watchdog: 'Attiva Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se abilitato, cambierà automaticamente la tua fonte di finanziamento in VoidWallet se il tuo saldo è inferiore al saldo LNbits. Dovrai abilitarlo manualmente dopo un aggiornamento.',
|
||||
@ -198,7 +192,6 @@ window.localisation.it = {
|
||||
more: 'più',
|
||||
less: 'meno',
|
||||
releases: 'Pubblicazioni',
|
||||
killswitch: 'Interruttore di spegnimento',
|
||||
watchdog: 'Cane da guardia',
|
||||
server_logs: 'Registri del server',
|
||||
ip_blocker: 'Blocco IP',
|
||||
|
@ -171,12 +171,6 @@ window.localisation.jp = {
|
||||
enable_notifications: '通知を有効にする',
|
||||
enable_notifications_desc:
|
||||
'有効にすると、セキュリティインシデントやアップデートのような最新のLNbitsステータス更新を取得します。',
|
||||
enable_killswitch: 'キルスイッチを有効にする',
|
||||
enable_killswitch_desc:
|
||||
'有効にすると、LNbitsからキルスイッチ信号が送信された場合に自動的に資金源をVoidWalletに切り替えます。更新後には手動で有効にする必要があります。',
|
||||
killswitch_interval: 'キルスイッチ間隔',
|
||||
killswitch_interval_desc:
|
||||
'バックグラウンドタスクがステータスソースからLNbitsキルスイッチ信号を確認する頻度(分単位)。',
|
||||
enable_watchdog: 'ウォッチドッグを有効にする',
|
||||
enable_watchdog_desc:
|
||||
'有効にすると、残高がLNbitsの残高より少ない場合に、資金源を自動的にVoidWalletに変更します。アップデート後は手動で有効にする必要があります。',
|
||||
@ -193,7 +187,6 @@ window.localisation.jp = {
|
||||
more: 'より多くの',
|
||||
less: '少ない',
|
||||
releases: 'リリース',
|
||||
killswitch: 'キルスイッチ',
|
||||
watchdog: 'ウォッチドッグ',
|
||||
server_logs: 'サーバーログ',
|
||||
ip_blocker: 'IPブロッカー',
|
||||
|
@ -173,12 +173,6 @@ window.localisation.kr = {
|
||||
enable_notifications: '알림 활성화',
|
||||
enable_notifications_desc:
|
||||
'활성화 시, 가장 최신의 보안 사고나 소프트웨어 업데이트 등의 LNbits 상황 업데이트를 불러옵니다.',
|
||||
enable_killswitch: '비상 정지 활성화',
|
||||
enable_killswitch_desc:
|
||||
'활성화 시, LNbits 메인 서버에서 비상 정지 신호를 보내면 자동으로 자금의 원천을 VoidWallet으로 변경합니다. 업데이트 이후 수동으로 활성화해 주어야 합니다.',
|
||||
killswitch_interval: '비상 정지 시간 간격',
|
||||
killswitch_interval_desc:
|
||||
'LNbits 메인 서버에서 나오는 비상 정지 신호를 백그라운드 작업으로 얼마나 자주 확인할 것인지를 결정합니다. (분 단위)',
|
||||
enable_watchdog: '와치독 활성화',
|
||||
enable_watchdog_desc:
|
||||
'활성화 시, LNbits 잔금보다 당신의 잔금이 지정한 수준보다 더 낮아질 경우 자동으로 자금의 원천을 VoidWallet으로 변경합니다. 업데이트 이후 수동으로 활성화해 주어야 합니다.',
|
||||
@ -195,7 +189,6 @@ window.localisation.kr = {
|
||||
more: '더 알아보기',
|
||||
less: '적게',
|
||||
releases: '배포 버전들',
|
||||
killswitch: '비상 정지',
|
||||
watchdog: '와치독',
|
||||
server_logs: '서버 로그',
|
||||
ip_blocker: 'IP 기반 차단기',
|
||||
|
@ -177,12 +177,6 @@ window.localisation.nl = {
|
||||
enable_notifications: 'Schakel meldingen in',
|
||||
enable_notifications_desc:
|
||||
'Indien ingeschakeld zal het de laatste LNbits Status updates ophalen, zoals veiligheidsincidenten en updates.',
|
||||
enable_killswitch: 'Activeer Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Indien ingeschakeld, zal het uw financieringsbron automatisch wijzigen naar VoidWallet als LNbits een killswitch-signaal verzendt. U zult het na een update handmatig moeten inschakelen.',
|
||||
killswitch_interval: 'Uitschakelschakelaar-interval',
|
||||
killswitch_interval_desc:
|
||||
'Hoe vaak de achtergrondtaak moet controleren op het LNbits killswitch signaal van de statusbron (in minuten).',
|
||||
enable_watchdog: 'Inschakelen Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Indien ingeschakeld, wordt uw betaalbron automatisch gewijzigd naar VoidWallet als uw saldo lager is dan het saldo van LNbits. U zult dit na een update handmatig moeten inschakelen.',
|
||||
@ -199,7 +193,6 @@ window.localisation.nl = {
|
||||
more: 'meer',
|
||||
less: 'minder',
|
||||
releases: 'Uitgaven',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Waakhond',
|
||||
server_logs: 'Serverlogboeken',
|
||||
ip_blocker: 'IP-blokkering',
|
||||
|
@ -175,12 +175,6 @@ window.localisation.pi = {
|
||||
enable_notifications: 'Enable Notifications',
|
||||
enable_notifications_desc:
|
||||
"If ye be allowin' it, it'll be fetchin' the latest LNbits Status updates, like security incidents and updates.",
|
||||
enable_killswitch: "Enabl' th' Killswitch",
|
||||
enable_killswitch_desc:
|
||||
"If enabled it'll be changin' yer fundin' source to VoidWallet automatically if LNbits sends out a killswitch signal, ye will. Ye'll be needin' t' enable manually after an update, arr.",
|
||||
killswitch_interval: 'Killswitch Interval',
|
||||
killswitch_interval_desc:
|
||||
"How oft th' background task should be checkin' fer th' LNbits killswitch signal from th' status source (in minutes).",
|
||||
enable_watchdog: 'Enable Seadog',
|
||||
enable_watchdog_desc:
|
||||
"If enabled, it will swap yer treasure source t' VoidWallet on its own if yer balance be lower than th' LNbits balance. Ye'll need t' enable by hand after an update.",
|
||||
@ -197,7 +191,6 @@ window.localisation.pi = {
|
||||
more: "Arr, 'tis more.",
|
||||
less: "Arr, 'tis more fewer.",
|
||||
releases: 'Releases',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Seadog',
|
||||
server_logs: 'Server Logs',
|
||||
ip_blocker: 'IP Blockar',
|
||||
|
@ -173,12 +173,6 @@ window.localisation.pl = {
|
||||
enable_notifications: 'Włącz powiadomienia',
|
||||
enable_notifications_desc:
|
||||
'Jeśli ta opcja zostanie włączona, będzie pobierać najnowsze informacje o statusie LNbits, takie jak incydenty bezpieczeństwa i aktualizacje.',
|
||||
enable_killswitch: 'Włącz Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli LNbits wyśle sygnał wyłączający. Po aktualizacji będziesz musiał włączyć to ręcznie.',
|
||||
killswitch_interval: 'Interwał wyłącznika awaryjnego',
|
||||
killswitch_interval_desc:
|
||||
'Jak często zadanie w tle powinno sprawdzać sygnał wyłącznika awaryjnego LNbits ze źródła statusu (w minutach).',
|
||||
enable_watchdog: 'Włącz Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Jeśli zostanie włączone, automatycznie zmieni źródło finansowania na VoidWallet, jeśli saldo jest niższe niż saldo LNbits. Po aktualizacji trzeba będzie włączyć ręcznie.',
|
||||
@ -195,7 +189,6 @@ window.localisation.pl = {
|
||||
more: 'więcej',
|
||||
less: 'mniej',
|
||||
releases: 'Wydania',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Pies gończy',
|
||||
server_logs: 'Dzienniki serwera',
|
||||
ip_blocker: 'Blokada IP',
|
||||
|
@ -176,12 +176,6 @@ window.localisation.pt = {
|
||||
enable_notifications: 'Ativar Notificações',
|
||||
enable_notifications_desc:
|
||||
'Se ativado, ele buscará as últimas atualizações de status do LNbits, como incidentes de segurança e atualizações.',
|
||||
enable_killswitch: 'Ativar Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Se ativado, ele mudará sua fonte de financiamento para VoidWallet automaticamente se o LNbits enviar um sinal de desativação. Você precisará ativar manualmente após uma atualização.',
|
||||
killswitch_interval: 'Intervalo do Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Com que frequência a tarefa de fundo deve verificar o sinal de desativação do LNbits proveniente da fonte de status (em minutos).',
|
||||
enable_watchdog: 'Ativar Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Se ativado, mudará automaticamente a sua fonte de financiamento para VoidWallet caso o seu saldo seja inferior ao saldo LNbits. Você precisará ativar manualmente após uma atualização.',
|
||||
@ -198,7 +192,6 @@ window.localisation.pt = {
|
||||
more: 'mais',
|
||||
less: 'menos',
|
||||
releases: 'Lançamentos',
|
||||
killswitch: 'Interruptor de desativação',
|
||||
watchdog: 'Cão de guarda',
|
||||
server_logs: 'Registros do Servidor',
|
||||
ip_blocker: 'Bloqueador de IP',
|
||||
|
@ -172,12 +172,6 @@ window.localisation.sk = {
|
||||
enable_notifications: 'Povoliť Notifikácie',
|
||||
enable_notifications_desc:
|
||||
'Ak povolené, budú sa načítavať najnovšie aktualizácie stavu LNbits, ako sú bezpečnostné incidenty a aktualizácie.',
|
||||
enable_killswitch: 'Povoliť Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Ak povolené, vaš zdroj financovania sa automaticky zmení na VoidWallet, ak LNbits vysielajú signál killswitch. Po aktualizácii bude treba povoliť manuálne.',
|
||||
killswitch_interval: 'Interval Killswitch',
|
||||
killswitch_interval_desc:
|
||||
'Ako často by malo pozadie kontrolovať signál killswitch od LNbits zo zdroja stavu (v minútach).',
|
||||
enable_watchdog: 'Povoliť Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Ak povolené, vaš zdroj financovania sa automaticky zmení na VoidWallet, ak je váš zostatok nižší ako zostatok LNbits. Po aktualizácii bude treba povoliť manuálne.',
|
||||
@ -194,7 +188,6 @@ window.localisation.sk = {
|
||||
more: 'viac',
|
||||
less: 'menej',
|
||||
releases: 'Vydania',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Watchdog',
|
||||
server_logs: 'Logy servera',
|
||||
ip_blocker: 'Blokovanie IP',
|
||||
|
@ -173,12 +173,6 @@ window.localisation.we = {
|
||||
enable_notifications: 'Galluogi Hysbysiadau',
|
||||
enable_notifications_desc:
|
||||
"Os bydd wedi'i alluogi bydd yn nôl y diweddariadau Statws LNbits diweddaraf, fel digwyddiadau diogelwch a diweddariadau.",
|
||||
enable_killswitch: 'Galluogi Killswitch',
|
||||
enable_killswitch_desc:
|
||||
'Os bydd yn galluogi, bydd yn newid eich ffynhonnell arian i VoidWallet yn awtomatig os bydd LNbits yn anfon arwydd killswitch. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
|
||||
killswitch_interval: 'Amlder Cyllell Dorri',
|
||||
killswitch_interval_desc:
|
||||
"Pa mor aml y dylai'r dasg gefndir wirio am signal killswitch LNbits o'r ffynhonnell statws (mewn munudau).",
|
||||
enable_watchdog: 'Galluogi Watchdog',
|
||||
enable_watchdog_desc:
|
||||
'Os bydd yn cael ei alluogi bydd yn newid eich ffynhonnell ariannu i VoidWallet yn awtomatig os bydd eich balans yn is na balans LNbits. Bydd angen i chi alluogi â llaw ar ôl diweddariad.',
|
||||
@ -195,7 +189,6 @@ window.localisation.we = {
|
||||
more: 'mwy',
|
||||
less: 'llai',
|
||||
releases: 'Rhyddhau',
|
||||
killswitch: 'Killswitch',
|
||||
watchdog: 'Gwyliwr',
|
||||
server_logs: 'Logiau Gweinydd',
|
||||
ip_blocker: 'Rheolydd IP',
|
||||
|
@ -41,6 +41,7 @@ window.AdminPageLogic = {
|
||||
formAddAdmin: '',
|
||||
formAddUser: '',
|
||||
formAddExtensionsManifest: '',
|
||||
nostrNotificationIdentifier: '',
|
||||
formAllowedIPs: '',
|
||||
formBlockedIPs: '',
|
||||
nostrAcceptedUrl: '',
|
||||
@ -238,6 +239,23 @@ window.AdminPageLogic = {
|
||||
m => m !== manifest
|
||||
)
|
||||
},
|
||||
addNostrNotificationIdentifier() {
|
||||
const identifer = this.nostrNotificationIdentifier.trim()
|
||||
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
|
||||
if (identifer && identifer.length && !identifiers.includes(identifer)) {
|
||||
this.formData.lnbits_nostr_notifications_identifiers = [
|
||||
...identifiers,
|
||||
identifer
|
||||
]
|
||||
this.nostrNotificationIdentifier = ''
|
||||
}
|
||||
},
|
||||
removeNostrNotificationIdentifier(identifer) {
|
||||
const identifiers = this.formData.lnbits_nostr_notifications_identifiers
|
||||
this.formData.lnbits_nostr_notifications_identifiers = identifiers.filter(
|
||||
m => m !== identifer
|
||||
)
|
||||
},
|
||||
async toggleServerLog() {
|
||||
this.serverlogEnabled = !this.serverlogEnabled
|
||||
if (this.serverlogEnabled) {
|
||||
@ -377,23 +395,9 @@ window.AdminPageLogic = {
|
||||
formatDate(date) {
|
||||
return moment(date * 1000).fromNow()
|
||||
},
|
||||
getNotifications() {
|
||||
if (this.settings.lnbits_notifications) {
|
||||
axios
|
||||
.get(this.settings.lnbits_status_manifest)
|
||||
.then(response => {
|
||||
this.statusData = response.data
|
||||
})
|
||||
.catch(error => {
|
||||
this.formData.lnbits_notifications = false
|
||||
error.response.data = {}
|
||||
error.response.data.message = 'Could not fetch status manifest.'
|
||||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
async getAudit() {
|
||||
await LNbits.api
|
||||
|
||||
getAudit() {
|
||||
LNbits.api
|
||||
.request('GET', '/admin/api/v1/audit', this.g.user.wallets[0].adminkey)
|
||||
.then(response => {
|
||||
this.auditData = response.data
|
||||
@ -421,7 +425,6 @@ window.AdminPageLogic = {
|
||||
this.isSuperUser = response.data.is_super_user || false
|
||||
this.settings = response.data
|
||||
this.formData = {...this.settings}
|
||||
this.getNotifications()
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
@ -441,8 +444,7 @@ window.AdminPageLogic = {
|
||||
.then(response => {
|
||||
this.needsRestart =
|
||||
this.settings.lnbits_backend_wallet_class !==
|
||||
this.formData.lnbits_backend_wallet_class ||
|
||||
this.settings.lnbits_killswitch !== this.formData.lnbits_killswitch
|
||||
this.formData.lnbits_backend_wallet_class
|
||||
this.settings = this.formData
|
||||
this.formData = _.clone(this.settings)
|
||||
Quasar.Notify.create({
|
||||
|
@ -555,6 +555,7 @@
|
||||
filled
|
||||
:label="$t('credit_label', {denomination: denomination})"
|
||||
v-model="scope.value"
|
||||
type="number"
|
||||
dense
|
||||
autofocus
|
||||
@keyup.enter="updateBalance(scope)"
|
||||
|
@ -1,13 +1,22 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Dict, Union
|
||||
import re
|
||||
from typing import Dict, Tuple, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import secp256k1
|
||||
from bech32 import bech32_decode, bech32_encode, convertbits
|
||||
from Cryptodome import Random
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Util.Padding import pad, unpad
|
||||
from pynostr.key import PrivateKey
|
||||
|
||||
|
||||
def generate_keypair() -> Tuple[str, str]:
|
||||
private_key = PrivateKey()
|
||||
public_key = private_key.public_key
|
||||
return private_key.hex(), public_key.hex()
|
||||
|
||||
|
||||
def encrypt_content(
|
||||
@ -155,22 +164,30 @@ def json_dumps(data: Union[Dict, list]) -> str:
|
||||
return json.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
||||
|
||||
|
||||
def normalize_public_key(pubkey: str) -> str:
|
||||
if pubkey.startswith("npub1"):
|
||||
_, decoded_data = bech32_decode(pubkey)
|
||||
assert decoded_data, "Public Key is not valid npub."
|
||||
def normalize_public_key(key: str) -> str:
|
||||
return normalize_bech32_key("npub1", key)
|
||||
|
||||
|
||||
def normalize_private_key(key: str) -> str:
|
||||
return normalize_bech32_key("nsec1", key)
|
||||
|
||||
|
||||
def normalize_bech32_key(hrp: str, key: str) -> str:
|
||||
if key.startswith(hrp):
|
||||
_, decoded_data = bech32_decode(key)
|
||||
assert decoded_data, f"Key is not valid {hrp}."
|
||||
|
||||
decoded_data_bits = convertbits(decoded_data, 5, 8, False)
|
||||
assert decoded_data_bits, "Public Key is not valid npub."
|
||||
assert decoded_data_bits, f"Key is not valid {hrp}."
|
||||
|
||||
return bytes(decoded_data_bits).hex()
|
||||
|
||||
assert len(pubkey) == 64, "Public key has wrong length."
|
||||
assert len(key) == 64, "Key has wrong length."
|
||||
try:
|
||||
int(pubkey, 16)
|
||||
int(key, 16)
|
||||
except Exception as exc:
|
||||
raise AssertionError("Public Key is not valid hex.") from exc
|
||||
return pubkey
|
||||
raise AssertionError("Key is not valid hex.") from exc
|
||||
return key
|
||||
|
||||
|
||||
def hex_to_npub(hex_pubkey: str) -> str:
|
||||
@ -188,3 +205,46 @@ def hex_to_npub(hex_pubkey: str) -> str:
|
||||
bits = convertbits(pubkey_bytes, 8, 5, True)
|
||||
assert bits
|
||||
return bech32_encode("npub", bits)
|
||||
|
||||
|
||||
def normalize_identifier(identifier: str):
|
||||
identifier = identifier.lower().split("@")[0]
|
||||
validate_identifier(identifier)
|
||||
return identifier
|
||||
|
||||
|
||||
def validate_pub_key(pubkey: str) -> str:
|
||||
if pubkey.startswith("npub"):
|
||||
_, data = bech32_decode(pubkey)
|
||||
if data:
|
||||
decoded_data = convertbits(data, 5, 8, False)
|
||||
if decoded_data:
|
||||
pubkey = bytes(decoded_data).hex()
|
||||
try:
|
||||
_hex = bytes.fromhex(pubkey)
|
||||
except Exception as exc:
|
||||
raise ValueError("Pubkey must be in npub or hex format.") from exc
|
||||
|
||||
if len(_hex) != 32:
|
||||
raise ValueError("Pubkey length incorrect.")
|
||||
|
||||
return pubkey
|
||||
|
||||
|
||||
def validate_identifier(local_part: str):
|
||||
regex = re.compile(r"^[a-z0-9_.]+$")
|
||||
if not re.fullmatch(regex, local_part.lower()):
|
||||
raise ValueError(
|
||||
f"Identifier '{local_part}' not allowed! "
|
||||
"Only a-z, 0-9 and .-_ are allowed characters, case insensitive."
|
||||
)
|
||||
|
||||
|
||||
def is_ws_url(url):
|
||||
try:
|
||||
result = urlparse(url)
|
||||
if not all([result.scheme, result.netloc]):
|
||||
return False
|
||||
return result.scheme in ["ws", "wss"]
|
||||
except ValueError:
|
||||
return False
|
||||
|
152
poetry.lock
generated
152
poetry.lock
generated
@ -1587,6 +1587,30 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
|
||||
[package.extras]
|
||||
dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
|
||||
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mdurl = ">=0.1,<1.0"
|
||||
|
||||
[package.extras]
|
||||
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
|
||||
code-style = ["pre-commit (>=3.0,<4.0)"]
|
||||
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
|
||||
linkify = ["linkify-it-py (>=1,<3)"]
|
||||
plugins = ["mdit-py-plugins"]
|
||||
profiling = ["gprof2dot"]
|
||||
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
|
||||
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.3"
|
||||
@ -1676,6 +1700,17 @@ docs = ["alabaster (==0.7.13)", "autodocsumm (==0.2.11)", "sphinx (==7.0.1)", "s
|
||||
lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
description = "Markdown URL utilities"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mock"
|
||||
version = "5.1.0"
|
||||
@ -2090,6 +2125,20 @@ typing-extensions = ">=4.2.0"
|
||||
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||
email = ["email-validator (>=1.0.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
|
||||
{file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.9.0"
|
||||
@ -2151,6 +2200,29 @@ coincurve = ">=20,<21"
|
||||
cryptography = ">=42,<43"
|
||||
PySocks = ">=1,<2"
|
||||
|
||||
[[package]]
|
||||
name = "pynostr"
|
||||
version = "0.6.2"
|
||||
description = "Python Library for nostr."
|
||||
optional = false
|
||||
python-versions = ">3.7.0"
|
||||
files = [
|
||||
{file = "pynostr-0.6.2-py3-none-any.whl", hash = "sha256:d43fb236c73174093275ee0080b2f8ed17e974b2b516f0d73da4c9a3e908ddc5"},
|
||||
{file = "pynostr-0.6.2.tar.gz", hash = "sha256:2974ea05b3ff41a1a4060e3b1813eb0ce0e60c0b81fbe668afaa65164c7f82f4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
coincurve = ">=1.8.0"
|
||||
cryptography = ">=37.0.4"
|
||||
requests = "*"
|
||||
rich = "*"
|
||||
tlv8 = "*"
|
||||
tornado = "*"
|
||||
typer = "*"
|
||||
|
||||
[package.extras]
|
||||
websocket-client = ["websocket-client (>=1.3.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyqrcode"
|
||||
version = "1.2.1"
|
||||
@ -2351,7 +2423,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
@ -2436,6 +2507,25 @@ files = [
|
||||
[package.dependencies]
|
||||
six = "*"
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.9.4"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"},
|
||||
{file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
markdown-it-py = ">=2.2.0"
|
||||
pygments = ">=2.13.0,<3.0.0"
|
||||
typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "rpds-py"
|
||||
version = "0.20.0"
|
||||
@ -2625,6 +2715,17 @@ files = [
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
description = "Tool to Detect Surrounding Shell"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
|
||||
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shortuuid"
|
||||
version = "1.0.13"
|
||||
@ -2787,6 +2888,16 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""
|
||||
[package.extras]
|
||||
full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "tlv8"
|
||||
version = "0.10.0"
|
||||
description = "Python module to handle type-length-value (TLV) encoded data 8-bit type, 8-bit length, and N-byte value as described within the Apple HomeKit Accessory Protocol Specification Non-Commercial Version Release R2."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "tlv8-0.10.0.tar.gz", hash = "sha256:7930a590267b809952272ac2a27ee81b99ec5191fa2eba08050e0daee4262684"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
@ -2798,6 +2909,26 @@ files = [
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.4.2"
|
||||
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"},
|
||||
{file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"},
|
||||
{file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.66.4"
|
||||
@ -2818,6 +2949,23 @@ notebook = ["ipywidgets (>=6)"]
|
||||
slack = ["slack-sdk"]
|
||||
telegram = ["requests"]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.1"
|
||||
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"},
|
||||
{file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
rich = ">=10.11.0"
|
||||
shellingham = ">=1.3.0"
|
||||
typing-extensions = ">=3.7.4.3"
|
||||
|
||||
[[package]]
|
||||
name = "types-mock"
|
||||
version = "5.1.0.20240425"
|
||||
@ -3234,4 +3382,4 @@ liquid = ["wallycore"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12 | ^3.11 | ^3.10 | ^3.9"
|
||||
content-hash = "63319dd52462ba4066b53ad3770eb8adff345d92593fa9111f08b126ac3eb68a"
|
||||
content-hash = "a8a4a09601a1c5dcbbcf35aab4e1a2cd3f6ed4f3445bf2c47b055318121eda9d"
|
||||
|
@ -61,6 +61,7 @@ wallycore = {version = "1.3.0", optional = true}
|
||||
breez-sdk = {version = "0.6.6", optional = true}
|
||||
|
||||
jsonpath-ng = "^1.7.0"
|
||||
pynostr = "^0.6.2"
|
||||
[tool.poetry.extras]
|
||||
breez = ["breez-sdk"]
|
||||
liquid = ["wallycore"]
|
||||
@ -136,6 +137,7 @@ module = [
|
||||
"bitstring.*",
|
||||
"ecdsa.*",
|
||||
"pyngrok.*",
|
||||
"pynostr.*",
|
||||
"pyln.client.*",
|
||||
"py_vapid.*",
|
||||
"pywebpush.*",
|
||||
|
@ -21,7 +21,7 @@ from .helpers import (
|
||||
|
||||
async def get_node_balance_sats():
|
||||
balance = await get_balance_delta()
|
||||
return balance.node_balance_msats / 1000
|
||||
return balance.node_balance_sats
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
Loading…
x
Reference in New Issue
Block a user