diff --git a/docs/Cryptography/WhaleSong.md b/docs/Cryptography/WhaleSong.md new file mode 100644 index 00000000..34f19618 --- /dev/null +++ b/docs/Cryptography/WhaleSong.md @@ -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 diff --git a/docs/Daisy/Cache.md b/docs/Daisy/Cache.md new file mode 100644 index 00000000..6c799022 --- /dev/null +++ b/docs/Daisy/Cache.md @@ -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 diff --git a/docs/Daisy/Catch.md b/docs/Daisy/Catch.md new file mode 100644 index 00000000..6e675e5e --- /dev/null +++ b/docs/Daisy/Catch.md @@ -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 diff --git a/docs/Daisy/Daisy.md b/docs/Daisy/Daisy.md new file mode 100644 index 00000000..88bccce3 --- /dev/null +++ b/docs/Daisy/Daisy.md @@ -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 diff --git a/docs/Daisy/Soil.md b/docs/Daisy/Soil.md new file mode 100644 index 00000000..edc68d08 --- /dev/null +++ b/docs/Daisy/Soil.md @@ -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 diff --git a/docs/Daisy/Store.md b/docs/Daisy/Store.md new file mode 100644 index 00000000..c5018389 --- /dev/null +++ b/docs/Daisy/Store.md @@ -0,0 +1,5 @@ +Daisy based key value +store with recursion +========================== + +### *class* Daisy.Store.Store(store: str, path: str, nodeNickname: str) diff --git a/docs/Splash/serve.md b/docs/Splash/serve.md new file mode 100644 index 00000000..2f23ccbc --- /dev/null +++ b/docs/Splash/serve.md @@ -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 diff --git a/docs/Transceiver/Transceiver.md b/docs/Transceiver/Transceiver.md new file mode 100644 index 00000000..78f88568 --- /dev/null +++ b/docs/Transceiver/Transceiver.md @@ -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=) + +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 diff --git a/src/Cryptography/WhaleSong.py b/src/Cryptography/WhaleSong.py new file mode 100755 index 00000000..3a2b8f95 --- /dev/null +++ b/src/Cryptography/WhaleSong.py @@ -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)) diff --git a/src/Cryptography/WhaleSong.rst b/src/Cryptography/WhaleSong.rst new file mode 100644 index 00000000..911bc653 --- /dev/null +++ b/src/Cryptography/WhaleSong.rst @@ -0,0 +1,7 @@ + +Diffie hellman ephemeral +Fernet based encryption +========================== + +.. autoclass:: Cryptography.WhaleSong.DHEFern + :members: diff --git a/src/Daisy/Cache.py b/src/Daisy/Cache.py new file mode 100755 index 00000000..f7b273b2 --- /dev/null +++ b/src/Daisy/Cache.py @@ -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 diff --git a/src/Daisy/Cache.rst b/src/Daisy/Cache.rst new file mode 100644 index 00000000..ba997ad1 --- /dev/null +++ b/src/Daisy/Cache.rst @@ -0,0 +1,5 @@ +Daisy based cache +========================== + +.. autoclass:: Daisy.Cache.Cache + :members: diff --git a/src/Daisy/Catch.py b/src/Daisy/Catch.py new file mode 100755 index 00000000..f5a13b70 --- /dev/null +++ b/src/Daisy/Catch.py @@ -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] diff --git a/src/Daisy/Catch.rst b/src/Daisy/Catch.rst new file mode 100644 index 00000000..e11608e3 --- /dev/null +++ b/src/Daisy/Catch.rst @@ -0,0 +1,6 @@ +Daisy cache for catchs, +PierMesh's domain analog +========================== + +.. autoclass:: Daisy.Catch.Catch + :members: diff --git a/src/Daisy/Daisy.py b/src/Daisy/Daisy.py new file mode 100755 index 00000000..176ef5b9 --- /dev/null +++ b/src/Daisy/Daisy.py @@ -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 `_ + + 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 diff --git a/src/Daisy/Daisy.rst b/src/Daisy/Daisy.rst new file mode 100644 index 00000000..6e82bac4 --- /dev/null +++ b/src/Daisy/Daisy.rst @@ -0,0 +1,6 @@ +Schemaless binary database +base class +========================== + +.. autoclass:: Daisy.Daisy.Daisy + :members: diff --git a/src/Daisy/Soil.py b/src/Daisy/Soil.py new file mode 100644 index 00000000..e1e842f4 --- /dev/null +++ b/src/Daisy/Soil.py @@ -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() diff --git a/src/Daisy/Soil.rst b/src/Daisy/Soil.rst new file mode 100644 index 00000000..5e3a3651 --- /dev/null +++ b/src/Daisy/Soil.rst @@ -0,0 +1,5 @@ +Daisy signal management +========================== + +.. autoclass:: Daisy.Soil.Compound + :members: diff --git a/src/Daisy/Store.py b/src/Daisy/Store.py new file mode 100755 index 00000000..1c12159d --- /dev/null +++ b/src/Daisy/Store.py @@ -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 diff --git a/src/Daisy/Store.rst b/src/Daisy/Store.rst new file mode 100644 index 00000000..305e7f9b --- /dev/null +++ b/src/Daisy/Store.rst @@ -0,0 +1,6 @@ +Daisy based key value +store with recursion +========================== + +.. autoclass:: Daisy.Store.Store + :members: diff --git a/src/Packets/Message.py b/src/Packets/Message.py new file mode 100755 index 00000000..06194193 --- /dev/null +++ b/src/Packets/Message.py @@ -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 diff --git a/src/Packets/Message.srt b/src/Packets/Message.srt new file mode 100644 index 00000000..ec67be10 --- /dev/null +++ b/src/Packets/Message.srt @@ -0,0 +1,5 @@ +Full message +=============================== + +.. autoclass:: Packets.Message.Message + :members: diff --git a/src/Splash/build.py b/src/Splash/build.py new file mode 100755 index 00000000..d2089e09 --- /dev/null +++ b/src/Splash/build.py @@ -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()) diff --git a/src/Splash/diagrams/markdown/bubble.mmd b/src/Splash/diagrams/markdown/bubble.mmd new file mode 100644 index 00000000..1f8cfdb0 --- /dev/null +++ b/src/Splash/diagrams/markdown/bubble.mmd @@ -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" diff --git a/src/Splash/diagrams/markdown/catch.mmd b/src/Splash/diagrams/markdown/catch.mmd new file mode 100644 index 00000000..e432c321 --- /dev/null +++ b/src/Splash/diagrams/markdown/catch.mmd @@ -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" diff --git a/src/Splash/diagrams/markdown/daisy.mmd b/src/Splash/diagrams/markdown/daisy.mmd new file mode 100644 index 00000000..2fb18b82 --- /dev/null +++ b/src/Splash/diagrams/markdown/daisy.mmd @@ -0,0 +1,5 @@ +--- +title: "🌼 Daisy" +--- +erDiagram + diff --git a/src/Splash/diagrams/markdown/microplane.mmd b/src/Splash/diagrams/markdown/microplane.mmd new file mode 100644 index 00000000..e69de29b diff --git a/src/Splash/diagrams/markdown/overview.mmd b/src/Splash/diagrams/markdown/overview.mmd new file mode 100755 index 00000000..6f282365 --- /dev/null +++ b/src/Splash/diagrams/markdown/overview.mmd @@ -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" diff --git a/src/Splash/diagrams/markdown/sieve.mmd b/src/Splash/diagrams/markdown/sieve.mmd new file mode 100644 index 00000000..e69de29b diff --git a/src/Splash/diagrams/markdown/transmission.mmd b/src/Splash/diagrams/markdown/transmission.mmd new file mode 100644 index 00000000..f946aa65 --- /dev/null +++ b/src/Splash/diagrams/markdown/transmission.mmd @@ -0,0 +1,5 @@ +--- +title: "📤 Transmitter" +--- +erDiagram + diff --git a/src/Splash/diagrams/renders/dia.png b/src/Splash/diagrams/renders/dia.png new file mode 100755 index 00000000..c2a24d08 Binary files /dev/null and b/src/Splash/diagrams/renders/dia.png differ diff --git a/src/Splash/renderdia.sh b/src/Splash/renderdia.sh new file mode 100755 index 00000000..468aa286 --- /dev/null +++ b/src/Splash/renderdia.sh @@ -0,0 +1 @@ +mmdc -i res/misc/dia.mmd -o res/misc/dia.png diff --git a/src/Splash/res/css/fonts.css b/src/Splash/res/css/fonts.css new file mode 100755 index 00000000..699ae14b --- /dev/null +++ b/src/Splash/res/css/fonts.css @@ -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; +} diff --git a/src/Splash/res/css/style.css b/src/Splash/res/css/style.css new file mode 100755 index 00000000..8279db36 --- /dev/null +++ b/src/Splash/res/css/style.css @@ -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; +} diff --git a/src/Splash/res/diagrams/bubble.mmd b/src/Splash/res/diagrams/bubble.mmd new file mode 100644 index 00000000..1f8cfdb0 --- /dev/null +++ b/src/Splash/res/diagrams/bubble.mmd @@ -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" diff --git a/src/Splash/res/diagrams/catch.mmd b/src/Splash/res/diagrams/catch.mmd new file mode 100644 index 00000000..e432c321 --- /dev/null +++ b/src/Splash/res/diagrams/catch.mmd @@ -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" diff --git a/src/Splash/res/diagrams/daisy.mmd b/src/Splash/res/diagrams/daisy.mmd new file mode 100644 index 00000000..2fb18b82 --- /dev/null +++ b/src/Splash/res/diagrams/daisy.mmd @@ -0,0 +1,5 @@ +--- +title: "🌼 Daisy" +--- +erDiagram + diff --git a/src/Splash/res/diagrams/microplane.mmd b/src/Splash/res/diagrams/microplane.mmd new file mode 100644 index 00000000..e69de29b diff --git a/src/Splash/res/diagrams/overview.mmd b/src/Splash/res/diagrams/overview.mmd new file mode 100755 index 00000000..6f282365 --- /dev/null +++ b/src/Splash/res/diagrams/overview.mmd @@ -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" diff --git a/src/Splash/res/diagrams/sieve.mmd b/src/Splash/res/diagrams/sieve.mmd new file mode 100644 index 00000000..e69de29b diff --git a/src/Splash/res/diagrams/transmission.mmd b/src/Splash/res/diagrams/transmission.mmd new file mode 100644 index 00000000..f946aa65 --- /dev/null +++ b/src/Splash/res/diagrams/transmission.mmd @@ -0,0 +1,5 @@ +--- +title: "📤 Transmitter" +--- +erDiagram + diff --git a/src/Splash/res/fonts/UbuntuNF.eot b/src/Splash/res/fonts/UbuntuNF.eot new file mode 100755 index 00000000..157496ab Binary files /dev/null and b/src/Splash/res/fonts/UbuntuNF.eot differ diff --git a/src/Splash/res/fonts/UbuntuNF.woff b/src/Splash/res/fonts/UbuntuNF.woff new file mode 100755 index 00000000..6d9dfd24 Binary files /dev/null and b/src/Splash/res/fonts/UbuntuNF.woff differ diff --git a/src/Splash/res/fonts/UbuntuNF.woff2 b/src/Splash/res/fonts/UbuntuNF.woff2 new file mode 100755 index 00000000..408e05bb Binary files /dev/null and b/src/Splash/res/fonts/UbuntuNF.woff2 differ diff --git a/src/Splash/res/fonts/UbuntuNerdFont-Regular.ttf b/src/Splash/res/fonts/UbuntuNerdFont-Regular.ttf new file mode 100755 index 00000000..87e8e42f Binary files /dev/null and b/src/Splash/res/fonts/UbuntuNerdFont-Regular.ttf differ diff --git a/src/Splash/res/img/bubbledisplay.png b/src/Splash/res/img/bubbledisplay.png new file mode 100644 index 00000000..490eaadc Binary files /dev/null and b/src/Splash/res/img/bubbledisplay.png differ diff --git a/src/Splash/res/img/catchdisplay.png b/src/Splash/res/img/catchdisplay.png new file mode 100644 index 00000000..407a809c Binary files /dev/null and b/src/Splash/res/img/catchdisplay.png differ diff --git a/src/Splash/res/img/diagrams/bubble.png b/src/Splash/res/img/diagrams/bubble.png new file mode 100644 index 00000000..5eb6b949 Binary files /dev/null and b/src/Splash/res/img/diagrams/bubble.png differ diff --git a/src/Splash/res/img/diagrams/catch.png b/src/Splash/res/img/diagrams/catch.png new file mode 100644 index 00000000..68236cb5 Binary files /dev/null and b/src/Splash/res/img/diagrams/catch.png differ diff --git a/src/Splash/res/img/diagrams/daisy.png b/src/Splash/res/img/diagrams/daisy.png new file mode 100644 index 00000000..32f95c3a Binary files /dev/null and b/src/Splash/res/img/diagrams/daisy.png differ diff --git a/src/Splash/res/img/diagrams/overview.png b/src/Splash/res/img/diagrams/overview.png new file mode 100644 index 00000000..c2a24d08 Binary files /dev/null and b/src/Splash/res/img/diagrams/overview.png differ diff --git a/src/Splash/res/img/diagrams/transmission.png b/src/Splash/res/img/diagrams/transmission.png new file mode 100644 index 00000000..bd729bbc Binary files /dev/null and b/src/Splash/res/img/diagrams/transmission.png differ diff --git a/src/Splash/res/img/logo.png b/src/Splash/res/img/logo.png new file mode 100644 index 00000000..3b83d0ca Binary files /dev/null and b/src/Splash/res/img/logo.png differ diff --git a/src/Splash/res/js/buildmodelviewer b/src/Splash/res/js/buildmodelviewer new file mode 100644 index 00000000..7c2d180c --- /dev/null +++ b/src/Splash/res/js/buildmodelviewer @@ -0,0 +1 @@ +npx vite build diff --git a/src/Splash/res/js/package-lock.json b/src/Splash/res/js/package-lock.json new file mode 100755 index 00000000..0bda9c7f --- /dev/null +++ b/src/Splash/res/js/package-lock.json @@ -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 + } + } + } + } +} diff --git a/src/Splash/res/js/package.json b/src/Splash/res/js/package.json new file mode 100755 index 00000000..e852c331 --- /dev/null +++ b/src/Splash/res/js/package.json @@ -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" + } +} diff --git a/src/Splash/res/js/ws.js b/src/Splash/res/js/ws.js new file mode 100755 index 00000000..be1f08db --- /dev/null +++ b/src/Splash/res/js/ws.js @@ -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} */ + 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
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]) + } + } + } +})() diff --git a/src/Splash/serve.py b/src/Splash/serve.py new file mode 100755 index 00000000..18e1a806 --- /dev/null +++ b/src/Splash/serve.py @@ -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/") + 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 `_ + """ + 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( + """ +

Peer ID: {0}

+ """.format( + peerID + ) + ) + await ws.send( + """ + + """.format( + peerID + ) + ) + await ws.send( + """ +

Node ID: {0}

+ """.format( + self.nodeID + ) + ) + await ws.send( + """ """.format( + self.nodeID + ) + ) + await ws.send( + "".format( + peerID + ) + ) + await ws.send( + "".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('
{0}
'.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( + """ +
  • OK
+ """ + ) + else: + await ws.send( + """
hi
""" + ) + + @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( + "
  • {0}
".format(data) + ) diff --git a/src/Splash/serve.rst b/src/Splash/serve.rst new file mode 100644 index 00000000..10317ea5 --- /dev/null +++ b/src/Splash/serve.rst @@ -0,0 +1,5 @@ +serve: Web UI server +============================ + +.. autoclass:: Splash.serve.Server + :members: diff --git a/src/Splash/templates/index/index.html b/src/Splash/templates/index/index.html new file mode 100755 index 00000000..b5dc5ac9 --- /dev/null +++ b/src/Splash/templates/index/index.html @@ -0,0 +1,22 @@ +{% extends "shared/base.html" %} +{% block body %} + +PierMesh logo +
+
+{% include "shared/catch.nav.html" %} +
+{% include "shared/catch.editor.html" %} +
+
+
+
+

Peer ID:

+ +

Node ID:

+ + +
+
+{% include "shared/messenger.html" %} +{% endblock %} diff --git a/src/Splash/templates/shared/base.html b/src/Splash/templates/shared/base.html new file mode 100755 index 00000000..054866e5 --- /dev/null +++ b/src/Splash/templates/shared/base.html @@ -0,0 +1,21 @@ + + + + + + + {{ title }} + + + + + + + + {% include "shared/nav.html" %} + {% block body %} + {% endblock %} + + + diff --git a/src/Splash/templates/shared/catch.editor.html b/src/Splash/templates/shared/catch.editor.html new file mode 100644 index 00000000..c575d5f5 --- /dev/null +++ b/src/Splash/templates/shared/catch.editor.html @@ -0,0 +1,23 @@ +
+ +
+ Catch publisher
+
    +
+ + Head

+ Seperator

+ Body

+ Fins
+
    +
  • +
  • +
+ Content +
+ +
+ + + +
diff --git a/src/Splash/templates/shared/catch.html b/src/Splash/templates/shared/catch.html new file mode 100644 index 00000000..69e5bf25 --- /dev/null +++ b/src/Splash/templates/shared/catch.html @@ -0,0 +1 @@ +
diff --git a/src/Splash/templates/shared/catch.nav.html b/src/Splash/templates/shared/catch.nav.html new file mode 100755 index 00000000..b087f9f5 --- /dev/null +++ b/src/Splash/templates/shared/catch.nav.html @@ -0,0 +1,31 @@ +
+ +
+ Catch

+
+ +
+ +
+ +
+
+ +
+ +
    + Fins: +
  • + +
  • +
  • + +
  • +
+ +
+ Results: +
+ {% include "shared/catch.html" %} +
+
diff --git a/src/Splash/templates/shared/messenger.html b/src/Splash/templates/shared/messenger.html new file mode 100755 index 00000000..39a8ad5d --- /dev/null +++ b/src/Splash/templates/shared/messenger.html @@ -0,0 +1,20 @@ +
+ +
+ Bubble +
+
+ Responses:
    +
+
+
+ Peer ID:
+
+ Node ID:
+
+ Data
+
+ + +
+
diff --git a/src/Splash/templates/shared/nav.html b/src/Splash/templates/shared/nav.html new file mode 100755 index 00000000..8b137891 --- /dev/null +++ b/src/Splash/templates/shared/nav.html @@ -0,0 +1 @@ + diff --git a/src/Transceiver/Transceiver.py b/src/Transceiver/Transceiver.py new file mode 100644 index 00000000..dcd9ca59 --- /dev/null +++ b/src/Transceiver/Transceiver.py @@ -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) diff --git a/src/Transceiver/Transceiver.rst b/src/Transceiver/Transceiver.rst new file mode 100644 index 00000000..5b94c4cc --- /dev/null +++ b/src/Transceiver/Transceiver.rst @@ -0,0 +1,5 @@ +Layer 0 data transceiving +=============================== + +.. autoclass:: Transceiver.Transceiver.Transceiver + :members: diff --git a/src/stale/daisy.py b/src/stale/daisy.py new file mode 100755 index 00000000..6ebe209a --- /dev/null +++ b/src/stale/daisy.py @@ -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 `_ + + 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 +