mirror of
https://github.com/lnbits/lnbits-legend.git
synced 2025-02-26 15:42:30 +01:00
Merge branch 'nostr' into diagon-alley
This commit is contained in:
commit
62f5376c11
15 changed files with 814 additions and 15 deletions
|
@ -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"
|
||||
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
3
lnbits/extensions/nostradmin/README.md
Normal file
3
lnbits/extensions/nostradmin/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Nostr
|
||||
|
||||
Opens a Nostr daemon
|
16
lnbits/extensions/nostradmin/__init__.py
Normal file
16
lnbits/extensions/nostradmin/__init__.py
Normal 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
|
6
lnbits/extensions/nostradmin/config.json
Normal file
6
lnbits/extensions/nostradmin/config.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "NostrAdmin",
|
||||
"short_description": "Admin daemon for Nostr",
|
||||
"icon": "swap_horizontal_circle",
|
||||
"contributors": ["arcbtc"]
|
||||
}
|
146
lnbits/extensions/nostradmin/crud.py
Normal file
146
lnbits/extensions/nostradmin/crud.py
Normal 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
|
74
lnbits/extensions/nostradmin/migrations.py
Normal file
74
lnbits/extensions/nostradmin/migrations.py
Normal 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
|
||||
);
|
||||
"""
|
||||
)
|
52
lnbits/extensions/nostradmin/models.py
Normal file
52
lnbits/extensions/nostradmin/models.py
Normal 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]
|
303
lnbits/extensions/nostradmin/templates/nostradmin/index.html
Normal file
303
lnbits/extensions/nostradmin/templates/nostradmin/index.html
Normal 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 %}
|
102
lnbits/extensions/nostradmin/views.py
Normal file
102
lnbits/extensions/nostradmin/views.py
Normal 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
|
63
lnbits/extensions/nostradmin/views_api.py
Normal file
63
lnbits/extensions/nostradmin/views_api.py
Normal 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)
|
|
@ -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"),
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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,7 +187,7 @@ 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(
|
||||
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue