improve frontend, no reloading. types in views_api, formatting

This commit is contained in:
dni ⚡ 2023-03-19 10:04:06 +01:00
parent 802d01cc4b
commit bb1941445d
No known key found for this signature in database
GPG key ID: 886317704CC4E618
19 changed files with 134 additions and 119 deletions

View file

@ -1,9 +1,8 @@
from fastapi import APIRouter from fastapi import APIRouter
from starlette.staticfiles import StaticFiles
from lnbits.db import Database from lnbits.db import Database
from lnbits.helpers import template_renderer from lnbits.helpers import template_renderer
from lnbits.tasks import catch_everything_and_restart from lnbits.tasks import catch_everything_and_restart
from starlette.staticfiles import StaticFiles
db = Database("ext_nostrclient") db = Database("ext_nostrclient")
@ -22,11 +21,10 @@ def nostr_renderer():
return template_renderer(["lnbits/extensions/nostrclient/templates"]) return template_renderer(["lnbits/extensions/nostrclient/templates"])
from .tasks import init_relays, subscribe_events
from .views import * # noqa from .views import * # noqa
from .views_api import * # noqa from .views_api import * # noqa
from .tasks import init_relays, subscribe_events
def nostrclient_start(): def nostrclient_start():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()

View file

@ -1,7 +1,8 @@
from typing import List, Optional, Union from typing import List, Optional, Union
from lnbits.helpers import urlsafe_short_hash
import shortuuid import shortuuid
from lnbits.helpers import urlsafe_short_hash
from . import db from . import db
from .models import Relay, RelayList from .models import Relay, RelayList

View file

@ -1,12 +1,10 @@
from typing import List, Dict from dataclasses import dataclass
from typing import Optional from typing import Dict, List, Optional
from fastapi import Request from fastapi import Request
from pydantic import BaseModel, Field
from fastapi.param_functions import Query from fastapi.param_functions import Query
from dataclasses import dataclass
from lnbits.helpers import urlsafe_short_hash from lnbits.helpers import urlsafe_short_hash
from pydantic import BaseModel, Field
class Relay(BaseModel): class Relay(BaseModel):

View file

@ -23,21 +23,25 @@
from enum import Enum from enum import Enum
class Encoding(Enum): class Encoding(Enum):
"""Enumeration type to list the various supported encodings.""" """Enumeration type to list the various supported encodings."""
BECH32 = 1 BECH32 = 1
BECH32M = 2 BECH32M = 2
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
BECH32M_CONST = 0x2bc830a3 BECH32M_CONST = 0x2BC830A3
def bech32_polymod(values): def bech32_polymod(values):
"""Internal function that computes the Bech32 checksum.""" """Internal function that computes the Bech32 checksum."""
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] generator = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
chk = 1 chk = 1
for value in values: for value in values:
top = chk >> 25 top = chk >> 25
chk = (chk & 0x1ffffff) << 5 ^ value chk = (chk & 0x1FFFFFF) << 5 ^ value
for i in range(5): for i in range(5):
chk ^= generator[i] if ((top >> i) & 1) else 0 chk ^= generator[i] if ((top >> i) & 1) else 0
return chk return chk
@ -57,6 +61,7 @@ def bech32_verify_checksum(hrp, data):
return Encoding.BECH32M return Encoding.BECH32M
return None return None
def bech32_create_checksum(hrp, data, spec): def bech32_create_checksum(hrp, data, spec):
"""Compute the checksum values given HRP and data.""" """Compute the checksum values given HRP and data."""
values = bech32_hrp_expand(hrp) + data values = bech32_hrp_expand(hrp) + data
@ -68,26 +73,29 @@ def bech32_create_checksum(hrp, data, spec):
def bech32_encode(hrp, data, spec): def bech32_encode(hrp, data, spec):
"""Compute a Bech32 string given HRP and data values.""" """Compute a Bech32 string given HRP and data values."""
combined = data + bech32_create_checksum(hrp, data, spec) combined = data + bech32_create_checksum(hrp, data, spec)
return hrp + '1' + ''.join([CHARSET[d] for d in combined]) return hrp + "1" + "".join([CHARSET[d] for d in combined])
def bech32_decode(bech): def bech32_decode(bech):
"""Validate a Bech32/Bech32m string, and determine HRP and data.""" """Validate a Bech32/Bech32m string, and determine HRP and data."""
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or if (any(ord(x) < 33 or ord(x) > 126 for x in bech)) or (
(bech.lower() != bech and bech.upper() != bech)): bech.lower() != bech and bech.upper() != bech
):
return (None, None, None) return (None, None, None)
bech = bech.lower() bech = bech.lower()
pos = bech.rfind('1') pos = bech.rfind("1")
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
return (None, None, None) return (None, None, None)
if not all(x in CHARSET for x in bech[pos+1:]): if not all(x in CHARSET for x in bech[pos + 1 :]):
return (None, None, None) return (None, None, None)
hrp = bech[:pos] hrp = bech[:pos]
data = [CHARSET.find(x) for x in bech[pos+1:]] data = [CHARSET.find(x) for x in bech[pos + 1 :]]
spec = bech32_verify_checksum(hrp, data) spec = bech32_verify_checksum(hrp, data)
if spec is None: if spec is None:
return (None, None, None) return (None, None, None)
return (hrp, data[:-6], spec) return (hrp, data[:-6], spec)
def convertbits(data, frombits, tobits, pad=True): def convertbits(data, frombits, tobits, pad=True):
"""General power-of-2 base conversion.""" """General power-of-2 base conversion."""
acc = 0 acc = 0
@ -123,7 +131,12 @@ def decode(hrp, addr):
return (None, None) return (None, None)
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32:
return (None, None) return (None, None)
if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: if (
data[0] == 0
and spec != Encoding.BECH32
or data[0] != 0
and spec != Encoding.BECH32M
):
return (None, None) return (None, None)
return (data[0], decoded) return (data[0], decoded)

