lnbits 0.12.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lnbits/__init__.py +0 -0
- lnbits/__main__.py +3 -0
- lnbits/app.py +617 -0
- lnbits/bolt11.py +7 -0
- lnbits/commands.py +718 -0
- lnbits/core/__init__.py +38 -0
- lnbits/core/crud.py +1309 -0
- lnbits/core/db.py +5 -0
- lnbits/core/helpers.py +119 -0
- lnbits/core/migrations.py +473 -0
- lnbits/core/models.py +426 -0
- lnbits/core/services.py +793 -0
- lnbits/core/sso/keycloak.py +37 -0
- lnbits/core/tasks.py +182 -0
- lnbits/core/templates/admin/_tab_funding.html +96 -0
- lnbits/core/templates/admin/_tab_security.html +404 -0
- lnbits/core/templates/admin/_tab_security_notifications.html +77 -0
- lnbits/core/templates/admin/_tab_server.html +186 -0
- lnbits/core/templates/admin/_tab_theme.html +128 -0
- lnbits/core/templates/admin/_tab_users.html +78 -0
- lnbits/core/templates/admin/index.html +172 -0
- lnbits/core/templates/core/_api_docs.html +156 -0
- lnbits/core/templates/core/account.html +436 -0
- lnbits/core/templates/core/extensions.html +1003 -0
- lnbits/core/templates/core/first_install.html +139 -0
- lnbits/core/templates/core/index.html +525 -0
- lnbits/core/templates/core/wallet.html +951 -0
- lnbits/core/templates/node/_tab_channels.html +308 -0
- lnbits/core/templates/node/_tab_dashboard.html +68 -0
- lnbits/core/templates/node/_tab_transactions.html +310 -0
- lnbits/core/templates/node/index.html +376 -0
- lnbits/core/templates/node/public.html +136 -0
- lnbits/core/templates/service-worker.js +85 -0
- lnbits/core/views/__init__.py +0 -0
- lnbits/core/views/admin_api.py +168 -0
- lnbits/core/views/api.py +246 -0
- lnbits/core/views/auth_api.py +346 -0
- lnbits/core/views/extension_api.py +270 -0
- lnbits/core/views/generic.py +540 -0
- lnbits/core/views/node_api.py +199 -0
- lnbits/core/views/payment_api.py +473 -0
- lnbits/core/views/public_api.py +58 -0
- lnbits/core/views/tinyurl_api.py +105 -0
- lnbits/core/views/wallet_api.py +77 -0
- lnbits/core/views/webpush_api.py +60 -0
- lnbits/core/views/websocket_api.py +40 -0
- lnbits/db.py +509 -0
- lnbits/decorators.py +333 -0
- lnbits/extension_manager.py +726 -0
- lnbits/helpers.py +207 -0
- lnbits/jinja2_templating.py +28 -0
- lnbits/lnurl.py +1 -0
- lnbits/middleware.py +232 -0
- lnbits/nodes/__init__.py +15 -0
- lnbits/nodes/base.py +236 -0
- lnbits/nodes/cln.py +336 -0
- lnbits/nodes/lndrest.py +388 -0
- lnbits/requestvars.py +10 -0
- lnbits/server.py +99 -0
- lnbits/settings.py +541 -0
- lnbits/static/bundle.min.css +1 -0
- lnbits/static/bundle.min.js +56 -0
- lnbits/static/css/base.css +556 -0
- lnbits/static/favicon.ico +0 -0
- lnbits/static/fonts/material-icons-v50.woff2 +0 -0
- lnbits/static/i18n/br.js +245 -0
- lnbits/static/i18n/cn.js +233 -0
- lnbits/static/i18n/cs.js +242 -0
- lnbits/static/i18n/de.js +249 -0
- lnbits/static/i18n/en.js +250 -0
- lnbits/static/i18n/es.js +247 -0
- lnbits/static/i18n/fi.js +245 -0
- lnbits/static/i18n/fr.js +251 -0
- lnbits/static/i18n/i18n.js +1 -0
- lnbits/static/i18n/it.js +248 -0
- lnbits/static/i18n/jp.js +244 -0
- lnbits/static/i18n/kr.js +241 -0
- lnbits/static/i18n/nl.js +247 -0
- lnbits/static/i18n/pi.js +244 -0
- lnbits/static/i18n/pl.js +243 -0
- lnbits/static/i18n/pt.js +244 -0
- lnbits/static/i18n/sk.js +243 -0
- lnbits/static/i18n/we.js +243 -0
- lnbits/static/images/alby.png +0 -0
- lnbits/static/images/albyl.png +0 -0
- lnbits/static/images/blitz.png +0 -0
- lnbits/static/images/blitzl.png +0 -0
- lnbits/static/images/cln.png +0 -0
- lnbits/static/images/clnl.png +0 -0
- lnbits/static/images/default_voucher.png +0 -0
- lnbits/static/images/github-logo.png +0 -0
- lnbits/static/images/google-logo.png +0 -0
- lnbits/static/images/keycloak-logo.png +0 -0
- lnbits/static/images/lnbits-shop-dark.png +0 -0
- lnbits/static/images/lnbits-shop-light.png +0 -0
- lnbits/static/images/lnd.png +0 -0
- lnbits/static/images/lnpay.png +0 -0
- lnbits/static/images/lnpayl.png +0 -0
- lnbits/static/images/logos/lnbits.png +0 -0
- lnbits/static/images/logos/lnbits.svg +4 -0
- lnbits/static/images/maskable_icon.png +0 -0
- lnbits/static/images/maskable_icon_x192.png +0 -0
- lnbits/static/images/maskable_icon_x512.png +0 -0
- lnbits/static/images/maskable_icon_x96.png +0 -0
- lnbits/static/images/mynode.png +0 -0
- lnbits/static/images/mynodel.png +0 -0
- lnbits/static/images/opennode.png +0 -0
- lnbits/static/images/opennodel.png +0 -0
- lnbits/static/images/screenshot_desktop.png +0 -0
- lnbits/static/images/screenshot_phone.png +0 -0
- lnbits/static/images/spark.png +0 -0
- lnbits/static/images/sparkl.png +0 -0
- lnbits/static/images/start9.png +0 -0
- lnbits/static/images/start9l.png +0 -0
- lnbits/static/images/templatead.png +0 -0
- lnbits/static/images/umbrel.png +0 -0
- lnbits/static/images/umbrell.png +0 -0
- lnbits/static/images/voltage.png +0 -0
- lnbits/static/images/voltagel.png +0 -0
- lnbits/static/images/voucher_template.svg +16 -0
- lnbits/static/images/zbd.png +0 -0
- lnbits/static/images/zbdl.png +0 -0
- lnbits/static/js/account.js +122 -0
- lnbits/static/js/admin.js +312 -0
- lnbits/static/js/base.js +630 -0
- lnbits/static/js/bolt11-decoder.js +347 -0
- lnbits/static/js/components/extension-settings.js +90 -0
- lnbits/static/js/components/lnbits-funding-sources.js +182 -0
- lnbits/static/js/components.js +729 -0
- lnbits/static/js/event-reactions.js +560 -0
- lnbits/static/js/index.js +102 -0
- lnbits/static/js/node.js +268 -0
- lnbits/static/js/wallet.js +865 -0
- lnbits/static/scss/base.scss +233 -0
- lnbits/static/vendor/Chart.bundle.js +20776 -0
- lnbits/static/vendor/Chart.css +47 -0
- lnbits/static/vendor/VueQrcodeReader.umd.js +11616 -0
- lnbits/static/vendor/axios.js +3011 -0
- lnbits/static/vendor/moment.js +5685 -0
- lnbits/static/vendor/quasar.css +11680 -0
- lnbits/static/vendor/quasar.ie.polyfills.umd.min.js +6 -0
- lnbits/static/vendor/quasar.umd.js +37923 -0
- lnbits/static/vendor/showdown.js +5157 -0
- lnbits/static/vendor/underscore.js +2042 -0
- lnbits/static/vendor/vue-i18n.js +2312 -0
- lnbits/static/vendor/vue-qrcode.js +5509 -0
- lnbits/static/vendor/vue-router.js +3061 -0
- lnbits/static/vendor/vue.js +11965 -0
- lnbits/static/vendor/vuex.js +1246 -0
- lnbits/static/vendor.json +44 -0
- lnbits/tasks.py +207 -0
- lnbits/templates/base.html +271 -0
- lnbits/templates/error.html +64 -0
- lnbits/templates/macros.jinja +14 -0
- lnbits/templates/print.html +51 -0
- lnbits/templates/public.html +14 -0
- lnbits/utils/__init__.py +0 -0
- lnbits/utils/cache.py +66 -0
- lnbits/utils/crypto.py +75 -0
- lnbits/utils/exchange_rates.py +299 -0
- lnbits/wallets/__init__.py +48 -0
- lnbits/wallets/alby.py +189 -0
- lnbits/wallets/base.py +148 -0
- lnbits/wallets/cliche.py +184 -0
- lnbits/wallets/corelightning.py +248 -0
- lnbits/wallets/corelightningrest.py +300 -0
- lnbits/wallets/eclair.py +236 -0
- lnbits/wallets/fake.py +135 -0
- lnbits/wallets/lnbits.py +190 -0
- lnbits/wallets/lnd_grpc_files/__init__.py +0 -0
- lnbits/wallets/lnd_grpc_files/lightning_pb2.py +3281 -0
- lnbits/wallets/lnd_grpc_files/lightning_pb2_grpc.py +3340 -0
- lnbits/wallets/lnd_grpc_files/router_pb2.py +665 -0
- lnbits/wallets/lnd_grpc_files/router_pb2_grpc.py +871 -0
- lnbits/wallets/lndgrpc.py +277 -0
- lnbits/wallets/lndrest.py +302 -0
- lnbits/wallets/lnpay.py +150 -0
- lnbits/wallets/lntips.py +182 -0
- lnbits/wallets/macaroon/__init__.py +1 -0
- lnbits/wallets/macaroon/macaroon.py +45 -0
- lnbits/wallets/opennode.py +146 -0
- lnbits/wallets/spark.py +265 -0
- lnbits/wallets/void.py +41 -0
- lnbits/wallets/zbd.py +159 -0
- lnbits-0.12.6.dist-info/LICENSE +21 -0
- lnbits-0.12.6.dist-info/METADATA +51 -0
- lnbits-0.12.6.dist-info/RECORD +189 -0
- lnbits-0.12.6.dist-info/WHEEL +4 -0
- lnbits-0.12.6.dist-info/entry_points.txt +4 -0
lnbits/__init__.py
ADDED
|
File without changes
|
lnbits/__main__.py
ADDED
lnbits/app.py
ADDED
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import glob
|
|
3
|
+
import importlib
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from hashlib import sha256
|
|
11
|
+
from http import HTTPStatus
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Callable, List, Optional
|
|
14
|
+
|
|
15
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
16
|
+
from fastapi.exceptions import RequestValidationError
|
|
17
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
18
|
+
from fastapi.responses import RedirectResponse
|
|
19
|
+
from fastapi.staticfiles import StaticFiles
|
|
20
|
+
from loguru import logger
|
|
21
|
+
from slowapi import Limiter
|
|
22
|
+
from slowapi.util import get_remote_address
|
|
23
|
+
from starlette.middleware.sessions import SessionMiddleware
|
|
24
|
+
from starlette.responses import JSONResponse
|
|
25
|
+
|
|
26
|
+
from lnbits.core.crud import get_dbversions, get_installed_extensions
|
|
27
|
+
from lnbits.core.helpers import migrate_extension_database
|
|
28
|
+
from lnbits.core.services import websocket_updater
|
|
29
|
+
from lnbits.core.tasks import ( # watchdog_task
|
|
30
|
+
killswitch_task,
|
|
31
|
+
wait_for_paid_invoices,
|
|
32
|
+
)
|
|
33
|
+
from lnbits.settings import settings
|
|
34
|
+
from lnbits.tasks import (
|
|
35
|
+
cancel_all_tasks,
|
|
36
|
+
create_permanent_task,
|
|
37
|
+
register_invoice_listener,
|
|
38
|
+
)
|
|
39
|
+
from lnbits.utils.cache import cache
|
|
40
|
+
from lnbits.wallets import get_funding_source, set_funding_source
|
|
41
|
+
|
|
42
|
+
from .commands import migrate_databases
|
|
43
|
+
from .core import init_core_routers
|
|
44
|
+
from .core.db import core_app_extra
|
|
45
|
+
from .core.services import check_admin_settings, check_webpush_settings
|
|
46
|
+
from .core.views.extension_api import add_installed_extension
|
|
47
|
+
from .core.views.generic import update_installed_extension_state
|
|
48
|
+
from .extension_manager import (
|
|
49
|
+
Extension,
|
|
50
|
+
InstallableExtension,
|
|
51
|
+
get_valid_extensions,
|
|
52
|
+
version_parse,
|
|
53
|
+
)
|
|
54
|
+
from .helpers import template_renderer
|
|
55
|
+
from .middleware import (
|
|
56
|
+
CustomGZipMiddleware,
|
|
57
|
+
ExtensionsRedirectMiddleware,
|
|
58
|
+
InstalledExtensionMiddleware,
|
|
59
|
+
add_first_install_middleware,
|
|
60
|
+
add_ip_block_middleware,
|
|
61
|
+
add_ratelimit_middleware,
|
|
62
|
+
)
|
|
63
|
+
from .requestvars import g
|
|
64
|
+
from .tasks import (
|
|
65
|
+
check_pending_payments,
|
|
66
|
+
internal_invoice_listener,
|
|
67
|
+
invoice_listener,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def startup(app: FastAPI):
|
|
72
|
+
|
|
73
|
+
# wait till migration is done
|
|
74
|
+
await migrate_databases()
|
|
75
|
+
|
|
76
|
+
# setup admin settings
|
|
77
|
+
await check_admin_settings()
|
|
78
|
+
await check_webpush_settings()
|
|
79
|
+
|
|
80
|
+
log_server_info()
|
|
81
|
+
|
|
82
|
+
# initialize WALLET
|
|
83
|
+
try:
|
|
84
|
+
set_funding_source()
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"Error initializing {settings.lnbits_backend_wallet_class}: {e}")
|
|
87
|
+
set_void_wallet_class()
|
|
88
|
+
|
|
89
|
+
# initialize funding source
|
|
90
|
+
await check_funding_source()
|
|
91
|
+
|
|
92
|
+
# register core routes
|
|
93
|
+
init_core_routers(app)
|
|
94
|
+
|
|
95
|
+
# check extensions after restart
|
|
96
|
+
if not settings.lnbits_extensions_deactivate_all:
|
|
97
|
+
await check_installed_extensions(app)
|
|
98
|
+
register_all_ext_routes(app)
|
|
99
|
+
|
|
100
|
+
if settings.lnbits_admin_ui:
|
|
101
|
+
initialize_server_logger()
|
|
102
|
+
|
|
103
|
+
# initialize tasks
|
|
104
|
+
register_async_tasks()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def shutdown():
|
|
108
|
+
# shutdown event
|
|
109
|
+
cancel_all_tasks()
|
|
110
|
+
|
|
111
|
+
# wait a bit to allow them to finish, so that cleanup can run without problems
|
|
112
|
+
await asyncio.sleep(0.1)
|
|
113
|
+
funding_source = get_funding_source()
|
|
114
|
+
await funding_source.cleanup()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@asynccontextmanager
|
|
118
|
+
async def lifespan(app: FastAPI):
|
|
119
|
+
await startup(app)
|
|
120
|
+
yield
|
|
121
|
+
await shutdown()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def create_app() -> FastAPI:
|
|
125
|
+
configure_logger()
|
|
126
|
+
app = FastAPI(
|
|
127
|
+
title=settings.lnbits_title,
|
|
128
|
+
description=(
|
|
129
|
+
"API for LNbits, the free and open source bitcoin wallet and "
|
|
130
|
+
"accounts system with plugins."
|
|
131
|
+
),
|
|
132
|
+
version=settings.version,
|
|
133
|
+
lifespan=lifespan,
|
|
134
|
+
license_info={
|
|
135
|
+
"name": "MIT License",
|
|
136
|
+
"url": "https://raw.githubusercontent.com/lnbits/lnbits/main/LICENSE",
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Allow registering new extensions routes without direct access to the `app` object
|
|
141
|
+
setattr(core_app_extra, "register_new_ext_routes", register_new_ext_routes(app))
|
|
142
|
+
setattr(core_app_extra, "register_new_ratelimiter", register_new_ratelimiter(app))
|
|
143
|
+
|
|
144
|
+
# register static files
|
|
145
|
+
static_path = Path("lnbits", "static")
|
|
146
|
+
static = StaticFiles(directory=static_path)
|
|
147
|
+
app.mount("/static", static, name="static")
|
|
148
|
+
|
|
149
|
+
g().base_url = f"http://{settings.host}:{settings.port}"
|
|
150
|
+
|
|
151
|
+
app.add_middleware(
|
|
152
|
+
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
app.add_middleware(
|
|
156
|
+
CustomGZipMiddleware, minimum_size=1000, exclude_paths=["/api/v1/payments/sse"]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# required for SSO login
|
|
160
|
+
app.add_middleware(SessionMiddleware, secret_key=settings.auth_secret_key)
|
|
161
|
+
|
|
162
|
+
# order of these two middlewares is important
|
|
163
|
+
app.add_middleware(InstalledExtensionMiddleware)
|
|
164
|
+
app.add_middleware(ExtensionsRedirectMiddleware)
|
|
165
|
+
|
|
166
|
+
register_custom_extensions_path()
|
|
167
|
+
|
|
168
|
+
add_first_install_middleware(app)
|
|
169
|
+
|
|
170
|
+
# adds security middleware
|
|
171
|
+
add_ip_block_middleware(app)
|
|
172
|
+
add_ratelimit_middleware(app)
|
|
173
|
+
|
|
174
|
+
register_exception_handlers(app)
|
|
175
|
+
|
|
176
|
+
return app
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def check_funding_source() -> None:
|
|
180
|
+
|
|
181
|
+
funding_source = get_funding_source()
|
|
182
|
+
|
|
183
|
+
max_retries = settings.funding_source_max_retries
|
|
184
|
+
retry_counter = 0
|
|
185
|
+
|
|
186
|
+
while True:
|
|
187
|
+
try:
|
|
188
|
+
logger.info(f"Connecting to backend {funding_source.__class__.__name__}...")
|
|
189
|
+
error_message, balance = await funding_source.status()
|
|
190
|
+
if not error_message:
|
|
191
|
+
retry_counter = 0
|
|
192
|
+
logger.success(
|
|
193
|
+
f"✔️ Backend {funding_source.__class__.__name__} connected "
|
|
194
|
+
f"and with a balance of {balance} msat."
|
|
195
|
+
)
|
|
196
|
+
break
|
|
197
|
+
logger.error(
|
|
198
|
+
f"The backend for {funding_source.__class__.__name__} isn't "
|
|
199
|
+
f"working properly: '{error_message}'",
|
|
200
|
+
RuntimeWarning,
|
|
201
|
+
)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(
|
|
204
|
+
f"Error connecting to {funding_source.__class__.__name__}: {e}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if retry_counter >= max_retries:
|
|
208
|
+
set_void_wallet_class()
|
|
209
|
+
funding_source = get_funding_source()
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
retry_counter += 1
|
|
213
|
+
sleep_time = 0.25 * (2**retry_counter)
|
|
214
|
+
logger.warning(
|
|
215
|
+
f"Retrying connection to backend in {sleep_time} seconds... "
|
|
216
|
+
f"({retry_counter}/{max_retries})"
|
|
217
|
+
)
|
|
218
|
+
await asyncio.sleep(sleep_time)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def set_void_wallet_class():
|
|
222
|
+
logger.warning(
|
|
223
|
+
"Fallback to VoidWallet, because the backend for "
|
|
224
|
+
f"{settings.lnbits_backend_wallet_class} isn't working properly"
|
|
225
|
+
)
|
|
226
|
+
set_funding_source("VoidWallet")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def check_installed_extensions(app: FastAPI):
|
|
230
|
+
"""
|
|
231
|
+
Check extensions that have been installed, but for some reason no longer present in
|
|
232
|
+
the 'lnbits/extensions' directory. One reason might be a docker-container that was
|
|
233
|
+
re-created. The 'data' directory (where the '.zip' files live) is expected to
|
|
234
|
+
persist state. Zips that are missing will be re-downloaded.
|
|
235
|
+
"""
|
|
236
|
+
shutil.rmtree(os.path.join("lnbits", "upgrades"), True)
|
|
237
|
+
installed_extensions = await build_all_installed_extensions_list(False)
|
|
238
|
+
|
|
239
|
+
for ext in installed_extensions:
|
|
240
|
+
try:
|
|
241
|
+
installed = await check_installed_extension_files(ext)
|
|
242
|
+
if not installed:
|
|
243
|
+
await restore_installed_extension(app, ext)
|
|
244
|
+
logger.info(
|
|
245
|
+
"✔️ Successfully re-installed extension: "
|
|
246
|
+
f"{ext.id} ({ext.installed_version})"
|
|
247
|
+
)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.warning(e)
|
|
250
|
+
logger.warning(
|
|
251
|
+
f"Failed to re-install extension: {ext.id} ({ext.installed_version})"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
logger.info(f"Installed Extensions ({len(installed_extensions)}):")
|
|
255
|
+
for ext in installed_extensions:
|
|
256
|
+
logger.info(f"{ext.id} ({ext.installed_version})")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def build_all_installed_extensions_list(
|
|
260
|
+
include_deactivated: Optional[bool] = True,
|
|
261
|
+
) -> List[InstallableExtension]:
|
|
262
|
+
"""
|
|
263
|
+
Returns a list of all the installed extensions plus the extensions that
|
|
264
|
+
MUST be installed by default (see LNBITS_EXTENSIONS_DEFAULT_INSTALL).
|
|
265
|
+
"""
|
|
266
|
+
installed_extensions = await get_installed_extensions()
|
|
267
|
+
|
|
268
|
+
installed_extensions_ids = [e.id for e in installed_extensions]
|
|
269
|
+
for ext_id in settings.lnbits_extensions_default_install:
|
|
270
|
+
if ext_id in installed_extensions_ids:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
ext_releases = await InstallableExtension.get_extension_releases(ext_id)
|
|
274
|
+
ext_releases = sorted(
|
|
275
|
+
ext_releases, key=lambda r: version_parse(r.version), reverse=True
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
release = next((e for e in ext_releases if e.is_version_compatible), None)
|
|
279
|
+
|
|
280
|
+
if release:
|
|
281
|
+
ext_info = InstallableExtension(
|
|
282
|
+
id=ext_id, name=ext_id, installed_release=release, icon=release.icon
|
|
283
|
+
)
|
|
284
|
+
installed_extensions.append(ext_info)
|
|
285
|
+
|
|
286
|
+
if include_deactivated:
|
|
287
|
+
return installed_extensions
|
|
288
|
+
|
|
289
|
+
if settings.lnbits_extensions_deactivate_all:
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
return [
|
|
293
|
+
e
|
|
294
|
+
for e in installed_extensions
|
|
295
|
+
if e.id not in settings.lnbits_deactivated_extensions
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
async def check_installed_extension_files(ext: InstallableExtension) -> bool:
|
|
300
|
+
if ext.has_installed_version:
|
|
301
|
+
return True
|
|
302
|
+
|
|
303
|
+
zip_files = glob.glob(os.path.join(settings.lnbits_data_folder, "zips", "*.zip"))
|
|
304
|
+
|
|
305
|
+
if f"./{str(ext.zip_path)}" not in zip_files:
|
|
306
|
+
await ext.download_archive()
|
|
307
|
+
ext.extract_archive()
|
|
308
|
+
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
async def restore_installed_extension(app: FastAPI, ext: InstallableExtension):
|
|
313
|
+
await add_installed_extension(ext)
|
|
314
|
+
await update_installed_extension_state(ext_id=ext.id, active=True)
|
|
315
|
+
|
|
316
|
+
extension = Extension.from_installable_ext(ext)
|
|
317
|
+
register_ext_routes(app, extension)
|
|
318
|
+
|
|
319
|
+
current_version = (await get_dbversions()).get(ext.id, 0)
|
|
320
|
+
await migrate_extension_database(extension, current_version)
|
|
321
|
+
|
|
322
|
+
# mount routes for the new version
|
|
323
|
+
core_app_extra.register_new_ext_routes(extension)
|
|
324
|
+
if extension.upgrade_hash:
|
|
325
|
+
ext.notify_upgrade()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def register_custom_extensions_path():
|
|
329
|
+
if settings.has_default_extension_path:
|
|
330
|
+
return
|
|
331
|
+
default_ext_path = os.path.join("lnbits", "extensions")
|
|
332
|
+
if os.path.isdir(default_ext_path) and len(os.listdir(default_ext_path)) != 0:
|
|
333
|
+
logger.warning(
|
|
334
|
+
"You are using a custom extensions path, "
|
|
335
|
+
+ "but the default extensions directory is not empty. "
|
|
336
|
+
+ f"Please clean-up the '{default_ext_path}' directory."
|
|
337
|
+
)
|
|
338
|
+
logger.warning(
|
|
339
|
+
f"You can move the existing '{default_ext_path}' directory to: "
|
|
340
|
+
+ f" '{settings.lnbits_extensions_path}/extensions'"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
sys.path.append(str(Path(settings.lnbits_extensions_path, "extensions")))
|
|
344
|
+
sys.path.append(str(Path(settings.lnbits_extensions_path, "upgrades")))
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def register_new_ext_routes(app: FastAPI) -> Callable:
|
|
348
|
+
# Returns a function that registers new routes for an extension.
|
|
349
|
+
# The returned function encapsulates (creates a closure around)
|
|
350
|
+
# the `app` object but does expose it.
|
|
351
|
+
def register_new_ext_routes_fn(ext: Extension):
|
|
352
|
+
register_ext_routes(app, ext)
|
|
353
|
+
|
|
354
|
+
return register_new_ext_routes_fn
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def register_new_ratelimiter(app: FastAPI) -> Callable:
|
|
358
|
+
def register_new_ratelimiter_fn():
|
|
359
|
+
limiter = Limiter(
|
|
360
|
+
key_func=get_remote_address,
|
|
361
|
+
default_limits=[
|
|
362
|
+
f"{settings.lnbits_rate_limit_no}/{settings.lnbits_rate_limit_unit}"
|
|
363
|
+
],
|
|
364
|
+
)
|
|
365
|
+
app.state.limiter = limiter
|
|
366
|
+
|
|
367
|
+
return register_new_ratelimiter_fn
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def register_ext_routes(app: FastAPI, ext: Extension) -> None:
|
|
371
|
+
"""Register FastAPI routes for extension."""
|
|
372
|
+
ext_module = importlib.import_module(ext.module_name)
|
|
373
|
+
|
|
374
|
+
ext_route = getattr(ext_module, f"{ext.code}_ext")
|
|
375
|
+
|
|
376
|
+
if hasattr(ext_module, f"{ext.code}_start"):
|
|
377
|
+
ext_start_func = getattr(ext_module, f"{ext.code}_start")
|
|
378
|
+
ext_start_func()
|
|
379
|
+
|
|
380
|
+
if hasattr(ext_module, f"{ext.code}_static_files"):
|
|
381
|
+
ext_statics = getattr(ext_module, f"{ext.code}_static_files")
|
|
382
|
+
for s in ext_statics:
|
|
383
|
+
static_dir = Path(
|
|
384
|
+
settings.lnbits_extensions_path, "extensions", *s["path"].split("/")
|
|
385
|
+
)
|
|
386
|
+
app.mount(s["path"], StaticFiles(directory=static_dir), s["name"])
|
|
387
|
+
|
|
388
|
+
if hasattr(ext_module, f"{ext.code}_redirect_paths"):
|
|
389
|
+
ext_redirects = getattr(ext_module, f"{ext.code}_redirect_paths")
|
|
390
|
+
settings.lnbits_extensions_redirects = [
|
|
391
|
+
r for r in settings.lnbits_extensions_redirects if r["ext_id"] != ext.code
|
|
392
|
+
]
|
|
393
|
+
for r in ext_redirects:
|
|
394
|
+
r["ext_id"] = ext.code
|
|
395
|
+
settings.lnbits_extensions_redirects.append(r)
|
|
396
|
+
|
|
397
|
+
logger.trace(f"adding route for extension {ext_module}")
|
|
398
|
+
|
|
399
|
+
prefix = f"/upgrades/{ext.upgrade_hash}" if ext.upgrade_hash != "" else ""
|
|
400
|
+
app.include_router(router=ext_route, prefix=prefix)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def register_all_ext_routes(app: FastAPI):
|
|
404
|
+
for ext in get_valid_extensions(False):
|
|
405
|
+
try:
|
|
406
|
+
register_ext_routes(app, ext)
|
|
407
|
+
except Exception as e:
|
|
408
|
+
logger.error(f"Could not load extension `{ext.code}`: {str(e)}")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def initialize_server_logger():
|
|
412
|
+
super_user_hash = sha256(settings.super_user.encode("utf-8")).hexdigest()
|
|
413
|
+
|
|
414
|
+
serverlog_queue = asyncio.Queue()
|
|
415
|
+
|
|
416
|
+
async def update_websocket_serverlog():
|
|
417
|
+
while True:
|
|
418
|
+
msg = await serverlog_queue.get()
|
|
419
|
+
await websocket_updater(super_user_hash, msg)
|
|
420
|
+
|
|
421
|
+
create_permanent_task(update_websocket_serverlog)
|
|
422
|
+
|
|
423
|
+
logger.add(
|
|
424
|
+
lambda msg: serverlog_queue.put_nowait(msg),
|
|
425
|
+
format=Formatter().format,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def log_server_info():
|
|
430
|
+
logger.info("Starting LNbits")
|
|
431
|
+
logger.info(f"Version: {settings.version}")
|
|
432
|
+
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
|
|
433
|
+
logger.info(f"Host: {settings.host}")
|
|
434
|
+
logger.info(f"Port: {settings.port}")
|
|
435
|
+
logger.info(f"Debug: {settings.debug}")
|
|
436
|
+
logger.info(f"Site title: {settings.lnbits_site_title}")
|
|
437
|
+
logger.info(f"Funding source: {settings.lnbits_backend_wallet_class}")
|
|
438
|
+
logger.info(f"Data folder: {settings.lnbits_data_folder}")
|
|
439
|
+
logger.info(f"Database: {get_db_vendor_name()}")
|
|
440
|
+
logger.info(f"Service fee: {settings.lnbits_service_fee}")
|
|
441
|
+
logger.info(f"Service fee max: {settings.lnbits_service_fee_max}")
|
|
442
|
+
logger.info(f"Service fee wallet: {settings.lnbits_service_fee_wallet}")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def get_db_vendor_name():
|
|
446
|
+
db_url = settings.lnbits_database_url
|
|
447
|
+
return (
|
|
448
|
+
"PostgreSQL"
|
|
449
|
+
if db_url and db_url.startswith("postgres://")
|
|
450
|
+
else (
|
|
451
|
+
"CockroachDB"
|
|
452
|
+
if db_url and db_url.startswith("cockroachdb://")
|
|
453
|
+
else "SQLite"
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def register_async_tasks():
|
|
459
|
+
create_permanent_task(check_pending_payments)
|
|
460
|
+
create_permanent_task(invoice_listener)
|
|
461
|
+
create_permanent_task(internal_invoice_listener)
|
|
462
|
+
create_permanent_task(cache.invalidate_forever)
|
|
463
|
+
|
|
464
|
+
# core invoice listener
|
|
465
|
+
invoice_queue = asyncio.Queue(5)
|
|
466
|
+
register_invoice_listener(invoice_queue, "core")
|
|
467
|
+
create_permanent_task(lambda: wait_for_paid_invoices(invoice_queue))
|
|
468
|
+
|
|
469
|
+
# TODO: implement watchdog properly
|
|
470
|
+
# create_permanent_task(watchdog_task)
|
|
471
|
+
create_permanent_task(killswitch_task)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def register_exception_handlers(app: FastAPI):
|
|
475
|
+
@app.exception_handler(Exception)
|
|
476
|
+
async def exception_handler(request: Request, exc: Exception):
|
|
477
|
+
etype, _, tb = sys.exc_info()
|
|
478
|
+
traceback.print_exception(etype, exc, tb)
|
|
479
|
+
logger.error(f"Exception: {str(exc)}")
|
|
480
|
+
# Only the browser sends "text/html" request
|
|
481
|
+
# not fail proof, but everything else get's a JSON response
|
|
482
|
+
if (
|
|
483
|
+
request.headers
|
|
484
|
+
and "accept" in request.headers
|
|
485
|
+
and "text/html" in request.headers["accept"]
|
|
486
|
+
):
|
|
487
|
+
return template_renderer().TemplateResponse(
|
|
488
|
+
request, "error.html", {"err": f"Error: {str(exc)}"}
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
return JSONResponse(
|
|
492
|
+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
|
493
|
+
content={"detail": str(exc)},
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
@app.exception_handler(RequestValidationError)
|
|
497
|
+
async def validation_exception_handler(
|
|
498
|
+
request: Request, exc: RequestValidationError
|
|
499
|
+
):
|
|
500
|
+
logger.error(f"RequestValidationError: {str(exc)}")
|
|
501
|
+
# Only the browser sends "text/html" request
|
|
502
|
+
# not fail proof, but everything else get's a JSON response
|
|
503
|
+
|
|
504
|
+
if (
|
|
505
|
+
request.headers
|
|
506
|
+
and "accept" in request.headers
|
|
507
|
+
and "text/html" in request.headers["accept"]
|
|
508
|
+
):
|
|
509
|
+
return template_renderer().TemplateResponse(
|
|
510
|
+
request,
|
|
511
|
+
"error.html",
|
|
512
|
+
{"err": f"Error: {str(exc)}"},
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
return JSONResponse(
|
|
516
|
+
status_code=HTTPStatus.BAD_REQUEST,
|
|
517
|
+
content={"detail": str(exc)},
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
@app.exception_handler(HTTPException)
|
|
521
|
+
async def http_exception_handler(request: Request, exc: HTTPException):
|
|
522
|
+
logger.error(f"HTTPException {exc.status_code}: {exc.detail}")
|
|
523
|
+
# Only the browser sends "text/html" request
|
|
524
|
+
# not fail proof, but everything else get's a JSON response
|
|
525
|
+
|
|
526
|
+
if (
|
|
527
|
+
request.headers
|
|
528
|
+
and "accept" in request.headers
|
|
529
|
+
and "text/html" in request.headers["accept"]
|
|
530
|
+
):
|
|
531
|
+
if exc.headers and "token-expired" in exc.headers:
|
|
532
|
+
response = RedirectResponse("/")
|
|
533
|
+
response.delete_cookie("cookie_access_token")
|
|
534
|
+
response.delete_cookie("is_lnbits_user_authorized")
|
|
535
|
+
response.set_cookie("is_access_token_expired", "true")
|
|
536
|
+
return response
|
|
537
|
+
|
|
538
|
+
return template_renderer().TemplateResponse(
|
|
539
|
+
request,
|
|
540
|
+
"error.html",
|
|
541
|
+
{
|
|
542
|
+
"request": request,
|
|
543
|
+
"err": f"HTTP Error {exc.status_code}: {exc.detail}",
|
|
544
|
+
},
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
return JSONResponse(
|
|
548
|
+
status_code=exc.status_code,
|
|
549
|
+
content={"detail": exc.detail},
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def configure_logger() -> None:
|
|
554
|
+
logger.remove()
|
|
555
|
+
log_level: str = "DEBUG" if settings.debug else "INFO"
|
|
556
|
+
formatter = Formatter()
|
|
557
|
+
logger.add(sys.stdout, level=log_level, format=formatter.format)
|
|
558
|
+
|
|
559
|
+
if settings.enable_log_to_file:
|
|
560
|
+
logger.add(
|
|
561
|
+
Path(settings.lnbits_data_folder, "logs", "lnbits.log"),
|
|
562
|
+
rotation=settings.log_rotation,
|
|
563
|
+
retention=settings.log_retention,
|
|
564
|
+
level="INFO",
|
|
565
|
+
format=formatter.format,
|
|
566
|
+
)
|
|
567
|
+
logger.add(
|
|
568
|
+
Path(settings.lnbits_data_folder, "logs", "debug.log"),
|
|
569
|
+
rotation=settings.log_rotation,
|
|
570
|
+
retention=settings.log_retention,
|
|
571
|
+
level="DEBUG",
|
|
572
|
+
format=formatter.format,
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
logging.getLogger("uvicorn").handlers = [InterceptHandler()]
|
|
576
|
+
logging.getLogger("uvicorn.access").handlers = [InterceptHandler()]
|
|
577
|
+
logging.getLogger("uvicorn.error").handlers = [InterceptHandler()]
|
|
578
|
+
logging.getLogger("uvicorn.error").propagate = False
|
|
579
|
+
|
|
580
|
+
logging.getLogger("sqlalchemy").handlers = [InterceptHandler()]
|
|
581
|
+
logging.getLogger("sqlalchemy.engine.base").handlers = [InterceptHandler()]
|
|
582
|
+
logging.getLogger("sqlalchemy.engine.base").propagate = False
|
|
583
|
+
logging.getLogger("sqlalchemy.engine.base.Engine").handlers = [InterceptHandler()]
|
|
584
|
+
logging.getLogger("sqlalchemy.engine.base.Engine").propagate = False
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
class Formatter:
|
|
588
|
+
def __init__(self):
|
|
589
|
+
self.padding = 0
|
|
590
|
+
self.minimal_fmt = (
|
|
591
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | <level>{level}</level> | "
|
|
592
|
+
"<level>{message}</level>\n"
|
|
593
|
+
)
|
|
594
|
+
if settings.debug:
|
|
595
|
+
self.fmt = (
|
|
596
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SS}</green> | "
|
|
597
|
+
"<level>{level: <4}</level> | "
|
|
598
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
|
|
599
|
+
"<level>{message}</level>\n"
|
|
600
|
+
)
|
|
601
|
+
else:
|
|
602
|
+
self.fmt = self.minimal_fmt
|
|
603
|
+
|
|
604
|
+
def format(self, record):
|
|
605
|
+
function = "{function}".format(**record)
|
|
606
|
+
if function == "emit": # uvicorn logs
|
|
607
|
+
return self.minimal_fmt
|
|
608
|
+
return self.fmt
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class InterceptHandler(logging.Handler):
|
|
612
|
+
def emit(self, record):
|
|
613
|
+
try:
|
|
614
|
+
level = logger.level(record.levelname).name
|
|
615
|
+
except ValueError:
|
|
616
|
+
level = record.levelno
|
|
617
|
+
logger.log(level, record.getMessage())
|