lnbits-legend/lnbits/core/views/generic.py

469 lines
16 KiB
Python
Raw Normal View History

import asyncio
from http import HTTPStatus
from pathlib import Path
from typing import List, Optional
[FEAT] Push notification integration into core (#1393) * push notification integration into core added missing component fixed bell working on all pages - made pubkey global template env var - had to move `get_push_notification_pubkey` to `helpers.py` because of circular reference with `tasks.py` formay trying to fix mypy added py-vapid to requirements Trying to fix stub mypy issue * removed key files * webpush key pair is saved in db `webpush_settings` * removed lnaddress extension changes * support for multi user account subscriptions, subscriptions are stored user based fixed syntax error fixed syntax error removed unused line * fixed subscribed user storage with local storage, no get request required * method is singular now * cleanup unsubscribed or expired push subscriptions fixed flake8 errors fixed poetry errors * updating to latest lnbits formatting, rebase error fix * remove unused? * revert * relock * remove * do not create settings table use adminsettings mypy fix * cleanup old code * catch case when client tries to recreate existing webpush subscription e.g. on cleared local storage * show notification bell on user related pages only * use local storage with one key like array, some refactoring * fixed crud import * fixed too long line * removed unused imports * ruff * make webpush editable * fixed privkey encoding * fix ruff * fix migration --------- Co-authored-by: schneimi <admin@schneimi.de> Co-authored-by: schneimi <dev@schneimi.de> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-09-11 15:48:49 +02:00
from urllib.parse import urlparse
from fastapi import Depends, Query, Request, status
2021-08-29 19:38:42 +02:00
from fastapi.exceptions import HTTPException
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse
from fastapi.routing import APIRouter
2022-07-16 14:23:03 +02:00
from loguru import logger
from pydantic.types import UUID4
from lnbits.core.db import db
from lnbits.core.helpers import to_valid_user_id
2021-09-11 20:44:22 +02:00
from lnbits.core.models import User
from lnbits.decorators import check_admin, check_user_exists
2021-10-25 19:26:21 +01:00
from lnbits.helpers import template_renderer, url_for
from lnbits.settings import settings
from lnbits.wallets import get_wallet_class
2021-10-17 18:33:29 +01:00
2023-01-20 10:06:32 +02:00
from ...extension_manager import InstallableExtension, get_valid_extensions
from ...utils.exchange_rates import currencies
2021-10-17 18:33:29 +01:00
from ..crud import (
create_account,
create_wallet,
get_balance_check,
get_dbversions,
get_inactive_extensions,
2023-01-17 16:28:24 +02:00
get_installed_extensions,
2021-10-17 18:33:29 +01:00
get_user,
save_balance_notify,
update_installed_extension_state,
2021-10-17 18:33:29 +01:00
update_user_extension,
)
from ..services import pay_invoice, redeem_lnurl_withdraw
generic_router = APIRouter(
2023-08-24 11:52:12 +02:00
tags=["Core NON-API Website Routes"], include_in_schema=False
)
2022-09-22 10:46:11 +02:00
@generic_router.get("/favicon.ico", response_class=FileResponse)
async def favicon():
return FileResponse(Path("lnbits", "static", "favicon.ico"))
2021-10-17 18:33:29 +01:00
@generic_router.get("/", response_class=HTMLResponse)
2022-12-23 11:53:45 +01:00
async def home(request: Request, lightning: str = ""):
2021-10-17 18:33:29 +01:00
return template_renderer().TemplateResponse(
"core/index.html", {"request": request, "lnurl": lightning}
)
@generic_router.get("/robots.txt", response_class=HTMLResponse)
2023-01-20 09:25:46 +01:00
async def robots():
data = """
User-agent: *
Disallow: /
"""
return HTMLResponse(content=data, media_type="text/plain")
@generic_router.get(
"/extensions", name="install.extensions", response_class=HTMLResponse
)
async def extensions_install(
2021-10-17 18:33:29 +01:00
request: Request,
user: User = Depends(check_user_exists),
activate: str = Query(None),
deactivate: str = Query(None),
enable: str = Query(None),
disable: str = Query(None),
2021-10-17 18:33:29 +01:00
):
await toggle_extension(enable, disable, user.id)
2021-09-11 20:44:22 +02:00
# Update user as his extensions have been updated
if enable or disable:
user = await get_user(user.id) # type: ignore
try:
installed_exts: List["InstallableExtension"] = await get_installed_extensions()
installed_exts_ids = [e.id for e in installed_exts]
2023-01-17 17:07:52 +02:00
installable_exts: List[
InstallableExtension
2023-01-17 17:07:52 +02:00
] = await InstallableExtension.get_installable_extensions()
installable_exts += [
e for e in installed_exts if e.id not in installed_exts_ids
2023-01-17 17:07:52 +02:00
]
for e in installable_exts:
2023-01-20 16:11:21 +02:00
installed_ext = next((ie for ie in installed_exts if e.id == ie.id), None)
if installed_ext:
e.installed_release = installed_ext.installed_release
# use the installed extension values
2023-01-20 16:11:21 +02:00
e.name = installed_ext.name
e.short_description = installed_ext.short_description
e.icon = installed_ext.icon
2023-01-17 17:07:52 +02:00
except Exception as ex:
logger.warning(ex)
installable_exts = []
try:
ext_id = activate or deactivate
if ext_id and user.admin:
if deactivate and deactivate not in settings.lnbits_deactivated_extensions:
settings.lnbits_deactivated_extensions += [deactivate]
elif activate:
settings.lnbits_deactivated_extensions = list(
filter(
lambda e: e != activate, settings.lnbits_deactivated_extensions
)
)
await update_installed_extension_state(
2023-01-21 15:08:59 +00:00
ext_id=ext_id, active=activate is not None
)
all_extensions = list(map(lambda e: e.code, get_valid_extensions()))
inactive_extensions = await get_inactive_extensions()
db_version = await get_dbversions()
extensions = list(
map(
lambda ext: {
"id": ext.id,
"name": ext.name,
"icon": ext.icon,
"shortDescription": ext.short_description,
"stars": ext.stars,
"isFeatured": ext.featured,
"dependencies": ext.dependencies,
"isInstalled": ext.id in installed_exts_ids,
"hasDatabaseTables": ext.id in db_version,
2023-01-17 17:34:50 +02:00
"isAvailable": ext.id in all_extensions,
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
2023-01-21 15:07:40 +00:00
"isActive": ext.id not in inactive_extensions,
"latestRelease": (
dict(ext.latest_release) if ext.latest_release else None
),
"installedRelease": (
dict(ext.installed_release) if ext.installed_release else None
),
},
installable_exts,
)
)
return template_renderer().TemplateResponse(
"core/extensions.html",
{
"request": request,
"user": user.dict(),
"extensions": extensions,
},
)
except Exception as e:
logger.warning(e)
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e))
@generic_router.get(
"/wallet",
response_class=HTMLResponse,
description="show wallet page",
)
2021-10-17 18:33:29 +01:00
async def wallet(
request: Request,
usr: UUID4 = Query(...),
wal: Optional[UUID4] = Query(None),
2021-10-17 18:33:29 +01:00
):
user_id = usr.hex
user = await get_user(user_id)
if not user:
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "User does not exist."}
2021-10-17 18:33:29 +01:00
)
if not wal:
if len(user.wallets) == 0:
wallet = await create_wallet(user_id=user.id)
return RedirectResponse(url=f"/wallet?usr={user_id}&wal={wallet.id}")
return RedirectResponse(url=f"/wallet?usr={user_id}&wal={user.wallets[0].id}")
else:
wallet_id = wal.hex
userwallet = user.get_wallet(wallet_id)
if not userwallet or userwallet.deleted:
2021-10-17 18:33:29 +01:00
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "Wallet not found"}
)
if (
len(settings.lnbits_allowed_users) > 0
and user_id not in settings.lnbits_allowed_users
and user_id not in settings.lnbits_admin_users
and user_id != settings.super_user
):
return template_renderer().TemplateResponse(
"error.html", {"request": request, "err": "User not authorized."}
)
if user_id == settings.super_user or user_id in settings.lnbits_admin_users:
user.admin = True
if user_id == settings.super_user:
user.super_user = True
logger.debug(f"Access user {user.id} wallet {userwallet.name}")
return template_renderer().TemplateResponse(
2021-10-17 18:33:29 +01:00
"core/wallet.html",
{
"request": request,
"user": user.dict(),
2022-07-25 13:27:31 +02:00
"wallet": userwallet.dict(),
"service_fee": settings.lnbits_service_fee,
"service_fee_max": settings.lnbits_service_fee_max,
"web_manifest": f"/manifest/{user.id}.webmanifest",
2021-10-17 18:33:29 +01:00
},
)
@generic_router.get("/withdraw", response_class=JSONResponse)
async def lnurl_full_withdraw(request: Request):
usr_param = request.query_params.get("usr")
if not usr_param:
return {"status": "ERROR", "reason": "usr parameter not provided."}
user = await get_user(usr_param)
2021-04-17 18:27:15 -03:00
if not user:
2021-08-20 21:31:01 +01:00
return {"status": "ERROR", "reason": "User does not exist."}
2021-04-17 18:27:15 -03:00
wal_param = request.query_params.get("wal")
if not wal_param:
return {"status": "ERROR", "reason": "wal parameter not provided."}
wallet = user.get_wallet(wal_param)
2021-04-17 18:27:15 -03:00
if not wallet:
2021-10-17 18:33:29 +01:00
return {"status": "ERROR", "reason": "Wallet does not exist."}
2021-04-17 18:27:15 -03:00
2021-08-20 21:31:01 +01:00
return {
2021-10-17 18:33:29 +01:00
"tag": "withdrawRequest",
"callback": url_for("/withdraw/cb", external=True, usr=user.id, wal=wallet.id),
"k1": "0",
"minWithdrawable": 1000 if wallet.withdrawable_balance else 0,
"maxWithdrawable": wallet.withdrawable_balance,
"defaultDescription": (
f"{settings.lnbits_site_title} balance withdraw from {wallet.id[0:5]}"
),
2021-10-17 18:33:29 +01:00
"balanceCheck": url_for("/withdraw", external=True, usr=user.id, wal=wallet.id),
}
2021-04-17 18:27:15 -03:00
@generic_router.get("/withdraw/cb", response_class=JSONResponse)
async def lnurl_full_withdraw_callback(request: Request):
usr_param = request.query_params.get("usr")
if not usr_param:
return {"status": "ERROR", "reason": "usr parameter not provided."}
user = await get_user(usr_param)
2021-04-17 18:27:15 -03:00
if not user:
2021-08-20 21:31:01 +01:00
return {"status": "ERROR", "reason": "User does not exist."}
2021-04-17 18:27:15 -03:00
wal_param = request.query_params.get("wal")
if not wal_param:
return {"status": "ERROR", "reason": "wal parameter not provided."}
wallet = user.get_wallet(wal_param)
2021-04-17 18:27:15 -03:00
if not wallet:
2021-08-20 21:31:01 +01:00
return {"status": "ERROR", "reason": "Wallet does not exist."}
2021-04-17 18:27:15 -03:00
pr = request.query_params.get("pr")
if not pr:
return {"status": "ERROR", "reason": "payment_request not provided."}
2021-04-17 18:27:15 -03:00
async def pay():
try:
await pay_invoice(wallet_id=wallet.id, payment_request=pr)
except Exception:
pass
2021-04-17 18:27:15 -03:00
asyncio.create_task(pay())
2021-04-17 18:27:15 -03:00
balance_notify = request.query_params.get("balanceNotify")
2021-04-17 18:27:15 -03:00
if balance_notify:
await save_balance_notify(wallet.id, balance_notify)
2021-08-20 21:31:01 +01:00
return {"status": "OK"}
2021-04-17 18:27:15 -03:00
@generic_router.get("/withdraw/notify/{service}")
async def lnurl_balance_notify(request: Request, service: str):
wal_param = request.query_params.get("wal")
if not wal_param:
return {"status": "ERROR", "reason": "wal parameter not provided."}
bc = await get_balance_check(wal_param, service)
2021-04-17 18:27:15 -03:00
if bc:
2022-07-19 18:51:35 +02:00
await redeem_lnurl_withdraw(bc.wallet, bc.url)
2021-04-17 18:27:15 -03:00
@generic_router.get(
2022-06-01 14:53:05 +02:00
"/lnurlwallet", response_class=RedirectResponse, name="core.lnurlwallet"
)
async def lnurlwallet(request: Request):
async with db.connect() as conn:
account = await create_account(conn=conn)
user = await get_user(account.id, conn=conn)
assert user, "Newly created user not found."
wallet = await create_wallet(user_id=user.id, conn=conn)
lightning_param = request.query_params.get("lightning")
if not lightning_param:
return {"status": "ERROR", "reason": "lightning parameter not provided."}
asyncio.create_task(
redeem_lnurl_withdraw(
2021-08-29 19:38:42 +02:00
wallet.id,
lightning_param,
2021-08-29 19:38:42 +02:00
"LNbits initial funding: voucher redeem.",
{"tag": "lnurlwallet"},
2021-10-17 18:33:29 +01:00
5, # wait 5 seconds before sending the invoice to the service
)
)
2021-10-17 18:33:29 +01:00
return RedirectResponse(
f"/wallet?usr={user.id}&wal={wallet.id}",
2021-10-17 18:33:29 +01:00
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
@generic_router.get("/service-worker.js", response_class=FileResponse)
async def service_worker():
return FileResponse(Path("lnbits", "static", "js", "service-worker.js"))
2022-07-05 09:14:24 -06:00
@generic_router.get("/manifest/{usr}.webmanifest")
[FEAT] Push notification integration into core (#1393) * push notification integration into core added missing component fixed bell working on all pages - made pubkey global template env var - had to move `get_push_notification_pubkey` to `helpers.py` because of circular reference with `tasks.py` formay trying to fix mypy added py-vapid to requirements Trying to fix stub mypy issue * removed key files * webpush key pair is saved in db `webpush_settings` * removed lnaddress extension changes * support for multi user account subscriptions, subscriptions are stored user based fixed syntax error fixed syntax error removed unused line * fixed subscribed user storage with local storage, no get request required * method is singular now * cleanup unsubscribed or expired push subscriptions fixed flake8 errors fixed poetry errors * updating to latest lnbits formatting, rebase error fix * remove unused? * revert * relock * remove * do not create settings table use adminsettings mypy fix * cleanup old code * catch case when client tries to recreate existing webpush subscription e.g. on cleared local storage * show notification bell on user related pages only * use local storage with one key like array, some refactoring * fixed crud import * fixed too long line * removed unused imports * ruff * make webpush editable * fixed privkey encoding * fix ruff * fix migration --------- Co-authored-by: schneimi <admin@schneimi.de> Co-authored-by: schneimi <dev@schneimi.de> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-09-11 15:48:49 +02:00
async def manifest(request: Request, usr: str):
host = urlparse(str(request.url)).netloc
user = await get_user(usr)
if not user:
2021-09-11 11:02:48 +02:00
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
2021-08-20 21:31:01 +01:00
return {
"short_name": settings.lnbits_site_title,
"name": settings.lnbits_site_title + " Wallet",
2021-10-17 18:33:29 +01:00
"icons": [
{
"src": (
settings.lnbits_custom_logo
if settings.lnbits_custom_logo
else "https://cdn.jsdelivr.net/gh/lnbits/lnbits@main/docs/logos/lnbits.png"
),
2021-10-17 18:33:29 +01:00
"type": "image/png",
"sizes": "900x900",
}
],
"start_url": f"/wallet?usr={usr}&wal={user.wallets[0].id}",
"background_color": "#1F2234",
"description": "Bitcoin Lightning Wallet",
2021-10-17 18:33:29 +01:00
"display": "standalone",
"scope": "/",
"theme_color": "#1F2234",
2021-10-17 18:33:29 +01:00
"shortcuts": [
{
"name": wallet.name,
"short_name": wallet.name,
"description": wallet.name,
"url": f"/wallet?usr={usr}&wal={wallet.id}",
2021-10-17 18:33:29 +01:00
}
for wallet in user.wallets
],
[FEAT] Push notification integration into core (#1393) * push notification integration into core added missing component fixed bell working on all pages - made pubkey global template env var - had to move `get_push_notification_pubkey` to `helpers.py` because of circular reference with `tasks.py` formay trying to fix mypy added py-vapid to requirements Trying to fix stub mypy issue * removed key files * webpush key pair is saved in db `webpush_settings` * removed lnaddress extension changes * support for multi user account subscriptions, subscriptions are stored user based fixed syntax error fixed syntax error removed unused line * fixed subscribed user storage with local storage, no get request required * method is singular now * cleanup unsubscribed or expired push subscriptions fixed flake8 errors fixed poetry errors * updating to latest lnbits formatting, rebase error fix * remove unused? * revert * relock * remove * do not create settings table use adminsettings mypy fix * cleanup old code * catch case when client tries to recreate existing webpush subscription e.g. on cleared local storage * show notification bell on user related pages only * use local storage with one key like array, some refactoring * fixed crud import * fixed too long line * removed unused imports * ruff * make webpush editable * fixed privkey encoding * fix ruff * fix migration --------- Co-authored-by: schneimi <admin@schneimi.de> Co-authored-by: schneimi <dev@schneimi.de> Co-authored-by: dni ⚡ <office@dnilabs.com>
2023-09-11 15:48:49 +02:00
"url_handlers": [{"origin": f"https://{host}"}],
2021-10-17 18:33:29 +01:00
}
[FEAT] Node Managment (#1895) * [FEAT] Node Managment feat: node dashboard channels and transactions fix: update channel variables better types refactor ui add onchain balances and backend_name mock values for fake wallet remove app tab start implementing peers and channel management peer and channel management implement channel closing add channel states, better errors seperate payments and invoices on transactions tab display total channel balance feat: optional public page feat: show node address fix: port conversion feat: details dialog on transactions fix: peer info without alias fix: rename channel balances small improvements to channels tab feat: pagination on transactions tab test caching transactions refactor: move WALLET into wallets module fix: backwards compatibility refactor: move get_node_class to nodes modules post merge bundle fundle feat: disconnect peer feat: initial lnd support only use filtered channels for total balance adjust closing logic add basic node tests add setting for disabling transactions tab revert unnecessary changes add tests for invoices and payments improve payment and invoice implementations the previously used invoice fixture has a session scope, but a new invoice is required tests and bug fixes for channels api use query instead of body in channel delete delete requests should generally not use a body take node id through path instead of body for delete endpoint add peer management tests more tests for errors improve error handling rename id and pubkey to peer_id for consistency remove dead code fix http status codes make cache keys safer cache node public info comments for node settings rename node prop in frontend adjust tests to new status codes cln: use amount_msat instead of value for onchain balance turn transactions tab off by default enable transactions in tests only allow super user to create or delete fix prop name in admin navbar --------- Co-authored-by: jacksn <jkranawetter05@gmail.com>
2023-09-25 15:04:44 +02:00
@generic_router.get("/node", response_class=HTMLResponse)
async def node(request: Request, user: User = Depends(check_admin)):
if not settings.lnbits_node_ui:
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
WALLET = get_wallet_class()
_, balance = await WALLET.status()
return template_renderer().TemplateResponse(
"node/index.html",
{
"request": request,
"user": user.dict(),
"settings": settings.dict(),
"balance": balance,
"wallets": user.wallets[0].dict(),
},
)
@generic_router.get("/node/public", response_class=HTMLResponse)
async def node_public(request: Request):
if not settings.lnbits_public_node_ui:
raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE)
WALLET = get_wallet_class()
_, balance = await WALLET.status()
return template_renderer().TemplateResponse(
"node/public.html",
{
"request": request,
"settings": settings.dict(),
"balance": balance,
},
)
@generic_router.get("/admin", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_admin)):
if not settings.lnbits_admin_ui:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND)
WALLET = get_wallet_class()
_, balance = await WALLET.status()
return template_renderer().TemplateResponse(
"admin/index.html",
{
"request": request,
"user": user.dict(),
"settings": settings.dict(),
"balance": balance,
"currencies": list(currencies.keys()),
},
)
@generic_router.get("/uuidv4/{hex_value}")
async def hex_to_uuid4(hex_value: str):
try:
user_id = to_valid_user_id(hex_value).hex
return RedirectResponse(url=f"/wallet?usr={user_id}")
except Exception as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
async def toggle_extension(extension_to_enable, extension_to_disable, user_id):
if extension_to_enable and extension_to_disable:
raise HTTPException(
HTTPStatus.BAD_REQUEST, "You can either `enable` or `disable` an extension."
)
# check if extension exists
if extension_to_enable or extension_to_disable:
ext = extension_to_enable or extension_to_disable
if ext not in [e.code for e in get_valid_extensions()]:
raise HTTPException(
HTTPStatus.BAD_REQUEST, f"Extension '{ext}' doesn't exist."
)
if extension_to_enable:
logger.info(f"Enabling extension: {extension_to_enable} for user {user_id}")
await update_user_extension(
user_id=user_id, extension=extension_to_enable, active=True
)
elif extension_to_disable:
logger.info(f"Disabling extension: {extension_to_disable} for user {user_id}")
await update_user_extension(
user_id=user_id, extension=extension_to_disable, active=False
)