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.
Files changed (189) hide show
  1. lnbits/__init__.py +0 -0
  2. lnbits/__main__.py +3 -0
  3. lnbits/app.py +617 -0
  4. lnbits/bolt11.py +7 -0
  5. lnbits/commands.py +718 -0
  6. lnbits/core/__init__.py +38 -0
  7. lnbits/core/crud.py +1309 -0
  8. lnbits/core/db.py +5 -0
  9. lnbits/core/helpers.py +119 -0
  10. lnbits/core/migrations.py +473 -0
  11. lnbits/core/models.py +426 -0
  12. lnbits/core/services.py +793 -0
  13. lnbits/core/sso/keycloak.py +37 -0
  14. lnbits/core/tasks.py +182 -0
  15. lnbits/core/templates/admin/_tab_funding.html +96 -0
  16. lnbits/core/templates/admin/_tab_security.html +404 -0
  17. lnbits/core/templates/admin/_tab_security_notifications.html +77 -0
  18. lnbits/core/templates/admin/_tab_server.html +186 -0
  19. lnbits/core/templates/admin/_tab_theme.html +128 -0
  20. lnbits/core/templates/admin/_tab_users.html +78 -0
  21. lnbits/core/templates/admin/index.html +172 -0
  22. lnbits/core/templates/core/_api_docs.html +156 -0
  23. lnbits/core/templates/core/account.html +436 -0
  24. lnbits/core/templates/core/extensions.html +1003 -0
  25. lnbits/core/templates/core/first_install.html +139 -0
  26. lnbits/core/templates/core/index.html +525 -0
  27. lnbits/core/templates/core/wallet.html +951 -0
  28. lnbits/core/templates/node/_tab_channels.html +308 -0
  29. lnbits/core/templates/node/_tab_dashboard.html +68 -0
  30. lnbits/core/templates/node/_tab_transactions.html +310 -0
  31. lnbits/core/templates/node/index.html +376 -0
  32. lnbits/core/templates/node/public.html +136 -0
  33. lnbits/core/templates/service-worker.js +85 -0
  34. lnbits/core/views/__init__.py +0 -0
  35. lnbits/core/views/admin_api.py +168 -0
  36. lnbits/core/views/api.py +246 -0
  37. lnbits/core/views/auth_api.py +346 -0
  38. lnbits/core/views/extension_api.py +270 -0
  39. lnbits/core/views/generic.py +540 -0
  40. lnbits/core/views/node_api.py +199 -0
  41. lnbits/core/views/payment_api.py +473 -0
  42. lnbits/core/views/public_api.py +58 -0
  43. lnbits/core/views/tinyurl_api.py +105 -0
  44. lnbits/core/views/wallet_api.py +77 -0
  45. lnbits/core/views/webpush_api.py +60 -0
  46. lnbits/core/views/websocket_api.py +40 -0
  47. lnbits/db.py +509 -0
  48. lnbits/decorators.py +333 -0
  49. lnbits/extension_manager.py +726 -0
  50. lnbits/helpers.py +207 -0
  51. lnbits/jinja2_templating.py +28 -0
  52. lnbits/lnurl.py +1 -0
  53. lnbits/middleware.py +232 -0
  54. lnbits/nodes/__init__.py +15 -0
  55. lnbits/nodes/base.py +236 -0
  56. lnbits/nodes/cln.py +336 -0
  57. lnbits/nodes/lndrest.py +388 -0
  58. lnbits/requestvars.py +10 -0
  59. lnbits/server.py +99 -0
  60. lnbits/settings.py +541 -0
  61. lnbits/static/bundle.min.css +1 -0
  62. lnbits/static/bundle.min.js +56 -0
  63. lnbits/static/css/base.css +556 -0
  64. lnbits/static/favicon.ico +0 -0
  65. lnbits/static/fonts/material-icons-v50.woff2 +0 -0
  66. lnbits/static/i18n/br.js +245 -0
  67. lnbits/static/i18n/cn.js +233 -0
  68. lnbits/static/i18n/cs.js +242 -0
  69. lnbits/static/i18n/de.js +249 -0
  70. lnbits/static/i18n/en.js +250 -0
  71. lnbits/static/i18n/es.js +247 -0
  72. lnbits/static/i18n/fi.js +245 -0
  73. lnbits/static/i18n/fr.js +251 -0
  74. lnbits/static/i18n/i18n.js +1 -0
  75. lnbits/static/i18n/it.js +248 -0
  76. lnbits/static/i18n/jp.js +244 -0
  77. lnbits/static/i18n/kr.js +241 -0
  78. lnbits/static/i18n/nl.js +247 -0
  79. lnbits/static/i18n/pi.js +244 -0
  80. lnbits/static/i18n/pl.js +243 -0
  81. lnbits/static/i18n/pt.js +244 -0
  82. lnbits/static/i18n/sk.js +243 -0
  83. lnbits/static/i18n/we.js +243 -0
  84. lnbits/static/images/alby.png +0 -0
  85. lnbits/static/images/albyl.png +0 -0
  86. lnbits/static/images/blitz.png +0 -0
  87. lnbits/static/images/blitzl.png +0 -0
  88. lnbits/static/images/cln.png +0 -0
  89. lnbits/static/images/clnl.png +0 -0
  90. lnbits/static/images/default_voucher.png +0 -0
  91. lnbits/static/images/github-logo.png +0 -0
  92. lnbits/static/images/google-logo.png +0 -0
  93. lnbits/static/images/keycloak-logo.png +0 -0
  94. lnbits/static/images/lnbits-shop-dark.png +0 -0
  95. lnbits/static/images/lnbits-shop-light.png +0 -0
  96. lnbits/static/images/lnd.png +0 -0
  97. lnbits/static/images/lnpay.png +0 -0
  98. lnbits/static/images/lnpayl.png +0 -0
  99. lnbits/static/images/logos/lnbits.png +0 -0
  100. lnbits/static/images/logos/lnbits.svg +4 -0
  101. lnbits/static/images/maskable_icon.png +0 -0
  102. lnbits/static/images/maskable_icon_x192.png +0 -0
  103. lnbits/static/images/maskable_icon_x512.png +0 -0
  104. lnbits/static/images/maskable_icon_x96.png +0 -0
  105. lnbits/static/images/mynode.png +0 -0
  106. lnbits/static/images/mynodel.png +0 -0
  107. lnbits/static/images/opennode.png +0 -0
  108. lnbits/static/images/opennodel.png +0 -0
  109. lnbits/static/images/screenshot_desktop.png +0 -0
  110. lnbits/static/images/screenshot_phone.png +0 -0
  111. lnbits/static/images/spark.png +0 -0
  112. lnbits/static/images/sparkl.png +0 -0
  113. lnbits/static/images/start9.png +0 -0
  114. lnbits/static/images/start9l.png +0 -0
  115. lnbits/static/images/templatead.png +0 -0
  116. lnbits/static/images/umbrel.png +0 -0
  117. lnbits/static/images/umbrell.png +0 -0
  118. lnbits/static/images/voltage.png +0 -0
  119. lnbits/static/images/voltagel.png +0 -0
  120. lnbits/static/images/voucher_template.svg +16 -0
  121. lnbits/static/images/zbd.png +0 -0
  122. lnbits/static/images/zbdl.png +0 -0
  123. lnbits/static/js/account.js +122 -0
  124. lnbits/static/js/admin.js +312 -0
  125. lnbits/static/js/base.js +630 -0
  126. lnbits/static/js/bolt11-decoder.js +347 -0
  127. lnbits/static/js/components/extension-settings.js +90 -0
  128. lnbits/static/js/components/lnbits-funding-sources.js +182 -0
  129. lnbits/static/js/components.js +729 -0
  130. lnbits/static/js/event-reactions.js +560 -0
  131. lnbits/static/js/index.js +102 -0
  132. lnbits/static/js/node.js +268 -0
  133. lnbits/static/js/wallet.js +865 -0
  134. lnbits/static/scss/base.scss +233 -0
  135. lnbits/static/vendor/Chart.bundle.js +20776 -0
  136. lnbits/static/vendor/Chart.css +47 -0
  137. lnbits/static/vendor/VueQrcodeReader.umd.js +11616 -0
  138. lnbits/static/vendor/axios.js +3011 -0
  139. lnbits/static/vendor/moment.js +5685 -0
  140. lnbits/static/vendor/quasar.css +11680 -0
  141. lnbits/static/vendor/quasar.ie.polyfills.umd.min.js +6 -0
  142. lnbits/static/vendor/quasar.umd.js +37923 -0
  143. lnbits/static/vendor/showdown.js +5157 -0
  144. lnbits/static/vendor/underscore.js +2042 -0
  145. lnbits/static/vendor/vue-i18n.js +2312 -0
  146. lnbits/static/vendor/vue-qrcode.js +5509 -0
  147. lnbits/static/vendor/vue-router.js +3061 -0
  148. lnbits/static/vendor/vue.js +11965 -0
  149. lnbits/static/vendor/vuex.js +1246 -0
  150. lnbits/static/vendor.json +44 -0
  151. lnbits/tasks.py +207 -0
  152. lnbits/templates/base.html +271 -0
  153. lnbits/templates/error.html +64 -0
  154. lnbits/templates/macros.jinja +14 -0
  155. lnbits/templates/print.html +51 -0
  156. lnbits/templates/public.html +14 -0
  157. lnbits/utils/__init__.py +0 -0
  158. lnbits/utils/cache.py +66 -0
  159. lnbits/utils/crypto.py +75 -0
  160. lnbits/utils/exchange_rates.py +299 -0
  161. lnbits/wallets/__init__.py +48 -0
  162. lnbits/wallets/alby.py +189 -0
  163. lnbits/wallets/base.py +148 -0
  164. lnbits/wallets/cliche.py +184 -0
  165. lnbits/wallets/corelightning.py +248 -0
  166. lnbits/wallets/corelightningrest.py +300 -0
  167. lnbits/wallets/eclair.py +236 -0
  168. lnbits/wallets/fake.py +135 -0
  169. lnbits/wallets/lnbits.py +190 -0
  170. lnbits/wallets/lnd_grpc_files/__init__.py +0 -0
  171. lnbits/wallets/lnd_grpc_files/lightning_pb2.py +3281 -0
  172. lnbits/wallets/lnd_grpc_files/lightning_pb2_grpc.py +3340 -0
  173. lnbits/wallets/lnd_grpc_files/router_pb2.py +665 -0
  174. lnbits/wallets/lnd_grpc_files/router_pb2_grpc.py +871 -0
  175. lnbits/wallets/lndgrpc.py +277 -0
  176. lnbits/wallets/lndrest.py +302 -0
  177. lnbits/wallets/lnpay.py +150 -0
  178. lnbits/wallets/lntips.py +182 -0
  179. lnbits/wallets/macaroon/__init__.py +1 -0
  180. lnbits/wallets/macaroon/macaroon.py +45 -0
  181. lnbits/wallets/opennode.py +146 -0
  182. lnbits/wallets/spark.py +265 -0
  183. lnbits/wallets/void.py +41 -0
  184. lnbits/wallets/zbd.py +159 -0
  185. lnbits-0.12.6.dist-info/LICENSE +21 -0
  186. lnbits-0.12.6.dist-info/METADATA +51 -0
  187. lnbits-0.12.6.dist-info/RECORD +189 -0
  188. lnbits-0.12.6.dist-info/WHEEL +4 -0
  189. lnbits-0.12.6.dist-info/entry_points.txt +4 -0
lnbits/__init__.py ADDED
File without changes
lnbits/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .app import create_app
2
+
3
+ app = create_app()
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())
lnbits/bolt11.py ADDED
@@ -0,0 +1,7 @@
1
+ from bolt11 import (
2
+ Bolt11 as Invoice, # noqa: F401
3
+ )
4
+ from bolt11 import (
5
+ decode, # noqa: F401
6
+ encode, # noqa: F401
7
+ )