Merge branch 'nostr' into diagon-alley

This commit is contained in:
benarc 2022-02-10 11:42:06 +00:00
commit 62f5376c11
15 changed files with 814 additions and 15 deletions

View file

@ -5,11 +5,15 @@ QUART_DEBUG=true
HOST=127.0.0.1
PORT=5000
LNBITS_ALLOWED_USERS=""
LNBITS_ADMIN_USERS=""
# Extensions only admin can access
LNBITS_ADMIN_EXTENSIONS="nostradmin"
LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
# Disable extensions for all users, use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk"
# Database: to use SQLite, specify LNBITS_DATA_FOLDER
# to use PostgreSQL, specify LNBITS_DATABASE_URL=postgres://...
# to use CockroachDB, specify LNBITS_DATABASE_URL=cockroachdb://...
@ -18,8 +22,6 @@ LNBITS_DEFAULT_WALLET_NAME="LNbits wallet"
LNBITS_DATA_FOLDER="./data"
# LNBITS_DATABASE_URL="postgres://user:password@host:port/databasename"
# disable selected extensions, or use "all" to disable all extensions
LNBITS_DISABLED_EXTENSIONS="amilk,ngrok"
LNBITS_FORCE_HTTPS=true
LNBITS_SERVICE_FEE="0.0"

View file

@ -6,7 +6,7 @@ from urllib.parse import urlparse
from lnbits import bolt11
from lnbits.db import Connection, POSTGRES, COCKROACH
from lnbits.settings import DEFAULT_WALLET_NAME
from lnbits.settings import DEFAULT_WALLET_NAME, LNBITS_ADMIN_USERS
from . import db
from .models import User, Wallet, Payment, BalanceCheck
@ -61,6 +61,7 @@ async def get_user(user_id: str, conn: Optional[Connection] = None) -> Optional[
email=user["email"],
extensions=[e[0] for e in extensions],
wallets=[Wallet(**w) for w in wallets],
admin=LNBITS_ADMIN_USERS and user["id"] in [x.strip() for x in LNBITS_ADMIN_USERS]
)

View file

