[feat] User notifications backend (#3280)

Co-authored-by: Arc <33088785+arcbtc@users.noreply.github.com>
Co-authored-by: dni  <office@dnilabs.com>
This commit is contained in:
Vlad Stan 2025-07-17 17:11:40 +03:00 committed by GitHub
parent a36ab2d408
commit dce2bfb440
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 306 additions and 207 deletions

View file

@ -27,7 +27,7 @@ from lnbits.core.crud.extensions import create_installed_extension
from lnbits.core.helpers import migrate_extension_database from lnbits.core.helpers import migrate_extension_database
from lnbits.core.models.notifications import NotificationType from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions from lnbits.core.services.extensions import deactivate_extension, get_valid_extensions
from lnbits.core.services.notifications import enqueue_notification from lnbits.core.services.notifications import enqueue_admin_notification
from lnbits.core.services.payments import check_pending_payments from lnbits.core.services.payments import check_pending_payments
from lnbits.core.tasks import ( from lnbits.core.tasks import (
audit_queue, audit_queue,
@ -104,7 +104,7 @@ async def startup(app: FastAPI):
# initialize tasks # initialize tasks
register_async_tasks() register_async_tasks()
enqueue_notification( enqueue_admin_notification(
NotificationType.server_start_stop, NotificationType.server_start_stop,
{ {
"message": "LNbits server started.", "message": "LNbits server started.",
@ -118,7 +118,7 @@ async def startup(app: FastAPI):
async def shutdown(): async def shutdown():
logger.warning("LNbits shutting down...") logger.warning("LNbits shutting down...")
enqueue_notification( enqueue_admin_notification(
NotificationType.server_start_stop, NotificationType.server_start_stop,
{ {
"message": "LNbits server shutting down...", "message": "LNbits server shutting down...",

View file

@ -2,6 +2,8 @@ from enum import Enum
from pydantic import BaseModel from pydantic import BaseModel
from lnbits.core.models.users import UserNotifications
class NotificationType(Enum): class NotificationType(Enum):
server_status = "server_status" server_status = "server_status"
@ -18,6 +20,7 @@ class NotificationType(Enum):
class NotificationMessage(BaseModel): class NotificationMessage(BaseModel):
message_type: NotificationType message_type: NotificationType
values: dict values: dict
user_notifications: UserNotifications | None = None
NOTIFICATION_TEMPLATES = { NOTIFICATION_TEMPLATES = {

View file

@ -20,14 +20,20 @@ from lnbits.settings import settings
from .wallets import Wallet from .wallets import Wallet
class UserNotifications(BaseModel):
nostr_identifier: str | None = None
telegram_chat_id: str | None = None
email_address: str | None = None
excluded_wallets: list[str] = []
outgoing_payments_sats: int = 0
incoming_payments_sats: int = 0
class UserExtra(BaseModel): class UserExtra(BaseModel):
email_verified: bool | None = False email_verified: bool | None = False
first_name: str | None = None first_name: str | None = None
last_name: str | None = None last_name: str | None = None
display_name: str | None = None display_name: str | None = None
nostr_notification_identifiers: list[str] = []
telegram_chat_id: str | None = None
notifications_excluded_wallets: list[str] = []
picture: str | None = None picture: str | None = None
# Auth provider, possible values: # Auth provider, possible values:
# - "env": the user was created automatically by the system # - "env": the user was created automatically by the system
@ -38,6 +44,8 @@ class UserExtra(BaseModel):
# how many wallets are shown in the user interface # how many wallets are shown in the user interface
visible_wallet_count: int | None = 10 visible_wallet_count: int | None = 10
notifications: UserNotifications = UserNotifications()
class EndpointAccess(BaseModel): class EndpointAccess(BaseModel):
path: str path: str

View file

@ -3,7 +3,7 @@ from .funding_source import (
switch_to_voidwallet, switch_to_voidwallet,
) )
from .lnurl import fetch_lnurl_pay_request from .lnurl import fetch_lnurl_pay_request
from .notifications import enqueue_notification, send_payment_notification from .notifications import enqueue_admin_notification, send_payment_notification
from .payments import ( from .payments import (
calculate_fiat_amounts, calculate_fiat_amounts,
cancel_hold_invoice, cancel_hold_invoice,
@ -49,7 +49,7 @@ __all__ = [
"create_user_account", "create_user_account",
"create_user_account_no_ckeck", "create_user_account_no_ckeck",
"create_wallet_invoice", "create_wallet_invoice",
"enqueue_notification", "enqueue_admin_notification",
"fee_reserve", "fee_reserve",
"fee_reserve_total", "fee_reserve_total",
"fetch_lnurl_pay_request", "fetch_lnurl_pay_request",

View file

@ -1,7 +1,7 @@
from loguru import logger from loguru import logger
from lnbits.core.models.notifications import NotificationType from lnbits.core.models.notifications import NotificationType
from lnbits.core.services.notifications import enqueue_notification from lnbits.core.services.notifications import enqueue_admin_notification
from lnbits.settings import settings from lnbits.settings import settings
from lnbits.wallets import get_funding_source, set_funding_source from lnbits.wallets import get_funding_source, set_funding_source
@ -51,7 +51,7 @@ async def check_server_balance_against_node():
f"Balance delta reached: {status.delta_sats} sats." f"Balance delta reached: {status.delta_sats} sats."
f" Switch to void wallet: {use_voidwallet}." f" Switch to void wallet: {use_voidwallet}."
) )
enqueue_notification( enqueue_admin_notification(
NotificationType.watchdog_check, NotificationType.watchdog_check,
{ {
"delta_sats": status.delta_sats, "delta_sats": status.delta_sats,
@ -71,7 +71,7 @@ async def check_balance_delta_changed():
settings.latest_balance_delta_sats = status.delta_sats settings.latest_balance_delta_sats = status.delta_sats
return return
if status.delta_sats != settings.latest_balance_delta_sats: if status.delta_sats != settings.latest_balance_delta_sats:
enqueue_notification( enqueue_admin_notification(
NotificationType.balance_delta, NotificationType.balance_delta,
{ {
"delta_sats": status.delta_sats, "delta_sats": status.delta_sats,

View file

@ -16,12 +16,14 @@ from lnbits.core.crud import (
get_webpush_subscriptions_for_user, get_webpush_subscriptions_for_user,
mark_webhook_sent, mark_webhook_sent,
) )
from lnbits.core.crud.users import get_user
from lnbits.core.models import Payment, Wallet from lnbits.core.models import Payment, Wallet
from lnbits.core.models.notifications import ( from lnbits.core.models.notifications import (
NOTIFICATION_TEMPLATES, NOTIFICATION_TEMPLATES,
NotificationMessage, NotificationMessage,
NotificationType, NotificationType,
) )
from lnbits.core.models.users import UserNotifications
from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm from lnbits.core.services.nostr import fetch_nip5_details, send_nostr_dm
from lnbits.core.services.websockets import websocket_manager from lnbits.core.services.websockets import websocket_manager
from lnbits.helpers import check_callback_url, is_valid_email_address from lnbits.helpers import check_callback_url, is_valid_email_address
@ -31,8 +33,8 @@ from lnbits.utils.nostr import normalize_private_key
notifications_queue: asyncio.Queue[NotificationMessage] = asyncio.Queue() notifications_queue: asyncio.Queue[NotificationMessage] = asyncio.Queue()
def enqueue_notification(message_type: NotificationType, values: dict) -> None: def enqueue_admin_notification(message_type: NotificationType, values: dict) -> None:
if not is_message_type_enabled(message_type): if not _is_message_type_enabled(message_type):
return return
try: try:
notifications_queue.put_nowait( notifications_queue.put_nowait(
@ -42,62 +44,125 @@ def enqueue_notification(message_type: NotificationType, values: dict) -> None:
logger.error(f"Error enqueuing notification: {e}") logger.error(f"Error enqueuing notification: {e}")
def enqueue_user_notification(
message_type: NotificationType, values: dict, user_notifications: UserNotifications
) -> None:
try:
notifications_queue.put_nowait(
NotificationMessage(
message_type=message_type,
values=values,
user_notifications=user_notifications,
)
)
except Exception as e:
logger.error(f"Error enqueuing notification: {e}")
async def process_next_notification() -> None: async def process_next_notification() -> None:
notification_message = await notifications_queue.get() notification_message = await notifications_queue.get()
message_type, text = _notification_message_to_text(notification_message) message_type, text = _notification_message_to_text(notification_message)
await send_notification(text, message_type) user_notifications = notification_message.user_notifications
if user_notifications:
await send_user_notification(user_notifications, text, message_type)
else:
await send_admin_notification(text, message_type)
async def send_admin_notification(
message: str,
message_type: Optional[str] = None,
) -> None:
return await send_notification(
settings.lnbits_telegram_notifications_chat_id,
settings.lnbits_nostr_notifications_identifiers,
settings.lnbits_email_notifications_to_emails,
message,
message_type,
)
async def send_user_notification(
user_notifications: UserNotifications,
message: str,
message_type: Optional[str] = None,
) -> None:
email_address = (
[user_notifications.email_address] if user_notifications.email_address else []
)
nostr_identifiers = (
[user_notifications.nostr_identifier]
if user_notifications.nostr_identifier
else []
)
return await send_notification(
user_notifications.telegram_chat_id,
nostr_identifiers,
email_address,
message,
message_type,
)
async def send_notification( async def send_notification(
telegram_chat_id: str | None,
nostr_identifiers: list[str] | None,
email_addresses: list[str] | None,
message: str, message: str,
message_type: Optional[str] = None, message_type: Optional[str] = None,
) -> None: ) -> None:
try: try:
if settings.lnbits_telegram_notifications_enabled: if telegram_chat_id and settings.is_telegram_notifications_configured():
await send_telegram_notification(message) await send_telegram_notification(telegram_chat_id, message)
logger.debug(f"Sent telegram notification: {message_type}") logger.debug(f"Sent telegram notification: {message_type}")
except Exception as e: except Exception as e:
logger.error(f"Error sending telegram notification {message_type}: {e}") logger.error(f"Error sending telegram notification {message_type}: {e}")
try: try:
if settings.lnbits_nostr_notifications_enabled: if nostr_identifiers and settings.is_nostr_notifications_configured():
await send_nostr_notification(message) await send_nostr_notifications(nostr_identifiers, message)
logger.debug(f"Sent nostr notification: {message_type}") logger.debug(f"Sent nostr notification: {message_type}")
except Exception as e: except Exception as e:
logger.error(f"Error sending nostr notification {message_type}: {e}") logger.error(f"Error sending nostr notification {message_type}: {e}")
try: try:
if settings.lnbits_email_notifications_enabled: if email_addresses and settings.lnbits_email_notifications_enabled:
await send_email_notification(message) await send_email_notification(email_addresses, message)
logger.debug(f"Sent email notification: {message_type}") logger.debug(f"Sent email notification: {message_type}")
except Exception as e: except Exception as e:
logger.error(f"Error sending email notification {message_type}: {e}") logger.error(f"Error sending email notification {message_type}: {e}")
async def send_nostr_notification(message: str) -> dict: async def send_nostr_notifications(identifiers: list[str], message: str) -> list[str]:
for i in settings.lnbits_nostr_notifications_identifiers: success_sent: list[str] = []
for identifier in identifiers:
try: try:
identifier = await fetch_nip5_details(i) await send_nostr_notification(identifier, message)
user_pubkey = identifier[0] success_sent.append(identifier)
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: except Exception as e:
logger.warning(f"Error notifying identifier {i}: {e}") logger.warning(f"Error notifying identifier {identifier}: {e}")
return success_sent
return {"status": "ok"}
async def send_telegram_notification(message: str) -> dict: async def send_nostr_notification(identifier: str, message: str):
nip5 = await fetch_nip5_details(identifier)
user_pubkey = nip5[0]
relays = nip5[1]
server_private_key = normalize_private_key(
settings.lnbits_nostr_notifications_private_key
)
await send_nostr_dm(
server_private_key,
user_pubkey,
message,
relays,
)
async def send_telegram_notification(chat_id: str, message: str) -> dict:
return await send_telegram_message( return await send_telegram_message(
settings.lnbits_telegram_notifications_access_token, settings.lnbits_telegram_notifications_access_token,
settings.lnbits_telegram_notifications_chat_id, chat_id,
message, message,
) )
@ -112,7 +177,7 @@ async def send_telegram_message(token: str, chat_id: str, message: str) -> dict:
async def send_email_notification( async def send_email_notification(
message: str, subject: str = "LNbits Notification" to_emails: list[str], message: str, subject: str = "LNbits Notification"
) -> dict: ) -> dict:
if not settings.lnbits_email_notifications_enabled: if not settings.lnbits_email_notifications_enabled:
return {"status": "error", "message": "Email notifications are disabled"} return {"status": "error", "message": "Email notifications are disabled"}
@ -123,7 +188,7 @@ async def send_email_notification(
settings.lnbits_email_notifications_username, settings.lnbits_email_notifications_username,
settings.lnbits_email_notifications_password, settings.lnbits_email_notifications_password,
settings.lnbits_email_notifications_email, settings.lnbits_email_notifications_email,
settings.lnbits_email_notifications_to_emails, to_emails,
subject, subject,
message, message,
) )
@ -163,41 +228,6 @@ async def send_email(
return True return True
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
async def dispatch_webhook(payment: Payment): async def dispatch_webhook(payment: Payment):
""" """
Dispatches the webhook to the webhook url. Dispatches the webhook to the webhook url.
@ -231,7 +261,7 @@ async def send_payment_notification(wallet: Wallet, payment: Payment):
except Exception as e: except Exception as e:
logger.error(f"Error sending websocket payment notification {e!s}") logger.error(f"Error sending websocket payment notification {e!s}")
try: try:
send_chat_payment_notification(wallet, payment) await send_chat_payment_notification(wallet, payment)
except Exception as e: except Exception as e:
logger.error(f"Error sending chat payment notification {e!s}") logger.error(f"Error sending chat payment notification {e!s}")
try: try:
@ -268,7 +298,7 @@ async def send_ws_payment_notification(wallet: Wallet, payment: Payment):
) )
def send_chat_payment_notification(wallet: Wallet, payment: Payment): async def send_chat_payment_notification(wallet: Wallet, payment: Payment):
amount_sats = abs(payment.sat) amount_sats = abs(payment.sat)
values: dict = { values: dict = {
"wallet_id": wallet.id, "wallet_id": wallet.id,
@ -283,10 +313,23 @@ def send_chat_payment_notification(wallet: Wallet, payment: Payment):
if payment.is_out: if payment.is_out:
if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats: if amount_sats >= settings.lnbits_notification_outgoing_payment_amount_sats:
enqueue_notification(NotificationType.outgoing_payment, values) enqueue_admin_notification(NotificationType.outgoing_payment, values)
else: elif amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats:
if amount_sats >= settings.lnbits_notification_incoming_payment_amount_sats: enqueue_admin_notification(NotificationType.incoming_payment, values)
enqueue_notification(NotificationType.incoming_payment, values)
user = await get_user(wallet.user)
user_notifications = user.extra.notifications if user else None
if user_notifications and wallet.id not in user_notifications.excluded_wallets:
out_limit = user_notifications.outgoing_payments_sats
in_limit = user_notifications.incoming_payments_sats
if payment.is_out and (amount_sats >= out_limit):
enqueue_user_notification(
NotificationType.outgoing_payment, values, user_notifications
)
elif amount_sats >= in_limit:
enqueue_user_notification(
NotificationType.incoming_payment, values, user_notifications
)
async def send_payment_push_notification(wallet: Wallet, payment: Payment): async def send_payment_push_notification(wallet: Wallet, payment: Payment):
@ -330,3 +373,38 @@ async def send_push_notification(subscription, title, body, url=""):
f"failed sending push notification: " f"failed sending push notification: "
f"{e.response.text if e.response else e}" f"{e.response.text if e.response else e}"
) )
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

View file

@ -91,12 +91,8 @@ async def pay_invoice(
payment = await _pay_invoice(wallet.id, create_payment_model, conn) payment = await _pay_invoice(wallet.id, create_payment_model, conn)
service_fee_memo = f"""
Service fee for payment of {abs(payment.sat)} sats.
Wallet: '{wallet.name}' ({wallet.id})."""
async with db.reuse_conn(conn) if conn else db.connect() as new_conn: async with db.reuse_conn(conn) if conn else db.connect() as new_conn:
await _credit_service_fee_wallet(payment, service_fee_memo, new_conn) await _credit_service_fee_wallet(wallet, payment, new_conn)
return payment return payment
@ -905,12 +901,16 @@ def _validate_payment_request(
async def _credit_service_fee_wallet( async def _credit_service_fee_wallet(
payment: Payment, memo: str, conn: Optional[Connection] = None wallet: Wallet, payment: Payment, conn: Optional[Connection] = None
): ):
service_fee_msat = service_fee(payment.amount, internal=payment.is_internal) service_fee_msat = service_fee(payment.amount, internal=payment.is_internal)
if not settings.lnbits_service_fee_wallet or not service_fee_msat: if not settings.lnbits_service_fee_wallet or not service_fee_msat:
return return
memo = f"""
Service fee for payment of {abs(payment.sat)} sats.
Wallet: '{wallet.name}' ({wallet.id})."""
create_payment_model = CreatePayment( create_payment_model = CreatePayment(
wallet_id=settings.lnbits_service_fee_wallet, wallet_id=settings.lnbits_service_fee_wallet,
bolt11=payment.bolt11, bolt11=payment.bolt11,

View file

@ -22,7 +22,7 @@ from lnbits.core.services.funding_source import (
get_balance_delta, get_balance_delta,
) )
from lnbits.core.services.notifications import ( from lnbits.core.services.notifications import (
enqueue_notification, enqueue_admin_notification,
process_next_notification, process_next_notification,
send_payment_notification, send_payment_notification,
) )
@ -87,7 +87,7 @@ async def _notify_server_status() -> None:
"lnbits_balance_sats": status.lnbits_balance_sats, "lnbits_balance_sats": status.lnbits_balance_sats,
"node_balance_sats": status.node_balance_sats, "node_balance_sats": status.node_balance_sats,
} }
enqueue_notification(NotificationType.server_status, values) enqueue_admin_notification(NotificationType.server_status, values)
async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None: async def wait_for_paid_invoices(invoice_paid_queue: asyncio.Queue) -> None:
@ -123,7 +123,7 @@ async def wait_notification_messages() -> None:
try: try:
await process_next_notification() await process_next_notification()
except Exception as ex: except Exception as ex:
logger.log("error", ex) logger.warning("Payment notification error", ex)
await asyncio.sleep(3) await asyncio.sleep(3)

View file

@ -7,43 +7,71 @@
<div class="row q-col-gutter-md q-mb-md"> <div class="row q-col-gutter-md q-mb-md">
<div class="col-12"> <div class="col-12">
<q-card> <q-card>
<div> <div class="q-pa-sm">
<div class="q-gutter-y-md"> <div class="row items-center justify-between q-gutter-xs">
<q-tabs v-model="tab" align="left"> <div class="col">
<q-tab <q-btn @click="updateAccount" unelevated color="primary">
name="user" <span v-text="$t('update_account')"></span>
:label="$t('account_settings')" </q-btn>
@update="val => tab = val.name" </div>
></q-tab>
<q-tab
name="theme"
:label="$t('look_and_feel')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="notifications"
:label="$t('notifications')"
@update="val => tab = val.name"
></q-tab>
<q-tab
name="api_acls"
:label="$t('access_control_list')"
@update="val => tab = val.name"
></q-tab>
</q-tabs>
</div> </div>
</div> </div>
</q-card> </q-card>
</div> </div>
</div> </div>
<div class="row q-col-gutter-md"> <div class="row q-col-gutter-md q-mb-md">
<div v-if="user" class="col-md-12 col-lg-6 q-gutter-y-md"> <div class="col-12">
<q-card> <q-card>
<q-card-section> <q-splitter>
<div class="row"> <template v-slot:before>
<div class="col"> <q-tabs v-model="tab" vertical active-color="primary">
<q-tab-panels v-model="tab"> <q-tab
name="user"
icon="person"
:label="$q.screen.gt.sm ? $t('account_settings') : ''"
@update="val => tab = val.name"
>
<q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('account_settings')"></span
></q-tooltip>
</q-tab>
<q-tab
name="notifications"
icon="notifications"
:label="$q.screen.gt.sm ? $t('notifications') : ''"
@update="val => tab = val.name"
>
<q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('notifications')"></span
></q-tooltip>
</q-tab>
<q-tab
name="theme"
icon="palette"
:label="$q.screen.gt.sm ? $t('look_and_feel') : ''"
@update="val => tab = val.name"
>
<q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('look_and_feel')"></span
></q-tooltip>
</q-tab>
<q-tab
name="api_acls"
icon="lock"
:label="$q.screen.gt.sm ? $t('access_control_list') : ''"
@update="val => tab = val.name"
>
<q-tooltip v-if="!$q.screen.gt.sm"
><span v-text="$t('access_control_list')"></span
></q-tooltip>
</q-tab>
</q-tabs>
</template>
<template v-slot:after>
<q-scroll-area style="height: 80vh">
<q-tab-panels v-if="user" v-model="tab">
<q-tab-panel name="user"> <q-tab-panel name="user">
<div v-if="credentialsData.show"> <div v-if="credentialsData.show">
<q-card-section> <q-card-section>
@ -298,9 +326,6 @@
</q-card-section> </q-card-section>
<q-separator></q-separator> <q-separator></q-separator>
<q-card-section> <q-card-section>
<q-btn @click="updateAccount" unelevated color="primary">
<span v-text="$t('update_account')"></span>
</q-btn>
<q-btn <q-btn
@click="showUpdateCredentials()" @click="showUpdateCredentials()"
:label="$t('change_password')" :label="$t('change_password')"
@ -564,63 +589,80 @@
</q-select> </q-select>
</div> </div>
</div> </div>
<q-separator></q-separator>
<div class="row q-mb-md q-mt-md">
<q-btn @click="updateAccount" unelevated color="primary">
<span v-text="$t('update_account')"></span>
</q-btn>
</div>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="notifications"> <q-tab-panel name="notifications">
<q-card-section> <q-card-section>
<div class="row q-mb-md"> <div class="row q-mb-md">
<div class="col-4"> <div class="col-4">
<span <span
v-text="$t('notifications_nostr_identifiers')" v-text="$t('notifications_nostr_identifier')"
></span> ></span>
{%if not nostr_configured%}
<br />
<q-badge v-text="$t('not_connected')"></q-badge>
{%endif%}
</div> </div>
<div class="col-8"> <div class="col-8">
<q-input <q-input
filled filled
dense dense
v-model="notifications.nostr.identifier" v-model="user.extra.notifications.nostr_identifier"
:hint="$t('notifications_nostr_identifiers_desc')" :hint="$t('notifications_nostr_identifier_desc')"
@keydown.enter="addNostrNotificationIdentifier"
> >
<q-btn
@click="addNostrNotificationIdentifier()"
dense
flat
icon="add"
></q-btn>
</q-input> </q-input>
<div>
<q-chip
v-for="identifier in user.extra.nostr_notification_identifiers"
:key="identifier"
removable
@remove="removeNostrNotificationIdentifier(identifier)"
color="primary"
text-color="white"
><span class="ellipsis" v-text="identifier"></span
></q-chip>
</div>
</div> </div>
</div> </div>
<div class="row q-mb-md"> <div class="row q-mb-md">
<div class="col-4"> <div class="col-4">
<span v-text="$t('notifications_chat_id')"></span> <span v-text="$t('notifications_chat_id')"></span>
{%if not telegram_configured%}
<br />
<q-badge v-text="$t('not_connected')"></q-badge>
{%endif%}
</div> </div>
<div class="col-8"> <div class="col-8">
<q-input <q-input
filled filled
dense dense
v-model="user.extra.telegram_chat_id" v-model="user.extra.notifications.telegram_chat_id"
:hint="$t('notifications_chat_id_desc')" :hint="$t('notifications_chat_id_desc')"
/> />
</div> </div>
</div> </div>
<q-separator class="q-mb-md"></q-separator>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('notification_outgoing_payment')"></span>
</div>
<div class="col-8">
<q-input
filled
dense
type="number"
min="0"
step="1"
v-model="user.extra.notifications.outgoing_payments_sats"
:hint="$t('notification_outgoing_payment_desc')"
/>
</div>
</div>
<div class="row q-mb-md">
<div class="col-4">
<span v-text="$t('notification_incoming_payment')"></span>
</div>
<div class="col-8">
<q-input
filled
dense
type="number"
min="0"
step="1"
v-model="user.extra.notifications.incoming_payments_sats"
:hint="$t('notification_incoming_payment_desc')"
/>
</div>
</div>
<div class="row q-mb-md"> <div class="row q-mb-md">
<div class="col-4"> <div class="col-4">
<span v-text="$t('exclude_wallets')"></span> <span v-text="$t('exclude_wallets')"></span>
@ -632,7 +674,7 @@
emit-value emit-value
map-options map-options
multiple multiple
v-model="user.extra.notifications_excluded_wallets" v-model="user.extra.notifications.excluded_wallets"
:options="g.user.walletOptions" :options="g.user.walletOptions"
:label="$t('exclude_wallets')" :label="$t('exclude_wallets')"
:hint="$t('notifications_excluded_wallets_desc')" :hint="$t('notifications_excluded_wallets_desc')"
@ -642,14 +684,6 @@
</div> </div>
</div> </div>
</q-card-section> </q-card-section>
<q-card-section>
<q-separator></q-separator>
<div class="row q-mb-md q-mt-md">
<q-btn @click="updateAccount" unelevated color="primary">
<span v-text="$t('update_account')"></span>
</q-btn>
</div>
</q-card-section>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="api_acls"> <q-tab-panel name="api_acls">
<div class="row q-mb-md"> <div class="row q-mb-md">
@ -876,16 +910,9 @@
</q-card-section> </q-card-section>
</q-tab-panel> </q-tab-panel>
</q-tab-panels> </q-tab-panels>
</div> </q-scroll-area>
</div> </template>
</q-card-section> </q-splitter>
</q-card>
</div>
<div v-else class="col-12 col-md-6 q-gutter-y-md">
<q-card>
<q-card-section>
<h4 class="q-my-none"><span v-text="$t('account')"></span></h4>
</q-card-section>
</q-card> </q-card>
</div> </div>
</div> </div>

View file

@ -16,7 +16,7 @@ from lnbits.core.models import User
from lnbits.core.models.misc import Image, SimpleStatus from lnbits.core.models.misc import Image, SimpleStatus
from lnbits.core.models.notifications import NotificationType from lnbits.core.models.notifications import NotificationType
from lnbits.core.services import ( from lnbits.core.services import (
enqueue_notification, enqueue_admin_notification,
get_balance_delta, get_balance_delta,
update_cached_settings, update_cached_settings,
) )
@ -65,7 +65,9 @@ async def api_monitor():
) )
async def api_test_email(): async def api_test_email():
return await send_email_notification( return await send_email_notification(
"This is a LNbits test email.", "LNbits Test Email" settings.lnbits_email_notifications_to_emails,
"This is a LNbits test email.",
"LNbits Test Email",
) )
@ -82,7 +84,9 @@ async def api_get_settings(
status_code=HTTPStatus.OK, status_code=HTTPStatus.OK,
) )
async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)): async def api_update_settings(data: UpdateSettings, user: User = Depends(check_admin)):
enqueue_notification(NotificationType.settings_update, {"username": user.username}) enqueue_admin_notification(
NotificationType.settings_update, {"username": user.username}
)
await update_admin_settings(data) await update_admin_settings(data)
admin_settings = await get_admin_settings(user.super_user) admin_settings = await get_admin_settings(user.super_user)
if not admin_settings: if not admin_settings:
@ -113,7 +117,9 @@ async def api_reset_settings(field_name: str):
@admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK) @admin_router.delete("/api/v1/settings", status_code=HTTPStatus.OK)
async def api_delete_settings(user: User = Depends(check_super_user)) -> None: async def api_delete_settings(user: User = Depends(check_super_user)) -> None:
enqueue_notification(NotificationType.settings_update, {"username": user.username}) enqueue_admin_notification(
NotificationType.settings_update, {"username": user.username}
)
await reset_core_settings() await reset_core_settings()
server_restart.set() server_restart.set()

View file

@ -345,7 +345,6 @@ async def node(request: Request, user: User = Depends(check_admin)):
"node/index.html", "node/index.html",
{ {
"user": user.json(), "user": user.json(),
"settings": settings.dict(),
"balance": balance, "balance": balance,
"wallets": user.wallets[0].json(), "wallets": user.wallets[0].json(),
"ajax": _is_ajax_request(request), "ajax": _is_ajax_request(request),
@ -383,7 +382,6 @@ async def admin_index(request: Request, user: User = Depends(check_admin)):
"admin/index.html", "admin/index.html",
{ {
"user": user.json(), "user": user.json(),
"settings": settings.dict(),
"balance": balance, "balance": balance,
"currencies": list(currencies.keys()), "currencies": list(currencies.keys()),
"ajax": _is_ajax_request(request), "ajax": _is_ajax_request(request),

View file

@ -35,7 +35,7 @@ from lnbits.core.models.notifications import NotificationType
from lnbits.core.models.users import Account from lnbits.core.models.users import Account
from lnbits.core.services import ( from lnbits.core.services import (
create_user_account_no_ckeck, create_user_account_no_ckeck,
enqueue_notification, enqueue_admin_notification,
update_user_account, update_user_account,
update_user_extensions, update_user_extensions,
update_wallet_balance, update_wallet_balance,
@ -321,7 +321,7 @@ async def api_update_balance(data: UpdateBalance) -> SimpleStatus:
if not wallet: if not wallet:
raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.") raise HTTPException(HTTPStatus.NOT_FOUND, "Wallet not found.")
await update_wallet_balance(wallet=wallet, amount=int(data.amount)) await update_wallet_balance(wallet=wallet, amount=int(data.amount))
enqueue_notification( enqueue_admin_notification(
NotificationType.balance_update, NotificationType.balance_update,
{ {
"amount": int(data.amount), "amount": int(data.amount),

File diff suppressed because one or more lines are too long

View file

@ -230,6 +230,9 @@ window.localisation.en = {
notifications_nostr_private_key: 'Nostr Private Key', notifications_nostr_private_key: 'Nostr Private Key',
notifications_nostr_private_key_desc: notifications_nostr_private_key_desc:
'Private key (hex or nsec) to sign the messages sent to Nostr', 'Private key (hex or nsec) to sign the messages sent to Nostr',
notifications_nostr_identifier: 'Nostr Identifier',
notifications_nostr_identifier_desc:
'Nip5 identifier to send notifications to',
notifications_nostr_identifiers: 'Nostr Identifiers', notifications_nostr_identifiers: 'Nostr Identifiers',
notifications_nostr_identifiers_desc: notifications_nostr_identifiers_desc:
'List of identifiers to send notifications to', 'List of identifiers to send notifications to',
@ -646,5 +649,7 @@ window.localisation.en = {
'Signing secret for the webhook. Messages will be signed with this secret.', 'Signing secret for the webhook. Messages will be signed with this secret.',
callback_success_url: 'Callback Success URL', callback_success_url: 'Callback Success URL',
callback_success_url_hint: callback_success_url_hint:
'The user will be redirected to this URL after the payment is successful' 'The user will be redirected to this URL after the payment is successful',
connected: 'Connected',
not_connected: 'Not Connected'
} }

View file

@ -399,32 +399,6 @@ window.AccountPageLogic = {
} finally { } finally {
this.apiAcl.password = '' this.apiAcl.password = ''
} }
},
addNostrNotificationIdentifier() {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(this.notifications.nostr.identifier)) {
Quasar.Notify.create({
type: 'warning',
message: 'Invalid email format.'
})
return
}
const identifier = this.user.extra.nostr_notification_identifiers.find(
i => i === this.notifications.nostr.identifier
)
if (identifier) {
return
}
this.user.extra.nostr_notification_identifiers.push(
this.notifications.nostr.identifier
)
this.notifications.nostr.identifier = ''
},
removeNostrNotificationIdentifier(identifier) {
this.user.extra.nostr_notification_identifiers =
this.user.extra.nostr_notification_identifiers.filter(
i => i !== identifier
)
} }
}, },