Second
|
@ -0,0 +1,117 @@
|
||||||
|
Diffie hellman ephemeral
|
||||||
|
Fernet based encryption
|
||||||
|
==========================
|
||||||
|
|
||||||
|
### *class* Cryptography.WhaleSong.DHEFern(cache, nodeNickname, cLog)
|
||||||
|
|
||||||
|
#### cLog
|
||||||
|
|
||||||
|
Method reference to run.Node.cLog so we can log to the ui from here
|
||||||
|
|
||||||
|
#### loadedParams
|
||||||
|
|
||||||
|
In memory representations of cryptography parameters
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
dict
|
||||||
|
|
||||||
|
#### loadedKeys
|
||||||
|
|
||||||
|
In memory representations of cryptography keys
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
dict
|
||||||
|
|
||||||
|
#### nodeNickname
|
||||||
|
|
||||||
|
Name of node for isolating configs when running multiple nodes
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
str
|
||||||
|
|
||||||
|
#### cache
|
||||||
|
|
||||||
|
Daisy cache for use in storing cryptography information
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
Components.daisy.Cache
|
||||||
|
|
||||||
|
#### publicKey
|
||||||
|
|
||||||
|
Public key for node
|
||||||
|
|
||||||
|
#### privateKey
|
||||||
|
|
||||||
|
Private key for node
|
||||||
|
|
||||||
|
#### checkInMem(store: str, nodeID: str)
|
||||||
|
|
||||||
|
Check if parameters or keys are loaded for node of nodeID
|
||||||
|
|
||||||
|
* **Parameters:**
|
||||||
|
**store** (*str*) – Whether to check loaded keys or parameters
|
||||||
|
|
||||||
|
#### decrypt(data, nodeID: str)
|
||||||
|
|
||||||
|
Decrypt bytes and return either str or dict (TODO: Check whether to msgpack load)
|
||||||
|
|
||||||
|
#### encrypt(data, nodeID: str, isDict: bool = True)
|
||||||
|
|
||||||
|
Do Fernet encryption
|
||||||
|
|
||||||
|
data
|
||||||
|
: Either bytes or dict to encrypt
|
||||||
|
|
||||||
|
isDict: bool
|
||||||
|
: Whether data is a dictionary
|
||||||
|
|
||||||
|
#### genKeyPair(paramsOverride=False, setSelf: bool = True)
|
||||||
|
|
||||||
|
Generate public and private keys from self.params (TODO: Gen from passed params)
|
||||||
|
|
||||||
|
paramsOverride
|
||||||
|
: False or parameters to use (TODO)
|
||||||
|
|
||||||
|
setSelf: bool
|
||||||
|
: Whether to set self.privateKey and self.publicKey
|
||||||
|
|
||||||
|
#### genParams()
|
||||||
|
|
||||||
|
Generate Diffie Hellman parameters
|
||||||
|
|
||||||
|
#### getParamsBytes()
|
||||||
|
|
||||||
|
Get bytes encoded from self.parameters (TODO: Encode from store)
|
||||||
|
|
||||||
|
#### getRecord(store: str, key: str)
|
||||||
|
|
||||||
|
Get record from store: store with key: key
|
||||||
|
|
||||||
|
#### getSalt()
|
||||||
|
|
||||||
|
Get random salt
|
||||||
|
|
||||||
|
#### initStore(store: str)
|
||||||
|
|
||||||
|
Initialize store: store
|
||||||
|
|
||||||
|
#### keyDerive(pubKey: bytes, salt: bytes, nodeID: str, params: bytes)
|
||||||
|
|
||||||
|
Derive shared key using Diffie Hellman
|
||||||
|
|
||||||
|
pubKey: bytes
|
||||||
|
: Public key
|
||||||
|
|
||||||
|
nodeID: str
|
||||||
|
: PierMesh node ID
|
||||||
|
|
||||||
|
params: bytes
|
||||||
|
: Encryption parameters
|
||||||
|
|
||||||
|
#### loadParamBytes(pemBytes: bytes)
|
||||||
|
|
||||||
|
Load parameters to self.params from given bytes (TODO: Load from store)
|
||||||
|
|
||||||
|
#### loadRecordToMem(store: str, nodeID: str)
|
||||||
|
|
||||||
|
Load record of nodeID from store to either keys or pameters
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Daisy based cache
|
||||||
|
|
||||||
|
### *class* Daisy.Cache.Cache(filepaths=None, cacheFile=None, path: str = 'daisy', walk: bool = False, isCatch: bool = False)
|
||||||
|
|
||||||
|
In memory collection of Daisy records
|
||||||
|
|
||||||
|
#### create(path: str, data: dict)
|
||||||
|
|
||||||
|
Create new record
|
||||||
|
|
||||||
|
* **Parameters:**
|
||||||
|
* **path** (*str*) – Path to create record at
|
||||||
|
* **data** (*dict*) – Data to populate record with
|
||||||
|
|
||||||
|
#### get(path: str)
|
||||||
|
|
||||||
|
Get record at path, else return False
|
||||||
|
|
||||||
|
path: str
|
||||||
|
: Path of record
|
||||||
|
|
||||||
|
#### refresh()
|
||||||
|
|
||||||
|
Reload from disk to memory
|
||||||
|
|
||||||
|
#### search(keydict: dict, strict: bool = True)
|
||||||
|
|
||||||
|
Search cache for record for records with values
|
||||||
|
|
||||||
|
keydict: dict
|
||||||
|
: Values to search for
|
||||||
|
|
||||||
|
strict: bool
|
||||||
|
: Whether to require values match
|
|
@ -0,0 +1,22 @@
|
||||||
|
Daisy cache for catchs,
|
||||||
|
PierMesh’s domain analog
|
||||||
|
==========================
|
||||||
|
|
||||||
|
### *class* Daisy.Catch.Catch(path: str = 'catch', filepaths=None, catchFile=None, walk: bool = False)
|
||||||
|
|
||||||
|
Sub class of Cache for handling catchs
|
||||||
|
|
||||||
|
![image](https://git.utopic.work/PierMesh/piermesh/raw/branch/main/imgs/catchdisplay.png)
|
||||||
|
|
||||||
|
#### get(head: str, tail: str, fins=None)
|
||||||
|
|
||||||
|
Get catch by pieces
|
||||||
|
|
||||||
|
* **Parameters:**
|
||||||
|
* **head** (*str*) – First part of catch (maximum: 4 characters)
|
||||||
|
* **tail** (*str*) – Second part of catch (maximum: 16 characters)
|
||||||
|
* **fins** – List of (maximum 8 characters) strings at the end of the catch oe None if none
|
||||||
|
|
||||||
|
#### sget(path: str)
|
||||||
|
|
||||||
|
Call Cache’s get to get record
|
|
@ -0,0 +1,54 @@
|
||||||
|
Schemaless binary database
|
||||||
|
base class
|
||||||
|
==========================
|
||||||
|
|
||||||
|
### *class* Daisy.Daisy.Daisy(filepath: str, templates: dict = {}, template: bool = False, prefillDict: bool = False)
|
||||||
|
|
||||||
|
Base class for Daisy data representation
|
||||||
|
|
||||||
|
[🔗 Source](https://git.utopic.work/PierMesh/piermesh/src/branch/main/Components/daisy.py)
|
||||||
|
|
||||||
|
#### filepath
|
||||||
|
|
||||||
|
Path to file representation on disk
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
str
|
||||||
|
|
||||||
|
#### msg
|
||||||
|
|
||||||
|
In memory representation
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
dict
|
||||||
|
|
||||||
|
#### get()
|
||||||
|
|
||||||
|
Get record dictionary from memory
|
||||||
|
|
||||||
|
* **Returns:**
|
||||||
|
**self.msg**
|
||||||
|
* **Return type:**
|
||||||
|
dict
|
||||||
|
|
||||||
|
#### read(decrypt: bool = False, decryptKey=False)
|
||||||
|
|
||||||
|
Read record from disk to memory
|
||||||
|
|
||||||
|
* **Parameters:**
|
||||||
|
* **decrypt** (*bool*) – Whether to decrypt record
|
||||||
|
* **decryptKey** – Key to decrypt record
|
||||||
|
|
||||||
|
#### sublist()
|
||||||
|
|
||||||
|
Lists contents of directory if object is a directory, otherwise return None
|
||||||
|
|
||||||
|
#### write(override=False, encrypt: bool = False, encryptKey=None, recur: bool = False)
|
||||||
|
|
||||||
|
Write record to disk
|
||||||
|
|
||||||
|
* **Parameters:**
|
||||||
|
* **override** – Either false or a dictionary of values to set on the record
|
||||||
|
* **encrypt** (*bool*) – Whether to encrypt the record (TODO)
|
||||||
|
* **encryptKey** – Key to encrypt record with, or None if not set
|
||||||
|
* **recur** (*bool*) – Whether to recursively handle keys
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Daisy signal management
|
||||||
|
|
||||||
|
### *class* Daisy.Soil.Compound(cache, isCatch: bool = False)
|
||||||
|
|
||||||
|
File system watcher to propagate disk changes
|
||||||
|
|
||||||
|
#### on_any_event(event)
|
||||||
|
|
||||||
|
Called when a CRUD operation is performed on a record file
|
||||||
|
|
||||||
|
* **Parameters:**
|
||||||
|
**event** – Event object provided by watchdog
|
|
@ -0,0 +1,5 @@
|
||||||
|
Daisy based key value
|
||||||
|
store with recursion
|
||||||
|
==========================
|
||||||
|
|
||||||
|
### *class* Daisy.Store.Store(store: str, path: str, nodeNickname: str)
|
|
@ -0,0 +1,52 @@
|
||||||
|
# serve: Web UI server
|
||||||
|
|
||||||
|
### *class* Splash.serve.Server(transmitter, catch, onodeID, network, cLog)
|
||||||
|
|
||||||
|
Web server that serves the web ui and provides web to node communication
|
||||||
|
|
||||||
|
#### cLog
|
||||||
|
|
||||||
|
Reference to run.Node.cLog for logging
|
||||||
|
|
||||||
|
#### transmitter
|
||||||
|
|
||||||
|
Reference to our Transmission.transmission.Transmitter instance
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
Transmission.transmission.Transmitter
|
||||||
|
|
||||||
|
#### network
|
||||||
|
|
||||||
|
Reference to our Siph.Network.Network
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
Siph.Network.Network
|
||||||
|
|
||||||
|
#### nodeID
|
||||||
|
|
||||||
|
String converted PierMesh node ID
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
str
|
||||||
|
|
||||||
|
#### peerIDs
|
||||||
|
|
||||||
|
Map of peer IDs to Websocket sessions
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
dict
|
||||||
|
|
||||||
|
#### app
|
||||||
|
|
||||||
|
Microdot server instance
|
||||||
|
|
||||||
|
#### catch
|
||||||
|
|
||||||
|
Reference to our Catch Cache instance to pull from for servijg Catchs
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
[Daisy.Catch.Catch](/PierMesh/piermesh/src/branch/main/docs/Daisy/Catch.md#Daisy.Catch.Catch)
|
||||||
|
|
||||||
|
#### *async* sendToPeer(peerID: str, data: str)
|
||||||
|
|
||||||
|
Send data to Websocket of peer with peerID
|
|
@ -0,0 +1,123 @@
|
||||||
|
# Layer 0 data transceiving
|
||||||
|
|
||||||
|
### *class* Transceiver.Transceiver.Transceiver(device, filter, onodeID, cache, catch, cryptographyInfo, cLog)
|
||||||
|
|
||||||
|
Handling LoRa transceiving
|
||||||
|
|
||||||
|
#### cLog
|
||||||
|
|
||||||
|
Reference to run.Node.cLog for logging
|
||||||
|
|
||||||
|
#### cryptographyInfo
|
||||||
|
|
||||||
|
Cryptography instance for encrypting transmissions
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
[Cryptography.WhaleSong.DHEFern](/PierMesh/piermesh/src/branch/main/docs/Cryptography/WhaleSong.md#Cryptography.WhaleSong.DHEFern)
|
||||||
|
|
||||||
|
#### filter
|
||||||
|
|
||||||
|
Sponge.base.Filter instance for filtering packets
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
[Sponge.base.Filter](/PierMesh/piermesh/src/branch/main/docs/Sponge/base.md#Sponge.base.Filter)
|
||||||
|
|
||||||
|
#### tcache
|
||||||
|
|
||||||
|
Data backend Daisy Cache
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
[Daisy.Cache.Cache](/PierMesh/piermesh/src/branch/main/docs/Daisy/Cache.md#Daisy.Cache.Cache)
|
||||||
|
|
||||||
|
#### tcatch
|
||||||
|
|
||||||
|
Daisy Catch Cache for Catch operations
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
[Daisy.Catch.Catch](/PierMesh/piermesh/src/branch/main/docs/Daisy/Catch.md#Daisy.Catch.Catch)
|
||||||
|
|
||||||
|
#### notConnected
|
||||||
|
|
||||||
|
Whether the transceiver has been connected to yet
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
bool
|
||||||
|
|
||||||
|
#### acks
|
||||||
|
|
||||||
|
Acknowledgements received per packet
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
dict
|
||||||
|
|
||||||
|
#### onodeID
|
||||||
|
|
||||||
|
PierMesh node ID
|
||||||
|
|
||||||
|
#### messages
|
||||||
|
|
||||||
|
Message completion acknowldgements
|
||||||
|
|
||||||
|
* **Type:**
|
||||||
|
dict
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
TODO: Check if we can remove cpid
|
||||||
|
|
||||||
|
#### *async* addPackets(data, sender, senderName, recipient, recipientNode, directID=False, packetsClass=None, encrypt=False)
|
||||||
|
|
||||||
|
Convert binary data to Message and send each packet
|
||||||
|
:param data: Data to send
|
||||||
|
:type data: bytes
|
||||||
|
:param sender: Peer/Node ID of sender
|
||||||
|
:param senderName: ID matching specific user title
|
||||||
|
:param recipient: Peer/Node ID of recipient
|
||||||
|
:param recipientNode: Node ID of node to route to
|
||||||
|
:param directID: If set send to this Node only
|
||||||
|
:param packetsClass: Protocol for message
|
||||||
|
|
||||||
|
#### *async* announce()
|
||||||
|
|
||||||
|
Announce loop runner
|
||||||
|
|
||||||
|
#### awaitFullResponse(pid)
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Wait for message completed response
|
||||||
|
|
||||||
|
#### *async* awaitResponse(pid)
|
||||||
|
|
||||||
|
Wait for acknowldgement response
|
||||||
|
|
||||||
|
#### *async* initNodeDH(dhefOb, recipientNode, onodeID)
|
||||||
|
|
||||||
|
Send Diffie Hellman initialization message
|
||||||
|
|
||||||
|
#### onConnection(interface, topic=<class 'pubsub.core.callables.AUTO_TOPIC'>)
|
||||||
|
|
||||||
|
When the node connects start announce loop and end the waiting loop
|
||||||
|
|
||||||
|
#### onReceive(packet, interface)
|
||||||
|
|
||||||
|
Run each received packet through Sponge.base.Filters sieve using a new event loop
|
||||||
|
|
||||||
|
#### *async* progressCheck()
|
||||||
|
|
||||||
|
Checks if acknowldgement was received per packet and if not resends
|
||||||
|
|
||||||
|
#### responseCheck(packet)
|
||||||
|
|
||||||
|
On acknowldgement response set acks based on response
|
||||||
|
|
||||||
|
#### send(packet, recipientNode=False)
|
||||||
|
|
||||||
|
Send individual packet
|
||||||
|
|
||||||
|
* **Parameters:**
|
||||||
|
**recipientNode** – If set send to specified node
|
||||||
|
|
||||||
|
#### *async* sendAnnounce()
|
||||||
|
|
||||||
|
Send an announce packet (contains basic network mapping information) every so often so new nodes autoconnect
|
|
@ -0,0 +1,294 @@
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import dh
|
||||||
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||||
|
from cryptography.hazmat.primitives.serialization import (
|
||||||
|
Encoding,
|
||||||
|
NoEncryption,
|
||||||
|
ParameterFormat,
|
||||||
|
PublicFormat,
|
||||||
|
PrivateFormat,
|
||||||
|
)
|
||||||
|
import cryptography.hazmat.primitives.serialization as Serialization
|
||||||
|
import msgpack
|
||||||
|
from Daisy.Store import Store
|
||||||
|
|
||||||
|
# TODO: Different store directories per node
|
||||||
|
|
||||||
|
|
||||||
|
class DHEFern:
|
||||||
|
"""
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
cLog
|
||||||
|
Method reference to `run.Node.cLog` so we can log to the ui from here
|
||||||
|
|
||||||
|
loadedParams: dict
|
||||||
|
In memory representations of cryptography parameters
|
||||||
|
|
||||||
|
loadedKeys: dict
|
||||||
|
In memory representations of cryptography keys
|
||||||
|
|
||||||
|
nodeNickname: str
|
||||||
|
Name of node for isolating configs when running multiple nodes
|
||||||
|
|
||||||
|
cache: Components.daisy.Cache
|
||||||
|
Daisy cache for use in storing cryptography information
|
||||||
|
|
||||||
|
publicKey
|
||||||
|
Public key for node
|
||||||
|
|
||||||
|
privateKey
|
||||||
|
Private key for node
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cache, nodeNickname, cLog):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cache: Components.daisy.Cache
|
||||||
|
Reference to the node instances Daisy cache
|
||||||
|
|
||||||
|
nodeNickname: str
|
||||||
|
Node nickname for record storage
|
||||||
|
|
||||||
|
cLog
|
||||||
|
Reference to `run.Node.cLog`
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.cLog = cLog
|
||||||
|
self.stores = {}
|
||||||
|
self.loadedParams = {}
|
||||||
|
self.loadedKeys = {}
|
||||||
|
self.nodeNickname = nodeNickname
|
||||||
|
self.cache = cache
|
||||||
|
if os.path.exists("daisy/cryptography/{0}/param".format(nodeNickname)) == False:
|
||||||
|
self.initStore("param")
|
||||||
|
else:
|
||||||
|
self.stores["param"] = Store("param", "cryptography", nodeNickname)
|
||||||
|
self.params = self.loadParamBytes(self.stores["param"].get()["self"])
|
||||||
|
self.cLog(20, "Param store initialized")
|
||||||
|
if os.path.exists("daisy/cryptography/{0}/key".format(nodeNickname)) == False:
|
||||||
|
self.cLog(20, "Key store DNE, initializing")
|
||||||
|
self.initStore("key")
|
||||||
|
self.genKeyPair()
|
||||||
|
else:
|
||||||
|
self.cLog(20, "Key store exists, loading")
|
||||||
|
self.stores["key"] = Store("key", "cryptography", nodeNickname)
|
||||||
|
self.cLog(20, "Store loaded")
|
||||||
|
# tks = self.stores["key"].get()
|
||||||
|
# self.publicKey = tks["self"]["publicKey"]
|
||||||
|
# self.privateKey = tks["self"]["privateKey"]
|
||||||
|
self.cLog(20, "Key store initialized")
|
||||||
|
|
||||||
|
def checkInMem(self, store: str, nodeID: str):
|
||||||
|
"""
|
||||||
|
Check if parameters or keys are loaded for node of nodeID
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
store: str
|
||||||
|
Whether to check loaded keys or parameters
|
||||||
|
|
||||||
|
"""
|
||||||
|
if store == "param":
|
||||||
|
return nodeID in self.loadedParams.keys()
|
||||||
|
elif store == "key":
|
||||||
|
return nodeID in self.loadedKeys.keys()
|
||||||
|
|
||||||
|
def loadRecordToMem(self, store: str, nodeID: str):
|
||||||
|
"""
|
||||||
|
Load record of nodeID from store to either keys or pameters
|
||||||
|
"""
|
||||||
|
r = self.getRecord(store, nodeID)
|
||||||
|
if r == False:
|
||||||
|
self.cLog(
|
||||||
|
30, "Tried to load nonexistent {0} for node {1}".format(store, nodeID)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
elif self.checkInMem(store, nodeID):
|
||||||
|
self.cLog(10, "{0}s already deserialized, skipping".format(store))
|
||||||
|
else:
|
||||||
|
if store == "param":
|
||||||
|
self.loadedParams[nodeID] = self.loadParamBytes(r)
|
||||||
|
elif store == "key":
|
||||||
|
self.loadedKeys[nodeID] = {
|
||||||
|
"publicKey": Serialization.load_pem_public_key(r["publicKey"]),
|
||||||
|
"privateKey": Serialization.load_pem_private_key(
|
||||||
|
r["privateKey"], None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getRecord(self, store: str, key: str):
|
||||||
|
"""
|
||||||
|
Get record from store: store with key: key
|
||||||
|
"""
|
||||||
|
r = stores[store].getRecord(key)
|
||||||
|
if r == False:
|
||||||
|
self.cLog(20, "Record does not exist")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return r
|
||||||
|
|
||||||
|
def initStore(self, store: str):
|
||||||
|
"""
|
||||||
|
Initialize store: store
|
||||||
|
"""
|
||||||
|
self.stores[store] = Store(store, "cryptography", self.nodeNickname)
|
||||||
|
if store == "param":
|
||||||
|
self.genParams()
|
||||||
|
self.stores[store].update("self", self.getParamsBytes(), recur=False)
|
||||||
|
elif store == "key":
|
||||||
|
self.stores[store].update("self", {}, recur=False)
|
||||||
|
else:
|
||||||
|
self.cLog(30, "Store not defined")
|
||||||
|
|
||||||
|
def genParams(self):
|
||||||
|
"""
|
||||||
|
Generate Diffie Hellman parameters
|
||||||
|
"""
|
||||||
|
params = dh.generate_parameters(generator=2, key_size=2048)
|
||||||
|
self.params = params
|
||||||
|
return params
|
||||||
|
|
||||||
|
def getParamsBytes(self):
|
||||||
|
"""
|
||||||
|
Get bytes encoded from self.parameters (TODO: Encode from store)
|
||||||
|
"""
|
||||||
|
return self.params.parameter_bytes(Encoding.PEM, ParameterFormat.PKCS3)
|
||||||
|
|
||||||
|
def loadParamBytes(self, pemBytes: bytes):
|
||||||
|
"""
|
||||||
|
Load parameters to self.params from given bytes (TODO: Load from store)
|
||||||
|
"""
|
||||||
|
self.params = Serialization.load_pem_parameters(pemBytes)
|
||||||
|
return self.params
|
||||||
|
|
||||||
|
def genKeyPair(self, paramsOverride=False, setSelf: bool = True):
|
||||||
|
"""
|
||||||
|
Generate public and private keys from self.params (TODO: Gen from passed params)
|
||||||
|
|
||||||
|
paramsOverride
|
||||||
|
False or parameters to use (TODO)
|
||||||
|
|
||||||
|
setSelf: bool
|
||||||
|
Whether to set self.privateKey and self.publicKey
|
||||||
|
"""
|
||||||
|
privateKey = self.params.generate_private_key()
|
||||||
|
if setSelf:
|
||||||
|
self.privateKey = privateKey
|
||||||
|
publicKey = privateKey.public_key()
|
||||||
|
if setSelf:
|
||||||
|
self.publicKey = publicKey
|
||||||
|
self.updateStore(
|
||||||
|
"key",
|
||||||
|
"self",
|
||||||
|
{
|
||||||
|
"publicKey": self.publicKey.public_bytes(
|
||||||
|
Encoding.PEM, PublicFormat.SubjectPublicKeyInfo
|
||||||
|
),
|
||||||
|
"privateKey": self.privateKey.private_bytes(
|
||||||
|
Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return [privateKey, publicKey]
|
||||||
|
else:
|
||||||
|
publicKey = publicKey.public_bytes(
|
||||||
|
Encoding.PEM, PublicFormat.SubjectPublicKeyInfo
|
||||||
|
)
|
||||||
|
privateKey = privateKey.private_bytes(
|
||||||
|
Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()
|
||||||
|
)
|
||||||
|
return [privateKey, publicKey]
|
||||||
|
|
||||||
|
def keyDerive(self, pubKey: bytes, salt: bytes, nodeID: str, params: bytes):
|
||||||
|
"""
|
||||||
|
Derive shared key using Diffie Hellman
|
||||||
|
|
||||||
|
pubKey: bytes
|
||||||
|
Public key
|
||||||
|
|
||||||
|
nodeID: str
|
||||||
|
PierMesh node ID
|
||||||
|
|
||||||
|
params: bytes
|
||||||
|
Encryption parameters
|
||||||
|
"""
|
||||||
|
if self.checkInMem("param", nodeID) == False:
|
||||||
|
if self.getRecord("param", nodeID) == False:
|
||||||
|
self.updateStore("param", nodeID, params, recur=False)
|
||||||
|
self.loadRecordToMem("param", nodeID)
|
||||||
|
self.cLog(20, "Precheck done for key derivation")
|
||||||
|
|
||||||
|
# TODO: Load them and if private key exists load it, otherwise generate a private key
|
||||||
|
if self.checkInMem("key", nodeID) == False:
|
||||||
|
if self.getRecord("key", nodeID) == False:
|
||||||
|
privateKey, publicKey = self.genKeyPair(setSelf=False)
|
||||||
|
self.updateStore(
|
||||||
|
"key", nodeID, {"publicKey": publicKey, "privateKey": privateKey}
|
||||||
|
)
|
||||||
|
self.loadRecordToMem("key", nodeID)
|
||||||
|
|
||||||
|
sharedKey = self.loadedKeys[nodeID]["privateKey"].exchange(
|
||||||
|
Serialization.load_pem_public_key(pubKey)
|
||||||
|
)
|
||||||
|
# Perform key derivation.
|
||||||
|
self.cLog(20, "Performing key derivation")
|
||||||
|
derivedKey = HKDF(
|
||||||
|
algorithm=hashes.SHA256(), length=32, salt=salt, info=b"handshake data"
|
||||||
|
).derive(sharedKey)
|
||||||
|
self.cLog(20, "Derived key")
|
||||||
|
ederivedKey = base64.urlsafe_b64encode(derivedKey)
|
||||||
|
tr = self.getRecord("key", nodeID)
|
||||||
|
tr["derivedKey"] = ederivedKey
|
||||||
|
self.updateStore("key", nodeID, tr)
|
||||||
|
self.cLog(20, "Done with cryptography store updates")
|
||||||
|
return ederivedKey
|
||||||
|
|
||||||
|
def getSalt(self):
|
||||||
|
"""
|
||||||
|
Get random salt
|
||||||
|
"""
|
||||||
|
return os.urandom(16)
|
||||||
|
|
||||||
|
def encrypt(self, data, nodeID: str, isDict: bool = True):
|
||||||
|
"""
|
||||||
|
Do Fernet encryption
|
||||||
|
|
||||||
|
data
|
||||||
|
Either bytes or dict to encrypt
|
||||||
|
|
||||||
|
isDict: bool
|
||||||
|
Whether data is a dictionary
|
||||||
|
"""
|
||||||
|
r = self.getRecord("key", nodeID)
|
||||||
|
if r == False:
|
||||||
|
self.cLog(20, "Node {0} not in keystore".format(nodeID))
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
derivedKey = r["derivedKey"]
|
||||||
|
fernet = Fernet(derivedKey)
|
||||||
|
if isDict:
|
||||||
|
data = msgpack.dumps(data)
|
||||||
|
token = fernet.encrypt(data)
|
||||||
|
return token
|
||||||
|
|
||||||
|
def decrypt(self, data, nodeID: str):
|
||||||
|
"""
|
||||||
|
Decrypt bytes and return either str or dict (TODO: Check whether to msgpack load)
|
||||||
|
"""
|
||||||
|
r = self.getRecord("key", nodeID)
|
||||||
|
if r == False:
|
||||||
|
self.cLog(20, "No record of node " + nodeID)
|
||||||
|
return False
|
||||||
|
elif not "derivedKey" in r.keys():
|
||||||
|
self.cLog(20, "No key derived for node " + nodeID)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
fernet = Fernet(self.getRecord("key", nodeID)["derivedKey"])
|
||||||
|
return msgpack.loads(fernet.decrypt(data))
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
Diffie hellman ephemeral
|
||||||
|
Fernet based encryption
|
||||||
|
==========================
|
||||||
|
|
||||||
|
.. autoclass:: Cryptography.WhaleSong.DHEFern
|
||||||
|
:members:
|
|
@ -0,0 +1,135 @@
|
||||||
|
from Daisy.Daisy import Daisy
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import msgpack
|
||||||
|
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
|
||||||
|
# TODO: Dumping to cacheFile
|
||||||
|
|
||||||
|
|
||||||
|
class Cache:
|
||||||
|
"""
|
||||||
|
In memory collection of Daisy records
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
filepaths=None,
|
||||||
|
cacheFile=None,
|
||||||
|
path: str = "daisy",
|
||||||
|
walk: bool = False,
|
||||||
|
isCatch: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filepaths
|
||||||
|
Either a list of filepaths to load or None
|
||||||
|
|
||||||
|
cacheFile
|
||||||
|
Path to a cache file which is a collection of paths to load
|
||||||
|
|
||||||
|
path: str
|
||||||
|
Path prefix to load records from
|
||||||
|
|
||||||
|
walk: bool
|
||||||
|
Whether to automatically walk the path and load records
|
||||||
|
|
||||||
|
isCatch: bool
|
||||||
|
Whether this cache is for catchs
|
||||||
|
"""
|
||||||
|
self.data = {}
|
||||||
|
self.path = path
|
||||||
|
|
||||||
|
if filepaths != None:
|
||||||
|
for fp in filepaths:
|
||||||
|
fp = path + "/" + fp
|
||||||
|
if os.path.isfile(fp):
|
||||||
|
self.data[fp] = Daisy(fp)
|
||||||
|
elif cacheFile != None:
|
||||||
|
with open(cacheFile, "r") as f:
|
||||||
|
for fp in f.read().split("\n"):
|
||||||
|
self.data[fp] = Daisy(fp)
|
||||||
|
elif walk:
|
||||||
|
for root, dirs, files in os.walk(self.path):
|
||||||
|
for p in dirs + files:
|
||||||
|
if not (".json" in p):
|
||||||
|
if not (".md" in p):
|
||||||
|
tpath = root + "/" + p
|
||||||
|
self.data[tpath] = Daisy(tpath)
|
||||||
|
|
||||||
|
def create(self, path: str, data: dict):
|
||||||
|
"""
|
||||||
|
Create new record
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
Path to create record at
|
||||||
|
|
||||||
|
data: dict
|
||||||
|
Data to populate record with
|
||||||
|
"""
|
||||||
|
with open(self.path + "/" + path, "wb") as f:
|
||||||
|
f.write(msgpack.dumps(data))
|
||||||
|
# logging.log(10, "Done creating record")
|
||||||
|
self.data[path] = Daisy(self.path + "/" + path)
|
||||||
|
# logging.log(10, "Done loading to Daisy")
|
||||||
|
return self.data[path]
|
||||||
|
|
||||||
|
def get(self, path: str):
|
||||||
|
"""
|
||||||
|
Get record at path, else return False
|
||||||
|
|
||||||
|
path: str
|
||||||
|
Path of record
|
||||||
|
"""
|
||||||
|
if path in self.data.keys():
|
||||||
|
return self.data[path]
|
||||||
|
else:
|
||||||
|
if os.path.exists(self.path + "/" + path):
|
||||||
|
self.data[path] = Daisy(self.path + "/" + path)
|
||||||
|
return self.data[path]
|
||||||
|
else:
|
||||||
|
# logging.log(10, "File does not exist")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""
|
||||||
|
Reload from disk to memory
|
||||||
|
"""
|
||||||
|
for key in self.data.keys():
|
||||||
|
self.data[key].read()
|
||||||
|
|
||||||
|
def search(self, keydict: dict, strict: bool = True):
|
||||||
|
"""
|
||||||
|
Search cache for record for records with values
|
||||||
|
|
||||||
|
keydict: dict
|
||||||
|
Values to search for
|
||||||
|
|
||||||
|
strict: bool
|
||||||
|
Whether to require values match
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for key, val in self.data.items():
|
||||||
|
val = val.get()
|
||||||
|
if strict and type(val) != str:
|
||||||
|
addcheck = False
|
||||||
|
for k, v in keydict.items():
|
||||||
|
if k in val.keys():
|
||||||
|
if v in val[k]:
|
||||||
|
addcheck = True
|
||||||
|
else:
|
||||||
|
addcheck = False
|
||||||
|
break
|
||||||
|
if addcheck:
|
||||||
|
results.append([key, val])
|
||||||
|
elif type(val) != str:
|
||||||
|
for k, v in keydict.items():
|
||||||
|
if k in val.keys():
|
||||||
|
if v in val[k]:
|
||||||
|
results.append([key, val])
|
||||||
|
return results
|
|
@ -0,0 +1,5 @@
|
||||||
|
Daisy based cache
|
||||||
|
==========================
|
||||||
|
|
||||||
|
.. autoclass:: Daisy.Cache.Cache
|
||||||
|
:members:
|
|
@ -0,0 +1,67 @@
|
||||||
|
from Daisy.Cache import Cache
|
||||||
|
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class Catch(Cache):
|
||||||
|
"""
|
||||||
|
Sub class of Cache for handling catchs
|
||||||
|
|
||||||
|
.. image:: https://git.utopic.work/PierMesh/piermesh/raw/branch/main/imgs/catchdisplay.png
|
||||||
|
"""
|
||||||
|
|
||||||
|
catches = {}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, path: str = "catch", filepaths=None, catchFile=None, walk: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Basically the same initialization parameters as Catch
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
filepaths=filepaths, cacheFile=catchFile, path=path, walk=walk, isCatch=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Fins
|
||||||
|
|
||||||
|
def sget(self, path: str):
|
||||||
|
"""
|
||||||
|
Call Cache's get to get record
|
||||||
|
"""
|
||||||
|
return super().get(path)
|
||||||
|
|
||||||
|
# TODO: Rename
|
||||||
|
def get(self, head: str, tail: str, fins=None):
|
||||||
|
"""
|
||||||
|
Get catch by pieces
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
head: str
|
||||||
|
First part of catch (maximum: 4 characters)
|
||||||
|
|
||||||
|
tail: str
|
||||||
|
Second part of catch (maximum: 16 characters)
|
||||||
|
|
||||||
|
fins
|
||||||
|
List of (maximum 8 characters) strings at the end of the catch oe None if none
|
||||||
|
"""
|
||||||
|
r = self.search({"head": head, "tail": tail})
|
||||||
|
return r[0][1]["html"]
|
||||||
|
|
||||||
|
def addc(self, peer, node, seperator, head, tail, data, fins=None):
|
||||||
|
tnpath = "catch/" + node
|
||||||
|
if os.path.exists(tnpath) != True:
|
||||||
|
os.makedirs(tnpath)
|
||||||
|
tppath = tnpath + "/" + peer
|
||||||
|
if os.path.exists(tppath) != True:
|
||||||
|
os.makedirs(tppath)
|
||||||
|
sid = str(random.randrange(0, 999999)).zfill(6)
|
||||||
|
data["seperator"] = seperator
|
||||||
|
data["head"] = head
|
||||||
|
data["tail"] = tail
|
||||||
|
if fins != None:
|
||||||
|
data["fins"] = fins
|
||||||
|
res = self.create("{0}/{1}/{2}".format(node, peer, sid), data)
|
||||||
|
return [sid, res]
|
|
@ -0,0 +1,6 @@
|
||||||
|
Daisy cache for catchs,
|
||||||
|
PierMesh's domain analog
|
||||||
|
==========================
|
||||||
|
|
||||||
|
.. autoclass:: Daisy.Catch.Catch
|
||||||
|
:members:
|
|
@ -0,0 +1,189 @@
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import msgpack
|
||||||
|
|
||||||
|
# TODO: delete
|
||||||
|
# TODO: propagate json changes to msgpack automatically
|
||||||
|
# TODO: propagate msgpack changes to cache automatically
|
||||||
|
# TODO: Indexing
|
||||||
|
|
||||||
|
|
||||||
|
def _json_to_msg(path: str):
|
||||||
|
"""
|
||||||
|
Convert json at the path plus .json to a msgpack binary
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
Path to json minus the extension
|
||||||
|
"""
|
||||||
|
rpath = path + ".json"
|
||||||
|
res = b""
|
||||||
|
with open(rpath) as f:
|
||||||
|
res = msgpack.dumps(json.load(f))
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(res)
|
||||||
|
|
||||||
|
|
||||||
|
class Daisy:
|
||||||
|
"""
|
||||||
|
Base class for Daisy data representation
|
||||||
|
|
||||||
|
`🔗 Source <https://git.utopic.work/PierMesh/piermesh/src/branch/main/Components/daisy.py>`_
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
filepath: str
|
||||||
|
Path to file representation on disk
|
||||||
|
|
||||||
|
msg: dict
|
||||||
|
In memory representation
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
filepath: str,
|
||||||
|
templates: dict = {},
|
||||||
|
template: bool = False,
|
||||||
|
prefillDict: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filepath: str
|
||||||
|
Path to disk location
|
||||||
|
|
||||||
|
templates: dict
|
||||||
|
Dictionary of templates to Use
|
||||||
|
|
||||||
|
template: bool
|
||||||
|
Which template to Use
|
||||||
|
|
||||||
|
prefillDict: bool
|
||||||
|
Whether to fill the record with a template
|
||||||
|
"""
|
||||||
|
self.filepath = filepath
|
||||||
|
if os.path.exists(filepath) != True:
|
||||||
|
with open(filepath, "wb") as f:
|
||||||
|
if template != False:
|
||||||
|
if template in templates.keys():
|
||||||
|
t = templates[template].get()
|
||||||
|
if prefillDict != False:
|
||||||
|
for k in prefillDict.keys():
|
||||||
|
t[k] = prefillDict[k]
|
||||||
|
f.write(msgpack.dumps(t))
|
||||||
|
self.msg = t
|
||||||
|
else:
|
||||||
|
print("No such template as: " + template)
|
||||||
|
else:
|
||||||
|
f.write(msgpack.dumps({}))
|
||||||
|
self.msg = {}
|
||||||
|
elif os.path.isdir(filepath):
|
||||||
|
self.msg = "directory"
|
||||||
|
else:
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
self.msg = msgpack.loads(f.read())
|
||||||
|
|
||||||
|
# Use override for updating
|
||||||
|
|
||||||
|
def write(
|
||||||
|
self,
|
||||||
|
override=False,
|
||||||
|
encrypt: bool = False,
|
||||||
|
encryptKey=None,
|
||||||
|
recur: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Write record to disk
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
override
|
||||||
|
Either false or a dictionary of values to set on the record
|
||||||
|
|
||||||
|
encrypt: bool
|
||||||
|
Whether to encrypt the record (TODO)
|
||||||
|
|
||||||
|
encryptKey
|
||||||
|
Key to encrypt record with, or None if not set
|
||||||
|
|
||||||
|
recur: bool
|
||||||
|
Whether to recursively handle keys
|
||||||
|
"""
|
||||||
|
if override != False:
|
||||||
|
for key in override.keys():
|
||||||
|
# TODO: Deeper recursion
|
||||||
|
if recur:
|
||||||
|
if not key in self.msg.keys():
|
||||||
|
self.msg[key] = {}
|
||||||
|
for ikey in override[key].keys():
|
||||||
|
self.msg[key][ikey] = override[key][ikey]
|
||||||
|
else:
|
||||||
|
self.msg[key] = override[key]
|
||||||
|
data = msgpack.dumps(self.msg)
|
||||||
|
with open(self.filepath, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
# Use for refreshing
|
||||||
|
|
||||||
|
def read(self, decrypt: bool = False, decryptKey=False):
|
||||||
|
"""
|
||||||
|
Read record from disk to memory
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
decrypt: bool
|
||||||
|
Whether to decrypt record
|
||||||
|
|
||||||
|
decryptKey
|
||||||
|
Key to decrypt record
|
||||||
|
"""
|
||||||
|
if os.path.isdir(self.filepath):
|
||||||
|
self.msg = "directory"
|
||||||
|
else:
|
||||||
|
with open(self.filepath, "rb") as f:
|
||||||
|
self.msg = msgpack.loads(f.read())
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
Get record dictionary from memory
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
self.msg: dict
|
||||||
|
"""
|
||||||
|
return self.msg
|
||||||
|
|
||||||
|
def sublist(self):
|
||||||
|
"""
|
||||||
|
Lists contents of directory if object is a directory, otherwise return None
|
||||||
|
"""
|
||||||
|
fpath = self.filepath
|
||||||
|
if os.path.isdir(fpath):
|
||||||
|
return ["messages/" + x for x in os.listdir(fpath)]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def loadTemplates(templatePath: str = "templates"):
|
||||||
|
"""Load templates for prefilling records
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
templatePath: str
|
||||||
|
Path to templates
|
||||||
|
"""
|
||||||
|
templates = {}
|
||||||
|
for p in os.listdir(templatePath):
|
||||||
|
p = templatePath + "/" + p
|
||||||
|
if os.path.isdir(p):
|
||||||
|
for ip in os.listdir(p):
|
||||||
|
ip = p + "/" + ip
|
||||||
|
if os.path.isdir(ip):
|
||||||
|
print("Too deep, skipping: " + ip)
|
||||||
|
else:
|
||||||
|
templates[ip] = Daisy(ip)
|
||||||
|
else:
|
||||||
|
templates[p] = Daisy(p)
|
||||||
|
self.templates = templates
|
||||||
|
return templates
|
|
@ -0,0 +1,6 @@
|
||||||
|
Schemaless binary database
|
||||||
|
base class
|
||||||
|
==========================
|
||||||
|
|
||||||
|
.. autoclass:: Daisy.Daisy.Daisy
|
||||||
|
:members:
|
|
@ -0,0 +1,47 @@
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
global garden
|
||||||
|
"""
|
||||||
|
Map of instances to list of signals
|
||||||
|
to be processed
|
||||||
|
"""
|
||||||
|
garden = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Compound(FileSystemEventHandler):
|
||||||
|
"""
|
||||||
|
File system watcher to propagate disk changes
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cache, isCatch: bool = False):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cache: Cache
|
||||||
|
Daisy cache to update
|
||||||
|
|
||||||
|
isCatch: bool
|
||||||
|
Is the cache for catchs
|
||||||
|
"""
|
||||||
|
self.cache = cache
|
||||||
|
self.isCatch = isCatch
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def on_any_event(self, event):
|
||||||
|
"""
|
||||||
|
Called when a CRUD operation is performed on a record file
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
event
|
||||||
|
Event object provided by watchdog
|
||||||
|
"""
|
||||||
|
if not (".json" in event.src_path):
|
||||||
|
if not (".md" in event.src_path):
|
||||||
|
tpath = "/".join(event.src_path.split("/")[1:])
|
||||||
|
if tpath != "":
|
||||||
|
if self.isCatch:
|
||||||
|
self.cache.sget(tpath)
|
||||||
|
else:
|
||||||
|
self.cache.get(tpath).get()
|
|
@ -0,0 +1,5 @@
|
||||||
|
Daisy signal management
|
||||||
|
==========================
|
||||||
|
|
||||||
|
.. autoclass:: Daisy.Soil.Compound
|
||||||
|
:members:
|
|
@ -0,0 +1,27 @@
|
||||||
|
from Daisy.Daisy import Daisy
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class Store(Daisy):
|
||||||
|
def __init__(self, store: str, path: str, nodeNickname: str):
|
||||||
|
fpath = "daisy/{0}/{1}".format(path, nodeNickname)
|
||||||
|
cpath = "{0}/{1}/{2}".format(path, nodeNickname, store)
|
||||||
|
if not os.path.exists(fpath):
|
||||||
|
os.mkdir(fpath)
|
||||||
|
super().__init__("daisy/" + cpath)
|
||||||
|
|
||||||
|
def update(self, entry: str, data, recur: bool = True):
|
||||||
|
if recur:
|
||||||
|
for key in data.keys():
|
||||||
|
self.msg[entry][key] = data[key]
|
||||||
|
else:
|
||||||
|
self.msg[entry] = data
|
||||||
|
self.write()
|
||||||
|
|
||||||
|
def getRecord(self, key: str):
|
||||||
|
if key in self.get().keys():
|
||||||
|
return self.get()[key]
|
||||||
|
else:
|
||||||
|
self.cLog(20, "Record does not exist")
|
||||||
|
return False
|
|
@ -0,0 +1,6 @@
|
||||||
|
Daisy based key value
|
||||||
|
store with recursion
|
||||||
|
==========================
|
||||||
|
|
||||||
|
.. autoclass:: Daisy.Store.Store
|
||||||
|
:members:
|
|
@ -0,0 +1,123 @@
|
||||||
|
import Packets.Packet as p
|
||||||
|
import Packets.HeaderPacket as h
|
||||||
|
import lzma
|
||||||
|
import msgpack
|
||||||
|
import random
|
||||||
|
import math
|
||||||
|
|
||||||
|
# DO NOT CHANGE DATA SIZE UNLESS YOU KNOW WHAT YOURE DOING
|
||||||
|
|
||||||
|
|
||||||
|
class Message:
|
||||||
|
"""
|
||||||
|
Full message which is composed of `Packets.Packet.Packet`s
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
packets: list[Packets.Packet.Packet]
|
||||||
|
List of packets making up the Message
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
bytesObject: bytes,
|
||||||
|
sender: int,
|
||||||
|
senderDisplayName: int,
|
||||||
|
recipient: int,
|
||||||
|
recipientNode: int,
|
||||||
|
dataSize: int = 128,
|
||||||
|
wantFullResponse: bool = False,
|
||||||
|
packetsClass: int = 0,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
bytesObject: bytes
|
||||||
|
Bytes to split into packets
|
||||||
|
|
||||||
|
sender: int
|
||||||
|
6 digit (maximum) node or peer ID
|
||||||
|
|
||||||
|
senderDisplayName: int
|
||||||
|
3 digit (maximum) ID for mapping display names to a given user
|
||||||
|
|
||||||
|
recipient: int
|
||||||
|
6 digit (maximum) node or peer ID
|
||||||
|
|
||||||
|
recipientNode: int
|
||||||
|
6 digit (maximum) node ID to route the packet to
|
||||||
|
|
||||||
|
dataSize: int
|
||||||
|
Size to cut the bytesObject into per packet
|
||||||
|
|
||||||
|
wantFullResponse: bool
|
||||||
|
Whether to send a response when the message has completed reception (TODO: Kill all retries for associated packets when received)
|
||||||
|
|
||||||
|
packetsClass: int
|
||||||
|
Which protocol the packets are using
|
||||||
|
"""
|
||||||
|
if isinstance(bytesObject, list):
|
||||||
|
packets = [h.Header(bytesObject[0])]
|
||||||
|
for packet in bytesObject:
|
||||||
|
packets.append(
|
||||||
|
p.Packet(
|
||||||
|
packet["data"],
|
||||||
|
packetsID=packet["packetsID"],
|
||||||
|
packetNumber=packet["packetNumber"],
|
||||||
|
packetsClass=packetsClass,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.packets = packets
|
||||||
|
else:
|
||||||
|
bytesObject = lzma.compress(bytesObject)
|
||||||
|
packets = []
|
||||||
|
self.packetsID = random.randrange(0, 999999)
|
||||||
|
pnum = 1
|
||||||
|
blen = math.ceil(len(bytesObject) / dataSize)
|
||||||
|
tb = b""
|
||||||
|
for it in range(blen):
|
||||||
|
if it >= (blen - 1):
|
||||||
|
b = bytesObject[it * dataSize :]
|
||||||
|
else:
|
||||||
|
b = bytesObject[it * dataSize : (it * dataSize + dataSize)]
|
||||||
|
packets.append(
|
||||||
|
p.Packet(b, self.packetsID, pnum, packetsClass=packetsClass)
|
||||||
|
)
|
||||||
|
pnum += 1
|
||||||
|
tb += b
|
||||||
|
packets.insert(
|
||||||
|
0,
|
||||||
|
h.Header(
|
||||||
|
self.packetsID,
|
||||||
|
pnum,
|
||||||
|
sender,
|
||||||
|
senderDisplayName,
|
||||||
|
recipient,
|
||||||
|
recipientNode,
|
||||||
|
wantFullResponse=wantFullResponse,
|
||||||
|
packetsClass=packetsClass,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for it in range(pnum):
|
||||||
|
packet = msgpack.loads(packets[it].dump())
|
||||||
|
packet["packetCount"] = pnum
|
||||||
|
|
||||||
|
packets[it] = msgpack.dumps(packet)
|
||||||
|
|
||||||
|
self.packets = packets
|
||||||
|
|
||||||
|
def get(self) -> list[p.Packet]:
|
||||||
|
"""
|
||||||
|
Get and return all packets
|
||||||
|
"""
|
||||||
|
return self.packets
|
||||||
|
|
||||||
|
def reassemble(self, completedMessage: dict):
|
||||||
|
"""
|
||||||
|
Reassemble packets from a completed message in `Sponge.base`
|
||||||
|
"""
|
||||||
|
data = b""
|
||||||
|
for it in range(1, int(completedMessage["packetCount"])):
|
||||||
|
data += completedMessage["data"][completedMessage["dataOrder"].index(it)]
|
||||||
|
res = msgpack.loads(lzma.decompress(data))
|
||||||
|
return res
|
|
@ -0,0 +1,5 @@
|
||||||
|
Full message
|
||||||
|
===============================
|
||||||
|
|
||||||
|
.. autoclass:: Packets.Message.Message
|
||||||
|
:members:
|
|
@ -0,0 +1,40 @@
|
||||||
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
|
import os, markdown2
|
||||||
|
import json, msgpack, subprocess
|
||||||
|
import shutil
|
||||||
|
from distutils.dir_util import copy_tree
|
||||||
|
|
||||||
|
env = Environment(loader=FileSystemLoader("templates"))
|
||||||
|
|
||||||
|
# subprocess.check_call("mmdc -i * -e png")
|
||||||
|
|
||||||
|
# TODO: Generating mmd from docstrings
|
||||||
|
|
||||||
|
for path in os.listdir("diagrams/markdown"):
|
||||||
|
fname = path.split(".")[0]
|
||||||
|
try:
|
||||||
|
subprocess.check_call(
|
||||||
|
"mmdc -i diagrams/markdown/{0} -o res/img/diagrams/{1}.png".format(
|
||||||
|
path, fname
|
||||||
|
),
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print("Empty file or other error")
|
||||||
|
|
||||||
|
|
||||||
|
copy_tree("diagrams/markdown", "res/diagrams")
|
||||||
|
copy_tree("res", "build/res")
|
||||||
|
shutil.copyfile("htmx-extensions/src/ws/ws.js", "build/res/js/ws.js")
|
||||||
|
|
||||||
|
tpath = "templates/"
|
||||||
|
|
||||||
|
for path in os.listdir(tpath):
|
||||||
|
if ("base" in path) != True:
|
||||||
|
for t in os.listdir(tpath + path):
|
||||||
|
if os.path.exists("build/" + path) != True:
|
||||||
|
os.makedirs("build/" + path)
|
||||||
|
ipath = tpath + path + "/" + t
|
||||||
|
template = env.get_template(path + "/" + t)
|
||||||
|
with open("build/{0}/{1}".format(path, t), "w") as f:
|
||||||
|
f.write(template.render())
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
title: "🔵 Bubble"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
"👥 Peer" |{..o| "🗄️ Server" : "🔌 WS"
|
||||||
|
"👥 Peer" |{..o| "🗄️ Server": "📄 HTTP/S"
|
||||||
|
"🗄️ Server" |o..o| "📤 Transmitter": "❔ Queries"
|
||||||
|
"📤 Transmitter" |o..o| "📻 PierMesh": "📻 send"
|
||||||
|
"📤 Transmitter" |o..o| "📻 PierMesh": "📻 onReceive"
|
||||||
|
"📤 Transmitter" |o..o| "🧽 Sieve": "📻 onReceive"
|
||||||
|
"🧽 Sieve" |o..o| "💿 Cache": "➕ Write"
|
||||||
|
"💿 Cache" |o..o| "👂 fListen": "➕ Write event"
|
||||||
|
"👂 fListen" |o..o| "🗄️ Server": "✉️ Pass message"
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
title: "🐟 Catch"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
"👥 Peer" |{..o| "🗄️ Server": "🔌 WS"
|
||||||
|
"👥 Peer" |{..o| "🗄️ Server": "📄 HTTP/S"
|
||||||
|
"🗄️ Server" |o..o| "🐟 Catch": "❔ Queries"
|
||||||
|
"📻 PierMesh" |o..o| "🧽 Sieve": "🧽 Filters"
|
||||||
|
"🧽 Sieve" |o..o| "👂 fListen": "👂 Listens for messages"
|
||||||
|
"👂 fListen" |o..o| "🐟 Catch": "❔ Queries"
|
||||||
|
"🐟 Catch" |o..}| "🌼 Daisy": "📄 Store references"
|
||||||
|
"🌼 Daisy" {
|
||||||
|
string filepath
|
||||||
|
dictionary msg
|
||||||
|
}
|
||||||
|
"🌼 Daisy" |o..o| "📁 File system": "📁 CRUD"
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: "🌼 Daisy"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
title: "📻 PierMesh"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
"👥 Peer" }|..|{ "🗄️Server" : "🔌 WS"
|
||||||
|
"👥 Peer" }|..|{ "🗄️Server": "📄 HTTP/S"
|
||||||
|
"🗄️Server" |o..o| "🐟 Catch": "❔ Queries"
|
||||||
|
"🗄️Server" |o..o| "💿 Cache": "❔ Queries"
|
||||||
|
"🗄️Server" |o..o| "📤 Transmitter": "❔ Queries"
|
||||||
|
"🐟 Catch" |o..o| "📤 Transmitter": "❔ Queries"
|
||||||
|
"🐟 Catch" |o..o| "👥 Peer": "🔌 WS"
|
||||||
|
"🐟 Catch" |o..o| "📤 Transmitter": "✉️ Sync packet"
|
||||||
|
"💿 Cache" |o..o| "📤 Transmitter": "✉️ Sync packet"
|
||||||
|
"👂 fListen" |o..o| "💿 Cache": "👂 Listen for completed messages"
|
||||||
|
"👂 fListen" |o..o| "👥 Peer": "🔌 WS"
|
||||||
|
"📤 Transmitter" |o..o| "🔽 onReceive": "✉️ Get packet"
|
||||||
|
"🔽 onReceive" |o..o| "🧽 Sieve": "🧽 Filter packet"
|
||||||
|
"🧽 Sieve" |o..o| "💿 Cache": "➕ Push completed messages"
|
||||||
|
"📤 Transmitter" |o..o| "📻 PierMesh": "📻 Send"
|
||||||
|
"📻 PierMesh" |o..o| "🔽 onReceive": "🔽 Receive"
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: "📤 Transmitter"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
|
After Width: | Height: | Size: 89 KiB |
|
@ -0,0 +1 @@
|
||||||
|
mmdc -i res/misc/dia.mmd -o res/misc/dia.png
|
|
@ -0,0 +1,10 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Nerd Font';
|
||||||
|
src: url('/res/fonts/UbuntuNF.eot');
|
||||||
|
src: url('/res/fonts/UbuntuNF.eot?#iefix') format('embedded-opentype'),
|
||||||
|
url('/res/fonts/UbuntuNF.woff2') format('woff2'),
|
||||||
|
url('/res/fonts/UbuntuNF.woff') format('woff');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
:root {
|
||||||
|
--palette-text-white: #FFFFFF;
|
||||||
|
--palette-text-black: #000000;
|
||||||
|
--palette-text-three: #3A424D;
|
||||||
|
--palette-text-four: #5B8080;
|
||||||
|
|
||||||
|
--palette-one: #3A4D24;
|
||||||
|
--palette-two: #A6B08E;
|
||||||
|
--palette-three: #879B77;
|
||||||
|
--palette-four: #61805B;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: var(--palette-one);
|
||||||
|
color: var(--palette-text-white);
|
||||||
|
font-family: 'Ubuntu Nerd Font';
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plank {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: var(--palette-two);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding: 0;
|
||||||
|
list-style-type: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
padding-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text],
|
||||||
|
input[type=number] {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
title: "🔵 Bubble"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
"👥 Peer" |{..o| "🗄️ Server" : "🔌 WS"
|
||||||
|
"👥 Peer" |{..o| "🗄️ Server": "📄 HTTP/S"
|
||||||
|
"🗄️ Server" |o..o| "📤 Transmitter": "❔ Queries"
|
||||||
|
"📤 Transmitter" |o..o| "📻 PierMesh": "📻 send"
|
||||||
|
"📤 Transmitter" |o..o| "📻 PierMesh": "📻 onReceive"
|
||||||
|
"📤 Transmitter" |o..o| "🧽 Sieve": "📻 onReceive"
|
||||||
|
"🧽 Sieve" |o..o| "💿 Cache": "➕ Write"
|
||||||
|
"💿 Cache" |o..o| "👂 fListen": "➕ Write event"
|
||||||
|
"👂 fListen" |o..o| "🗄️ Server": "✉️ Pass message"
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
title: "🐟 Catch"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
"👥 Peer" |{..o| "🗄️ Server": "🔌 WS"
|
||||||
|
"👥 Peer" |{..o| "🗄️ Server": "📄 HTTP/S"
|
||||||
|
"🗄️ Server" |o..o| "🐟 Catch": "❔ Queries"
|
||||||
|
"📻 PierMesh" |o..o| "🧽 Sieve": "🧽 Filters"
|
||||||
|
"🧽 Sieve" |o..o| "👂 fListen": "👂 Listens for messages"
|
||||||
|
"👂 fListen" |o..o| "🐟 Catch": "❔ Queries"
|
||||||
|
"🐟 Catch" |o..}| "🌼 Daisy": "📄 Store references"
|
||||||
|
"🌼 Daisy" {
|
||||||
|
string filepath
|
||||||
|
dictionary msg
|
||||||
|
}
|
||||||
|
"🌼 Daisy" |o..o| "📁 File system": "📁 CRUD"
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: "🌼 Daisy"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
title: "📻 PierMesh"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
"👥 Peer" }|..|{ "🗄️Server" : "🔌 WS"
|
||||||
|
"👥 Peer" }|..|{ "🗄️Server": "📄 HTTP/S"
|
||||||
|
"🗄️Server" |o..o| "🐟 Catch": "❔ Queries"
|
||||||
|
"🗄️Server" |o..o| "💿 Cache": "❔ Queries"
|
||||||
|
"🗄️Server" |o..o| "📤 Transmitter": "❔ Queries"
|
||||||
|
"🐟 Catch" |o..o| "📤 Transmitter": "❔ Queries"
|
||||||
|
"🐟 Catch" |o..o| "👥 Peer": "🔌 WS"
|
||||||
|
"🐟 Catch" |o..o| "📤 Transmitter": "✉️ Sync packet"
|
||||||
|
"💿 Cache" |o..o| "📤 Transmitter": "✉️ Sync packet"
|
||||||
|
"👂 fListen" |o..o| "💿 Cache": "👂 Listen for completed messages"
|
||||||
|
"👂 fListen" |o..o| "👥 Peer": "🔌 WS"
|
||||||
|
"📤 Transmitter" |o..o| "🔽 onReceive": "✉️ Get packet"
|
||||||
|
"🔽 onReceive" |o..o| "🧽 Sieve": "🧽 Filter packet"
|
||||||
|
"🧽 Sieve" |o..o| "💿 Cache": "➕ Push completed messages"
|
||||||
|
"📤 Transmitter" |o..o| "📻 PierMesh": "📻 Send"
|
||||||
|
"📻 PierMesh" |o..o| "🔽 onReceive": "🔽 Receive"
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: "📤 Transmitter"
|
||||||
|
---
|
||||||
|
erDiagram
|
||||||
|
|
After Width: | Height: | Size: 606 B |
After Width: | Height: | Size: 662 B |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 89 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.3 KiB |
|
@ -0,0 +1 @@
|
||||||
|
npx vite build
|
|
@ -0,0 +1,815 @@
|
||||||
|
{
|
||||||
|
"name": "piermesh",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "piermesh",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"htmx.org": "2.0.0",
|
||||||
|
"three": "^0.166.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^5.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-JlPfZ/C7yn5S5p0yKk7uhHTTnFlvTgLetl2VxqE518QgyM7C9bSfFTYvB/Q/ftkq0RIPY4ySxTz+/wKJ/dXC0w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-RDxUSY8D1tWYfn00DDi5myxKgOk6RvWPxhmWexcICt/MEC6yEMr4HNCu1sXXYLw8iAsg0D44NuU+qNq7zVWCrw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-emvKHL4B15x6nlNTBMtIaC9tLPRpeA5jMvRLXVbl/W9Ie7HhkrE7KQjvgS9uxgatL1HmHWDXk5TTS4IaNJxbAA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-fO28cWA1dC57qCd+D0rfLC4VPbh6EOJXrreBmFLWPGI9dpMlER2YwSPZzSGfq11XgcEpPukPTfEVFtw2q2nYJg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-2Rn36Ubxdv32NUcfm0wB1tgKqkQuft00PtM23VqLuCUR4N5jcNWDoV5iBC9jeGdgS38WK66ElncprqgMUOyomw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-gJuzIVdq/X1ZA2bHeCGCISe0VWqCoNT8BvkQ+BfsixXwTOndhtLUpOg0A1Fcx/+eA6ei6rMBzlOz4JzmiDw7JQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-0EkX2HYPkSADo9cfeGFoQ7R0/wTKb7q6DdwI4Yn/ULFE1wuRRCHybxpl2goQrx4c/yzK3I8OlgtBu4xvted0ug==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-GlIQRj9px52ISomIOEUq/IojLZqzkvRpdP3cLgIE1wUWaiU5Takwlzpz002q0Nxxr1y2ZgxC2obWxjr13lvxNQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-N6cFJzssruDLUOKfEKeovCKiHcdwVYOT1Hs6dovDQ61+Y9n3Ek4zXvtghPPelt6U0AH4aDGnDLb83uiJMkWYzQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-2DnD3mkS2uuam/alF+I7M84koGwvn3ZVD7uG+LEWpyzo/bq8+kKnus2EVCkcvh6PlNB8QPNFOz6fWd5N8o1CYg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-D6pkaF7OpE7lzlTOFCB2m3Ngzu2ykw40Nka9WmKGUOTS3xcIieHe82slQlNq69sVB04ch73thKYIWz/Ian8DUA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-HBndjQLP8OsdJNSxpNIN0einbDmRFg9+UQeZV1eiYupIRuZsDEoeGU43NQsS34Pp166DtwQOnpcbV/zQxM+rWA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-HxfbvfCKJe/RMYJJn0a12eiOI9OOtAUF4G6ozrFUK95BNyoJaSiBjIOHjZskTUffUrB84IPKkFG9H9nEvJGW6A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-HxDMKIhmcguGTiP5TsLNolwBUK3nGGUEoV/BO9ldUBoMLBssvh4J0X8pf11i1fTV7WShWItB1bKAKjX4RQeYmg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-xItlIAZZaiG/u0wooGzRsx11rokP4qyc/79LkAOdznGRAbOFc+SfEdfUOszG1odsHNgwippUJavag/+W/Etc6Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-xNo5fV5ycvCCKqiZcpB65VMR11NJB+StnxHz20jdqRAktfdfzhgjTiJ2doTDQE/7dqGaV5I7ZGqKpgph6lCIag==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@types/estree": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.21.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||||
|
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.21.5",
|
||||||
|
"@esbuild/android-arm": "0.21.5",
|
||||||
|
"@esbuild/android-arm64": "0.21.5",
|
||||||
|
"@esbuild/android-x64": "0.21.5",
|
||||||
|
"@esbuild/darwin-arm64": "0.21.5",
|
||||||
|
"@esbuild/darwin-x64": "0.21.5",
|
||||||
|
"@esbuild/freebsd-arm64": "0.21.5",
|
||||||
|
"@esbuild/freebsd-x64": "0.21.5",
|
||||||
|
"@esbuild/linux-arm": "0.21.5",
|
||||||
|
"@esbuild/linux-arm64": "0.21.5",
|
||||||
|
"@esbuild/linux-ia32": "0.21.5",
|
||||||
|
"@esbuild/linux-loong64": "0.21.5",
|
||||||
|
"@esbuild/linux-mips64el": "0.21.5",
|
||||||
|
"@esbuild/linux-ppc64": "0.21.5",
|
||||||
|
"@esbuild/linux-riscv64": "0.21.5",
|
||||||
|
"@esbuild/linux-s390x": "0.21.5",
|
||||||
|
"@esbuild/linux-x64": "0.21.5",
|
||||||
|
"@esbuild/netbsd-x64": "0.21.5",
|
||||||
|
"@esbuild/openbsd-x64": "0.21.5",
|
||||||
|
"@esbuild/sunos-x64": "0.21.5",
|
||||||
|
"@esbuild/win32-arm64": "0.21.5",
|
||||||
|
"@esbuild/win32-ia32": "0.21.5",
|
||||||
|
"@esbuild/win32-x64": "0.21.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/htmx.org": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-N0r1VjrqeCpig0mTi2/sooDZBeQlp1RBohnWQ/ufqc7ICaI0yjs04fNGhawm6+/HWhJFlcXn8MqOjWI9QGG2lQ=="
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.4.39",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
||||||
|
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.7",
|
||||||
|
"picocolors": "^1.0.1",
|
||||||
|
"source-map-js": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rollup": {
|
||||||
|
"version": "4.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.0.tgz",
|
||||||
|
"integrity": "sha512-5r7EYSQIowHsK4eTZ0Y81qpZuJz+MUuYeqmmYmRMl1nwhdmbiYqt5jwzf6u7wyOzJgYqtCRMtVRKOtHANBz7rA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "1.0.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rollup": "dist/bin/rollup"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-android-arm-eabi": "4.19.0",
|
||||||
|
"@rollup/rollup-android-arm64": "4.19.0",
|
||||||
|
"@rollup/rollup-darwin-arm64": "4.19.0",
|
||||||
|
"@rollup/rollup-darwin-x64": "4.19.0",
|
||||||
|
"@rollup/rollup-linux-arm-gnueabihf": "4.19.0",
|
||||||
|
"@rollup/rollup-linux-arm-musleabihf": "4.19.0",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "4.19.0",
|
||||||
|
"@rollup/rollup-linux-arm64-musl": "4.19.0",
|
||||||
|
"@rollup/rollup-linux-powerpc64le-gnu": "4.19.0",
|
||||||
|
"@rollup/rollup-linux-riscv64-gnu": "4.19.0",
|
||||||
|
"@rollup/rollup-linux-s390x-gnu": "4.19.0",
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.19.0",
|
||||||
|
"@rollup/rollup-linux-x64-musl": "4.19.0",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.19.0",
|
||||||
|
"@rollup/rollup-win32-ia32-msvc": "4.19.0",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.19.0",
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/three": {
|
||||||
|
"version": "0.166.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/three/-/three-0.166.1.tgz",
|
||||||
|
"integrity": "sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg=="
|
||||||
|
},
|
||||||
|
"node_modules/vite": {
|
||||||
|
"version": "5.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz",
|
||||||
|
"integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "^0.21.3",
|
||||||
|
"postcss": "^8.4.39",
|
||||||
|
"rollup": "^4.13.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite": "bin/vite.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.0.0 || >=20.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": "^18.0.0 || >=20.0.0",
|
||||||
|
"less": "*",
|
||||||
|
"lightningcss": "^1.21.0",
|
||||||
|
"sass": "*",
|
||||||
|
"stylus": "*",
|
||||||
|
"sugarss": "*",
|
||||||
|
"terser": "^5.4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"less": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"lightningcss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sass": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"stylus": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"sugarss": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"terser": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "piermesh",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"htmx.org": "2.0.0",
|
||||||
|
"three": "^0.166.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^5.3.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,467 @@
|
||||||
|
/*
|
||||||
|
WebSockets Extension
|
||||||
|
============================
|
||||||
|
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
/** @type {import("../htmx").HtmxInternalApi} */
|
||||||
|
var api
|
||||||
|
|
||||||
|
htmx.defineExtension('ws', {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* init is called once, when this extension is first registered.
|
||||||
|
* @param {import("../htmx").HtmxInternalApi} apiRef
|
||||||
|
*/
|
||||||
|
init: function(apiRef) {
|
||||||
|
// Store reference to internal API
|
||||||
|
api = apiRef
|
||||||
|
|
||||||
|
// Default function for creating new EventSource objects
|
||||||
|
if (!htmx.createWebSocket) {
|
||||||
|
htmx.createWebSocket = createWebSocket
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default setting for reconnect delay
|
||||||
|
if (!htmx.config.wsReconnectDelay) {
|
||||||
|
htmx.config.wsReconnectDelay = 'full-jitter'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* onEvent handles all events passed to this extension.
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {Event} evt
|
||||||
|
*/
|
||||||
|
onEvent: function(name, evt) {
|
||||||
|
var parent = evt.target || evt.detail.elt
|
||||||
|
switch (name) {
|
||||||
|
// Try to close the socket when elements are removed
|
||||||
|
case 'htmx:beforeCleanupElement':
|
||||||
|
|
||||||
|
var internalData = api.getInternalData(parent)
|
||||||
|
|
||||||
|
if (internalData.webSocket) {
|
||||||
|
internalData.webSocket.close()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
// Try to create websockets when elements are processed
|
||||||
|
case 'htmx:beforeProcessNode':
|
||||||
|
|
||||||
|
forEach(queryAttributeOnThisOrChildren(parent, 'ws-connect'), function(child) {
|
||||||
|
ensureWebSocket(child)
|
||||||
|
})
|
||||||
|
forEach(queryAttributeOnThisOrChildren(parent, 'ws-send'), function(child) {
|
||||||
|
ensureWebSocketSend(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function splitOnWhitespace(trigger) {
|
||||||
|
return trigger.trim().split(/\s+/)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLegacyWebsocketURL(elt) {
|
||||||
|
var legacySSEValue = api.getAttributeValue(elt, 'hx-ws')
|
||||||
|
if (legacySSEValue) {
|
||||||
|
var values = splitOnWhitespace(legacySSEValue)
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
var value = values[i].split(/:(.+)/)
|
||||||
|
if (value[0] === 'connect') {
|
||||||
|
return value[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ensureWebSocket creates a new WebSocket on the designated element, using
|
||||||
|
* the element's "ws-connect" attribute.
|
||||||
|
* @param {HTMLElement} socketElt
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function ensureWebSocket(socketElt) {
|
||||||
|
// If the element containing the WebSocket connection no longer exists, then
|
||||||
|
// do not connect/reconnect the WebSocket.
|
||||||
|
if (!api.bodyContains(socketElt)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the source straight from the element's value
|
||||||
|
var wssSource = api.getAttributeValue(socketElt, 'ws-connect')
|
||||||
|
|
||||||
|
if (wssSource == null || wssSource === '') {
|
||||||
|
var legacySource = getLegacyWebsocketURL(socketElt)
|
||||||
|
if (legacySource == null) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
wssSource = legacySource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guarantee that the wssSource value is a fully qualified URL
|
||||||
|
if (wssSource.indexOf('/') === 0) {
|
||||||
|
var base_part = location.hostname + (location.port ? ':' + location.port : '')
|
||||||
|
if (location.protocol === 'https:') {
|
||||||
|
wssSource = 'wss://' + base_part + wssSource
|
||||||
|
} else if (location.protocol === 'http:') {
|
||||||
|
wssSource = 'ws://' + base_part + wssSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var socketWrapper = createWebsocketWrapper(socketElt, function() {
|
||||||
|
return htmx.createWebSocket(wssSource)
|
||||||
|
})
|
||||||
|
|
||||||
|
socketWrapper.addEventListener('message', function(event) {
|
||||||
|
if (maybeCloseWebSocketSource(socketElt)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = event.data
|
||||||
|
if (!api.triggerEvent(socketElt, 'htmx:wsBeforeMessage', {
|
||||||
|
message: response,
|
||||||
|
socketWrapper: socketWrapper.publicInterface
|
||||||
|
})) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.withExtensions(socketElt, function(extension) {
|
||||||
|
response = extension.transformResponse(response, null, socketElt)
|
||||||
|
})
|
||||||
|
|
||||||
|
var settleInfo = api.makeSettleInfo(socketElt)
|
||||||
|
var fragment = api.makeFragment(response)
|
||||||
|
|
||||||
|
if (fragment.children.length) {
|
||||||
|
var children = Array.from(fragment.children)
|
||||||
|
for (var i = 0; i < children.length; i++) {
|
||||||
|
api.oobSwap(api.getAttributeValue(children[i], 'hx-swap-oob') || 'true', children[i], settleInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
api.settleImmediately(settleInfo.tasks)
|
||||||
|
api.triggerEvent(socketElt, 'htmx:wsAfterMessage', { message: response, socketWrapper: socketWrapper.publicInterface })
|
||||||
|
})
|
||||||
|
|
||||||
|
// Put the WebSocket into the HTML Element's custom data.
|
||||||
|
api.getInternalData(socketElt).webSocket = socketWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} WebSocketWrapper
|
||||||
|
* @property {WebSocket} socket
|
||||||
|
* @property {Array<{message: string, sendElt: Element}>} messageQueue
|
||||||
|
* @property {number} retryCount
|
||||||
|
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
|
||||||
|
* @property {(message: string, sendElt: Element) => void} send
|
||||||
|
* @property {(event: string, handler: Function) => void} addEventListener
|
||||||
|
* @property {() => void} handleQueuedMessages
|
||||||
|
* @property {() => void} init
|
||||||
|
* @property {() => void} close
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param socketElt
|
||||||
|
* @param socketFunc
|
||||||
|
* @returns {WebSocketWrapper}
|
||||||
|
*/
|
||||||
|
function createWebsocketWrapper(socketElt, socketFunc) {
|
||||||
|
var wrapper = {
|
||||||
|
socket: null,
|
||||||
|
messageQueue: [],
|
||||||
|
retryCount: 0,
|
||||||
|
|
||||||
|
/** @type {Object<string, Function[]>} */
|
||||||
|
events: {},
|
||||||
|
|
||||||
|
addEventListener: function(event, handler) {
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.addEventListener(event, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.events[event]) {
|
||||||
|
this.events[event] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
this.events[event].push(handler)
|
||||||
|
},
|
||||||
|
|
||||||
|
sendImmediately: function(message, sendElt) {
|
||||||
|
if (!this.socket) {
|
||||||
|
api.triggerErrorEvent()
|
||||||
|
}
|
||||||
|
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
|
||||||
|
message,
|
||||||
|
socketWrapper: this.publicInterface
|
||||||
|
})) {
|
||||||
|
this.socket.send(message)
|
||||||
|
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
|
||||||
|
message,
|
||||||
|
socketWrapper: this.publicInterface
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
send: function(message, sendElt) {
|
||||||
|
if (this.socket.readyState !== this.socket.OPEN) {
|
||||||
|
this.messageQueue.push({ message, sendElt })
|
||||||
|
} else {
|
||||||
|
this.sendImmediately(message, sendElt)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleQueuedMessages: function() {
|
||||||
|
while (this.messageQueue.length > 0) {
|
||||||
|
var queuedItem = this.messageQueue[0]
|
||||||
|
if (this.socket.readyState === this.socket.OPEN) {
|
||||||
|
this.sendImmediately(queuedItem.message, queuedItem.sendElt)
|
||||||
|
this.messageQueue.shift()
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
init: function() {
|
||||||
|
if (this.socket && this.socket.readyState === this.socket.OPEN) {
|
||||||
|
// Close discarded socket
|
||||||
|
this.socket.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new WebSocket and event handlers
|
||||||
|
/** @type {WebSocket} */
|
||||||
|
var socket = socketFunc()
|
||||||
|
|
||||||
|
// The event.type detail is added for interface conformance with the
|
||||||
|
// other two lifecycle events (open and close) so a single handler method
|
||||||
|
// can handle them polymorphically, if required.
|
||||||
|
api.triggerEvent(socketElt, 'htmx:wsConnecting', { event: { type: 'connecting' } })
|
||||||
|
|
||||||
|
this.socket = socket
|
||||||
|
|
||||||
|
socket.onopen = function(e) {
|
||||||
|
wrapper.retryCount = 0
|
||||||
|
api.triggerEvent(socketElt, 'htmx:wsOpen', { event: e, socketWrapper: wrapper.publicInterface })
|
||||||
|
wrapper.handleQueuedMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onclose = function(e) {
|
||||||
|
// If socket should not be connected, stop further attempts to establish connection
|
||||||
|
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
|
||||||
|
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
|
||||||
|
var delay = getWebSocketReconnectDelay(wrapper.retryCount)
|
||||||
|
setTimeout(function() {
|
||||||
|
wrapper.retryCount += 1
|
||||||
|
wrapper.init()
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify client code that connection has been closed. Client code can inspect `event` field
|
||||||
|
// to determine whether closure has been valid or abnormal
|
||||||
|
api.triggerEvent(socketElt, 'htmx:wsClose', { event: e, socketWrapper: wrapper.publicInterface })
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.onerror = function(e) {
|
||||||
|
api.triggerErrorEvent(socketElt, 'htmx:wsError', { error: e, socketWrapper: wrapper })
|
||||||
|
maybeCloseWebSocketSource(socketElt)
|
||||||
|
}
|
||||||
|
|
||||||
|
var events = this.events
|
||||||
|
Object.keys(events).forEach(function(k) {
|
||||||
|
events[k].forEach(function(e) {
|
||||||
|
socket.addEventListener(k, e)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
close: function() {
|
||||||
|
this.socket.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.init()
|
||||||
|
|
||||||
|
wrapper.publicInterface = {
|
||||||
|
send: wrapper.send.bind(wrapper),
|
||||||
|
sendImmediately: wrapper.sendImmediately.bind(wrapper),
|
||||||
|
queue: wrapper.messageQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ensureWebSocketSend attaches trigger handles to elements with
|
||||||
|
* "ws-send" attribute
|
||||||
|
* @param {HTMLElement} elt
|
||||||
|
*/
|
||||||
|
function ensureWebSocketSend(elt) {
|
||||||
|
var legacyAttribute = api.getAttributeValue(elt, 'hx-ws')
|
||||||
|
if (legacyAttribute && legacyAttribute !== 'send') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
|
||||||
|
processWebSocketSend(webSocketParent, elt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hasWebSocket function checks if a node has webSocket instance attached
|
||||||
|
* @param {HTMLElement} node
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function hasWebSocket(node) {
|
||||||
|
return api.getInternalData(node).webSocket != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* processWebSocketSend adds event listeners to the <form> element so that
|
||||||
|
* messages can be sent to the WebSocket server when the form is submitted.
|
||||||
|
* @param {HTMLElement} socketElt
|
||||||
|
* @param {HTMLElement} sendElt
|
||||||
|
*/
|
||||||
|
function processWebSocketSend(socketElt, sendElt) {
|
||||||
|
var nodeData = api.getInternalData(sendElt)
|
||||||
|
var triggerSpecs = api.getTriggerSpecs(sendElt)
|
||||||
|
triggerSpecs.forEach(function(ts) {
|
||||||
|
api.addTriggerHandler(sendElt, ts, nodeData, function(elt, evt) {
|
||||||
|
if (maybeCloseWebSocketSource(socketElt)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {WebSocketWrapper} */
|
||||||
|
var socketWrapper = api.getInternalData(socketElt).webSocket
|
||||||
|
var headers = api.getHeaders(sendElt, api.getTarget(sendElt))
|
||||||
|
var results = api.getInputValues(sendElt, 'post')
|
||||||
|
var errors = results.errors
|
||||||
|
var rawParameters = Object.assign({}, results.values)
|
||||||
|
var expressionVars = api.getExpressionVars(sendElt)
|
||||||
|
var allParameters = api.mergeObjects(rawParameters, expressionVars)
|
||||||
|
var filteredParameters = api.filterValues(allParameters, sendElt)
|
||||||
|
|
||||||
|
var sendConfig = {
|
||||||
|
parameters: filteredParameters,
|
||||||
|
unfilteredParameters: allParameters,
|
||||||
|
headers,
|
||||||
|
errors,
|
||||||
|
|
||||||
|
triggeringEvent: evt,
|
||||||
|
messageBody: undefined,
|
||||||
|
socketWrapper: socketWrapper.publicInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
api.triggerEvent(elt, 'htmx:validation:halted', errors)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = sendConfig.messageBody
|
||||||
|
if (body === undefined) {
|
||||||
|
var toSend = Object.assign({}, sendConfig.parameters)
|
||||||
|
if (sendConfig.headers) { toSend.HEADERS = headers }
|
||||||
|
body = JSON.stringify(toSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
socketWrapper.send(body, elt)
|
||||||
|
|
||||||
|
if (evt && api.shouldCancel(evt, elt)) {
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
|
||||||
|
* @param {number} retryCount // The number of retries that have already taken place
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function getWebSocketReconnectDelay(retryCount) {
|
||||||
|
/** @type {"full-jitter" | ((retryCount:number) => number)} */
|
||||||
|
var delay = htmx.config.wsReconnectDelay
|
||||||
|
if (typeof delay === 'function') {
|
||||||
|
return delay(retryCount)
|
||||||
|
}
|
||||||
|
if (delay === 'full-jitter') {
|
||||||
|
var exp = Math.min(retryCount, 6)
|
||||||
|
var maxDelay = 1000 * Math.pow(2, exp)
|
||||||
|
return maxDelay * Math.random()
|
||||||
|
}
|
||||||
|
|
||||||
|
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
|
||||||
|
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
|
||||||
|
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
|
||||||
|
* returns FALSE.
|
||||||
|
*
|
||||||
|
* @param {*} elt
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function maybeCloseWebSocketSource(elt) {
|
||||||
|
if (!api.bodyContains(elt)) {
|
||||||
|
api.getInternalData(elt).webSocket.close()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createWebSocket is the default method for creating new WebSocket objects.
|
||||||
|
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
|
||||||
|
*
|
||||||
|
* @param {string} url
|
||||||
|
* @returns WebSocket
|
||||||
|
*/
|
||||||
|
function createWebSocket(url) {
|
||||||
|
var sock = new WebSocket(url, [])
|
||||||
|
sock.binaryType = htmx.config.wsBinaryType
|
||||||
|
return sock
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} elt
|
||||||
|
* @param {string} attributeName
|
||||||
|
*/
|
||||||
|
function queryAttributeOnThisOrChildren(elt, attributeName) {
|
||||||
|
var result = []
|
||||||
|
|
||||||
|
// If the parent element also contains the requested attribute, then add it to the results too.
|
||||||
|
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, 'hx-ws')) {
|
||||||
|
result.push(elt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search all child nodes that match the requested attribute
|
||||||
|
elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + '], [data-hx-ws], [hx-ws]').forEach(function(node) {
|
||||||
|
result.push(node)
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T[]} arr
|
||||||
|
* @param {(T) => void} func
|
||||||
|
*/
|
||||||
|
function forEach(arr, func) {
|
||||||
|
if (arr) {
|
||||||
|
for (var i = 0; i < arr.length; i++) {
|
||||||
|
func(arr[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
|
@ -0,0 +1,175 @@
|
||||||
|
from microdot import Microdot
|
||||||
|
from microdot import send_file
|
||||||
|
from microdot.websocket import with_websocket
|
||||||
|
from microdot import Request
|
||||||
|
|
||||||
|
import random
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import msgpack
|
||||||
|
|
||||||
|
# Enable 500 kB files in the webui
|
||||||
|
Request.max_content_length = 1024 * 1024 * 0.5
|
||||||
|
Request.max_body_length = 1024 * 1024 * 0.5
|
||||||
|
Request.max_readline = 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
class Server:
|
||||||
|
"""
|
||||||
|
Web server that serves the web ui and provides web to node communication
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
cLog
|
||||||
|
Reference to `run.Node.cLog` for logging
|
||||||
|
|
||||||
|
transmitter: Transmission.transmission.Transmitter
|
||||||
|
Reference to our `Transmission.transmission.Transmitter` instance
|
||||||
|
|
||||||
|
network: Siph.Network.Network
|
||||||
|
Reference to our `Siph.Network.Network`
|
||||||
|
|
||||||
|
nodeID: str
|
||||||
|
String converted PierMesh node ID
|
||||||
|
|
||||||
|
peerIDs: dict
|
||||||
|
Map of peer IDs to Websocket sessions
|
||||||
|
|
||||||
|
app
|
||||||
|
Microdot server instance
|
||||||
|
|
||||||
|
catch: Daisy.Catch.Catch
|
||||||
|
Reference to our Catch Cache instance to pull from for serving Catchs
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, transceiver, catch, onodeID, network, cLog):
|
||||||
|
self.cLog = cLog
|
||||||
|
self.transceiver = transceiver
|
||||||
|
self.network = network
|
||||||
|
self.network.addLookup(onodeID, self.transceiver.interface.localNode.nodeNum)
|
||||||
|
self.nodeID = str(onodeID)
|
||||||
|
self.peerIDs = {}
|
||||||
|
self.app = Microdot()
|
||||||
|
self.catch = catch
|
||||||
|
# self.nmap = {self.nodeID: self.t.interface.localNode.nodeNum}
|
||||||
|
# self.cLog(20, "Initialized server")
|
||||||
|
|
||||||
|
@self.app.route("/res/<path:path>")
|
||||||
|
async def static(request, path):
|
||||||
|
"""
|
||||||
|
Static resources endpoint
|
||||||
|
"""
|
||||||
|
if ".." in path:
|
||||||
|
# directory traversal is not allowed
|
||||||
|
return "Not found", 404
|
||||||
|
return send_file("webui/build/res/" + path, max_age=86400)
|
||||||
|
|
||||||
|
@self.app.route("/bubble")
|
||||||
|
@with_websocket
|
||||||
|
async def bubble(request, ws):
|
||||||
|
"""
|
||||||
|
Websocket handler that bridges HTMX to our transmitter
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
`🔗 HTMX docs <https://htmx.org/docs/>`_
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
r = await ws.receive()
|
||||||
|
message = json.loads(r)
|
||||||
|
trigger = message["HEADERS"]["HX-Trigger"]
|
||||||
|
# TODO: Drop old id from cache on regen
|
||||||
|
if trigger == "gpID":
|
||||||
|
peerID = str(random.randrange(0, 1000000)).zfill(6)
|
||||||
|
await ws.send(
|
||||||
|
"""
|
||||||
|
<p id="vpeerID">Peer ID: {0}</p>
|
||||||
|
""".format(
|
||||||
|
peerID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await ws.send(
|
||||||
|
"""
|
||||||
|
<input id="peerID" type="hidden" value="{0}" >
|
||||||
|
""".format(
|
||||||
|
peerID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await ws.send(
|
||||||
|
"""
|
||||||
|
<p id="vnodeID">Node ID: {0}</p>
|
||||||
|
""".format(
|
||||||
|
self.nodeID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await ws.send(
|
||||||
|
""" <input id="nodeID" type="hidden" value="{0}" >""".format(
|
||||||
|
self.nodeID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await ws.send(
|
||||||
|
"<input id='gID' type='hidden' value='{0}' hx-swap-oob='true'>".format(
|
||||||
|
peerID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await ws.send(
|
||||||
|
"<input type='hidden' name='eID' value='{0}' hx-swap-oob='true'>".format(
|
||||||
|
peerID
|
||||||
|
)
|
||||||
|
)
|
||||||
|
peer = {"usedLast": round(time.time() * 1000), "ws": ws}
|
||||||
|
self.peerIDs[peerID] = peer
|
||||||
|
elif trigger == "bubble":
|
||||||
|
sender = message["bID"]
|
||||||
|
data = message["chat_message"]
|
||||||
|
# TODO: Setting sender name id
|
||||||
|
# senderName = message["senderID"]
|
||||||
|
senderName = 000000
|
||||||
|
recipient = message["recipientID"]
|
||||||
|
recipientNode = message["recipientNode"]
|
||||||
|
await self.t.addPackets(
|
||||||
|
msgpack.dumps({"data": data}),
|
||||||
|
sender,
|
||||||
|
senderName,
|
||||||
|
recipient,
|
||||||
|
int(recipientNode),
|
||||||
|
directID=self.network.doLookup(recipientNode),
|
||||||
|
packetsClass=2,
|
||||||
|
)
|
||||||
|
elif trigger == "catch":
|
||||||
|
res = self.catch.get(message["head"], message["body"])
|
||||||
|
await ws.send('<div id="catchDisplay">{0}</div>'.format(res))
|
||||||
|
# TODO: Catch update packets
|
||||||
|
elif trigger == "catchEdit":
|
||||||
|
self.catch.addc(
|
||||||
|
message["eID"],
|
||||||
|
self.nodeID,
|
||||||
|
message["sep"],
|
||||||
|
message["head"],
|
||||||
|
message["body"],
|
||||||
|
{"html": message["catchContent"]},
|
||||||
|
)
|
||||||
|
await ws.send(
|
||||||
|
"""
|
||||||
|
<ul id="resultsCatch" hx-swap-oob='true'><li>OK</li></ul>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await ws.send(
|
||||||
|
"""<div id="chat_room" hx-swap-oob="beforeend">hi</div>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
@self.app.route("/")
|
||||||
|
async def index(request):
|
||||||
|
"""
|
||||||
|
Static handler to serve the web ui
|
||||||
|
"""
|
||||||
|
return send_file("webui/build/index/index.html")
|
||||||
|
|
||||||
|
async def sendToPeer(self, peerID: str, data: str):
|
||||||
|
"""
|
||||||
|
Send data to Websocket of peer with peerID
|
||||||
|
"""
|
||||||
|
await self.peerIDs[peerID]["ws"].send(
|
||||||
|
"<ul id='chat_room' hx-swap-oob='afterend'><li>{0}</li></ul>".format(data)
|
||||||
|
)
|
|
@ -0,0 +1,5 @@
|
||||||
|
serve: Web UI server
|
||||||
|
============================
|
||||||
|
|
||||||
|
.. autoclass:: Splash.serve.Server
|
||||||
|
:members:
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "shared/base.html" %}
|
||||||
|
{% block body %}
|
||||||
|
|
||||||
|
<img alt="PierMesh logo" height="128px" src="/res/img/logo.png">
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
{% include "shared/catch.nav.html" %}
|
||||||
|
<br>
|
||||||
|
{% include "shared/catch.editor.html" %}
|
||||||
|
<div hx-history="false">
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="plank" hx-ext="ws" ws-connect="/bubble">
|
||||||
|
<p id="vpeerID">Peer ID:</p>
|
||||||
|
<input id="peerID" type="hidden" >
|
||||||
|
<p id="vnodeID">Node ID:</p>
|
||||||
|
<input id="peerID" type="hidden" >
|
||||||
|
<button id="gpID" ws-send>Connect</button>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
{% include "shared/messenger.html" %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/res/css/fonts.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="/res/css/style.css">
|
||||||
|
<script src="/res/js/node_modules/htmx.org/dist/htmx.min.js"></script>
|
||||||
|
<script src="/res/js/ws.js">
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% include "shared/nav.html" %}
|
||||||
|
{% block body %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<div class="plank" hx-ext="ws" ws-connect="/bubble">
|
||||||
|
<img src="/res/img/catchdisplay.png">
|
||||||
|
<br>
|
||||||
|
Catch publisher<br>
|
||||||
|
<ul id="resultsCatch">
|
||||||
|
</ul>
|
||||||
|
<form id="catchEdit" ws-send>
|
||||||
|
Head <br> <input type="text" name="head" size="4" maxlength="4"><br>
|
||||||
|
Seperator <br> <input type="text" name="sep" size="1" maxlength="1"><br>
|
||||||
|
Body <br> <input type="text" name="body" size="16" maxlength="16"><br>
|
||||||
|
Fins<br>
|
||||||
|
<ul id="fins">
|
||||||
|
<li class="fin"> <input type="text" size="8" maxlength="8"> </li>
|
||||||
|
<li><button>+</button></li>
|
||||||
|
</ul>
|
||||||
|
Content
|
||||||
|
<br>
|
||||||
|
<textarea style="min-width: 200px;min-height:200px;" name="catchContent"></textarea>
|
||||||
|
<br>
|
||||||
|
<button onclick="document.getElementById('eID').value = document.getElementById('peerID').value">Publish</button>
|
||||||
|
<input type="hidden" name="eID" id="eID">
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1 @@
|
||||||
|
<div style="background-color: var(--palette-three);" id="catchDisplay"></div>
|
|
@ -0,0 +1,31 @@
|
||||||
|
<div class="plank" hx-ext="ws" ws-connect="/bubble">
|
||||||
|
<img src="/res/img/catchdisplay.png">
|
||||||
|
<br>
|
||||||
|
Catch<br><br>
|
||||||
|
<form id="catch" ws-send>
|
||||||
|
<label for="head">Head (max. 4 characters)</label>
|
||||||
|
<br>
|
||||||
|
<input type="text" id="head" name="head" size="4" maxlength="4">
|
||||||
|
<br>
|
||||||
|
<label for="sep">Seperator</label>
|
||||||
|
<br>
|
||||||
|
<input type="text" id="sep" name="sep" size="1" maxlength="1"><br>
|
||||||
|
<label for="body">Body (max. 16 characters)</label>
|
||||||
|
<br>
|
||||||
|
<input type="text" id="body" name="body" size="16" maxlength="16">
|
||||||
|
<ul id="fins">
|
||||||
|
Fins:
|
||||||
|
<li class="fin">
|
||||||
|
<input type="text" size="8" maxlength="8">
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button>+</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button>Get</button>
|
||||||
|
</form>
|
||||||
|
Results:
|
||||||
|
<br>
|
||||||
|
{% include "shared/catch.html" %}
|
||||||
|
<br>
|
||||||
|
</div>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<div class="plank" hx-ext="ws" ws-connect="/bubble">
|
||||||
|
<img src="/res/img/bubbledisplay.png">
|
||||||
|
<br>
|
||||||
|
Bubble
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
Responses: <ul id="chat_room" hx-swap="afterend">
|
||||||
|
</ul>
|
||||||
|
<br>
|
||||||
|
<form id="bubble" ws-send>
|
||||||
|
Peer ID:<br>
|
||||||
|
<input name="recipientID" id="recipientID" type="number" max="999999"><br>
|
||||||
|
Node ID:<br>
|
||||||
|
<input name="recipientNode" id="recipientNode" type="number" max="999999"><br>
|
||||||
|
Data<br> <textarea style="min-width: 200px;min-height: 200px;" type="textarea" name="chat_message"></textarea>
|
||||||
|
<br>
|
||||||
|
<input type="hidden" name="bID" id="bID">
|
||||||
|
<button onclick="document.getElementById('bID').value = document.getElementById('peerID').value">Send</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1,302 @@
|
||||||
|
import meshtastic
|
||||||
|
import meshtastic.serial_interface
|
||||||
|
from pubsub import pub
|
||||||
|
from Packets.Message import Message
|
||||||
|
import time
|
||||||
|
|
||||||
|
import msgpack
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
class Transceiver:
|
||||||
|
"""
|
||||||
|
Handling LoRa transceiving
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
cLog
|
||||||
|
Reference to `run.Node.cLog` for logging
|
||||||
|
|
||||||
|
cryptographyInfo: Cryptography.WhaleSong.DHEFern
|
||||||
|
Cryptography instance for encrypting transmissions
|
||||||
|
|
||||||
|
filter: Sponge.base.Filter
|
||||||
|
`Sponge.base.Filter` instance for filtering packets
|
||||||
|
|
||||||
|
tcache: Daisy.Cache.Cache
|
||||||
|
Data backend Daisy Cache
|
||||||
|
|
||||||
|
tcatch: Daisy.Catch.Catch
|
||||||
|
Daisy Catch Cache for Catch operations
|
||||||
|
|
||||||
|
notConnected: bool
|
||||||
|
Whether the transceiver has been connected to yet
|
||||||
|
|
||||||
|
acks: dict
|
||||||
|
Acknowledgements received per packet
|
||||||
|
|
||||||
|
onodeID
|
||||||
|
PierMesh node ID
|
||||||
|
|
||||||
|
messages: dict
|
||||||
|
Message completion acknowldgements
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
TODO: Check if we can remove cpid
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, device, filter, onodeID, cache, catch, cryptographyInfo, cLog):
|
||||||
|
self.cLog = cLog
|
||||||
|
self.cryptographyInfo = cryptographyInfo
|
||||||
|
self.filter = filter
|
||||||
|
self.tcache = cache
|
||||||
|
self.tcatch = catch
|
||||||
|
self.notConnected = True
|
||||||
|
self.messages = {}
|
||||||
|
self.acks = {}
|
||||||
|
self.onodeID = onodeID
|
||||||
|
# Be careful with this
|
||||||
|
self.cpid = 0
|
||||||
|
self.tasks = {}
|
||||||
|
# TODO: use node id to deliver directly
|
||||||
|
pub.subscribe(self.onReceive, "meshtastic.receive")
|
||||||
|
pub.subscribe(self.onConnection, "meshtastic.connection.established")
|
||||||
|
self.interface = meshtastic.serial_interface.SerialInterface(device)
|
||||||
|
i = 0
|
||||||
|
while self.notConnected:
|
||||||
|
if i % 5000000 == 0:
|
||||||
|
self.cLog(20, "Waiting for node initialization...")
|
||||||
|
i += 1
|
||||||
|
self.cLog(20, "Initialized")
|
||||||
|
|
||||||
|
# TODO: Sending packets across multiple nodes/load balancing/distributed packet transmission/reception
|
||||||
|
def onReceive(self, packet, interface):
|
||||||
|
"""
|
||||||
|
Run each received packet through Sponge.base.Filters sieve using a new event loop
|
||||||
|
"""
|
||||||
|
asyncio.new_event_loop().run_until_complete(self.filter.sieve(packet))
|
||||||
|
self.tcache.refresh()
|
||||||
|
|
||||||
|
async def sendAnnounce(self):
|
||||||
|
"""
|
||||||
|
Send an announce packet (contains basic network mapping information) every so often so new nodes autoconnect
|
||||||
|
"""
|
||||||
|
await self.addPackets(
|
||||||
|
msgpack.dumps(
|
||||||
|
{
|
||||||
|
"onodeID": self.onodeID,
|
||||||
|
"mnodeID": self.interface.localNode.nodeNum,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
self.onodeID,
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
None,
|
||||||
|
packetsClass=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def onConnection(self, interface, topic=pub.AUTO_TOPIC):
|
||||||
|
"""
|
||||||
|
When the node connects start announce loop and end the waiting loop
|
||||||
|
"""
|
||||||
|
asyncio.run(self.sendAnnounce())
|
||||||
|
self.notConnected = False
|
||||||
|
|
||||||
|
def responseCheck(self, packet):
|
||||||
|
"""
|
||||||
|
On acknowldgement response set acks based on response
|
||||||
|
"""
|
||||||
|
rid = packet["decoded"]["requestId"]
|
||||||
|
if packet["decoded"]["routing"]["errorReason"] == "MAX_RETRANSMIT":
|
||||||
|
self.cLog(20, "Got ack error")
|
||||||
|
self.acks[str(rid)] = False
|
||||||
|
else:
|
||||||
|
self.acks[str(rid)] = True
|
||||||
|
|
||||||
|
# TODO: Threaded send method
|
||||||
|
|
||||||
|
def send(self, packet, recipientNode=False):
|
||||||
|
"""
|
||||||
|
Send individual packet
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
recipientNode
|
||||||
|
If set send to specified node
|
||||||
|
"""
|
||||||
|
interface = self.interface
|
||||||
|
if recipientNode == False:
|
||||||
|
pid = interface.sendData(
|
||||||
|
packet, wantAck=True, onResponse=self.responseCheck
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pid = interface.sendData(
|
||||||
|
packet,
|
||||||
|
destinationId=recipientNode,
|
||||||
|
wantAck=True,
|
||||||
|
onResponse=self.responseCheck,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Can I use waitForAckNak on cpid?
|
||||||
|
self.cpid = pid.id
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def awaitResponse(self, pid):
|
||||||
|
"""
|
||||||
|
Wait for acknowldgement response
|
||||||
|
"""
|
||||||
|
for i in range(120):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
if str(pid) in self.acks:
|
||||||
|
break
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def initNodeDH(self, dhefOb, recipientNode, onodeID):
|
||||||
|
"""
|
||||||
|
Send Diffie Hellman initialization message
|
||||||
|
"""
|
||||||
|
await self.addPackets(
|
||||||
|
msgpack.dumps(
|
||||||
|
{"params": dhefOb.getParamsBytes(), "publicKey": dhefOb.publicKey}
|
||||||
|
),
|
||||||
|
self.onodeID,
|
||||||
|
000000,
|
||||||
|
000000,
|
||||||
|
onodeID,
|
||||||
|
directID=recipientNode,
|
||||||
|
packetsClass=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
def awaitFullResponse(self, pid):
|
||||||
|
"""
|
||||||
|
TODO
|
||||||
|
|
||||||
|
Wait for message completed response
|
||||||
|
"""
|
||||||
|
for i in range(1_000_000_000):
|
||||||
|
time.sleep(5)
|
||||||
|
if pid in self.messages.keys():
|
||||||
|
if self.messages[pid]["finished"]:
|
||||||
|
break
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def addPackets(
|
||||||
|
self,
|
||||||
|
data,
|
||||||
|
sender,
|
||||||
|
senderName,
|
||||||
|
recipient,
|
||||||
|
recipientNode,
|
||||||
|
directID=False,
|
||||||
|
packetsClass=None,
|
||||||
|
encrypt=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Convert binary data to Message and send each packet
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: bytes
|
||||||
|
Data to send
|
||||||
|
|
||||||
|
sender
|
||||||
|
Peer/Node ID of sender
|
||||||
|
|
||||||
|
senderName
|
||||||
|
ID matching specific user title
|
||||||
|
|
||||||
|
recipient
|
||||||
|
Peer/Node ID of recipient
|
||||||
|
|
||||||
|
recipientNode
|
||||||
|
Node ID of node to route to
|
||||||
|
|
||||||
|
directID
|
||||||
|
If set send to this Node only
|
||||||
|
|
||||||
|
packetsClass
|
||||||
|
Protocol for message
|
||||||
|
"""
|
||||||
|
tp = Message(
|
||||||
|
data,
|
||||||
|
sender,
|
||||||
|
senderName,
|
||||||
|
recipient,
|
||||||
|
recipientNode,
|
||||||
|
packetsClass=packetsClass,
|
||||||
|
)
|
||||||
|
for p in tp.packets:
|
||||||
|
if recipientNode == None:
|
||||||
|
self.send(p)
|
||||||
|
else:
|
||||||
|
self.cLog(10, "Sending target: " + str(directID))
|
||||||
|
if directID != False:
|
||||||
|
recipientNode = directID
|
||||||
|
self.send(p, recipientNode=recipientNode)
|
||||||
|
awaitTask = asyncio.create_task(self.awaitResponse(self.cpid))
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
currentTask = {
|
||||||
|
"ob": awaitTask,
|
||||||
|
"pid": str(self.cpid),
|
||||||
|
"packet": p,
|
||||||
|
"retry": False,
|
||||||
|
}
|
||||||
|
self.tasks[str(self.cpid)] = currentTask
|
||||||
|
|
||||||
|
async def progressCheck(self):
|
||||||
|
"""
|
||||||
|
Checks if acknowldgement was received per packet and if not resends
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(90)
|
||||||
|
self.cLog(
|
||||||
|
20, "Checking progress of {0} tasks".format(len(self.tasks.keys()))
|
||||||
|
)
|
||||||
|
doneFlag = True
|
||||||
|
dcTasks = [k for k in self.tasks.keys()]
|
||||||
|
for task in dcTasks:
|
||||||
|
task = self.tasks[task]
|
||||||
|
if task["ob"]:
|
||||||
|
if task["pid"] in self.acks:
|
||||||
|
if not self.acks[task["pid"]]:
|
||||||
|
retry = task["retry"]
|
||||||
|
remove = False
|
||||||
|
if retry == False:
|
||||||
|
retry = 1
|
||||||
|
elif retry < 3:
|
||||||
|
retry += 1
|
||||||
|
else:
|
||||||
|
self.cLog(30, "Too many retries")
|
||||||
|
remove = True
|
||||||
|
if remove:
|
||||||
|
del self.tasks[task["pid"]]
|
||||||
|
else:
|
||||||
|
self.cLog(20, "Doing retry")
|
||||||
|
doneFlag = False
|
||||||
|
# TODO: Resend to specific node
|
||||||
|
self.send(task["packet"])
|
||||||
|
await_thread = asyncio.create_task(
|
||||||
|
self.awaitResponse(task["pid"])
|
||||||
|
)
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
currentTask = {
|
||||||
|
"ob": await_thread,
|
||||||
|
"pid": str(self.cpid),
|
||||||
|
"packet": task["packet"],
|
||||||
|
}
|
||||||
|
currentTask["retry"] = retry
|
||||||
|
self.tasks[task["pid"]] = currentTask
|
||||||
|
else:
|
||||||
|
del self.tasks[task["pid"]]
|
||||||
|
|
||||||
|
async def announce(self):
|
||||||
|
"""
|
||||||
|
Announce loop runner
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
self.cLog(10, "Announce")
|
||||||
|
await self.sendAnnounce()
|
||||||
|
await asyncio.sleep(180)
|
|
@ -0,0 +1,5 @@
|
||||||
|
Layer 0 data transceiving
|
||||||
|
===============================
|
||||||
|
|
||||||
|
.. autoclass:: Transceiver.Transceiver.Transceiver
|
||||||
|
:members:
|
|
@ -0,0 +1,454 @@
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import msgpack
|
||||||
|
import random
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
|
||||||
|
# TODO: delete
|
||||||
|
# TODO: propagate json changes to msgpack automatically
|
||||||
|
# TODO: propagate msgpack changes to cache automatically
|
||||||
|
# TODO: Indexing
|
||||||
|
|
||||||
|
|
||||||
|
def _json_to_msg(path: str):
|
||||||
|
"""
|
||||||
|
Convert json at the path plus .json to a msgpack binary
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
Path to json minus the extension
|
||||||
|
"""
|
||||||
|
rpath = path + ".json"
|
||||||
|
res = b""
|
||||||
|
with open(rpath) as f:
|
||||||
|
res = msgpack.dumps(json.load(f))
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(res)
|
||||||
|
|
||||||
|
|
||||||
|
class Daisy:
|
||||||
|
"""
|
||||||
|
Base class for Daisy data representation
|
||||||
|
|
||||||
|
`🔗 Source <https://git.utopic.work/PierMesh/piermesh/src/branch/main/Components/daisy.py>`_
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
filepath: str
|
||||||
|
Path to file representation on disk
|
||||||
|
|
||||||
|
msg: dict
|
||||||
|
In memory representation
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
filepath: str,
|
||||||
|
templates: dict = {},
|
||||||
|
template: bool = False,
|
||||||
|
prefillDict: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filepath: str
|
||||||
|
Path to disk location
|
||||||
|
|
||||||
|
templates: dict
|
||||||
|
Dictionary of templates to Use
|
||||||
|
|
||||||
|
template: bool
|
||||||
|
Which template to Use
|
||||||
|
|
||||||
|
prefillDict: bool
|
||||||
|
Whether to fill the record with a template
|
||||||
|
"""
|
||||||
|
self.filepath = filepath
|
||||||
|
if os.path.exists(filepath) != True:
|
||||||
|
with open(filepath, "wb") as f:
|
||||||
|
if template != False:
|
||||||
|
if template in templates.keys():
|
||||||
|
t = templates[template].get()
|
||||||
|
if prefillDict != False:
|
||||||
|
for k in prefillDict.keys():
|
||||||
|
t[k] = prefillDict[k]
|
||||||
|
f.write(msgpack.dumps(t))
|
||||||
|
self.msg = t
|
||||||
|
else:
|
||||||
|
print("No such template as: " + template)
|
||||||
|
else:
|
||||||
|
f.write(msgpack.dumps({}))
|
||||||
|
self.msg = {}
|
||||||
|
elif os.path.isdir(filepath):
|
||||||
|
self.msg = "directory"
|
||||||
|
else:
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
self.msg = msgpack.loads(f.read())
|
||||||
|
|
||||||
|
# Use override for updating
|
||||||
|
|
||||||
|
def write(
|
||||||
|
self,
|
||||||
|
override=False,
|
||||||
|
encrypt: bool = False,
|
||||||
|
encryptKey=None,
|
||||||
|
recur: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Write record to disk
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
override
|
||||||
|
Either false or a dictionary of values to set on the record
|
||||||
|
|
||||||
|
encrypt: bool
|
||||||
|
Whether to encrypt the record (TODO)
|
||||||
|
|
||||||
|
encryptKey
|
||||||
|
Key to encrypt record with, or None if not set
|
||||||
|
|
||||||
|
recur: bool
|
||||||
|
Whether to recursively handle keys
|
||||||
|
"""
|
||||||
|
if override != False:
|
||||||
|
for key in override.keys():
|
||||||
|
# TODO: Deeper recursion
|
||||||
|
if recur:
|
||||||
|
if not key in self.msg.keys():
|
||||||
|
self.msg[key] = {}
|
||||||
|
for ikey in override[key].keys():
|
||||||
|
self.msg[key][ikey] = override[key][ikey]
|
||||||
|
else:
|
||||||
|
self.msg[key] = override[key]
|
||||||
|
data = msgpack.dumps(self.msg)
|
||||||
|
with open(self.filepath, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
# Use for refreshing
|
||||||
|
|
||||||
|
def read(self, decrypt: bool = False, decryptKey=False):
|
||||||
|
"""
|
||||||
|
Read record from disk to memory
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
decrypt: bool
|
||||||
|
Whether to decrypt record
|
||||||
|
|
||||||
|
decryptKey
|
||||||
|
Key to decrypt record
|
||||||
|
"""
|
||||||
|
if os.path.isdir(self.filepath):
|
||||||
|
self.msg = "directory"
|
||||||
|
else:
|
||||||
|
with open(self.filepath, "rb") as f:
|
||||||
|
self.msg = msgpack.loads(f.read())
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
Get record dictionary from memory
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
self.msg: dict
|
||||||
|
"""
|
||||||
|
return self.msg
|
||||||
|
|
||||||
|
def sublist(self):
|
||||||
|
"""
|
||||||
|
Lists contents of directory if object is a directory, otherwise return None
|
||||||
|
"""
|
||||||
|
fpath = self.filepath
|
||||||
|
if os.path.isdir(fpath):
|
||||||
|
return ["messages/" + x for x in os.listdir(fpath)]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def loadTemplates(templatePath: str = "templates"):
|
||||||
|
"""Load templates for prefilling records
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
templatePath: str
|
||||||
|
Path to templates
|
||||||
|
"""
|
||||||
|
templates = {}
|
||||||
|
for p in os.listdir(templatePath):
|
||||||
|
p = templatePath + "/" + p
|
||||||
|
if os.path.isdir(p):
|
||||||
|
for ip in os.listdir(p):
|
||||||
|
ip = p + "/" + ip
|
||||||
|
if os.path.isdir(ip):
|
||||||
|
print("Too deep, skipping: " + ip)
|
||||||
|
else:
|
||||||
|
templates[ip] = Daisy(ip)
|
||||||
|
else:
|
||||||
|
templates[p] = Daisy(p)
|
||||||
|
self.templates = templates
|
||||||
|
return templates
|
||||||
|
|
||||||
|
|
||||||
|
class CFSHandler(FileSystemEventHandler):
|
||||||
|
"""
|
||||||
|
File system watchdog that propagates disk changes to records to their proper cache
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, cache, isCatch: bool = False):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cache: Cache
|
||||||
|
Daisy cache to update
|
||||||
|
|
||||||
|
isCatch: bool
|
||||||
|
Is the cache for catchs
|
||||||
|
"""
|
||||||
|
self.cache = cache
|
||||||
|
self.isCatch = isCatch
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def on_any_event(self, event):
|
||||||
|
"""
|
||||||
|
Called when a CRUD operation is performed on a record file
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
event
|
||||||
|
Event object provided by watchdog
|
||||||
|
"""
|
||||||
|
if not (".json" in event.src_path):
|
||||||
|
if not (".md" in event.src_path):
|
||||||
|
tpath = "/".join(event.src_path.split("/")[1:])
|
||||||
|
if tpath != "":
|
||||||
|
if self.isCatch:
|
||||||
|
self.cache.sget(tpath)
|
||||||
|
else:
|
||||||
|
self.cache.get(tpath).get()
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Dumping to cacheFile
|
||||||
|
|
||||||
|
|
||||||
|
class Cache:
|
||||||
|
"""
|
||||||
|
In memory collection of Daisy records
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
filepaths=None,
|
||||||
|
cacheFile=None,
|
||||||
|
path: str = "daisy",
|
||||||
|
walk: bool = False,
|
||||||
|
isCatch: bool = False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filepaths
|
||||||
|
Either a list of filepaths to load or None
|
||||||
|
|
||||||
|
cacheFile
|
||||||
|
Path to a cache file which is a collection of paths to load
|
||||||
|
|
||||||
|
path: str
|
||||||
|
Path prefix to load records from
|
||||||
|
|
||||||
|
walk: bool
|
||||||
|
Whether to automatically walk the path and load records
|
||||||
|
|
||||||
|
isCatch: bool
|
||||||
|
Whether this cache is for catchs
|
||||||
|
"""
|
||||||
|
self.data = {}
|
||||||
|
self.path = path
|
||||||
|
self.event_handler = CFSHandler(self, isCatch=isCatch)
|
||||||
|
self.observer = Observer()
|
||||||
|
self.observer.schedule(self.event_handler, self.path, recursive=True)
|
||||||
|
self.observer.start()
|
||||||
|
# TODO: Test
|
||||||
|
|
||||||
|
if filepaths != None:
|
||||||
|
for fp in filepaths:
|
||||||
|
fp = path + "/" + fp
|
||||||
|
if os.path.isfile(fp):
|
||||||
|
self.data[fp] = Daisy(fp)
|
||||||
|
elif cacheFile != None:
|
||||||
|
with open(cacheFile, "r") as f:
|
||||||
|
for fp in f.read().split("\n"):
|
||||||
|
self.data[fp] = Daisy(fp)
|
||||||
|
elif walk:
|
||||||
|
for root, dirs, files in os.walk(self.path):
|
||||||
|
for p in dirs + files:
|
||||||
|
# print("walking")
|
||||||
|
if not (".json" in p):
|
||||||
|
if not (".md" in p):
|
||||||
|
tpath = root + "/" + p
|
||||||
|
# print(p)
|
||||||
|
# print(tpath)
|
||||||
|
self.data[tpath] = Daisy(tpath)
|
||||||
|
|
||||||
|
def create(self, path: str, data: dict):
|
||||||
|
"""
|
||||||
|
Create new record
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
path: str
|
||||||
|
Path to create record at
|
||||||
|
|
||||||
|
data: dict
|
||||||
|
Data to populate record with
|
||||||
|
"""
|
||||||
|
with open(self.path + "/" + path, "wb") as f:
|
||||||
|
f.write(msgpack.dumps(data))
|
||||||
|
logging.log(10, "Done creating record")
|
||||||
|
self.data[path] = Daisy(self.path + "/" + path)
|
||||||
|
logging.log(10, "Done loading to Daisy")
|
||||||
|
return self.data[path]
|
||||||
|
|
||||||
|
def get(self, path: str):
|
||||||
|
"""
|
||||||
|
Get record at path, else return False
|
||||||
|
|
||||||
|
path: str
|
||||||
|
Path of record
|
||||||
|
"""
|
||||||
|
if path in self.data.keys():
|
||||||
|
return self.data[path]
|
||||||
|
else:
|
||||||
|
if os.path.exists(self.path + "/" + path):
|
||||||
|
self.data[path] = Daisy(self.path + "/" + path)
|
||||||
|
return self.data[path]
|
||||||
|
else:
|
||||||
|
logging.log(10, "File does not exist")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""
|
||||||
|
Reload from disk to memory
|
||||||
|
"""
|
||||||
|
for key in self.data.keys():
|
||||||
|
self.data[key].read()
|
||||||
|
|
||||||
|
def search(self, keydict: dict, strict: bool = True):
|
||||||
|
"""
|
||||||
|
Search cache for record for records with values
|
||||||
|
|
||||||
|
keydict: dict
|
||||||
|
Values to search for
|
||||||
|
|
||||||
|
strict: bool
|
||||||
|
Whether to require values match
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for key, val in self.data.items():
|
||||||
|
val = val.get()
|
||||||
|
if strict and type(val) != str:
|
||||||
|
addcheck = False
|
||||||
|
for k, v in keydict.items():
|
||||||
|
if k in val.keys():
|
||||||
|
if v in val[k]:
|
||||||
|
addcheck = True
|
||||||
|
else:
|
||||||
|
addcheck = False
|
||||||
|
break
|
||||||
|
if addcheck:
|
||||||
|
results.append([key, val])
|
||||||
|
elif type(val) != str:
|
||||||
|
for k, v in keydict.items():
|
||||||
|
if k in val.keys():
|
||||||
|
if v in val[k]:
|
||||||
|
results.append([key, val])
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class Catch(Cache):
|
||||||
|
"""
|
||||||
|
Sub class of Cache for handling catchs
|
||||||
|
|
||||||
|
.. image:: https://git.utopic.work/PierMesh/piermesh/raw/branch/main/imgs/catchdisplay.png
|
||||||
|
"""
|
||||||
|
|
||||||
|
catches = {}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, path: str = "catch", filepaths=None, catchFile=None, walk: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Basically the same initialization parameters as Catch
|
||||||
|
"""
|
||||||
|
super().__init__(
|
||||||
|
filepaths=filepaths, cacheFile=catchFile, path=path, walk=walk, isCatch=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Fins
|
||||||
|
|
||||||
|
def sget(self, path: str):
|
||||||
|
"""
|
||||||
|
Call Cache's get to get record
|
||||||
|
"""
|
||||||
|
return super().get(path)
|
||||||
|
|
||||||
|
def get(self, head: str, tail: str, fins=None):
|
||||||
|
"""
|
||||||
|
Get catch by pieces
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
head: str
|
||||||
|
First part of catch (maximum: 4 characters)
|
||||||
|
|
||||||
|
tail: str
|
||||||
|
Second part of catch (maximum: 16 characters)
|
||||||
|
|
||||||
|
fins
|
||||||
|
List of (maximum 8 characters) strings at the end of the catch oe None if none
|
||||||
|
"""
|
||||||
|
r = self.search({"head": head, "tail": tail})
|
||||||
|
return r[0][1]["html"]
|
||||||
|
|
||||||
|
def addc(self, peer, node, seperator, head, tail, data, fins=None):
|
||||||
|
tnpath = "catch/" + node
|
||||||
|
if os.path.exists(tnpath) != True:
|
||||||
|
os.makedirs(tnpath)
|
||||||
|
tppath = tnpath + "/" + peer
|
||||||
|
if os.path.exists(tppath) != True:
|
||||||
|
os.makedirs(tppath)
|
||||||
|
sid = str(random.randrange(0, 999999)).zfill(6)
|
||||||
|
data["seperator"] = seperator
|
||||||
|
data["head"] = head
|
||||||
|
data["tail"] = tail
|
||||||
|
if fins != None:
|
||||||
|
data["fins"] = fins
|
||||||
|
res = self.create("{0}/{1}/{2}".format(node, peer, sid), data)
|
||||||
|
return [sid, res]
|
||||||
|
|
||||||
|
|
||||||
|
class Store(Daisy):
|
||||||
|
def __init__(self, store: str, path: str, nodeNickname: str):
|
||||||
|
fpath = "daisy/{0}/{1}".format(path, nodeNickname)):
|
||||||
|
cpath = "{0}/{1}/{2}".format(path, nodeNickname, store)
|
||||||
|
if not os.path.exists(fpath):
|
||||||
|
os.mkdir(fpath)
|
||||||
|
super().__init__("daisy/" + cpath)
|
||||||
|
|
||||||
|
def update(self, entry: str, data, recur: bool=True):
|
||||||
|
if recur:
|
||||||
|
for key in data.keys():
|
||||||
|
self.msg[entry][key] = data[key]
|
||||||
|
else:
|
||||||
|
self.msg[entry] = data
|
||||||
|
self.write()
|
||||||
|
|
||||||
|
def getRecord(self, key: str):
|
||||||
|
if key in self.get().keys():
|
||||||
|
return self.get()[key]
|
||||||
|
else:
|
||||||
|
self.cLog(20, "Record does not exist")
|
||||||
|
return False
|
||||||
|
|