@ -13,7 +13,7 @@ from starlette.requests import Request
from lnbits.core.crud import get_user, get_wallet_for_key
from lnbits.core.models import User, Wallet
from lnbits.requestvars import g
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS
from lnbits.settings import LNBITS_ALLOWED_USERS, LNBITS_ADMIN_USERS, LNBITS_ADMIN_EXTENSIONS
class KeyChecker(SecurityBase):
@ -122,6 +122,7 @@ async def get_key_type(
# 0: admin
# 1: invoice
# 2: invalid
pathname = r['path'].split('/')[1]
if not api_key_header and not api_key_query:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST)
@ -131,7 +132,10 @@ async def get_key_type(
try:
checker = WalletAdminKeyChecker(api_key=token)
await checker.__call__(r)
return WalletTypeInfo(0, checker.wallet)
wallet = WalletTypeInfo(0, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
return wallet
except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST:
raise
@ -143,7 +147,10 @@ async def get_key_type(
try:
checker = WalletInvoiceKeyChecker(api_key=token)
await checker.__call__(r)
return WalletTypeInfo(1, checker.wallet)
wallet = WalletTypeInfo(0, checker.wallet)
if (LNBITS_ADMIN_USERS and wallet.wallet.user not in LNBITS_ADMIN_USERS) and (LNBITS_ADMIN_EXTENSIONS and pathname in LNBITS_ADMIN_EXTENSIONS):
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized.")
return wallet
except HTTPException as e:
if e.status_code == HTTPStatus.BAD_REQUEST:
raise

View file

@ -0,0 +1,3 @@
# Nostr
Opens a Nostr daemon

View file

@ -0,0 +1,16 @@
from fastapi import APIRouter
from lnbits.db import Database
from lnbits.helpers import template_renderer
db = Database("ext_nostradmin")
nostradmin_ext: APIRouter = APIRouter(prefix="/nostradmin", tags=["nostradmin"])
def nostr_renderer():
return template_renderer(["lnbits/extensions/nostradmin/templates"])
from .views import * # noqa
from .views_api import * # noqa

View file

@ -0,0 +1,6 @@
{
"name": "NostrAdmin",
"short_description": "Admin daemon for Nostr",
"icon": "swap_horizontal_circle",
"contributors": ["arcbtc"]
}

View file

@ -0,0 +1,146 @@
from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
import shortuuid
from . import db
from .models import (
nostrKeys,
nostrNotes,
nostrCreateRelays,
nostrRelays,
nostrConnections,
nostrCreateConnections,
nostrRelayList,
)
from .models import nostrKeys, nostrCreateRelays, nostrRelaySetList
###############KEYS##################
async def create_nostrkeys(data: nostrKeys) -> nostrKeys:
await db.execute(
"""
INSERT INTO nostradmin.keys (
pubkey,
privkey
)
VALUES (?, ?)
""",
(data.pubkey, data.privkey),
)
return await get_nostrkeys(nostrkey_id)
async def get_nostrkeys(pubkey: str) -> nostrKeys:
row = await db.fetchone("SELECT * FROM nostradmin.keys WHERE pubkey = ?", (pubkey,))
return nostrKeys(**row) if row else None
###############NOTES##################
async def create_nostrnotes(data: nostrNotes) -> nostrNotes:
await db.execute(
"""
INSERT INTO nostradmin.notes (
id,
pubkey,
created_at,
kind,
tags,
content,
sig
)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
data.id,
data.pubkey,
data.created_at,
data.kind,
data.tags,
data.content,
data.sig,
),
)
return await get_nostrnotes(data.id)
async def get_nostrnotes(nostrnote_id: str) -> nostrNotes:
row = await db.fetchone("SELECT * FROM nostradmin.notes WHERE id = ?", (nostrnote_id,))
return nostrNotes(**row) if row else None
###############RELAYS##################
async def create_nostrrelays(data: nostrCreateRelays) -> nostrCreateRelays:
nostrrelay_id = shortuuid.uuid(name=data.relay)
if await get_nostrrelay(nostrrelay_id):
return "error"
await db.execute(
"""
INSERT INTO nostradmin.relays (
id,
relay
)
VALUES (?, ?)
""",
(nostrrelay_id, data.relay),
)
return await get_nostrrelay(nostrrelay_id)
async def get_nostrrelays() -> nostrRelays:
rows = await db.fetchall("SELECT * FROM nostradmin.relays")
return [nostrRelays(**row) for row in rows]
async def get_nostrrelay(nostrrelay_id: str) -> nostrRelays:
row = await db.fetchone("SELECT * FROM nostradmin.relays WHERE id = ?", (nostrrelay_id,))
return nostrRelays(**row) if row else None
async def update_nostrrelaysetlist(data: nostrRelaySetList) -> nostrRelayList:
await db.execute(
"""
UPDATE nostradmin.relaylists SET
denylist = ?,
allowlist = ?
WHERE id = ?
""",
(data.denylist, data.allowlist, 1),
)
return await get_nostrrelaylist()
async def get_nostrrelaylist() -> nostrRelayList:
row = await db.fetchone("SELECT * FROM nostradmin.relaylists WHERE id = ?", (1,))
return nostrRelayList(**row) if row else None
###############CONNECTIONS##################
async def create_nostrconnections(
data: nostrCreateConnections
) -> nostrCreateConnections:
nostrrelay_id = shortuuid.uuid(name=data.relayid + data.pubkey)
await db.execute(
"""
INSERT INTO nostradmin.connections (
id,
pubkey,
relayid
)
VALUES (?, ?, ?)
""",
(data.id, data.pubkey, data.relayid),
)
return await get_nostrconnections(data.id)
async def get_nostrconnections(nostrconnections_id: str) -> nostrConnections:
row = await db.fetchone(
"SELECT * FROM nostradmin.connections WHERE id = ?", (nostrconnections_id,)
)
return nostrConnections(**row) if row else None

View file

@ -0,0 +1,74 @@
from lnbits.db import Database
async def m001_initial(db):
"""
Initial nostradmin table.
"""
await db.execute(
f"""
CREATE TABLE nostradmin.keys (
pubkey TEXT NOT NULL PRIMARY KEY,
privkey TEXT NOT NULL
);
"""
)
await db.execute(
f"""
CREATE TABLE nostradmin.notes (
id TEXT NOT NULL PRIMARY KEY,
pubkey TEXT NOT NULL,
created_at TEXT NOT NULL,
kind INT NOT NULL,
tags TEXT NOT NULL,
content TEXT NOT NULL,
sig TEXT NOT NULL
);
"""
)
await db.execute(
f"""
CREATE TABLE nostradmin.relays (
id TEXT NOT NULL PRIMARY KEY,
relay TEXT NOT NULL
);
"""
)
await db.execute(
f"""
CREATE TABLE nostradmin.relaylists (
id TEXT NOT NULL PRIMARY KEY DEFAULT 1,
allowlist TEXT,
denylist TEXT
);
"""
)
await db.execute(
"""
INSERT INTO nostradmin.relaylists (
id,
denylist
)
VALUES (?, ?)
""",
("1", "wss://zucks-meta-relay.com\nwss://nostr.cia.gov",),
)
await db.execute(
f"""
CREATE TABLE nostradmin.connections (
id TEXT NOT NULL PRIMARY KEY,
publickey TEXT NOT NULL,
relayid TEXT NOT NULL
);
"""
)
await db.execute(
f"""
CREATE TABLE nostradmin.subscribed (
id TEXT NOT NULL PRIMARY KEY,
userPubkey TEXT NOT NULL,
subscribedPubkey TEXT NOT NULL
);
"""
)

View file

@ -0,0 +1,52 @@
import json
from sqlite3 import Row
from typing import Optional
from fastapi import Request
from pydantic import BaseModel
from pydantic.main import BaseModel
from fastapi.param_functions import Query
class nostrKeys(BaseModel):
pubkey: str
privkey: str
class nostrNotes(BaseModel):
id: str
pubkey: str
created_at: str
kind: int
tags: str
content: str
sig: str
class nostrCreateRelays(BaseModel):
relay: str = Query(None)
class nostrCreateConnections(BaseModel):
pubkey: str = Query(None)
relayid: str = Query(None)
class nostrRelays(BaseModel):
id: Optional[str]
relay: Optional[str]
status: Optional[bool] = False
class nostrRelayList(BaseModel):
id: str
allowlist: Optional[str]
denylist: Optional[str]
class nostrRelaySetList(BaseModel):
allowlist: Optional[str]
denylist: Optional[str]
class nostrConnections(BaseModel):
id: str
pubkey: Optional[str]
relayid: Optional[str]
class nostrSubscriptions(BaseModel):
id: str
userPubkey: Optional[str]
subscribedPubkey: Optional[str]

View file

@ -0,0 +1,303 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md">
<div class="col-12 col-md-7 q-gutter-y-md">
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">NOSTR RELAYS ONLINE</h5>
</div>
<div class="col-auto">
<q-input
borderless
dense
debounce="300"
v-model="filter"
placeholder="Search"
>
<template v-slot:append>
<q-icon name="search"></q-icon>
</template>
</q-input>
<q-btn flat color="grey" @click="exportlnurldeviceCSV"
>Export to CSV</q-btn
>
</div>
</div>
<q-table
flat
dense
:data="nostrrelayLinks"
row-key="id"
:columns="nostrTable.columns"
:pagination.sync="nostrTable.pagination"
:filter="filter"
>
{% raw %}
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>{{ col.label }}</div>
</q-th>
<!-- <q-th auto-width></q-th> -->
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td
v-for="col in props.cols"
:key="col.name"
:props="props"
auto-width
>
<div v-if="col.name == 'id'"></div>
<div v-else>
<div v-if="col.value == true" style="background-color: green">
{{ col.value }}
</div>
<div v-else>{{ col.value }}</div>
</div>
</q-td>
</q-tr>
</template>
{% endraw %}
</q-table>
</q-card-section>
</q-card>
<q-card>
<q-tabs
v-model="listSelection"
dense
class="text-grey"
active-color="primary"
indicator-color="primary"
align="justify"
narrow-indicator
>
<q-tab name="denylist" label="Deny List"></q-tab>
<q-tab name="allowlist" label="Allow List"></q-tab>
</q-tabs>
<q-separator></q-separator>
<q-tab-panels v-model="listSelection" animated>
<q-tab-panel name="denylist">
<div class="text-h6">Relays in this list will NOT be used</div>
<q-form class="q-gutter-md q-y-md" @submit="setRelayList">
<div class="row">
<div class="col q-mx-lg">
<q-input
v-model="setList.denylist"
dense
filled
autogrow
></q-input>
</div>
<div
class="col q-mx-lg items-align flex items-center justify-center"
>
<q-btn unelevated color="primary" type="submit">
Update Deny List
</q-btn>
</div>
</div>
</q-form>
</q-tab-panel>
<q-tab-panel name="allowlist">
<div class="text-h6">ONLY relays in this list will be used</div>
<q-form class="q-gutter-md q-y-md" @submit="setRelayList">
<div class="row">
<div class="col q-mx-lg">
<q-input
v-model="setList.allowlist"
dense
filled
autogrow
></q-input>
</div>
<div
class="col q-mx-lg items-align flex items-center justify-center"
>
<q-btn unelevated color="primary" type="submit">
Update Allow List
</q-btn>
</div>
</div>
</q-form>
</q-tab-panel>
</q-tab-panels>
</q-card>
</div>
<div class="col-12 col-md-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Nostr Extension</h6>
<p>Only Admin users can manage this extension</p>
<q-card-section></q-card-section>
</q-card-section>
</q-card>
</div>
</div>
{% endblock %} {% block scripts %} {{ window_vars(user) }}
<script>
Vue.component(VueQrcode.name, VueQrcode)
var maplrelays = obj => {
obj._data = _.clone(obj)
obj.theTime = obj.time * 60 - (Date.now() / 1000 - obj.timestamp)
obj.time = obj.time + 'mins'
if (obj.time_elapsed) {
obj.date = 'Time elapsed'
} else {
obj.date = Quasar.utils.date.formatDate(
new Date((obj.theTime - 3600) * 1000),
'HH:mm:ss'
)
}
return obj
}
new Vue({
el: '#vue',
mixins: [windowMixin],
data: function () {
return {
listSelection: 'denylist',
setList: {
allowlist: '',
denylist: ''
},
nostrrelayLinks: [],
filter: '',
nostrTable: {
columns: [
{
name: 'wah',
align: 'left',
label: 'id',
field: 'id'
},
{
name: 'relay',
align: 'left',
label: 'relay',
field: 'relay'
},
{
name: 'status',
align: 'left',
label: 'status',
field: 'status'
}
],
pagination: {
rowsPerPage: 10
}
}
}
},
methods: {
getRelays: function () {
var self = this
LNbits.api
.request(
'GET',
'/nostradmin/api/v1/relays',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
if (response.data) {
console.log(response.data)
response.data.map(maplrelays)
self.nostrrelayLinks = response.data
console.log(self.nostrrelayLinks)
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
setAllowList: function () {
var self = this
LNbits.api
.request(
'POST',
'/nostradmin/api/v1/allowlist',
self.g.user.wallets[0].adminkey,
self.allowList
)
.then(function (response) {
if (response.data) {
self.allowList = response.data
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
setRelayList: function () {
var self = this
console.log(self.setList)
LNbits.api
.request(
'POST',
'/nostradmin/api/v1/setlist',
self.g.user.wallets[0].adminkey,
self.setList
)
.then(function (response) {
if (response.data) {
console.log(response.data)
// self.denyList = response.data
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
getLists: function () {
var self = this
LNbits.api
.request(
'GET',
'/nostradmin/api/v1/relaylist',
self.g.user.wallets[0].adminkey
)
.then(function (response) {
if (response.data) {
console.log(response.data)
self.setList.denylist = response.data.denylist
self.setList.allowlist = response.data.allowlist
}
})
.catch(function (error) {
LNbits.utils.notifyApiError(error)
})
},
exportlnurldeviceCSV: function () {
var self = this
LNbits.utils.exportCSV(self.nostrTable.columns, this.nostrLinks)
}
},
created: function () {
var self = this
this.getRelays()
this.getLists()
}
})
</script>
{% endblock %}

View file

@ -0,0 +1,102 @@
from http import HTTPStatus
import asyncio
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from . import nostradmin_ext, nostr_renderer
# FastAPI good for incoming
from fastapi import Request, WebSocket, WebSocketDisconnect
# Websockets needed for outgoing
import websockets
from lnbits.core.crud import update_payment_status
from lnbits.core.models import User
from lnbits.core.views.api import api_payment
from lnbits.decorators import check_user_exists
from .crud import get_nostrkeys, get_nostrrelay
templates = Jinja2Templates(directory="templates")
@nostradmin_ext.get("/", response_class=HTMLResponse)
async def index(request: Request, user: User = Depends(check_user_exists)):
return nostr_renderer().TemplateResponse(
"nostradmin/index.html", {"request": request, "user": user.dict()}
)
#####################################################################
#################### NOSTR WEBSOCKET THREAD #########################
##### THE QUEUE LOOP THREAD THING THAT LISTENS TO BUNCH OF ##########
### WEBSOCKET CONNECTIONS, STORING DATA IN DB/PUSHING TO FRONTEND ###
################### VIA updater() FUNCTION ##########################
#####################################################################
websocket_queue = asyncio.Queue(1000)
# while True:
async def nostr_subscribe():
return
# for the relays:
# async with websockets.connect("ws://localhost:8765") as websocket:
# for the public keys:
# await websocket.send("subscribe to events")
# await websocket.recv()
#####################################################################
################### LNBITS WEBSOCKET ROUTES #########################
#### HERE IS WHERE LNBITS FRONTEND CAN RECEIVE AND SEND MESSAGES ####
#####################################################################
class ConnectionManager:
def __init__(self):
self.active_connections: List[WebSocket] = []
async def connect(self, websocket: WebSocket, nostr_id: str):
await websocket.accept()
websocket.id = nostr_id
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def send_personal_message(self, message: str, nostr_id: str):
for connection in self.active_connections:
if connection.id == nostr_id:
await connection.send_text(message)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@nostradmin_ext.websocket("/nostradmin/ws/relayevents/{nostr_id}", name="nostr_id.websocket_by_id")
async def websocket_endpoint(websocket: WebSocket, nostr_id: str):
await manager.connect(websocket, nostr_id)
try:
while True:
data = await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(websocket)
async def updater(nostr_id, message):
copilot = await get_copilot(nostr_id)
if not copilot:
return
await manager.send_personal_message(f"{message}", nostr_id)
async def relay_check(relay: str):
async with websockets.connect(relay) as websocket:
if str(websocket.state) == "State.OPEN":
print(str(websocket.state))
return True
else:
return False

View file

@ -0,0 +1,63 @@
from http import HTTPStatus
import asyncio
from fastapi import Request
from fastapi.param_functions import Query
from fastapi.params import Depends
from starlette.exceptions import HTTPException
from lnbits.core.crud import get_user
from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key
from lnbits.utils.exchange_rates import currencies
from lnbits.settings import LNBITS_ADMIN_USERS
from . import nostradmin_ext
from .crud import (
create_nostrkeys,
get_nostrkeys,
create_nostrnotes,
get_nostrnotes,
create_nostrrelays,
get_nostrrelays,
get_nostrrelaylist,
update_nostrrelaysetlist,
create_nostrconnections,
get_nostrconnections,
)
from .models import nostrKeys, nostrCreateRelays, nostrRelaySetList
from .views import relay_check
@nostradmin_ext.get("/api/v1/relays")
async def api_relays_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)):
relays = await get_nostrrelays()
if not relays:
await create_nostrrelays(nostrCreateRelays(relay="wss://relayer.fiatjaf.com"))
await create_nostrrelays(
nostrCreateRelays(relay="wss://nostr-pub.wellorder.net")
)
relays = await get_nostrrelays()
if not relays:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
else:
for relay in relays:
relay.status = await relay_check(relay.relay)
return relays
@nostradmin_ext.get("/api/v1/relaylist")
async def api_relaylist(wallet: WalletTypeInfo = Depends(get_key_type)):
if wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
return await get_nostrrelaylist()
@nostradmin_ext.post("/api/v1/setlist")
async def api_relayssetlist(data: nostrRelaySetList, wallet: WalletTypeInfo = Depends(get_key_type)):
if wallet.wallet.user not in LNBITS_ADMIN_USERS:
raise HTTPException(
status_code=HTTPStatus.UNAUTHORIZED, detail="User not authorized."
)
return await update_nostrrelaysetlist(data)

View file

@ -15,6 +15,7 @@ import lnbits.settings as settings
class Extension(NamedTuple):
code: str
is_valid: bool
is_admin_only: bool
name: Optional[str] = None
short_description: Optional[str] = None
icon: Optional[str] = None
@ -25,6 +26,7 @@ class Extension(NamedTuple):
class ExtensionManager:
def __init__(self):
self._disabled: List[str] = settings.LNBITS_DISABLED_EXTENSIONS
self._admin_only: List[str] = [x.strip(' ') for x in settings.LNBITS_ADMIN_EXTENSIONS]
self._extension_folders: List[str] = [
x[1] for x in os.walk(os.path.join(settings.LNBITS_PATH, "extensions"))
][0]
@ -47,14 +49,17 @@ class ExtensionManager:
) as json_file:
config = json.load(json_file)
is_valid = True
is_admin_only = True if extension in self._admin_only else False
except Exception:
config = {}
is_valid = False
is_admin_only = False
output.append(
Extension(
extension,
is_valid,
is_admin_only,
config.get("name"),
config.get("short_description"),
config.get("icon"),

View file

@ -29,6 +29,7 @@ LNBITS_ALLOWED_USERS: List[str] = env.list(
"LNBITS_ALLOWED_USERS", default=[], subcast=str
)
LNBITS_ADMIN_USERS: List[str] = env.list("LNBITS_ADMIN_USERS", default=[], subcast=str)
LNBITS_ADMIN_EXTENSIONS: List[str] = env.list("LNBITS_ADMIN_EXTENSIONS", default=[], subcast=str)
LNBITS_DISABLED_EXTENSIONS: List[str] = env.list(
"LNBITS_DISABLED_EXTENSIONS", default=[], subcast=str
)

View file

@ -111,7 +111,7 @@ window.LNbits = {
'/wallet?' + (userId ? 'usr=' + userId + '&' : '') + 'nme=' + walletName
},
updateWallet: function (walletName, userId, walletId) {
window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}`
window.location.href = `/wallet?usr=${userId}&wal=${walletId}&nme=${walletName}`
},
deleteWallet: function (walletId, userId) {
window.location.href = '/deletewallet?usr=' + userId + '&wal=' + walletId
@ -123,6 +123,7 @@ window.LNbits = {
[
'code',
'isValid',
'isAdminOnly',
'name',
'shortDescription',
'icon',
@ -135,7 +136,12 @@ window.LNbits = {
return obj
},
user: function (data) {
var obj = {id: data.id, email: data.email, extensions: data.extensions, wallets: data.wallets}
var obj = {
id: data.id,
email: data.email,
extensions: data.extensions,
wallets: data.wallets
}
var mapWallet = this.wallet
obj.wallets = obj.wallets
.map(function (obj) {
@ -153,16 +159,23 @@ window.LNbits = {
return obj
},
wallet: function (data) {
newWallet = {id: data.id, name: data.name, adminkey: data.adminkey, inkey: data.inkey}
newWallet = {
id: data.id,
name: data.name,
adminkey: data.adminkey,
inkey: data.inkey
}
newWallet.msat = data.balance_msat
newWallet.sat = Math.round(data.balance_msat / 1000)
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(newWallet.sat)
newWallet.fsat = new Intl.NumberFormat(window.LOCALE).format(
newWallet.sat
)
newWallet.url = ['/wallet?usr=', data.user, '&wal=', data.id].join('')
return newWallet
},
payment: function (data) {
obj = {
checking_id:data.id,
checking_id: data.id,
pending: data.pending,
amount: data.amount,
fee: data.fee,
@ -174,8 +187,8 @@ window.LNbits = {
extra: data.extra,
wallet_id: data.wallet_id,
webhook: data.webhook,
webhook_status: data.webhook_status,
}
webhook_status: data.webhook_status
}
obj.date = Quasar.utils.date.formatDate(
new Date(obj.time * 1000),
@ -225,7 +238,8 @@ window.LNbits = {
Quasar.plugins.Notify.create({
timeout: 5000,
type: types[error.response.status] || 'warning',
message: error.response.data.message || error.response.data.detail || null,
message:
error.response.data.message || error.response.data.detail || null,
caption:
[error.response.status, ' ', error.response.statusText]
.join('')
@ -368,6 +382,10 @@ window.windowMixin = {
.filter(function (obj) {
return !obj.hidden
})
.filter(function (obj) {
if (window.user.admin) return obj
return !obj.isAdminOnly
})
.map(function (obj) {
if (user) {
obj.isEnabled = user.extensions.indexOf(obj.code) !== -1