View file

@ -1,4 +1,3 @@
from Cryptodome import Random from Cryptodome import Random
from Cryptodome.Cipher import AES from Cryptodome.Cipher import AES
@ -11,10 +10,10 @@ key = bytes.fromhex("3aa925cb69eb613e2928f8a18279c78b1dca04541dfd064df2eda66b598
BLOCK_SIZE = 16 BLOCK_SIZE = 16
class AESCipher(object):
"""This class is compatible with crypto.createCipheriv('aes-256-cbc')
""" class AESCipher(object):
"""This class is compatible with crypto.createCipheriv('aes-256-cbc')"""
def __init__(self, key=None): def __init__(self, key=None):
self.key = key self.key = key
@ -33,9 +32,10 @@ class AESCipher(object):
def decrypt(self, iv, enc_text): def decrypt(self, iv, enc_text):
cipher = AES.new(self.key, AES.MODE_CBC, iv=iv) cipher = AES.new(self.key, AES.MODE_CBC, iv=iv)
return self.unpad(cipher.decrypt(enc_text).decode("UTF-8")) return self.unpad(cipher.decrypt(enc_text).decode("UTF-8"))
if __name__ == "__main__": if __name__ == "__main__":
aes = AESCipher(key=key) aes = AESCipher(key=key)
iv, enc_text = aes.encrypt(plain_text) iv, enc_text = aes.encrypt(plain_text)
dec_text = aes.decrypt(iv, enc_text) dec_text = aes.decrypt(iv, enc_text)
print(dec_text) print(dec_text)

View file

@ -1,20 +1,15 @@
from typing import * import base64
import ssl
import time
import json import json
import os import os
import base64 import ssl
import time
from ..event import Event from typing import *
from ..relay_manager import RelayManager
from ..message_type import ClientMessageType
from ..key import PrivateKey, PublicKey
from ..event import EncryptedDirectMessage, Event, EventKind
from ..filter import Filter, Filters from ..filter import Filter, Filters
from ..event import Event, EventKind, EncryptedDirectMessage from ..key import PrivateKey, PublicKey
from ..relay_manager import RelayManager
from ..message_type import ClientMessageType from ..message_type import ClientMessageType
from ..relay_manager import RelayManager
# from aes import AESCipher # from aes import AESCipher
from . import cbc from . import cbc

View file

@ -7,23 +7,23 @@ class Delegation:
delegator_pubkey: str delegator_pubkey: str
delegatee_pubkey: str delegatee_pubkey: str
event_kind: int event_kind: int
duration_secs: int = 30*24*60 # default to 30 days duration_secs: int = 30 * 24 * 60 # default to 30 days
signature: str = None # set in PrivateKey.sign_delegation signature: str = None # set in PrivateKey.sign_delegation
@property @property
def expires(self) -> int: def expires(self) -> int:
return int(time.time()) + self.duration_secs return int(time.time()) + self.duration_secs
@property @property
def conditions(self) -> str: def conditions(self) -> str:
return f"kind={self.event_kind}&created_at<{self.expires}" return f"kind={self.event_kind}&created_at<{self.expires}"
@property @property
def delegation_token(self) -> str: def delegation_token(self) -> str:
return f"nostr:delegation:{self.delegatee_pubkey}:{self.conditions}" return f"nostr:delegation:{self.delegatee_pubkey}:{self.conditions}"
def get_tag(self) -> list[str]: def get_tag(self) -> list[str]:
""" Called by Event """ """Called by Event"""
return [ return [
"delegation", "delegation",
self.delegator_pubkey, self.delegator_pubkey,

View file

@ -1,10 +1,11 @@
import time
import json import json
import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import IntEnum from enum import IntEnum
from typing import List
from secp256k1 import PublicKey
from hashlib import sha256 from hashlib import sha256
from typing import List
from secp256k1 import PublicKey
from .message_type import ClientMessageType from .message_type import ClientMessageType

View file

@ -1,14 +1,15 @@
import secrets
import base64 import base64
import secp256k1 import secrets
from cffi import FFI
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from hashlib import sha256 from hashlib import sha256
import secp256k1
from cffi import FFI
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from . import bech32
from .delegation import Delegation from .delegation import Delegation
from .event import EncryptedDirectMessage, Event, EventKind from .event import EncryptedDirectMessage, Event, EventKind
from . import bech32
class PublicKey: class PublicKey:

View file

@ -1,8 +1,9 @@
import json import json
from queue import Queue from queue import Queue
from threading import Lock from threading import Lock
from .message_type import RelayMessageType
from .event import Event from .event import Event
from .message_type import RelayMessageType
class EventMessage: class EventMessage:

View file

@ -3,6 +3,7 @@ class ClientMessageType:
REQUEST = "REQ" REQUEST = "REQ"
CLOSE = "CLOSE" CLOSE = "CLOSE"
class RelayMessageType: class RelayMessageType:
EVENT = "EVENT" EVENT = "EVENT"
NOTICE = "NOTICE" NOTICE = "NOTICE"
@ -10,6 +11,10 @@ class RelayMessageType:
@staticmethod @staticmethod
def is_valid(type: str) -> bool: def is_valid(type: str) -> bool:
if type == RelayMessageType.EVENT or type == RelayMessageType.NOTICE or type == RelayMessageType.END_OF_STORED_EVENTS: if (
type == RelayMessageType.EVENT
or type == RelayMessageType.NOTICE
or type == RelayMessageType.END_OF_STORED_EVENTS
):
return True return True
return False return False

View file

@ -1,7 +1,9 @@
import time import time
from .event import Event from .event import Event
from .key import PrivateKey from .key import PrivateKey
def zero_bits(b: int) -> int: def zero_bits(b: int) -> int:
n = 0 n = 0
@ -14,10 +16,11 @@ def zero_bits(b: int) -> int:
return 7 - n return 7 - n
def count_leading_zero_bits(hex_str: str) -> int: def count_leading_zero_bits(hex_str: str) -> int:
total = 0 total = 0
for i in range(0, len(hex_str) - 2, 2): for i in range(0, len(hex_str) - 2, 2):
bits = zero_bits(int(hex_str[i:i+2], 16)) bits = zero_bits(int(hex_str[i : i + 2], 16))
total += bits total += bits
if bits != 8: if bits != 8:
@ -25,7 +28,10 @@ def count_leading_zero_bits(hex_str: str) -> int:
return total return total
def mine_event(content: str, difficulty: int, public_key: str, kind: int, tags: list=[]) -> Event:
def mine_event(
content: str, difficulty: int, public_key: str, kind: int, tags: list = []
) -> Event:
all_tags = [["nonce", "1", str(difficulty)]] all_tags = [["nonce", "1", str(difficulty)]]
all_tags.extend(tags) all_tags.extend(tags)
@ -43,6 +49,7 @@ def mine_event(content: str, difficulty: int, public_key: str, kind: int, tags:
return Event(public_key, content, created_at, kind, all_tags, event_id) return Event(public_key, content, created_at, kind, all_tags, event_id)
def mine_key(difficulty: int) -> PrivateKey: def mine_key(difficulty: int) -> PrivateKey:
sk = PrivateKey() sk = PrivateKey()
num_leading_zero_bits = count_leading_zero_bits(sk.public_key.hex()) num_leading_zero_bits = count_leading_zero_bits(sk.public_key.hex())

View file

@ -2,7 +2,9 @@ import json
import time import time
from queue import Queue from queue import Queue
from threading import Lock from threading import Lock
from websocket import WebSocketApp from websocket import WebSocketApp
from .event import Event from .event import Event
from .filter import Filters from .filter import Filters
from .message_pool import MessagePool from .message_pool import MessagePool

View file

@ -1,12 +1,10 @@
from .filter import Filters from .filter import Filters
class Subscription: class Subscription:
def __init__(self, id: str, filters: Filters=None) -> None: def __init__(self, id: str, filters: Filters = None) -> None:
self.id = id self.id = id
self.filters = filters self.filters = filters
def to_json_object(self): def to_json_object(self):
return { return {"id": self.id, "filters": self.filters.to_json_array()}
"id": self.id,
"filters": self.filters.to_json_array()
}

View file

@ -1,19 +1,17 @@
import asyncio import asyncio
import json import json
from typing import List, Union from typing import List, Union
from .models import RelayList, Relay, Event, Filter, Filters
from fastapi import WebSocket, WebSocketDisconnect
from lnbits.helpers import urlsafe_short_hash
from .models import Event, Filter, Filters, Relay, RelayList
from .nostr.event import Event as NostrEvent from .nostr.event import Event as NostrEvent
from .nostr.filter import Filter as NostrFilter from .nostr.filter import Filter as NostrFilter
from .nostr.filter import Filters as NostrFilters from .nostr.filter import Filters as NostrFilters
from .tasks import ( from .tasks import (client, received_event_queue,
client, received_subscription_eosenotices,
received_event_queue, received_subscription_events)
received_subscription_events,
received_subscription_eosenotices,
)
from fastapi import WebSocket, WebSocketDisconnect
from lnbits.helpers import urlsafe_short_hash
class NostrRouter: class NostrRouter:

View file

@ -4,11 +4,11 @@ import threading
from .nostr.client.client import NostrClient from .nostr.client.client import NostrClient
from .nostr.event import Event from .nostr.event import Event
from .nostr.message_pool import EventMessage, NoticeMessage, EndOfStoredEventsMessage
from .nostr.key import PublicKey from .nostr.key import PublicKey
from .nostr.message_pool import (EndOfStoredEventsMessage, EventMessage,
NoticeMessage)
from .nostr.relay_manager import RelayManager from .nostr.relay_manager import RelayManager
client = NostrClient( client = NostrClient(
connect=False, connect=False,
) )

View file

@ -75,7 +75,7 @@
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<div class="text-weight-bold"> Your endpoint: <div class="text-weight-bold"> Your endpoint:
@ -101,11 +101,11 @@
<div class="col-12 col-md-5 q-gutter-y-md"> <div class="col-12 col-md-5 q-gutter-y-md">
<q-card> <q-card>
<q-card-section> <q-card-section>
<h6 class="text-subtitle1 q-my-none">{{SITE_TITLE}} Nostrclient Extension</h6> <h6 class="text-subtitle1 q-my-none">Nostrclient Extension</h6>
<p> <p>
This extension is a always-on nostr client that other extensions can This extension is a always-on nostr client that other extensions can
use to send and receive events on nostr. use to send and receive events on nostr.
Add multiple nostr relays to connect to. The extension then opens a websocket for you to use Add multiple nostr relays to connect to. The extension then opens a websocket for you to use
at at
<p> <p>
@ -197,10 +197,8 @@
) )
.then(function (response) { .then(function (response) {
if (response.data) { if (response.data) {
console.log(response.data)
response.data.map(maplrelays) response.data.map(maplrelays)
self.nostrrelayLinks = response.data self.nostrrelayLinks = response.data
console.log(self.nostrrelayLinks)
} }
}) })
.catch(function (error) { .catch(function (error) {
@ -219,10 +217,10 @@
message: `Invalid relay URL.`, message: `Invalid relay URL.`,
caption: "Should start with 'wss://'' or 'ws://'" caption: "Should start with 'wss://'' or 'ws://'"
}) })
return return false;
} }
console.log('ADD RELAY ' + this.relayToAdd) console.log('ADD RELAY ' + this.relayToAdd)
var self = this let that = this
LNbits.api LNbits.api
.request( .request(
'POST', 'POST',
@ -231,17 +229,17 @@
{url: this.relayToAdd} {url: this.relayToAdd}
) )
.then(function (response) { .then(function (response) {
console.log("response:", response)
if (response.data) { if (response.data) {
console.log(response.data)
response.data.map(maplrelays) response.data.map(maplrelays)
self.nostrrelayLinks = response.data that.nostrrelayLinks = response.data
console.log(self.nostrrelayLinks) that.relayToAdd = ''
} }
}) })
.catch(function (error) { .catch(function (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
location.reload() return false;
}, },
deleteRelay(url) { deleteRelay(url) {
console.log('DELETE RELAY ' + url) console.log('DELETE RELAY ' + url)
@ -260,7 +258,6 @@
.catch(function (error) { .catch(function (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
}) })
location.reload()
}, },
exportlnurldeviceCSV: function () { exportlnurldeviceCSV: function () {
var self = this var self = this

View file

@ -1,20 +1,18 @@
from http import HTTPStatus
import asyncio import asyncio
from http import HTTPStatus
# FastAPI good for incoming
from fastapi import Request from fastapi import Request
from fastapi.param_functions import Query from fastapi.param_functions import Query
from fastapi.params import Depends from fastapi.params import Depends
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from starlette.exceptions import HTTPException
from starlette.responses import HTMLResponse
from . import nostrclient_ext, nostr_renderer
# FastAPI good for incoming
from fastapi import Request
from lnbits.core.crud import update_payment_status from lnbits.core.crud import update_payment_status
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.core.views.api import api_payment from lnbits.core.views.api import api_payment
from lnbits.decorators import check_user_exists, check_admin from lnbits.decorators import check_admin, check_user_exists
from starlette.responses import HTMLResponse
from . import nostr_renderer, nostrclient_ext
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")

View file

@ -1,33 +1,25 @@
from http import HTTPStatus
import asyncio import asyncio
from fastapi import WebSocket from http import HTTPStatus
from fastapi.params import Depends from typing import Optional
from fastapi import Depends, WebSocket
from lnbits.decorators import check_admin
from lnbits.helpers import urlsafe_short_hash
from loguru import logger
from starlette.exceptions import HTTPException
from . import nostrclient_ext from . import nostrclient_ext
from .tasks import client from .crud import add_relay, delete_relay, get_relays
from loguru import logger from .models import Relay, RelayList
from .crud import get_relays, add_relay, delete_relay
from .models import RelayList, Relay
from .services import NostrRouter from .services import NostrRouter
from .tasks import client, init_relays
from lnbits.decorators import (
WalletTypeInfo,
get_key_type,
require_admin_key,
check_admin,
)
from lnbits.helpers import urlsafe_short_hash
from .tasks import init_relays
# we keep this in # we keep this in
all_routers: list[NostrRouter] = [] all_routers: list[NostrRouter] = []
@nostrclient_ext.get("/api/v1/relays") @nostrclient_ext.get("/api/v1/relays")
async def api_get_relays(): # type: ignore async def api_get_relays() -> RelayList:
relays = RelayList(__root__=[]) relays = RelayList(__root__=[])
for url, r in client.relay_manager.relays.items(): for url, r in client.relay_manager.relays.items():
status_text = ( status_text = (
@ -52,20 +44,30 @@ async def api_get_relays(): # type: ignore
@nostrclient_ext.post( @nostrclient_ext.post(
"/api/v1/relay", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)] "/api/v1/relay", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
) )
async def api_add_relay(relay: Relay): # type: ignore async def api_add_relay(relay: Relay) -> Optional[RelayList]:
assert relay.url, "no URL" if not relay.url:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=f"Relay url not provided."
)
if relay.url in client.relay_manager.relays: if relay.url in client.relay_manager.relays:
return raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=f"Relay: {relay.url} already exists.",
)
relay.id = urlsafe_short_hash() relay.id = urlsafe_short_hash()
await add_relay(relay) await add_relay(relay)
await init_relays() await init_relays()
return await get_relays()
@nostrclient_ext.delete( @nostrclient_ext.delete(
"/api/v1/relay", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)] "/api/v1/relay", status_code=HTTPStatus.OK, dependencies=[Depends(check_admin)]
) )
async def api_delete_relay(relay: Relay): # type: ignore async def api_delete_relay(relay: Relay) -> None:
assert relay.url if not relay.url:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=f"Relay url not provided."
)
client.relay_manager.remove_relay(relay.url) client.relay_manager.remove_relay(relay.url)
await delete_relay(relay) await delete_relay(relay)
@ -91,7 +93,7 @@ async def api_stop():
@nostrclient_ext.websocket("/api/v1/relay") @nostrclient_ext.websocket("/api/v1/relay")
async def ws_relay(websocket: WebSocket): async def ws_relay(websocket: WebSocket) -> None:
"""Relay multiplexer: one client (per endpoint) <-> multiple relays""" """Relay multiplexer: one client (per endpoint) <-> multiple relays"""
await websocket.accept() await websocket.accept()
router = NostrRouter(websocket) router = NostrRouter(websocket)