From 630819200975ea5efe5a9ca2c9c4ce47590d3539 Mon Sep 17 00:00:00 2001 From: SebastianObi Date: Tue, 25 Apr 2023 07:15:17 +0200 Subject: [PATCH] Improvements, added example configuration --- lxmf_bridge_matrix/Examples/README.md | 4 + lxmf_bridge_meshtastic/Examples/README.md | 4 + lxmf_bridge_mqtt/Examples/README.md | 4 + lxmf_bridge_mqtt/lxmf_bridge_mqtt.py | 220 +++++- lxmf_bridge_telegram/Examples/README.md | 4 + lxmf_chatbot/Examples/README.md | 4 + lxmf_chatbot/lxmf_chatbot.py | 217 +++++- lxmf_cmd/Examples/README.md | 4 + lxmf_cmd/lxmf_cmd.py | 215 +++++- .../config.cfg.owr | 151 +++++ .../data.cfg | 52 ++ .../config.cfg.owr | 151 +++++ .../Group_for_Communicator_Software/data.cfg | 52 ++ lxmf_distribution_group/Examples/README.md | 4 + .../lxmf_distribution_group.py | 635 +++++++++++++---- .../Examples/README.md | 4 + .../lxmf_distribution_group_minimal.py | 219 ++++-- lxmf_echo/Examples/README.md | 4 + lxmf_echo/lxmf_echo.py | 215 +++++- lxmf_ping/Examples/README.md | 4 + lxmf_ping/lxmf_ping.py | 164 ++++- lxmf_provisioning/Examples/README.md | 4 + lxmf_provisioning/lxmf_provisioning.py | 640 ++++++++++++++++-- lxmf_terminal/Examples/README.md | 4 + lxmf_terminal/lxmf_terminal.py | 212 +++++- 25 files changed, 2747 insertions(+), 444 deletions(-) create mode 100644 lxmf_bridge_matrix/Examples/README.md create mode 100644 lxmf_bridge_meshtastic/Examples/README.md create mode 100644 lxmf_bridge_mqtt/Examples/README.md create mode 100644 lxmf_bridge_telegram/Examples/README.md create mode 100644 lxmf_chatbot/Examples/README.md create mode 100644 lxmf_cmd/Examples/README.md create mode 100644 lxmf_distribution_group/Examples/Channel_for_Communicator_Software/config.cfg.owr create mode 100644 lxmf_distribution_group/Examples/Channel_for_Communicator_Software/data.cfg create mode 100644 lxmf_distribution_group/Examples/Group_for_Communicator_Software/config.cfg.owr create mode 100644 lxmf_distribution_group/Examples/Group_for_Communicator_Software/data.cfg create mode 100644 lxmf_distribution_group/Examples/README.md create mode 100644 lxmf_distribution_group_minimal/Examples/README.md create mode 100644 lxmf_echo/Examples/README.md create mode 100644 lxmf_ping/Examples/README.md create mode 100644 lxmf_provisioning/Examples/README.md create mode 100644 lxmf_terminal/Examples/README.md diff --git a/lxmf_bridge_matrix/Examples/README.md b/lxmf_bridge_matrix/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_bridge_matrix/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_bridge_meshtastic/Examples/README.md b/lxmf_bridge_meshtastic/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_bridge_meshtastic/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_bridge_mqtt/Examples/README.md b/lxmf_bridge_mqtt/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_bridge_mqtt/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py index c173980..5660893 100755 --- a/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py +++ b/lxmf_bridge_mqtt/lxmf_bridge_mqtt.py @@ -48,6 +48,9 @@ import pickle #### String #### import string +#### Regex #### +import re + #### Process #### import signal import threading @@ -97,9 +100,10 @@ class lxmf_connection: message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None + config_set_callback = None - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file @@ -112,6 +116,7 @@ class lxmf_connection: self.display_name = display_name self.announce_data = announce_data + self.announce_hidden = announce_hidden self.send_delay = int(send_delay) @@ -120,6 +125,8 @@ class lxmf_connection: else: self.desired_method_direct = True self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup @@ -134,6 +141,10 @@ class lxmf_connection: self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) @@ -188,10 +199,18 @@ class lxmf_connection: self.destination.set_link_established_callback(self.client_connected) - self.autoselect_propagation_node() + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) if self.sync_startup or self.sync_periodic: self.sync(True) @@ -218,6 +237,10 @@ class lxmf_connection: self.message_notification_failed_callback = handler_function + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + def destination_hash(self): return self.destination.hash @@ -261,7 +284,7 @@ class lxmf_connection: return "" - def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""): + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] @@ -276,8 +299,13 @@ class lxmf_connection: log("LXMF - Destination is invalid", LOG_ERROR) return + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + destination_identity = RNS.Identity.recall(destination) - destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) @@ -346,7 +374,7 @@ class lxmf_connection: message.desired_method_str = "propagated" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: @@ -363,26 +391,29 @@ class lxmf_connection: announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) elif self.announce_data: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: self.destination.announce() @@ -424,24 +455,50 @@ class lxmf_connection: return False - def autoselect_propagation_node(self): - if self.propagation_node is not None: - if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): - log("LXMF - Propagation node length is invalid", LOG_ERROR) - else: - try: - propagation_hash = bytes.fromhex(self.propagation_node) - except Exception as e: - log("LXMF - Propagation node is invalid", LOG_ERROR) - return + def propagation_node_set(self, dest_str): + if not dest_str: + return False - node_identity = RNS.Identity.recall(propagation_hash) - if node_identity != None: - log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) - propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) - self.message_router.set_outbound_propagation_node(propagation_hash) - else: - log("LXMF - Propagation node identity not known", LOG_ERROR) + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" def client_connected(self, link): @@ -529,6 +586,43 @@ class lxmf_connection: log("- App Data: " + message.app_data, LOG_DEBUG) + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + ############################################################################################################## # LXMF Functions @@ -772,6 +866,9 @@ def mqtt_message_received_callback_send(client, userdata, message): if "title" not in message_data: message_data["title"] = "" + if "fields" not in message_data: + message_data["fields"] = None + timestamp = None if "timestamp" in message_data and timestamp is None: message_data["timestamp"] = message_data["timestamp"].strip() @@ -783,7 +880,7 @@ def mqtt_message_received_callback_send(client, userdata, message): if message_data["date_time"] != "": timestamp = time.mktime(datetime.datetime.strptime(message_data["date_time"], '%Y-%m-%d %H:%M:%S').timetuple()) - LXMF_CONNECTION.send(message_data["destination"].strip(), content, message_data["title"].strip(), None, timestamp) + LXMF_CONNECTION.send(message_data["destination"].strip(), content, message_data["title"].strip(), message_data["fields"], timestamp) ############################################################################################################## @@ -843,6 +940,36 @@ def config_getoption(config, section, key, default=False, lng_key=""): +#### Config - Set ##### +def config_set(key=None, value=""): + global PATH + + try: + file = PATH + "/config.cfg.owr" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + + file = PATH + "/config.cfg" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + except: + pass + + + + #### Config - Read ##### def config_read(file=None, file_override=None): global CONFIG @@ -936,15 +1063,15 @@ def config_default(file=None, file_override=None): # Value convert -def val_to_bool(val): +def val_to_bool(val, fallback_true=True, fallback_false=False): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": - return True + return fallback_true else: - return False + return fallback_false ############################################################################################################## @@ -1110,6 +1237,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) else: config_propagation_node = None + if CONFIG.has_option("lxmf", "propagation_node_active"): + config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"] + else: + config_propagation_node_active = None + if path is None: path = PATH @@ -1118,9 +1250,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) destination_name=CONFIG["lxmf"]["destination_name"], destination_type=CONFIG["lxmf"]["destination_type"], display_name=CONFIG["lxmf"]["display_name"], + announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"), send_delay=CONFIG["lxmf"]["send_delay"], desired_method=CONFIG["lxmf"]["desired_method"], propagation_node=config_propagation_node, + propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"), + propagation_node_active=config_propagation_node_active, try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], @@ -1134,6 +1269,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + LXMF_CONNECTION.register_config_set_callback(config_set) log("LXMF - Connected", LOG_DEBUG) @@ -1244,7 +1380,13 @@ display_name = desired_method = direct #direct/propagated # Propagation node address/hash. -propagation_node = ca2762fe5283873719aececfb9e18835 +propagation_node = + +# Set propagation node automatically. +propagation_node_auto = True + +# Current propagation node (Automatically set by the software). +propagation_node_active = # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. @@ -1260,6 +1402,10 @@ announce_startup_delay = 0 #Seconds announce_periodic = No announce_periodic_interval = 360 #Minutes +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + # Some waiting time after message send # for LXMF/Reticulum processing. send_delay = 0 #Seconds diff --git a/lxmf_bridge_telegram/Examples/README.md b/lxmf_bridge_telegram/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_bridge_telegram/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_chatbot/Examples/README.md b/lxmf_chatbot/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_chatbot/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_chatbot/lxmf_chatbot.py b/lxmf_chatbot/lxmf_chatbot.py index b47ba5d..22d47d1 100755 --- a/lxmf_chatbot/lxmf_chatbot.py +++ b/lxmf_chatbot/lxmf_chatbot.py @@ -47,6 +47,9 @@ import pickle #### String #### import string +#### Regex #### +import re + #### Process #### import signal import threading @@ -95,9 +98,10 @@ class lxmf_connection: message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None + config_set_callback = None - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file @@ -110,6 +114,7 @@ class lxmf_connection: self.display_name = display_name self.announce_data = announce_data + self.announce_hidden = announce_hidden self.send_delay = int(send_delay) @@ -118,6 +123,8 @@ class lxmf_connection: else: self.desired_method_direct = True self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup @@ -132,6 +139,10 @@ class lxmf_connection: self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) @@ -186,10 +197,18 @@ class lxmf_connection: self.destination.set_link_established_callback(self.client_connected) - self.autoselect_propagation_node() + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) if self.sync_startup or self.sync_periodic: self.sync(True) @@ -216,6 +235,10 @@ class lxmf_connection: self.message_notification_failed_callback = handler_function + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + def destination_hash(self): return self.destination.hash @@ -259,7 +282,7 @@ class lxmf_connection: return "" - def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""): + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] @@ -274,8 +297,13 @@ class lxmf_connection: log("LXMF - Destination is invalid", LOG_ERROR) return + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + destination_identity = RNS.Identity.recall(destination) - destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) @@ -344,7 +372,7 @@ class lxmf_connection: message.desired_method_str = "propagated" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: @@ -361,26 +389,29 @@ class lxmf_connection: announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) elif self.announce_data: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: self.destination.announce() @@ -422,24 +453,50 @@ class lxmf_connection: return False - def autoselect_propagation_node(self): - if self.propagation_node is not None: - if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): - log("LXMF - Propagation node length is invalid", LOG_ERROR) - else: - try: - propagation_hash = bytes.fromhex(self.propagation_node) - except Exception as e: - log("LXMF - Propagation node is invalid", LOG_ERROR) - return + def propagation_node_set(self, dest_str): + if not dest_str: + return False - node_identity = RNS.Identity.recall(propagation_hash) - if node_identity != None: - log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) - propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) - self.message_router.set_outbound_propagation_node(propagation_hash) - else: - log("LXMF - Propagation node identity not known", LOG_ERROR) + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" def client_connected(self, link): @@ -527,6 +584,43 @@ class lxmf_connection: log("- App Data: " + message.app_data, LOG_DEBUG) + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + ############################################################################################################## # LXMF Functions @@ -666,7 +760,37 @@ def config_getoption(config, section, key, default=False, lng_key=""): -#### Config - Read ##### +#### Config - Set ##### +def config_set(key=None, value=""): + global PATH + + try: + file = PATH + "/config.cfg.owr" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + + file = PATH + "/config.cfg" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + except: + pass + + + +#### Config +- Read ##### def config_read(file=None, file_override=None): global CONFIG @@ -759,15 +883,15 @@ def config_default(file=None, file_override=None): # Value convert -def val_to_bool(val): +def val_to_bool(val, fallback_true=True, fallback_false=False): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": - return True + return fallback_true else: - return False + return fallback_false ############################################################################################################## @@ -933,6 +1057,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) else: config_propagation_node = None + if CONFIG.has_option("lxmf", "propagation_node_active"): + config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"] + else: + config_propagation_node_active = None + if path is None: path = PATH @@ -941,9 +1070,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) destination_name=CONFIG["lxmf"]["destination_name"], destination_type=CONFIG["lxmf"]["destination_type"], display_name=CONFIG["lxmf"]["display_name"], + announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"), send_delay=CONFIG["lxmf"]["send_delay"], desired_method=CONFIG["lxmf"]["desired_method"], propagation_node=config_propagation_node, + propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"), + propagation_node_active=config_propagation_node_active, try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], @@ -957,6 +1089,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + LXMF_CONNECTION.register_config_set_callback(config_set) log("LXMF - Connected", LOG_DEBUG) @@ -1061,7 +1194,13 @@ display_name = Chatbot desired_method = direct #direct/propagated # Propagation node address/hash. -#propagation_node = +propagation_node = + +# Set propagation node automatically. +propagation_node_auto = True + +# Current propagation node (Automatically set by the software). +propagation_node_active = # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. @@ -1077,6 +1216,10 @@ announce_startup_delay = 0 #Seconds announce_periodic = No announce_periodic_interval = 360 #Minutes +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + # Some waiting time after message send # for LXMF/Reticulum processing. send_delay = 0 #Seconds diff --git a/lxmf_cmd/Examples/README.md b/lxmf_cmd/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_cmd/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_cmd/lxmf_cmd.py b/lxmf_cmd/lxmf_cmd.py index 8962174..232a033 100755 --- a/lxmf_cmd/lxmf_cmd.py +++ b/lxmf_cmd/lxmf_cmd.py @@ -47,6 +47,9 @@ import pickle #### String #### import string +#### Regex #### +import re + #### Process #### import signal import threading @@ -93,9 +96,10 @@ class lxmf_connection: message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None + config_set_callback = None - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file @@ -108,6 +112,7 @@ class lxmf_connection: self.display_name = display_name self.announce_data = announce_data + self.announce_hidden = announce_hidden self.send_delay = int(send_delay) @@ -116,6 +121,8 @@ class lxmf_connection: else: self.desired_method_direct = True self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup @@ -130,6 +137,10 @@ class lxmf_connection: self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) @@ -184,10 +195,18 @@ class lxmf_connection: self.destination.set_link_established_callback(self.client_connected) - self.autoselect_propagation_node() + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) if self.sync_startup or self.sync_periodic: self.sync(True) @@ -214,6 +233,10 @@ class lxmf_connection: self.message_notification_failed_callback = handler_function + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + def destination_hash(self): return self.destination.hash @@ -257,7 +280,7 @@ class lxmf_connection: return "" - def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""): + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] @@ -272,8 +295,13 @@ class lxmf_connection: log("LXMF - Destination is invalid", LOG_ERROR) return + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + destination_identity = RNS.Identity.recall(destination) - destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) @@ -342,7 +370,7 @@ class lxmf_connection: message.desired_method_str = "propagated" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: @@ -359,26 +387,29 @@ class lxmf_connection: announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) elif self.announce_data: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: self.destination.announce() @@ -420,24 +451,50 @@ class lxmf_connection: return False - def autoselect_propagation_node(self): - if self.propagation_node is not None: - if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): - log("LXMF - Propagation node length is invalid", LOG_ERROR) - else: - try: - propagation_hash = bytes.fromhex(self.propagation_node) - except Exception as e: - log("LXMF - Propagation node is invalid", LOG_ERROR) - return + def propagation_node_set(self, dest_str): + if not dest_str: + return False - node_identity = RNS.Identity.recall(propagation_hash) - if node_identity != None: - log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) - propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) - self.message_router.set_outbound_propagation_node(propagation_hash) - else: - log("LXMF - Propagation node identity not known", LOG_ERROR) + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" def client_connected(self, link): @@ -525,6 +582,43 @@ class lxmf_connection: log("- App Data: " + message.app_data, LOG_DEBUG) + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + ############################################################################################################## # LXMF Functions @@ -701,6 +795,36 @@ def config_getoption(config, section, key, default=False, lng_key=""): +#### Config - Set ##### +def config_set(key=None, value=""): + global PATH + + try: + file = PATH + "/config.cfg.owr" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + + file = PATH + "/config.cfg" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + except: + pass + + + + #### Config - Read ##### def config_read(file=None, file_override=None): global CONFIG @@ -794,15 +918,15 @@ def config_default(file=None, file_override=None): # Value convert -def val_to_bool(val): +def val_to_bool(val, fallback_true=True, fallback_false=False): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": - return True + return fallback_true else: - return False + return fallback_false ############################################################################################################## @@ -967,6 +1091,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) else: config_propagation_node = None + if CONFIG.has_option("lxmf", "propagation_node_active"): + config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"] + else: + config_propagation_node_active = None + if path is None: path = PATH @@ -975,9 +1104,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) destination_name=CONFIG["lxmf"]["destination_name"], destination_type=CONFIG["lxmf"]["destination_type"], display_name=CONFIG["lxmf"]["display_name"], + announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"), send_delay=CONFIG["lxmf"]["send_delay"], desired_method=CONFIG["lxmf"]["desired_method"], propagation_node=config_propagation_node, + propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"), + propagation_node_active=config_propagation_node_active, try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], @@ -991,6 +1123,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + LXMF_CONNECTION.register_config_set_callback(config_set) log("LXMF - Connected", LOG_DEBUG) @@ -1085,7 +1218,13 @@ display_name = CMD desired_method = direct #direct/propagated # Propagation node address/hash. -#propagation_node = +propagation_node = + +# Set propagation node automatically. +propagation_node_auto = True + +# Current propagation node (Automatically set by the software). +propagation_node_active = # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. @@ -1101,6 +1240,10 @@ announce_startup_delay = 0 #Seconds announce_periodic = No announce_periodic_interval = 360 #Minutes +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + # Some waiting time after message send # for LXMF/Reticulum processing. send_delay = 0 #Seconds diff --git a/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/config.cfg.owr b/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/config.cfg.owr new file mode 100644 index 0000000..43d930b --- /dev/null +++ b/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/config.cfg.owr @@ -0,0 +1,151 @@ +#### Main program settings #### +[main] +lng = de # en/de + +fields_announce = True +fields_message = True + + +#### LXMF connection settings #### +[lxmf] +destination_type_conv = 6 + +display_name = Test Channel + +propagation_node_auto = True + +try_propagation_on_fail = Yes + +announce_startup = Yes +announce_periodic = Yes +announce_periodic_interval = 30 #Minutes + +sync_startup = Yes +sync_periodic = Yes +sync_periodic_interval = 5 #Minutes +sync_limit = 0 + + +#### Cluster settings #### +[cluster] +enabled = False + + +#### Router settings #### +[router] +enabled = False + + +#### High availability settings #### +[high_availability] +enabled = False + + +#### Message settings #### +[message] +send_title_prefix = +send_prefix = + +cluster_receive_title_prefix = +cluster_receive_prefix = @!cluster_source!-> + +cluster_send_title_prefix = !source_name! +cluster_send_prefix = @!cluster_destination!!n! + +fields_remove = gps +fields_remove_anonymous = src,gps + + +#### Statistic/Counter settings #### +[statistic] +enabled = True + + +#### User rights assignment #### +[rights] +admin = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,anonymous,join +mod = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,anonymous,join +user = receive_local,join +guest = receive_local,join +wait = join + + +#### User cmd assignment #### +[cmds] +admin = update,leave,invite,kick,block,unblock,allow,deny +mod = update,leave,invite,kick,block,unblock,allow,deny +user = update,leave +guest = update,leave +wait = update,leave + + +#### User config assignment #### +[configs] +admin = tx_enabled=True,group_enabled=True +mod = tx_enabled=True,group_enabled=True +user = +guest = +wait = + + +#### Interface settings - Messages #### +[interface_messages] +auto_add_admin = Welcome to the channel "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all channel members. +auto_add_admin-de = Willkommen in dem Kanal "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Kanalmitglieder verteilt. +auto_add_mod = Welcome to the channel "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all channel members. +auto_add_mod-de = Willkommen in dem Kanal "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Kanalmitglieder verteilt. +auto_add_user = Welcome to the channel "!display_name!"!!n!!n!!description! +auto_add_user-de = Willkommen in dem Kanal "!display_name!"!!n!!n!!description! +auto_add_guest = Welcome to the channel "!display_name!"!!n!!n!!description! +auto_add_guest-de = Willkommen in dem Kanal "!display_name!"!!n!!n!!description! +auto_add_wait = Welcome to the channel "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. +auto_add_wait-de = Willkommen in dem Kanal "!display_name!"!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + +invite_admin = You have been invited to the channel "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all channel members. +invite_admin-de = Sie wurden in den Kanal "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Kanalmitglieder verteilt. +invite_mod = You have been invited to the channel "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all channel members. +invite_mod-de = Sie wurden in den Kanal "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Kanalmitglieder verteilt. +invite_user = You have been invited to the channel "!display_name!"!!n!!n!!description! +invite_user-de = Sie wurden in den Kanal "!display_name!" eingeladen!!n!!n!!description! +invite_guest = You have been invited to the channel "!display_name!"!!n!!n!!description! +invite_guest-de = Sie wurden in den Kanal "!display_name!" eingeladen!!n!!n!!description! +invite_wait = You have been invited to the channel "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. +invite_wait-de = Sie wurden in den Kanal "!display_name!" eingeladen!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + +allow_admin = You have been allowed to join the channel "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all channel members. +allow_admin-de = Sie wurden erlaubt dem Kanal "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Kanalmitglieder verteilt. +allow_mod = You have been allowed to join the channel "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all channel members. +allow_mod-de = Sie wurden erlaubt dem Kanal "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Kanalmitglieder verteilt. +allow_user = You have been allowed to join the channel "!display_name!"!!n!!n!!description! +allow_user-de = Sie wurden erlaubt dem Kanal "!display_name!" beizutreten!!n!!n!!description! +allow_guest = You have been allowed to join the channel "!display_name!"!!n!!n!!description! +allow_guest-de = Sie wurden erlaubt dem Kanal "!display_name!" beizutreten!!n!!n!!description! +allow_wait = +allow_wait-de = + +member_join = Joins the channel. +member_join-de = Tritt dem Kanal bei. +member_leave = Leave the channel. +member_leave-de = Verlässt den Kanal. +member_invite = Was invited to the channel by !source_name! +member_invite-de = Wurde in den Kanal eingeladen von !source_name! +member_kick = Was kicked out of the channel by !source_name! +member_kick-de = Wurde aus dem Kanal geworfen von !source_name! +member_block = Was blocked by !source_name! +member_block-de = Wurde geblockt von !source_name! +member_unblock = Was unblocked by !source_name! +member_unblock-de = Wurde entsperrt von !source_name! +member_allow = Was allowed by !source_name! +member_allow-de = Wurde erlaubt von !source_name! +member_deny = Was denied by !source_name! +member_deny-de = Wurde abgelehnt von !source_name! +member_name_def = Name defined +member_name_def-de = Name definiert +member_name_change = Name changed +member_name_change-de = Namen geändert + + +#### Interface settings - Menu/command #### +[interface_menu] +cmd_unknown = ERROR: Unknown command. +cmd_unknown-de = FEHLER: Unbekannter Befehl. diff --git a/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/data.cfg b/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/data.cfg new file mode 100644 index 0000000..5d0fbba --- /dev/null +++ b/lxmf_distribution_group/Examples/Channel_for_Communicator_Software/data.cfg @@ -0,0 +1,52 @@ +[high_availability] +role = master +last_heartbeat = 0000-00-00 00:00:00 + +[main] +enabled_local = True +enabled_cluster = True +auto_add_user = True +auto_add_user_type = user +auto_add_cluster = True +auto_add_router = True +invite_user = True +invite_user_type = user +allow_user = True +allow_user_type = user +deny_user = True +deny_user_type = block_wait +description = +description-de = +rules = +rules-de = + +[admin] + +[mod] + +[user] + +[guest] + +[wait] + +[block_admin] + +[block_mod] + +[block_user] + +[block_guest] + +[block_wait] + +[cluster] + +[block_cluster] + +[router] + +[block_router] + +[pin] + diff --git a/lxmf_distribution_group/Examples/Group_for_Communicator_Software/config.cfg.owr b/lxmf_distribution_group/Examples/Group_for_Communicator_Software/config.cfg.owr new file mode 100644 index 0000000..0577d35 --- /dev/null +++ b/lxmf_distribution_group/Examples/Group_for_Communicator_Software/config.cfg.owr @@ -0,0 +1,151 @@ +#### Main program settings #### +[main] +lng = de # en/de + +fields_announce = True +fields_message = True + + +#### LXMF connection settings #### +[lxmf] +destination_type_conv = 4 + +display_name = Test Group + +propagation_node_auto = True + +try_propagation_on_fail = Yes + +announce_startup = Yes +announce_periodic = Yes +announce_periodic_interval = 30 #Minutes + +sync_startup = Yes +sync_periodic = Yes +sync_periodic_interval = 5 #Minutes +sync_limit = 0 + + +#### Cluster settings #### +[cluster] +enabled = False + + +#### Router settings #### +[router] +enabled = False + + +#### High availability settings #### +[high_availability] +enabled = False + + +#### Message settings #### +[message] +send_title_prefix = !source_name! +send_prefix = + +cluster_receive_title_prefix = +cluster_receive_prefix = @!cluster_source!-> + +cluster_send_title_prefix = !source_name! +cluster_send_prefix = @!cluster_destination!!n! + +fields_remove = +fields_remove_anonymous = src,gps + + +#### Statistic/Counter settings #### +[statistic] +enabled = True + + +#### User rights assignment #### +[rights] +admin = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,join +mod = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,join +user = receive_local,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,send_local,join +guest = receive_local,join +wait = join,leave + + +#### User cmd assignment #### +[cmds] +admin = update,leave,invite,kick,block,unblock,allow,deny +mod = update,leave,invite,kick,block,unblock,allow,deny +user = update,leave +guest = update,leave +wait = update,leave + + +#### User config assignment #### +[configs] +admin = +mod = +user = +guest = +wait = + + +#### Interface settings - Messages #### +[interface_messages] +auto_add_admin = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members. +auto_add_admin-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt. +auto_add_mod = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members. +auto_add_mod-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt. +auto_add_user = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members. +auto_add_user-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt. +auto_add_guest = Welcome to the group "!display_name!"!!n!!n!!description!!n!!n!You can only receive messages. +auto_add_guest-de = Willkommen in der Gruppe "!display_name!"!!n!!n!!description!!n!!n!Sie können nur Nachrichten empfangen. +auto_add_wait = Welcome to the group "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. +auto_add_wait-de = Willkommen in der Gruppe "!display_name!"!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + +invite_admin = You have been invited to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members. +invite_admin-de = Sie wurden in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt. +invite_mod = You have been invited to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members. +invite_mod-de = Sie wurden in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt. +invite_user = You have been invited to the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members. +invite_user-de = Sie wurden in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt. +invite_guest = You have been invited to the group "!display_name!"!!n!!n!!description!!n!!n!You can only receive messages. +invite_guest-de = Sie wurden in die Gruppe "!display_name!" eingeladen!!n!!n!!description!!n!!n!Sie können nur Nachrichten empfangen. +invite_wait = You have been invited to the group "!display_name!"!!n!!n!You still need to be allowed to join. You will be notified automatically. +invite_wait-de = Sie wurden in die Gruppe "!display_name!" eingeladen!!n!!n!Der Beitritt muss ihnen noch erlaubt werden. Sie werden darüber automatisch benachrichtigt. + +allow_admin = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members. +allow_admin-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt. +allow_mod = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members. +allow_mod-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt. +allow_user = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!The messages sent here are distributed to all group members. +allow_user-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Die hier gesendeten Nachrichten werden an alle Gruppenmitglieder verteilt. +allow_guest = You have been allowed to join the group "!display_name!"!!n!!n!!description!!n!!n!You can only receive messages. +allow_guest-de = Sie wurden erlaubt der Gruppe "!display_name!" beizutreten!!n!!n!!description!!n!!n!Sie können nur Nachrichten empfangen. +allow_wait = +allow_wait-de = + +member_join = Joins the group. +member_join-de = Tritt der Gruppe bei. +member_leave = Leave the group. +member_leave-de = Verlässt die Gruppe. +member_invite = Was invited to the group by !source_name! +member_invite-de = Wurde in die Gruppe eingeladen von !source_name! +member_kick = Was kicked out of the group by !source_name! +member_kick-de = Wurde aus der Gruppe geworfen von !source_name! +member_block = Was blocked by !source_name! +member_block-de = Wurde geblockt von !source_name! +member_unblock = Was unblocked by !source_name! +member_unblock-de = Wurde entsperrt von !source_name! +member_allow = Was allowed by !source_name! +member_allow-de = Wurde erlaubt von !source_name! +member_deny = Was denied by !source_name! +member_deny-de = Wurde abgelehnt von !source_name! +member_name_def = Name defined +member_name_def-de = Name definiert +member_name_change = Name changed +member_name_change-de = Namen geändert + + +#### Interface settings - Menu/command #### +[interface_menu] +cmd_unknown = ERROR: Unknown command. +cmd_unknown-de = FEHLER: Unbekannter Befehl. diff --git a/lxmf_distribution_group/Examples/Group_for_Communicator_Software/data.cfg b/lxmf_distribution_group/Examples/Group_for_Communicator_Software/data.cfg new file mode 100644 index 0000000..20528b0 --- /dev/null +++ b/lxmf_distribution_group/Examples/Group_for_Communicator_Software/data.cfg @@ -0,0 +1,52 @@ +[high_availability] +role = master +last_heartbeat = 0000-00-00 00:00:00 + +[main] +enabled_local = True +enabled_cluster = True +auto_add_user = True +auto_add_user_type = user +auto_add_cluster = True +auto_add_router = True +invite_user = True +invite_user_type = user +allow_user = True +allow_user_type = user +deny_user = True +deny_user_type = block_wait +description = This group is for a first test of functionality. +description-de = Diese Gruppe dient einem ersten Test der Funktionalität. +rules = Please follow the general rules of etiquette which should be taken for granted!!n!Prohibited are:!n!Spam, insults, violence, sex, illegal topics +rules-de = Bitte befolgen Sie die allgemeinen benimm-dich-Regeln welche als selbstverständlich gelten sollten!!n!Verboten sind:!n!Spam, Beleidigungen, Gewalt, Sex, illegale Themen + +[admin] + +[mod] + +[user] + +[guest] + +[wait] + +[block_admin] + +[block_mod] + +[block_user] + +[block_guest] + +[block_wait] + +[cluster] + +[block_cluster] + +[router] + +[block_router] + +[pin] + diff --git a/lxmf_distribution_group/Examples/README.md b/lxmf_distribution_group/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_distribution_group/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_distribution_group/lxmf_distribution_group.py b/lxmf_distribution_group/lxmf_distribution_group.py index 07c7c28..0fda855 100755 --- a/lxmf_distribution_group/lxmf_distribution_group.py +++ b/lxmf_distribution_group/lxmf_distribution_group.py @@ -51,6 +51,9 @@ import pickle #### String #### import string +#### Regex #### +import re + #### Search #### import fnmatch @@ -104,9 +107,10 @@ class lxmf_connection: message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None + config_set_callback = None - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file @@ -119,6 +123,7 @@ class lxmf_connection: self.display_name = display_name self.announce_data = announce_data + self.announce_hidden = announce_hidden self.send_delay = int(send_delay) @@ -127,6 +132,8 @@ class lxmf_connection: else: self.desired_method_direct = True self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup @@ -141,6 +148,10 @@ class lxmf_connection: self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) @@ -195,10 +206,18 @@ class lxmf_connection: self.destination.set_link_established_callback(self.client_connected) - self.autoselect_propagation_node() + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) if self.sync_startup or self.sync_periodic: self.sync(True) @@ -225,6 +244,10 @@ class lxmf_connection: self.message_notification_failed_callback = handler_function + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + def destination_hash(self): return self.destination.hash @@ -268,7 +291,7 @@ class lxmf_connection: return "" - def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""): + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] @@ -283,8 +306,13 @@ class lxmf_connection: log("LXMF - Destination is invalid", LOG_ERROR) return + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + destination_identity = RNS.Identity.recall(destination) - destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) @@ -353,7 +381,7 @@ class lxmf_connection: message.desired_method_str = "propagated" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: @@ -370,26 +398,29 @@ class lxmf_connection: announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) elif self.announce_data: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: self.destination.announce() @@ -431,24 +462,50 @@ class lxmf_connection: return False - def autoselect_propagation_node(self): - if self.propagation_node is not None: - if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): - log("LXMF - Propagation node length is invalid", LOG_ERROR) - else: - try: - propagation_hash = bytes.fromhex(self.propagation_node) - except Exception as e: - log("LXMF - Propagation node is invalid", LOG_ERROR) - return + def propagation_node_set(self, dest_str): + if not dest_str: + return False - node_identity = RNS.Identity.recall(propagation_hash) - if node_identity != None: - log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) - propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) - self.message_router.set_outbound_propagation_node(propagation_hash) - else: - log("LXMF - Propagation node identity not known", LOG_ERROR) + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" def client_connected(self, link): @@ -536,12 +593,49 @@ class lxmf_connection: log("- App Data: " + message.app_data, LOG_DEBUG) + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + ############################################################################################################## # RNS Class class rns_connection: - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="rns", destination_type="connect", announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, announce_data=""): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="rns", destination_type="connect", announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, announce_data="", announce_hidden=False): self.storage_path = storage_path self.identity_file = identity_file @@ -559,6 +653,11 @@ class rns_connection: self.announce_periodic_interval = int(announce_periodic_interval) self.announce_data = announce_data + self.announce_hidden = announce_hidden + + if not self.storage_path: + log("RNS - No storage_path parameter", LOG_ERROR) + return if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) @@ -596,7 +695,7 @@ class rns_connection: self.destination.set_proof_strategy(RNS.Destination.PROVE_ALL) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) def register_announce_callback(self, handler_function): @@ -647,7 +746,9 @@ class rns_connection: return "" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): + announce_timer = None + if self.announce_periodic and self.announce_periodic_interval > 0: announce_timer = threading.Timer(self.announce_periodic_interval*60, self.announce) announce_timer.daemon = True @@ -656,31 +757,35 @@ class rns_connection: if initial: if self.announce_startup: if self.announce_startup_delay > 0: - announce_timer.cancel() + if announce_timer is not None: + announce_timer.cancel() announce_timer = threading.Timer(self.announce_startup_delay, self.announce) announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("RNS - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) @@ -727,13 +832,14 @@ class lxmf_announce_callback: content_group = config_get(CONFIG, "interface_messages", "member_"+content_type, "", lng_key) if content_group != "": + fields = fields_generate(lng_key, h=destination_hash ,n=value, tpl=content_type) content_group = replace(content_group, source_hash, value, "", lng_key) content_group = content_group + content_add for section in sections: if "receive_auto_"+content_type in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): if key != source_hash: - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") if CONFIG["main"].getboolean("auto_save_data"): DATA.remove_option("main", "unsaved") @@ -789,7 +895,7 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_block", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return source_rights = [] @@ -804,14 +910,18 @@ def lxmf_message_received_callback(message): if fields: if "c_n" in fields and "c_t" in fields and "m_t" in fields: if fields["c_n"] == CONFIG["cluster"]["name"] and fields["c_t"] == CONFIG["cluster"]["type"] and "cluster" in source_rights and config_getboolean(CONFIG, "cluster", "enabled"): + title_prefix = config_get(CONFIG, "message", "cluster_receive_title_prefix", "", lng_key) content_prefix = config_get(CONFIG, "message", "cluster_receive_prefix", "", lng_key) content_suffix = config_get(CONFIG, "message", "cluster_receive_suffix", "", lng_key) + title_prefix = replace(title_prefix, source_hash, source_name, source_right, lng_key) content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) source = source_name.rsplit('/', 1)[-1] destination = config_get(CONFIG, "cluster", "display_name", "", lng_key).rsplit('/', 1)[-1] + title_prefix = title_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + title_prefix = title_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) @@ -825,6 +935,7 @@ def lxmf_message_received_callback(message): if search != "": content = re.sub(search, config_get(CONFIG, "message", "cluster_receive_regex_replace"), content) + title = title_prefix + title content = content_prefix + content + content_suffix if config_get(CONFIG, "message", "timestamp", "", lng_key) == "client": @@ -834,12 +945,12 @@ def lxmf_message_received_callback(message): if CONFIG["message"].getboolean("fields"): if message.fields: - fields = message.fields + fields = fields_remove(message.fields, "fields_remove_anonymous" if "anonymous" in source_rights else "fields_remove") else: fields = {} else: fields = {} - fields["type"] = CONFIG["lxmf"]["destination_type_conv"] + fields = fields(fields) if CONFIG["statistic"].getboolean("enabled") and CONFIG["statistic"].getboolean("cluster"): statistic("add", "cluster_in_" + message.desired_method_str) @@ -892,23 +1003,24 @@ def lxmf_message_received_callback(message): log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature 'new'", LOG_DEBUG) return - key = DATA["main"]["auto_add_user_type"] - if DATA.has_section(key) and key != "main": + source_right = DATA["main"]["auto_add_user_type"] + if DATA.has_section(source_right) and source_right != "main": if CONFIG["main"].getboolean("auto_name_add"): app_data = RNS.Identity.recall_app_data(message.source_hash) if app_data != None: source_name = app_data.decode('utf-8') - DATA[key][source_hash] = source_name + DATA[source_right][source_hash] = source_name DATA.remove_option("main", "unsaved") - content = config_get(CONFIG, "interface_messages", "auto_add_"+key, "", lng_key) + content = config_get(CONFIG, "interface_messages", "auto_add_"+source_right, "", lng_key) content_group = config_get(CONFIG, "interface_messages", "member_join", "", lng_key) content_group = replace(content_group, source_hash, source_name, source_right, lng_key) if content_group != "": + fields = fields_generate(lng_key, h=message.source_hash ,n=source_name, m=True, tpl="join") for section in sections: if "receive_join" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): if key != source_hash: - LXMF_CONNECTION.send(key, content_group, title, {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, title, fields, None, "interface_send") if CONFIG["main"].getboolean("auto_save_data"): DATA.remove_option("main", "unsaved") if not data_save(PATH + "/data.cfg"): @@ -917,7 +1029,7 @@ def lxmf_message_received_callback(message): DATA["main"]["unsaved"] = "True" content = replace(content, source_hash, source_name, source_right, lng_key) if content != "": - LXMF_CONNECTION.send(source_hash, content, title, {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content, title, fields_generate(lng_key, m=True, d=True, r=True, cmd=source_right, config=source_right, tpl="info"), None, "interface_send") return elif source_right == "": log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " not exist (auto add disabled)", LOG_DEBUG) @@ -937,7 +1049,7 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_signature", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return @@ -948,7 +1060,7 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_length_min", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return @@ -959,10 +1071,11 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_length_max", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return + title_prefix = config_get(CONFIG, "message", "receive_title_prefix", "", lng_key) content_prefix = config_get(CONFIG, "message", "receive_prefix", "", lng_key) content_suffix = config_get(CONFIG, "message", "receive_suffix", "", lng_key) @@ -974,6 +1087,7 @@ def lxmf_message_received_callback(message): if search != "": content = re.sub(search, config_get(CONFIG, "message", "receive_regex_replace"), content) + title = title_prefix + title content = content_prefix + content + content_suffix @@ -985,7 +1099,7 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_interface_enabled", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return if "interface" not in source_rights: @@ -994,10 +1108,10 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_interface_right", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return - content = interface(content[len(CONFIG["interface"]["delimiter_input"]):], source_hash, source_name, source_right, source_rights, lng_key) + content = interface(content[len(CONFIG["interface"]["delimiter_input"]):], source_hash, source_name, source_right, source_rights, lng_key, message) if content == "": log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " 'interface' not allowed (empty response)", LOG_DEBUG) return @@ -1009,7 +1123,7 @@ def lxmf_message_received_callback(message): statistic("value_set", source_hash, "activity", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) statistic("value_set", source_hash, "activity_receive", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))) - LXMF_CONNECTION.send(source_hash, content, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content, "", fields_generate(lng_key), None, "interface_send") return @@ -1021,7 +1135,7 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_cluster_enabled", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return if "send_cluster" not in source_rights: @@ -1030,14 +1144,14 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_cluster_right", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return try: content = content[len(CONFIG["cluster"]["delimiter_input"]):] destination, content = content.split(" ", 1) except: - LXMF_CONNECTION.send(source_hash, config_get(CONFIG, "interface_menu", "cluster_format_error", "", lng_key) , "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, config_get(CONFIG, "interface_menu", "cluster_format_error", "", lng_key) , "", fields_generate(lng_key), None, "interface_send") return destinations = [] @@ -1046,7 +1160,7 @@ def lxmf_message_received_callback(message): destinations.append(key) if len(destinations) == 0: - LXMF_CONNECTION.send(source_hash, config_get(CONFIG, "interface_menu", "cluster_found_error", "", lng_key) , "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, config_get(CONFIG, "interface_menu", "cluster_found_error", "", lng_key) , "", fields_generate(lng_key), None, "interface_send") return length = config_getint(CONFIG, "message", "cluster_send_length_min", 0, lng_key) @@ -1056,7 +1170,7 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_length_min", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return length = config_getint(CONFIG, "message", "cluster_send_length_max", 0, lng_key) @@ -1066,16 +1180,25 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_length_max", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return + title_prefix = config_get(CONFIG, "message", "cluster_send_title_prefix", "", lng_key) content_prefix = config_get(CONFIG, "message", "cluster_send_prefix", "", lng_key) content_suffix = config_get(CONFIG, "message", "cluster_send_suffix", "", lng_key) - content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) - content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) + if "anonymous" in source_rights: + title_prefix = replace(title_prefix, "", "", source_right, lng_key) + content_prefix = replace(content_prefix, "", "", source_right, lng_key) + content_suffix = replace(content_suffix, "", "", source_right, lng_key) + else: + title_prefix = replace(title_prefix, source_hash, source_name, source_right, lng_key) + content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) + content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) source = config_get(CONFIG, "cluster", "display_name", "", lng_key).rsplit('/', 1)[-1] + title_prefix = title_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) + title_prefix = title_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) content_prefix = content_prefix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_destination"+CONFIG["interface"]["delimiter_output"], destination) content_suffix = content_suffix.replace(CONFIG["interface"]["delimiter_output"]+"cluster_source"+CONFIG["interface"]["delimiter_output"], source) @@ -1091,11 +1214,17 @@ def lxmf_message_received_callback(message): if CONFIG["message"].getboolean("fields"): if message.fields: - fields = message.fields + fields = fields_remove(message.fields, "fields_remove_anonymous" if "anonymous" in source_rights else "fields_remove") else: fields = {} else: fields = {} + if CONFIG["main"].getboolean("fields_message"): + fields["hash"] = message.hash + if not "anonymous" in source_rights: + fields["src"] = {} + fields["src"]["h"] = message.source_hash + fields["src"]["n"] = source_name fields["c_n"] = CONFIG["cluster"]["name"] fields["c_t"] = CONFIG["cluster"]["type"] @@ -1107,6 +1236,7 @@ def lxmf_message_received_callback(message): else: fields["m_t"] = "message" + title = title_prefix + title content = content_prefix + content + content_suffix if config_get(CONFIG, "message", "timestamp", "", lng_key) == "client": @@ -1131,12 +1261,20 @@ def lxmf_message_received_callback(message): if CONFIG["message"].getboolean("fields"): if message.fields: - fields = message.fields + fields = fields_remove(message.fields, "fields_remove_anonymous" if "anonymous" in source_rights else "fields_remove") else: fields = {} else: fields = {} - fields["type"] = CONFIG["lxmf"]["destination_type_conv"] + + if CONFIG["main"].getboolean("fields_message"): + if CONFIG["lxmf"]["destination_type_conv"] != "": + fields["type"] = CONFIG["lxmf"].getint("destination_type_conv") + fields["hash"] = message.hash + if not "anonymous" in source_rights: + fields["src"] = {} + fields["src"]["h"] = message.source_hash + fields["src"]["n"] = source_name for section in sections: if "receive_cluster_send" in config_get(CONFIG, "rights", section).split(",") or (cluster_loop and "receive_cluster_loop" in config_get(CONFIG, "rights", section).split(",")): @@ -1158,7 +1296,7 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_length_min", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return length = config_getint(CONFIG, "message", "send_length_max", 0, lng_key) @@ -1168,14 +1306,21 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_length_max", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return + title_prefix = config_get(CONFIG, "message", "send_title_prefix", "", lng_key) content_prefix = config_get(CONFIG, "message", "send_prefix", "", lng_key) content_suffix = config_get(CONFIG, "message", "send_suffix", "", lng_key) - content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) - content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) + if "anonymous" in source_rights: + title_prefix = replace(title_prefix, "", "", source_right, lng_key) + content_prefix = replace(content_prefix, "", "", source_right, lng_key) + content_suffix = replace(content_suffix, "", "", source_right, lng_key) + else: + title_prefix = replace(title_prefix, source_hash, source_name, source_right, lng_key) + content_prefix = replace(content_prefix, source_hash, source_name, source_right, lng_key) + content_suffix = replace(content_suffix, source_hash, source_name, source_right, lng_key) search = config_get(CONFIG, "message", "send_search") if search != "": @@ -1185,16 +1330,25 @@ def lxmf_message_received_callback(message): if search != "": content = re.sub(search, config_get(CONFIG, "message", "send_regex_replace"), content) + title = title_prefix + title content = content_prefix + content + content_suffix if CONFIG["message"].getboolean("fields"): if message.fields: - fields = message.fields + fields = fields_remove(message.fields, "fields_remove_anonymous" if "anonymous" in source_rights else "fields_remove") else: fields = {} else: fields = {} - fields["type"] = CONFIG["lxmf"]["destination_type_conv"] + + if CONFIG["main"].getboolean("fields_message"): + if CONFIG["lxmf"]["destination_type_conv"] != "": + fields["type"] = CONFIG["lxmf"].getint("destination_type_conv") + fields["hash"] = message.hash + if not "anonymous" in source_rights: + fields["src"] = {} + fields["src"]["h"] = message.source_hash + fields["src"]["n"] = source_name if config_get(CONFIG, "message", "timestamp", "", lng_key) == "client": timestamp = message.timestamp @@ -1221,13 +1375,13 @@ def lxmf_message_received_callback(message): content_user = config_get(CONFIG, "interface_messages", "reply_local_right", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") else: if "reply_local_enabled" in source_rights: content_user = config_get(CONFIG, "interface_messages", "reply_local_enabled", "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(source_hash, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(source_hash, content_user, "", fields_generate(lng_key), None, "interface_send") return @@ -1307,7 +1461,7 @@ class rns_announce_callback: for section in sections: if "receive_cluster_join" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields_generate(lng_key), None, "interface_send") DATA["cluster"][receive["h"]] = receive["c_n"] executed = True @@ -1330,7 +1484,7 @@ class rns_announce_callback: #### Interface ##### -def interface(cmd, source_hash, source_name, source_right, source_rights, lng_key): +def interface(cmd, source_hash, source_name, source_right, source_rights, lng_key, message): cmd = cmd.strip() content = "" @@ -1355,8 +1509,30 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content = replace(content, source_hash, source_name, source_right, lng_key) + # "/update" command. + elif (cmd == "update") and "update" in source_rights: + try: + content = config_get(CONFIG, "interface_menu", "update_ok", "", lng_key) + LXMF_CONNECTION.send(source_hash, content, "", fields_generate(lng_key, m=True, d=True, r=True, cmd=source_right, config=source_right, tpl="update"), None, "interface_send") + content = "" + except: + content = config_get(CONFIG, "interface_menu", "update_error", "", lng_key) + + + # "/join" command. + elif (cmd == "join" or cmd == "subscribe") and "join" in source_rights: + try: + content = config_get(CONFIG, "interface_messages", "auto_add_"+source_right, "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + if content != "": + LXMF_CONNECTION.send(source_hash, content, "", fields_generate(lng_key, m=True, d=True, r=True, cmd=source_right, config=source_right, tpl="info"), None, "interface_send") + content = "" + except: + content = config_get(CONFIG, "interface_menu", "join_error", "", lng_key) + + # "/leave" command. - elif (cmd == "leave" or cmd == "part") and "leave" in source_rights: + elif (cmd == "leave" or cmd == "unsubscribe" or cmd == "part") and "leave" in source_rights: try: for section in sections: for (key, val) in DATA.items(section): @@ -1369,12 +1545,17 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content_group = config_get(CONFIG, "interface_messages", "member_leave", "", lng_key) content_group = replace(content_group, source_hash, source_name, source_right, lng_key) if content_group != "": + fields = fields_generate(lng_key, h=message.source_hash ,n=source_name, m=True, tpl="leave") for section in sections: if "receive_leave" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") content = config_get(CONFIG, "interface_menu", "leave_ok", "", lng_key) + content = replace(content, source_hash, source_name, source_right, lng_key) + if content != "": + LXMF_CONNECTION.send(source_hash, content, "", {"data": None, "tpl": "info"}, None, "interface_send") + content = "" if CONFIG["main"].getboolean("auto_save_data"): DATA.remove_option("main", "unsaved") @@ -1408,13 +1589,14 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content_group = config_get(CONFIG, "interface_messages", "member_"+content_type, "", lng_key) if content_group != "": + fields = fields_generate(lng_key, h=message.source_hash ,n=source_name, tpl=content_type) content_group = replace(content_group, source_hash, source_name, source_right, lng_key) content_group = content_group + content_add for section in sections: if "receive_"+content_type in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): if key != source_hash: - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") content = config_get(CONFIG, "interface_menu", "name_ok", "", lng_key) + " " + value @@ -1476,7 +1658,7 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke if "receive_pin_add" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): if key != source_hash: - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields_generate(lng_key, h=message.source_hash ,n=source_name), None, "interface_send") content = config_get(CONFIG, "interface_menu", "pin_add_ok", "", lng_key) @@ -1507,7 +1689,7 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke if "receive_pin_add" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): if key != source_hash: - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields_generate(lng_key, h=message.source_hash ,n=source_name), None, "interface_send") content = config_get(CONFIG, "interface_menu", "pin_remove_ok", "", lng_key) @@ -1748,7 +1930,7 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke # "/delivery" command. #elif cmd == "delivery" and "delivery" in source_rights: - #todo + # TODO # "/enable_local" command. @@ -2000,7 +2182,7 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke if "receive_description" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): if key != source_hash: - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields_generate(lng_key, h=message.source_hash ,n=source_name, tpl="description"), None, "interface_send") content = config_get(CONFIG, "interface_menu", "description", "", lng_key) + " " + value DATA["main"]["unsaved"] = "True" @@ -2025,7 +2207,7 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke if "receive_rules" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): if key != source_hash: - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields_generate(lng_key, h=message.source_hash ,n=source_name, tpl="rules"), None, "interface_send") content = config_get(CONFIG, "interface_menu", "rules", "", lng_key) + " " + value DATA["main"]["unsaved"] = "True" @@ -2229,18 +2411,19 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content_user = replace(content_user, source_hash, source_name, source_right, lng_key) content_user = content_user.replace(delimiter+"user_name"+delimiter, user_name) if content_user != "": - LXMF_CONNECTION.send(value, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(value, content_user, "", fields_generate(lng_key, m=True, d=True, r=True, cmd=key, config=key), None, "interface_send") content_group = config_get(CONFIG, "interface_messages", "member_invite", "", lng_key) content_group = replace(content_group, source_hash, source_name, source_right, lng_key) content_group = content_group.replace(delimiter+"user_address"+delimiter, value) content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) if content_group != "": + fields = fields_generate(lng_key, h=bytes.fromhex(value) ,n=user_name, m=True, tpl="invite") for section in sections: if "receive_invite" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): if key != source_hash: - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") content = config_get(CONFIG, "interface_menu", "invite_ok", "", lng_key) + " <" + value + ">" @@ -2281,17 +2464,18 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content_user = config_get(CONFIG, "interface_messages", "kick_"+user_section, "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(value, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(value, content_user, "", fields_generate(lng_key), None, "interface_send") content_group = config_get(CONFIG, "interface_messages", "member_kick", "", lng_key) content_group = replace(content_group, source_hash, source_name, source_right, lng_key) content_group = content_group.replace(delimiter+"user_address"+delimiter, value) content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) if content_group != "": + fields = fields_generate(lng_key, h=bytes.fromhex(value) ,n=user_name, m=True, tpl="kick") for section in sections: if "receive_kick" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") content = config_get(CONFIG, "interface_menu", "kick_ok", "", lng_key) content = content.replace(delimiter+"user_address"+delimiter, value) @@ -2332,17 +2516,18 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content_user = config_get(CONFIG, "interface_messages", "block_"+user_section, "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(value, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(value, content_user, "", fields_generate(lng_key), None, "interface_send") content_group = config_get(CONFIG, "interface_messages", "member_block", "", lng_key) content_group = replace(content_group, source_hash, source_name, source_right, lng_key) content_group = content_group.replace(delimiter+"user_address"+delimiter, value) content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) if content_group != "": + fields = fields_generate(lng_key, h=bytes.fromhex(value) ,n=user_name, m=True, tpl="block") for section in sections: if "receive_block" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") content = config_get(CONFIG, "interface_menu", "block_ok", "", lng_key) content = content.replace(delimiter+"user_address"+delimiter, value) @@ -2384,17 +2569,18 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content_user = config_get(CONFIG, "interface_messages", "unblock_"+user_section, "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(value, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(value, content_user, "", fields_generate(lng_key, m=True, d=True, r=True, cmd=user_section, config=user_section), None, "interface_send") content_group = config_get(CONFIG, "interface_messages", "member_unblock", "", lng_key) content_group = replace(content_group, source_hash, source_name, source_right, lng_key) content_group = content_group.replace(delimiter+"user_address"+delimiter, value) content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) if content_group != "": + fields = fields_generate(lng_key, h=bytes.fromhex(value) ,n=user_name, m=True, tpl="unblock") for section in sections: if "receive_block" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") content = config_get(CONFIG, "interface_menu", "unblock_ok", "", lng_key) content = content.replace(delimiter+"user_address"+delimiter, value) @@ -2435,17 +2621,18 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content_user = config_get(CONFIG, "interface_messages", "allow_"+user_section, "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(value, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(value, content_user, "", fields_generate(lng_key, m=True, d=True, r=True, cmd=user_section, config=user_section), None, "interface_send") content_group = config_get(CONFIG, "interface_messages", "member_allow", "", lng_key) content_group = replace(content_group, source_hash, source_name, source_right, lng_key) content_group = content_group.replace(delimiter+"user_address"+delimiter, value) content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) if content_group != "": + fields = fields_generate(lng_key, h=bytes.fromhex(value) ,n=user_name, m=True, tpl="allow") for section in sections: if "receive_block" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") content = config_get(CONFIG, "interface_menu", "allow_ok", "", lng_key) content = content.replace(delimiter+"user_address"+delimiter, value) @@ -2487,17 +2674,18 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke content_user = config_get(CONFIG, "interface_messages", "deny_"+user_section, "", lng_key) content_user = replace(content_user, source_hash, source_name, source_right, lng_key) if content_user != "": - LXMF_CONNECTION.send(value, content_user, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(value, content_user, "", fields_generate(lng_key), None, "interface_send") content_group = config_get(CONFIG, "interface_messages", "member_deny", "", lng_key) content_group = replace(content_group, source_hash, source_name, source_right, lng_key) content_group = content_group.replace(delimiter+"user_address"+delimiter, value) content_group = content_group.replace(delimiter+"user_name"+delimiter, user_name) if content_group != "": + fields = fields_generate(lng_key, h=bytes.fromhex(value) ,n=user_name, m=True, tpl="deny") for section in sections: if "receive_block" in config_get(CONFIG, "rights", section).split(","): for (key, val) in DATA.items(section): - LXMF_CONNECTION.send(key, content_group, "", {"type": CONFIG["lxmf"]["destination_type_conv"]}, None, "interface_send") + LXMF_CONNECTION.send(key, content_group, "", fields, None, "interface_send") content = config_get(CONFIG, "interface_menu", "deny_ok", "", lng_key) content = content.replace(delimiter+"user_address"+delimiter, value) @@ -2663,6 +2851,87 @@ def interface(cmd, source_hash, source_name, source_right, source_rights, lng_ke +#### Fields ##### +def fields_remove(fields=None, key="fields_remove"): + search = config_get(CONFIG, "message", key).split(",") + + delete = [] + for field in fields: + if field in search: + delete.append(field) + + for field in delete: + del fields[field] + + return fields + + + + +#### Fields ##### +def fields_generate(lng_key, fields=None, h=None, n=None, m=False, d=False, r=False, cmd=None, config=None, tpl=None): + if not CONFIG["main"].getboolean("fields_message"): + return fields + + if not fields: + fields = {} + + if CONFIG["lxmf"]["destination_type_conv"] != "": + fields["type"] = CONFIG["lxmf"].getint("destination_type_conv") + + if h: + fields["src"] = {} + fields["src"]["h"] = h + if n: + fields["src"]["n"] = n + else: + fields["src"]["n"] = "" + + if m or d or r or cmd or config: + fields["data"] = {} + + if m: + fields["data"]["m"] = {} + for (key, val) in CONFIG.items("rights"): + if DATA.has_section(key): + fields["data"]["m"][key] = {} + for (section_key, section_val) in DATA.items(key): + try: + h = bytes.fromhex(LXMF_CONNECTION.destination_correct(section_key)) + fields["data"]["m"][key][h] = section_val + except: + pass + + if d: + fields["data"]["d"] = config_get(DATA, "main", "description", "", lng_key).replace(CONFIG["interface"]["delimiter_output"]+"n"+CONFIG["interface"]["delimiter_output"], "\n") + + if r: + fields["data"]["r"] = config_get(DATA, "main", "rules", "", lng_key).replace(CONFIG["interface"]["delimiter_output"]+"n"+CONFIG["interface"]["delimiter_output"], "\n") + + if cmd: + fields["data"]["cmd"] = [] + if CONFIG.has_option("cmds", cmd): + cmds = config_get(CONFIG, "cmds", cmd).split(",") + for cmd in cmds: + fields["data"]["cmd"].append({"c": "/"+cmd}) + + if config: + fields["data"]["config"] = {} + if CONFIG.has_option("configs", config): + configs = config_get(CONFIG, "configs", config).split(",") + for config in configs: + if config != "": + key, value = config.split("=", 1) + fields["data"]["config"][key] = val_to_bool(value, fallback_true=value, fallback_false=value) + + if tpl: + fields["tpl"] = tpl + + return fields + + + + #### Replace ##### def replace(text, source_hash, source_name, source_right, lng_key): delimiter = CONFIG["interface"]["delimiter_output"] @@ -2756,6 +3025,36 @@ def config_getoption(config, section, key, default=False, lng_key=""): +#### Config - Set ##### +def config_set(key=None, value=""): + global PATH + + try: + file = PATH + "/config.cfg.owr" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + + file = PATH + "/config.cfg" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + except: + pass + + + + #### Config - Read ##### def config_read(file=None, file_override=None): global CONFIG @@ -3238,15 +3537,15 @@ def statistic_default(section="global"): # Value convert -def val_to_bool(val): +def val_to_bool(val, fallback_true=True, fallback_false=False): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": - return True + return fallback_true else: - return False + return fallback_false ############################################################################################################## @@ -3351,7 +3650,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) global RNS_MAIN_CONNECTION global LXMF_CONNECTION global RNS_CONNECTION - + if path is not None: if path.endswith("/"): path = path[:-1] @@ -3402,6 +3701,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) if CONFIG["statistic"].getboolean("enabled"): statistic_read(PATH + "/statistic.cfg") + if CONFIG.has_section("cmds") and CONFIG.has_section("rights"): + for (key, val) in CONFIG.items("cmds"): + if val != "" and CONFIG.has_option("rights", key): + CONFIG["rights"][key] += ",interface,"+val + RNS_MAIN_CONNECTION = RNS.Reticulum(configdir=PATH_RNS, loglevel=rns_loglevel) log("...............................................................................", LOG_INFO) @@ -3420,13 +3724,22 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) else: config_propagation_node = None + if CONFIG.has_option("lxmf", "propagation_node_active"): + config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"] + else: + config_propagation_node_active = None + if path is None: path = PATH display_name = CONFIG["lxmf"]["display_name"] + announce_data = None if CONFIG["lxmf"]["destination_type_conv"] != "": try: - display_name += chr(int(CONFIG["lxmf"]["destination_type_conv"])) + if CONFIG["main"].getboolean("fields_announce"): + announce_data = umsgpack.packb({"c": CONFIG["lxmf"]["display_name"].encode("utf-8"), "t": None, "f": {"type": CONFIG["lxmf"].getint("destination_type_conv")}}) + else: + display_name += chr(CONFIG["lxmf"].getint("destination_type_conv")) except: pass @@ -3437,9 +3750,13 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) destination_name=CONFIG["lxmf"]["destination_name"], destination_type=CONFIG["lxmf"]["destination_type"], display_name=display_name, + announce_data = announce_data, + announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"), send_delay=CONFIG["lxmf"]["send_delay"], desired_method=CONFIG["lxmf"]["desired_method"], propagation_node=config_propagation_node, + propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"), + propagation_node_active=config_propagation_node_active, try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], @@ -3453,6 +3770,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + LXMF_CONNECTION.register_config_set_callback(config_set) if CONFIG["statistic"].getboolean("enabled"): LXMF_CONNECTION.register_message_notification_success_callback(lxmf_message_notification_success_callback) @@ -3498,7 +3816,8 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) announce_startup_delay=CONFIG["rns"]["announce_startup_delay"], announce_periodic=CONFIG["rns"].getboolean("announce_periodic"), announce_periodic_interval=CONFIG["rns"]["announce_periodic_interval"], - announce_data = json.dumps(announce_data, separators=(',', ':')) + announce_data = json.dumps(announce_data, separators=(',', ':')), + announce_hidden=CONFIG["rns"].getboolean("announce_hidden") ) RNS_CONNECTION.register_announce_callback(rns_announce_callback) log("RNS - Connected", LOG_DEBUG) @@ -3583,8 +3902,8 @@ lng = en # en/de # It is also used in the group description/info. display_name = Distribution Group -# Propagation node address/hash. -propagation_node = ca2762fe5283873719aececfb9e18835 +# Set propagation node automatically. +propagation_node_auto = True # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. @@ -3684,6 +4003,11 @@ auto_name_add = True auto_name_def = True auto_name_change = False +# Transport extended data in the announce and fields variable. +# This is needed for the integration of advanced client apps. +fields_announce = False +fields_message = False + @@ -3694,7 +4018,7 @@ auto_name_change = False # to be compatibel with other LXMF programs. destination_name = lxmf destination_type = delivery -destination_type_conv = 4 +destination_type_conv = #4=Group, 6=Channel # The name will be visible to other peers # on the network, and included in announces. @@ -3705,7 +4029,13 @@ display_name = Distribution Group desired_method = direct #direct/propagated # Propagation node address/hash. -propagation_node = ca2762fe5283873719aececfb9e18835 +propagation_node = + +# Set propagation node automatically. +propagation_node_auto = True + +# Current propagation node (Automatically set by the software). +propagation_node_active = # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. @@ -3721,6 +4051,10 @@ announce_startup_delay = 0 #Seconds announce_periodic = Yes announce_periodic_interval = 120 #Minutes +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + # Some waiting time after message send # for LXMF/Reticulum processing. send_delay = 0 #Seconds @@ -3767,6 +4101,10 @@ announce_startup_delay = 0 #Seconds announce_periodic = Yes announce_periodic_interval = 120 #Minutes +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + @@ -3774,7 +4112,7 @@ announce_periodic_interval = 120 #Minutes [cluster] # Enable/Disable this functionality. -enabled = True +enabled = False # To use several completely separate clusters/groups, # an individual name and type can be assigned here. @@ -3798,7 +4136,7 @@ delimiter_input = @ [router] # Enable/Disable router functionality. -enabled = True +enabled = False # Comma-separated list with the names for which the messages are to be routed/repeated. # The names and levels must match the used display_name of the cluster accordingly. @@ -3840,6 +4178,7 @@ heartbeat_timeout = 15 #Minutes ## Each message received (message and command) ## # Text is added. +receive_title_prefix = receive_prefix = receive_suffix = @@ -3859,6 +4198,7 @@ receive_length_max = 0 #0=any length ## Each message send (message) ## # Text is added. +send_title_prefix = #!source_name!!n!!n! send_prefix = !source_name!!n!!n! send_suffix = @@ -3878,6 +4218,7 @@ send_length_max = 0 #0=any length ## Each cluster message received (message and command) ## # Text is added. +cluster_receive_title_prefix = #@!cluster_source!-> cluster_receive_prefix = @!cluster_source!-> cluster_receive_suffix = @@ -3897,6 +4238,7 @@ cluster_receive_length_max = 0 #0=any length ## Each cluster message send (message) ## # Text is added. +cluster_send_title_prefix = #@!cluster_destination!!n!!source_name!!n!!n! cluster_send_prefix = @!cluster_destination!!n!!source_name!!n!!n! cluster_send_suffix = @@ -3925,6 +4267,10 @@ timestamp = client #client/server title = Yes fields = Yes +# Comma-separated list with fields which will be removed. +fields_remove = +fields_remove_anonymous = + @@ -3958,14 +4304,47 @@ user = True # Delimiter for different rights: , [rights] -admin = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_cluster_join,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,receive_pin_remove,receive_name_def,receive_name_change,receive_auto_name_def,receive_auto_name_change,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_full,statistic_cluster,statistic_router,statistic_local,statistic_interface,statistic_self,statistic_user,status,delivery,enable_local,enable_cluster,auto_add_user,auto_add_user_type,auto_add_cluster,auto_add_router,invite_user,invite_user_type,allow_user,allow_user_type,deny_user,deny_user_type,description_set,rules_set,announce,sync,show_run,show,add,del,move,rename,invite,kick,block,unblock,allow,deny,load,save,reload,reset,unsaved -mod = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_cluster,statistic_router,statistic_local,statistic_self,delivery,show,add,del,move,rename,invite,kick,block,unblock,allow,deny -user = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_description,receive_rules,receive_pin_add,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_cluster,statistic_router,statistic_local,statistic_self,delivery,invite -guest = interface,receive_local,receive_cluster,receive_cluster_loop,leave +admin = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_cluster_join,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,receive_pin_remove,receive_name_def,receive_name_change,receive_auto_name_def,receive_auto_name_change,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,update,join,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_full,statistic_cluster,statistic_router,statistic_local,statistic_interface,statistic_self,statistic_user,status,delivery,enable_local,enable_cluster,auto_add_user,auto_add_user_type,auto_add_cluster,auto_add_router,invite_user,invite_user_type,allow_user,allow_user_type,deny_user,deny_user_type,description_set,rules_set,announce,sync,show_run,show,add,del,move,rename,invite,kick,block,unblock,allow,deny,load,save,reload,reset,unsaved +mod = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_deny,receive_description,receive_rules,receive_pin_add,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,update,join,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_cluster,statistic_router,statistic_local,statistic_self,delivery,show,add,del,move,rename,invite,kick,block,unblock,allow,deny +user = interface,receive_local,receive_cluster,receive_cluster_pin_add,receive_cluster_loop,receive_join,receive_leave,receive_invite,receive_kick,receive_block,receive_unblock,receive_allow,receive_description,receive_rules,receive_pin_add,reply_signature,reply_cluster_enabled,reply_cluster_right,reply_interface_enabled,reply_interface_right,reply_local_enabled,reply_local_right,reply_block,reply_length_min,reply_length_max,send_local,send_cluster,help,update,join,leave,name,address,info,pin,pin_add,pin_remove,cluster_pin_add,description,rules,readme,time,version,groups,members,admins,moderators,users,guests,search,activitys,statistic,statistic_min,statistic_cluster,statistic_router,statistic_local,statistic_self,delivery,invite +guest = interface,receive_local,receive_cluster,receive_cluster_loop,update,join,leave +wait = interface,update,join,leave + + + + +#### User cmd assignment #### + +# Define the individual cmds for the different user types. +# Delimiter for different cmds: , +[cmds] + +admin = leave,invite,kick,block,unblock,allow,deny +mod = leave,invite,kick,block,unblock,allow,deny +user = leave,invite +guest = leave +wait = leave + + + + +#### User config assignment #### +# Define the individual configs for the different user types. +# Delimiter for different configs: , +[configs] +admin = #file_tx_enabled=True,audio_tx_enabled=True +mod = +user = +guest = wait = -# The following rights can be assigned: + + +#### User rights/cmds options #### + +# The following rights/cmds can be assigned: +# anonymous = Hide source identity. # interface = General function of the command interface. # receive_local = Receive local (own group) messages. # receive_cluster = Receive cluster (foreign group) messages. @@ -4002,6 +4381,8 @@ wait = # send_local = Allows you to send loacally in your own group. # send_cluster = Allows sending to another cluster/group. # help = Use of the "/help" command allowed. +# update = Use of the "/update" command allowed. +# join = Use of the "/join" command allowed. # leave = Use of the "/leave" command allowed. # name = Use of the "/name" command allowed. # address = Use of the "/address" command allowed. @@ -4265,6 +4646,16 @@ help_user-de = Gruppe:!n!!display_name!!n!!n!Beschreibung:!n!!description!!n!!n! help_guest = help_guest-de = +# "/update" command. +update_ok = OK: Data updated. +update_ok-de = OK: Daten aktualisiert. +update_error = ERROR: Updating data. +update_error-de = FEHLER: Daten aktualisieren. + +# "/join" command. +join_error = ERROR: While joining group. +join_error-de = FEHLER: Beim Beitritt in die Gruppe. + # "/leave" command. leave_ok = OK: You leaved the group. leave_ok-de = OK: Sie haben die Gruppe verlassen. @@ -4856,8 +5247,8 @@ allow_user = True allow_user_type = user deny_user = True deny_user_type = block_wait -description = This group is for a first test of functionality.!n!!n!To receive offline messages please use the following propagation node: -description-de = Diese Gruppe dient einem ersten Test der Funktionalität.!n!!n!Um offline Nachrichten zu empfangen bitte folgender Propagation Node verwenden: +description = +description-de = rules = Please follow the general rules of etiquette which should be taken for granted!!n!Prohibited are:!n!Spam, insults, violence, sex, illegal topics rules-de = Bitte befolgen Sie die allgemeinen benimm-dich-Regeln welche als selbstverständlich gelten sollten!!n!Verboten sind:!n!Spam, Beleidigungen, Gewalt, Sex, illegale Themen diff --git a/lxmf_distribution_group_minimal/Examples/README.md b/lxmf_distribution_group_minimal/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_distribution_group_minimal/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py index 76a1265..7b0efcc 100755 --- a/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py +++ b/lxmf_distribution_group_minimal/lxmf_distribution_group_minimal.py @@ -48,6 +48,9 @@ import pickle #### String #### import string +#### Regex #### +import re + #### Process #### import signal import threading @@ -91,9 +94,10 @@ class lxmf_connection: message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None + config_set_callback = None - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file @@ -106,6 +110,7 @@ class lxmf_connection: self.display_name = display_name self.announce_data = announce_data + self.announce_hidden = announce_hidden self.send_delay = int(send_delay) @@ -114,6 +119,8 @@ class lxmf_connection: else: self.desired_method_direct = True self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup @@ -128,6 +135,10 @@ class lxmf_connection: self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) @@ -182,10 +193,18 @@ class lxmf_connection: self.destination.set_link_established_callback(self.client_connected) - self.autoselect_propagation_node() + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) if self.sync_startup or self.sync_periodic: self.sync(True) @@ -212,6 +231,10 @@ class lxmf_connection: self.message_notification_failed_callback = handler_function + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + def destination_hash(self): return self.destination.hash @@ -255,7 +278,7 @@ class lxmf_connection: return "" - def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""): + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] @@ -270,8 +293,13 @@ class lxmf_connection: log("LXMF - Destination is invalid", LOG_ERROR) return + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + destination_identity = RNS.Identity.recall(destination) - destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) @@ -340,7 +368,7 @@ class lxmf_connection: message.desired_method_str = "propagated" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: @@ -357,26 +385,29 @@ class lxmf_connection: announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) elif self.announce_data: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: self.destination.announce() @@ -418,24 +449,50 @@ class lxmf_connection: return False - def autoselect_propagation_node(self): - if self.propagation_node is not None: - if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): - log("LXMF - Propagation node length is invalid", LOG_ERROR) - else: - try: - propagation_hash = bytes.fromhex(self.propagation_node) - except Exception as e: - log("LXMF - Propagation node is invalid", LOG_ERROR) - return + def propagation_node_set(self, dest_str): + if not dest_str: + return False - node_identity = RNS.Identity.recall(propagation_hash) - if node_identity != None: - log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) - propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) - self.message_router.set_outbound_propagation_node(propagation_hash) - else: - log("LXMF - Propagation node identity not known", LOG_ERROR) + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" def client_connected(self, link): @@ -523,6 +580,43 @@ class lxmf_connection: log("- App Data: " + message.app_data, LOG_DEBUG) + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + ############################################################################################################## # LXMF Functions @@ -704,6 +798,36 @@ def config_getoption(config, section, key, default=False, lng_key=""): +#### Config - Set ##### +def config_set(key=None, value=""): + global PATH + + try: + file = PATH + "/config.cfg.owr" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + + file = PATH + "/config.cfg" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + except: + pass + + + + #### Config - Read ##### def config_read(file=None, file_override=None): global CONFIG @@ -887,15 +1011,15 @@ def data_default(file=None): # Value convert -def val_to_bool(val): +def val_to_bool(val, fallback_true=True, fallback_false=False): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": - return True + return fallback_true else: - return False + return fallback_false ############################################################################################################## @@ -1065,6 +1189,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) else: config_propagation_node = None + if CONFIG.has_option("lxmf", "propagation_node_active"): + config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"] + else: + config_propagation_node_active = None + if path is None: path = PATH @@ -1073,9 +1202,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) destination_name=CONFIG["lxmf"]["destination_name"], destination_type=CONFIG["lxmf"]["destination_type"], display_name=CONFIG["lxmf"]["display_name"], + announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"), send_delay=CONFIG["lxmf"]["send_delay"], desired_method=CONFIG["lxmf"]["desired_method"], propagation_node=config_propagation_node, + propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"), + propagation_node_active=config_propagation_node_active, try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], @@ -1089,6 +1221,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + LXMF_CONNECTION.register_config_set_callback(config_set) log("LXMF - Connected", LOG_DEBUG) @@ -1162,8 +1295,8 @@ DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override t # It is also used in the group description/info. display_name = Distribution Group -# Propagation node address/hash. -propagation_node = ca2762fe5283873719aececfb9e18835 +# Set propagation node automatically. +propagation_node_auto = True # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. @@ -1205,7 +1338,13 @@ display_name = Distribution Group desired_method = direct #direct/propagated # Propagation node address/hash. -propagation_node = ca2762fe5283873719aececfb9e18835 +propagation_node = + +# Set propagation node automatically. +propagation_node_auto = True + +# Current propagation node (Automatically set by the software). +propagation_node_active = # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. @@ -1221,6 +1360,10 @@ announce_startup_delay = 0 #Seconds announce_periodic = Yes announce_periodic_interval = 120 #Minutes +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + # Some waiting time after message send # for LXMF/Reticulum processing. send_delay = 0 #Seconds diff --git a/lxmf_echo/Examples/README.md b/lxmf_echo/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_echo/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_echo/lxmf_echo.py b/lxmf_echo/lxmf_echo.py index 82d9ce9..4b349b6 100755 --- a/lxmf_echo/lxmf_echo.py +++ b/lxmf_echo/lxmf_echo.py @@ -47,6 +47,9 @@ import pickle #### String #### import string +#### Regex #### +import re + #### Process #### import signal import threading @@ -89,9 +92,10 @@ class lxmf_connection: message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None + config_set_callback = None - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file @@ -104,6 +108,7 @@ class lxmf_connection: self.display_name = display_name self.announce_data = announce_data + self.announce_hidden = announce_hidden self.send_delay = int(send_delay) @@ -112,6 +117,8 @@ class lxmf_connection: else: self.desired_method_direct = True self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup @@ -126,6 +133,10 @@ class lxmf_connection: self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) @@ -180,10 +191,18 @@ class lxmf_connection: self.destination.set_link_established_callback(self.client_connected) - self.autoselect_propagation_node() + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) if self.sync_startup or self.sync_periodic: self.sync(True) @@ -210,6 +229,10 @@ class lxmf_connection: self.message_notification_failed_callback = handler_function + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + def destination_hash(self): return self.destination.hash @@ -253,7 +276,7 @@ class lxmf_connection: return "" - def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""): + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] @@ -268,8 +291,13 @@ class lxmf_connection: log("LXMF - Destination is invalid", LOG_ERROR) return + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + destination_identity = RNS.Identity.recall(destination) - destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) @@ -338,7 +366,7 @@ class lxmf_connection: message.desired_method_str = "propagated" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: @@ -355,26 +383,29 @@ class lxmf_connection: announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) elif self.announce_data: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: self.destination.announce() @@ -416,24 +447,50 @@ class lxmf_connection: return False - def autoselect_propagation_node(self): - if self.propagation_node is not None: - if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): - log("LXMF - Propagation node length is invalid", LOG_ERROR) - else: - try: - propagation_hash = bytes.fromhex(self.propagation_node) - except Exception as e: - log("LXMF - Propagation node is invalid", LOG_ERROR) - return + def propagation_node_set(self, dest_str): + if not dest_str: + return False - node_identity = RNS.Identity.recall(propagation_hash) - if node_identity != None: - log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) - propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) - self.message_router.set_outbound_propagation_node(propagation_hash) - else: - log("LXMF - Propagation node identity not known", LOG_ERROR) + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" def client_connected(self, link): @@ -521,6 +578,43 @@ class lxmf_connection: log("- App Data: " + message.app_data, LOG_DEBUG) + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + ############################################################################################################## # LXMF Functions @@ -669,6 +763,36 @@ def config_getoption(config, section, key, default=False, lng_key=""): +#### Config - Set ##### +def config_set(key=None, value=""): + global PATH + + try: + file = PATH + "/config.cfg.owr" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + + file = PATH + "/config.cfg" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + except: + pass + + + + #### Config - Read ##### def config_read(file=None, file_override=None): global CONFIG @@ -762,15 +886,15 @@ def config_default(file=None, file_override=None): # Value convert -def val_to_bool(val): +def val_to_bool(val, fallback_true=True, fallback_false=False): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": - return True + return fallback_true else: - return False + return fallback_false ############################################################################################################## @@ -935,6 +1059,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) else: config_propagation_node = None + if CONFIG.has_option("lxmf", "propagation_node_active"): + config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"] + else: + config_propagation_node_active = None + if path is None: path = PATH @@ -943,9 +1072,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) destination_name=CONFIG["lxmf"]["destination_name"], destination_type=CONFIG["lxmf"]["destination_type"], display_name=CONFIG["lxmf"]["display_name"], + announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"), send_delay=CONFIG["lxmf"]["send_delay"], desired_method=CONFIG["lxmf"]["desired_method"], propagation_node=config_propagation_node, + propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"), + propagation_node_active=config_propagation_node_active, try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], @@ -959,6 +1091,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + LXMF_CONNECTION.register_config_set_callback(config_set) log("LXMF - Connected", LOG_DEBUG) @@ -1053,7 +1186,13 @@ display_name = Echo Test desired_method = direct #direct/propagated # Propagation node address/hash. -#propagation_node = +propagation_node = + +# Set propagation node automatically. +propagation_node_auto = True + +# Current propagation node (Automatically set by the software). +propagation_node_active = # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. @@ -1069,6 +1208,10 @@ announce_startup_delay = 0 #Seconds announce_periodic = Yes announce_periodic_interval = 360 #Minutes +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + # Some waiting time after message send # for LXMF/Reticulum processing. send_delay = 0 #Seconds diff --git a/lxmf_ping/Examples/README.md b/lxmf_ping/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_ping/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_ping/lxmf_ping.py b/lxmf_ping/lxmf_ping.py index ad66e73..08cda06 100755 --- a/lxmf_ping/lxmf_ping.py +++ b/lxmf_ping/lxmf_ping.py @@ -44,6 +44,9 @@ import pickle #### String #### import string +#### Regex #### +import re + #### Other #### import random import secrets @@ -90,9 +93,10 @@ class lxmf_connection: message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None + config_set_callback = None - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file @@ -105,6 +109,7 @@ class lxmf_connection: self.display_name = display_name self.announce_data = announce_data + self.announce_hidden = announce_hidden self.send_delay = int(send_delay) @@ -113,6 +118,8 @@ class lxmf_connection: else: self.desired_method_direct = True self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup @@ -127,6 +134,10 @@ class lxmf_connection: self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) @@ -181,10 +192,18 @@ class lxmf_connection: self.destination.set_link_established_callback(self.client_connected) - self.autoselect_propagation_node() + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) if self.sync_startup or self.sync_periodic: self.sync(True) @@ -211,6 +230,10 @@ class lxmf_connection: self.message_notification_failed_callback = handler_function + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + def destination_hash(self): return self.destination.hash @@ -254,7 +277,7 @@ class lxmf_connection: return "" - def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""): + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] @@ -269,8 +292,13 @@ class lxmf_connection: log("LXMF - Destination is invalid", LOG_ERROR) return + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + destination_identity = RNS.Identity.recall(destination) - destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) @@ -339,7 +367,7 @@ class lxmf_connection: message.desired_method_str = "propagated" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: @@ -356,26 +384,29 @@ class lxmf_connection: announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) elif self.announce_data: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: self.destination.announce() @@ -417,24 +448,50 @@ class lxmf_connection: return False - def autoselect_propagation_node(self): - if self.propagation_node is not None: - if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): - log("LXMF - Propagation node length is invalid", LOG_ERROR) - else: - try: - propagation_hash = bytes.fromhex(self.propagation_node) - except Exception as e: - log("LXMF - Propagation node is invalid", LOG_ERROR) - return + def propagation_node_set(self, dest_str): + if not dest_str: + return False - node_identity = RNS.Identity.recall(propagation_hash) - if node_identity != None: - log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) - propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) - self.message_router.set_outbound_propagation_node(propagation_hash) - else: - log("LXMF - Propagation node identity not known", LOG_ERROR) + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" def client_connected(self, link): @@ -522,6 +579,43 @@ class lxmf_connection: log("- App Data: " + message.app_data, LOG_DEBUG) + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + ############################################################################################################## # LXMF Functions @@ -575,15 +669,15 @@ def lxmf_failed(message): # Value convert -def val_to_bool(val): +def val_to_bool(val, fallback_true=True, fallback_false=False): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": - return True + return fallback_true else: - return False + return fallback_false ############################################################################################################## diff --git a/lxmf_provisioning/Examples/README.md b/lxmf_provisioning/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_provisioning/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_provisioning/lxmf_provisioning.py b/lxmf_provisioning/lxmf_provisioning.py index 7ceaf11..8ca78cd 100755 --- a/lxmf_provisioning/lxmf_provisioning.py +++ b/lxmf_provisioning/lxmf_provisioning.py @@ -47,6 +47,15 @@ import pickle #### String #### import string +#### Regex #### +import re + +#### UID #### +import uuid + +#### #### +import base64 + #### Process #### import signal import threading @@ -81,7 +90,10 @@ PATH_RNS = None #### Global Variables - System (Not changeable) #### -CACHE = [] +CACHE = {} +CACHE["in"] = {} +CACHE["out"] = {} +CACHE_CHANGE = False CONFIG = None RNS_CONNECTION = None LXMF_CONNECTION = None @@ -97,9 +109,10 @@ class lxmf_connection: message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None + config_set_callback = None - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file @@ -112,6 +125,7 @@ class lxmf_connection: self.display_name = display_name self.announce_data = announce_data + self.announce_hidden = announce_hidden self.send_delay = int(send_delay) @@ -120,6 +134,8 @@ class lxmf_connection: else: self.desired_method_direct = True self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup @@ -134,6 +150,10 @@ class lxmf_connection: self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) @@ -188,10 +208,18 @@ class lxmf_connection: self.destination.set_link_established_callback(self.client_connected) - self.autoselect_propagation_node() + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) if self.sync_startup or self.sync_periodic: self.sync(True) @@ -218,6 +246,10 @@ class lxmf_connection: self.message_notification_failed_callback = handler_function + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + def destination_hash(self): return self.destination.hash @@ -261,7 +293,7 @@ class lxmf_connection: return "" - def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""): + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] @@ -276,8 +308,13 @@ class lxmf_connection: log("LXMF - Destination is invalid", LOG_ERROR) return + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + destination_identity = RNS.Identity.recall(destination) - destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) @@ -346,7 +383,7 @@ class lxmf_connection: message.desired_method_str = "propagated" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: @@ -363,26 +400,29 @@ class lxmf_connection: announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) elif self.announce_data: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: self.destination.announce() @@ -424,24 +464,50 @@ class lxmf_connection: return False - def autoselect_propagation_node(self): - if self.propagation_node is not None: - if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): - log("LXMF - Propagation node length is invalid", LOG_ERROR) - else: - try: - propagation_hash = bytes.fromhex(self.propagation_node) - except Exception as e: - log("LXMF - Propagation node is invalid", LOG_ERROR) - return + def propagation_node_set(self, dest_str): + if not dest_str: + return False - node_identity = RNS.Identity.recall(propagation_hash) - if node_identity != None: - log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) - propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) - self.message_router.set_outbound_propagation_node(propagation_hash) - else: - log("LXMF - Propagation node identity not known", LOG_ERROR) + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" def client_connected(self, link): @@ -529,6 +595,43 @@ class lxmf_connection: log("- App Data: " + message.app_data, LOG_DEBUG) + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + ############################################################################################################## # LXMF Functions @@ -549,7 +652,7 @@ class lxmf_announce_callback: #### LXMF - Message #### def lxmf_message_received_callback(message): - global CACHE + global CACHE, CACHE_CHANGE if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG) @@ -558,33 +661,340 @@ def lxmf_message_received_callback(message): if not message.fields: return - if not "registration_request" in message.fields and not "telemetry" in message.fields: - return + hash_destination = RNS.hexrep(message.source_hash, delimit=False) + hash_identity = "" + #hash_identity = RNS.Identity.recall(message.source_hash) + #if hash_identity != None: + # hash_identity = RNS.hexrep(hash_identity, delimit=False) + #else: + # hash_identity = "" - db = None - try: - db = psycopg2.connect(user=CONFIG["database"]["user"], password=CONFIG["database"]["password"], host=CONFIG["database"]["host"], port=CONFIG["database"]["port"], database=CONFIG["database"]["database"]) - dbc = db.cursor() + for key in message.fields: + try: + data = message.fields[key] + if not isinstance(data, dict): + continue + if "type" not in data: + continue + if data["type"] == "": + continue - if CONFIG["features"].getboolean("registration") and "registration_request" in message.fields: - dbc.execute("INSERT INTO "+CONFIG["database"]["table_registration"]+" (hash, data) VALUES(%s, %s)", ( - RNS.hexrep(message.source_hash, delimit=False), - umsgpack.packb(message.fields["registration_request"])) - ) + data["hash_destination"] = hash_destination + data["hash_identity"] = hash_identity + data["timestamp_client"] = message.timestamp + data["timestamp_server"] = time.time() - if CONFIG["features"].getboolean("telemetry") and "telemetry" in message.fields: - dbc.execute("INSERT INTO "+CONFIG["database"]["table_telemetry"]+" (hash, data) VALUES(%s, %s)", ( - RNS.hexrep(message.source_hash, delimit=False), - umsgpack.packb(message.fields["telemetry"])) - ) + if "password" in data: + data["password"] = str(base64.b32encode(data["password"])) - db.commit() - except psycopg2.DatabaseError as e: - log("DB - Error: "+str(e), LOG_ERROR) - if db: - dbc.close() - db.close() - db = None + CACHE["in"][str(uuid.uuid4())] = data + CACHE_CHANGE = True + except: + pass + + + + +#### LXMF - Notification #### +def lxmf_message_notification_success_callback(message): + global CACHE, CACHE_CHANGE + + key = message.app_data + if key in CACHE["out"]: + del CACHE["out"][key] + CACHE_CHANGE = True + + + + +#### Jobs #### +def jobs_in(): + global CACHE, CACHE_CHANGE + + while True: + time.sleep(CONFIG["processing"].getint("interval_in")) + log("Jobs - Loop/Execute", LOG_DEBUG) + + if len(CACHE["in"]) > 0: + log("Cache - Available -> Execute", LOG_DEBUG) + + CACHE_DEL = [] + db = None + try: + db = psycopg2.connect(user=CONFIG["database"]["user"], password=CONFIG["database"]["password"], host=CONFIG["database"]["host"], port=CONFIG["database"]["port"], database=CONFIG["database"]["database"]) + dbc = db.cursor() + + for key in CACHE["in"]: + try: + log("-> Execute", LOG_EXTREME) + log(CACHE["in"][key], LOG_EXTREME) + + data = CACHE["in"][key] + + if data["type"] == "account_add" and CONFIG["features"].getboolean("account_add"): + # members + dbc.execute("SELECT member_user_id FROM members WHERE member_email = %s AND member_password = %s", (data["email"], data["password"])) + result = dbc.fetchall() + if len(result) == 0: + user_id = str(uuid.uuid4()) + dbc.execute("INSERT INTO members (member_user_id, member_email, member_password, member_dob, member_sex, member_introduction, member_country, member_state, member_city, member_occupation, member_skills, member_tasks, member_wallet_address, member_accept_rules, member_language, member_locale, member_status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, '0')", ( + user_id, + data["email"], + data["password"], + data["dob"], + data["sex"], + data["introduction"], + data["country"], + data["state"], + data["city"], + data["occupation"], + data["skills"], + data["tasks"], + data["wallet_address"], + data["accept_rules"], + data["language"], + data["language"] + ) + ) + if CONFIG["features"].getboolean("account_add_auth"): + fields = {} + if CONFIG["lxmf"]["destination_type_conv"] != "": + fields["type"] = CONFIG["lxmf"].getint("destination_type_conv") + fields["prov"] = {} + fields["prov"]["auth_state"] = CONFIG["features"].getint("account_add_auth_state") + fields["prov"]["auth_role"] = CONFIG["features"].getint("account_add_auth_role") + CACHE["out"][str(uuid.uuid4())] = {"hash_destination": data["hash_destination"], "content": "", "title": "", "fields": fields} + CACHE_CHANGE = True + elif len(result) == 1: + user_id = result[0][0] + else: + continue + + # devices + dbc.execute("DELETE FROM devices WHERE device_id = %s OR device_rns_id = %s", (data["device_id"], data["hash_destination"])) + dbc.execute("INSERT INTO devices (device_id, device_user_id, device_name, device_display_name, device_rns_id) VALUES (%s, %s, %s, %s, %s)", ( + data["device_id"], + user_id, + data["device_name"], + data["device_display_name"], + data["hash_destination"] + ) + ) + + db.commit() + CACHE_DEL.append(key) + + if data["type"] == "account_edit" and CONFIG["features"].getboolean("account_edit"): + # members + dbc.execute("SELECT member_user_id FROM members WHERE member_email = %s AND member_password = %s", (data["email"], data["password"])) + result = dbc.fetchall() + if len(result) == 0: + user_id = str(uuid.uuid4()) + dbc.execute("INSERT INTO members (member_user_id, member_email, member_password, member_dob, member_sex, member_introduction, member_country, member_state, member_city, member_occupation, member_skills, member_tasks, member_wallet_address, member_accept_rules, member_language, member_locale, member_status) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, '0')", ( + user_id, + data["email"], + data["password"], + data["dob"], + data["sex"], + data["introduction"], + data["country"], + data["state"], + data["city"], + data["occupation"], + data["skills"], + data["tasks"], + data["wallet_address"], + data["accept_rules"], + data["language"], + data["language"] + ) + ) + if CONFIG["features"].getboolean("account_add_auth"): + fields = {} + if CONFIG["lxmf"]["destination_type_conv"] != "": + fields["type"] = CONFIG["lxmf"].getint("destination_type_conv") + fields["prov"] = {} + fields["prov"]["auth_state"] = CONFIG["features"].getint("account_add_auth_state") + fields["prov"]["auth_role"] = CONFIG["features"].getint("account_add_auth_role") + CACHE["out"][str(uuid.uuid4())] = {"hash_destination": data["hash_destination"], "content": "", "title": "", "fields": fields} + CACHE_CHANGE = True + elif len(result) == 1: + user_id = result[0][0] + else: + continue + + # devices + dbc.execute("DELETE FROM devices WHERE device_id = %s OR device_rns_id = %s", (data["device_id"], data["hash_destination"])) + dbc.execute("INSERT INTO devices (device_id, device_user_id, device_name, device_display_name, device_rns_id) VALUES (%s, %s, %s, %s, %s)", ( + data["device_id"], + user_id, + data["device_name"], + data["device_display_name"], + data["hash_destination"] + ) + ) + + db.commit() + CACHE_DEL.append(key) + + if data["type"] == "account_prove" and CONFIG["features"].getboolean("account_prove"): + dbc.execute("SELECT device_user_id FROM devices LEFT JOIN members ON members.member_user_id = devices.device_user_id WHERE devices.device_rns_id = %s and members.member_status = '1'", (data["hash_destination"], )) + result = dbc.fetchall() + if len(result) == 1: + source_user_id = result[0][0] + dbc.execute("SELECT device_user_id FROM devices WHERE device_rns_id = %s", (data["prove"], )) + result = dbc.fetchall() + if len(result) == 1: + destination_user_id = result[0][0] + dbc.execute("INSERT INTO proves (prove_source_user_id, prove_destination_user_id) VALUES (%s, %s)", (source_user_id, destination_user_id)) + dbc.execute("SELECT member_status FROM members WHERE member_user_id = %s AND member_status = '0'", (destination_user_id, )) + result = dbc.fetchall() + if len(result) == 1: + dbc.execute("SELECT * FROM proves WHERE prove_destination_user_id = %s", (destination_user_id,)) + result = dbc.fetchall() + if len(result) >= 2: + dbc.execute("UPDATE members SET member_status = '1' WHERE member_user_id = %s AND member_status = '0'", (destination_user_id,)) + if CONFIG["features"].getboolean("account_prove_auth"): + fields = {} + if CONFIG["lxmf"]["destination_type_conv"] != "": + fields["type"] = CONFIG["lxmf"].getint("destination_type_conv") + fields["prov"] = {} + fields["prov"]["auth_state"] = CONFIG["features"].getint("account_prove_auth_state") + fields["prov"]["auth_role"] = CONFIG["features"].getint("account_prove_auth_role") + CACHE["out"][str(uuid.uuid4())] = {"hash_destination": data["prove"], "content": "", "title": "", "fields": fields} + CACHE_CHANGE = True + + db.commit() + CACHE_DEL.append(key) + + except psycopg2.DatabaseError as e: + log("Loop - DB - Error: "+str(e), LOG_ERROR) + db.rollback() + + except psycopg2.DatabaseError as e: + log("DB - Error: "+str(e), LOG_ERROR) + db.rollback() + + if len(CACHE_DEL) > 0: + for key in CACHE_DEL: + del CACHE["in"][key] + CACHE_CHANGE = True + + if db: + dbc.close() + db.close() + db = None + + if CACHE_CHANGE: + if cache_save(PATH + "/cache.data"): + CACHE_CHANGE = False + + + + +#### Jobs #### +def jobs_out(): + global CACHE, CACHE_CHANGE + + while True: + time.sleep(CONFIG["processing"].getint("interval_out")) + log("Jobs Out - Loop/Execute", LOG_DEBUG) + + if len(CACHE["out"]) > 0: + log("Cache - Available -> Execute", LOG_DEBUG) + + CACHE_DEL = [] + for key in CACHE["out"]: + try: + log("-> Execute", LOG_EXTREME) + log(CACHE["out"][key], LOG_EXTREME) + + data = CACHE["out"][key] + LXMF_CONNECTION.send(data["hash_destination"], data["content"], data["title"], data["fields"], app_data=key, destination_name="lxmf", destination_type="delivery") + except: + pass + + if len(CACHE_DEL) > 0: + for key in CACHE_DEL: + del CACHE["out"][key] + CACHE_CHANGE = True + + if CACHE_CHANGE: + if cache_save(PATH + "/cache.data"): + CACHE_CHANGE = False + + + + +############################################################################################################## +# Cache + + +#### Cache - Read ##### +def cache_read(file=None): + log("Cache - Read", LOG_DEBUG) + global CACHE + + if file is None: + return False + else: + if os.path.isfile(file): + try: + fh = open(file, "rb") + CACHE = umsgpack.unpackb(fh.read()) + fh.close() + except Exception as e: + return False + else: + if not cache_default(file=file): + return False + return True + + + + +#### Cache - Save ##### +def cache_save(file=None): + log("Cache - Save", LOG_DEBUG) + global CACHE + + if file is None: + return False + else: + if os.path.isfile(file): + try: + fh = open(file, "wb") + fh.write(umsgpack.packb(CACHE)) + fh.close() + except Exception as e: + return False + else: + return False + return True + + + + +#### Cache - Default ##### +def cache_default(file=None): + log("Cache - Default", LOG_DEBUG) + global CACHE + + if file is None: + return False + else: + if not os.path.isdir(os.path.dirname(file)): + try: + os.makedirs(os.path.dirname(file)) + except Exception: + return False + try: + fh = open(file, "wb") + fh.write(umsgpack.packb(CACHE)) + fh.close() + except: + return False + return True ############################################################################################################## @@ -644,6 +1054,36 @@ def config_getoption(config, section, key, default=False, lng_key=""): +#### Config - Set ##### +def config_set(key=None, value=""): + global PATH + + try: + file = PATH + "/config.cfg.owr" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + + file = PATH + "/config.cfg" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + except: + pass + + + + #### Config - Read ##### def config_read(file=None, file_override=None): global CONFIG @@ -737,15 +1177,15 @@ def config_default(file=None, file_override=None): # Value convert -def val_to_bool(val): +def val_to_bool(val, fallback_true=True, fallback_false=False): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": - return True + return fallback_true else: - return False + return fallback_false ############################################################################################################## @@ -881,6 +1321,10 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) print("Config - Error reading config file " + PATH + "/config.cfg") panic() + if not cache_read(PATH + "/cache.data"): + print("Cache - Error reading cache file " + PATH + "/cache.data") + panic() + if CONFIG["main"].getboolean("default_config"): print("Exit!") print("First start with the default config!") @@ -910,6 +1354,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) else: config_propagation_node = None + if CONFIG.has_option("lxmf", "propagation_node_active"): + config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"] + else: + config_propagation_node_active = None + if path is None: path = PATH @@ -925,10 +1374,13 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) destination_name=CONFIG["lxmf"]["destination_name"], destination_type=CONFIG["lxmf"]["destination_type"], display_name=CONFIG["lxmf"]["display_name"], + announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"), announce_data = umsgpack.packb(announce_data), send_delay=CONFIG["lxmf"]["send_delay"], desired_method=CONFIG["lxmf"]["desired_method"], propagation_node=config_propagation_node, + propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"), + propagation_node_active=config_propagation_node_active, try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], @@ -942,6 +1394,8 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + LXMF_CONNECTION.register_message_notification_success_callback(lxmf_message_notification_success_callback) + LXMF_CONNECTION.register_config_set_callback(config_set) log("LXMF - Connected", LOG_DEBUG) @@ -949,6 +1403,15 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE) log("...............................................................................", LOG_FORCE) + log("...............................................................................", LOG_EXTREME) + log(CACHE, LOG_EXTREME) + log("...............................................................................", LOG_EXTREME) + + jobs_in_thread = threading.Thread(target=jobs_in, daemon=True) + jobs_in_thread.start() + + jobs_out_thread = threading.Thread(target=jobs_out, daemon=True) + jobs_out_thread.start() while True: time.sleep(1) @@ -1007,9 +1470,16 @@ announce_periodic_interval = 15 #Minutes [features] announce_versions = True -registration = True +account_add = True +account_edit = True +account_del = True +account_prove = True telemetry = False +[processing] +interval_in = 5 #Seconds +interval_out = 60 #Seconds + [data] v_s = 0.0.0 #Version software v_c = 2022-01-01 00:00 #Version config @@ -1017,6 +1487,7 @@ v_d = 2022-01-01 00:00 #Version data v_a = 2022-01-01 00:00 #Version auth u_s = #URL Software i_s = #Info Software +cmd = #CMD ''' @@ -1045,6 +1516,7 @@ name = LXMF Provisioning Server # to be compatibel with other LXMF programs. destination_name = lxmf destination_type = provisioning +destination_type_conv = 11 # The name will be visible to other peers # on the network, and included in announces. @@ -1054,11 +1526,17 @@ display_name = LXMF Provisioning Server desired_method = direct #direct/propagated # Propagation node address/hash. -#propagation_node = +propagation_node = + +# Set propagation node automatically. +propagation_node_auto = True + +# Current propagation node (Automatically set by the software). +propagation_node_active = # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. -try_propagation_on_fail = No +try_propagation_on_fail = Yes # The peer is announced at startup # to let other peers reach it immediately. @@ -1070,6 +1548,10 @@ announce_startup_delay = 0 #Seconds announce_periodic = Yes announce_periodic_interval = 360 #Minutes +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + # Some waiting time after message send # for LXMF/Reticulum processing. send_delay = 0 #Seconds @@ -1091,7 +1573,7 @@ sync_periodic_interval = 360 #Minutes sync_limit = 8 # Allow only messages with valid signature. -signature_validated = Yes +signature_validated = No @@ -1104,8 +1586,6 @@ port = 5432 user = postgres password = password database = database -table_registration = tbl_account -table_telemetry = tbl_telemetry @@ -1114,12 +1594,37 @@ table_telemetry = tbl_telemetry [features] announce_versions = True -registration = True + +account_add = True +account_add_auth = False +account_add_auth_state = 1 +account_add_auth_role = 3 + +account_edit = True +account_edit_auth = False +account_edit_auth_state = 1 +account_edit_auth_role = 3 + +account_del = True + +account_prove = True +account_prove_auth = True +account_prove_auth_state = 1 +account_prove_auth_role = 3 + telemetry = False +#### Processing #### +[processing] +interval_in = 5 #Seconds +interval_out = 60 #Seconds + + + + #### Data settings #### [data] @@ -1129,6 +1634,7 @@ v_d = 2022-01-01 00:00 #Version data v_a = 2022-01-01 00:00 #Version auth u_s = #URL Software i_s = #Info Software +cmd = #CMD ''' diff --git a/lxmf_terminal/Examples/README.md b/lxmf_terminal/Examples/README.md new file mode 100644 index 0000000..beb122e --- /dev/null +++ b/lxmf_terminal/Examples/README.md @@ -0,0 +1,4 @@ +# Examples +This folder contains sample configurations for different application environments or scenarios. + +Copy these files accordingly to the appropriate configuration folder. Then customize the content of these files to your needs. diff --git a/lxmf_terminal/lxmf_terminal.py b/lxmf_terminal/lxmf_terminal.py index a557981..6ae7eef 100755 --- a/lxmf_terminal/lxmf_terminal.py +++ b/lxmf_terminal/lxmf_terminal.py @@ -193,9 +193,10 @@ class lxmf_connection: message_notification_callback = None message_notification_success_callback = None message_notification_failed_callback = None + config_set_callback = None - def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): + def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360): self.storage_path = storage_path self.identity_file = identity_file @@ -208,6 +209,7 @@ class lxmf_connection: self.display_name = display_name self.announce_data = announce_data + self.announce_hidden = announce_hidden self.send_delay = int(send_delay) @@ -216,6 +218,8 @@ class lxmf_connection: else: self.desired_method_direct = True self.propagation_node = propagation_node + self.propagation_node_auto = propagation_node_auto + self.propagation_node_active = propagation_node_active self.try_propagation_on_fail = try_propagation_on_fail self.announce_startup = announce_startup @@ -230,6 +234,10 @@ class lxmf_connection: self.sync_periodic = sync_periodic self.sync_periodic_interval = int(sync_periodic_interval) + if not self.storage_path: + log("LXMF - No storage_path parameter", LOG_ERROR) + return + if not os.path.isdir(self.storage_path): os.makedirs(self.storage_path) log("LXMF - Storage path was created", LOG_NOTICE) @@ -284,10 +292,18 @@ class lxmf_connection: self.destination.set_link_established_callback(self.client_connected) - self.autoselect_propagation_node() + if self.propagation_node_auto: + self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation") + RNS.Transport.register_announce_handler(self.propagation_callback) + if self.propagation_node_active: + self.propagation_node_set(self.propagation_node_active) + elif self.propagation_node: + self.propagation_node_set(self.propagation_node) + else: + self.propagation_node_set(self.propagation_node) if self.announce_startup or self.announce_periodic: - self.announce(True) + self.announce(initial=True) if self.sync_startup or self.sync_periodic: self.sync(True) @@ -314,6 +330,10 @@ class lxmf_connection: self.message_notification_failed_callback = handler_function + def register_config_set_callback(self, handler_function): + self.config_set_callback = handler_function + + def destination_hash(self): return self.destination.hash @@ -357,7 +377,7 @@ class lxmf_connection: return "" - def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""): + def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None): if type(destination) is not bytes: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: destination = destination[1:-1] @@ -372,8 +392,13 @@ class lxmf_connection: log("LXMF - Destination is invalid", LOG_ERROR) return + if destination_name == None: + destination_name = self.destination_name + if destination_type == None: + destination_type = self.destination_type + destination_identity = RNS.Identity.recall(destination) - destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type) + destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data) @@ -442,7 +467,7 @@ class lxmf_connection: message.desired_method_str = "propagated" - def announce(self, initial=False): + def announce(self, app_data=None, attached_interface=None, initial=False): announce_timer = None if self.announce_periodic and self.announce_periodic_interval > 0: @@ -459,26 +484,29 @@ class lxmf_connection: announce_timer.daemon = True announce_timer.start() else: - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) return - self.announce_now() + self.announce_now(app_data=app_data, attached_interface=attached_interface) - def announce_now(self, app_data=None): - if app_data: + def announce_now(self, app_data=None, attached_interface=None): + if self.announce_hidden: + self.destination.announce("".encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG) + elif app_data != None: if isinstance(app_data, str): - self.destination.announce(app_data.encode("utf-8")) - log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) + self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface) + log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG) else: - self.destination.announce(app_data) + self.destination.announce(app_data, attached_interface=attached_interface) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) elif self.announce_data: if isinstance(self.announce_data, str): - self.destination.announce(self.announce_data.encode("utf-8")) + self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG) else: - self.destination.announce(self.announce_data) + self.destination.announce(self.announce_data, attached_interface=attached_interface) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG) else: self.destination.announce() @@ -520,24 +548,50 @@ class lxmf_connection: return False - def autoselect_propagation_node(self): - if self.propagation_node is not None: - if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): - log("LXMF - Propagation node length is invalid", LOG_ERROR) - else: - try: - propagation_hash = bytes.fromhex(self.propagation_node) - except Exception as e: - log("LXMF - Propagation node is invalid", LOG_ERROR) - return + def propagation_node_set(self, dest_str): + if not dest_str: + return False - node_identity = RNS.Identity.recall(propagation_hash) - if node_identity != None: - log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) - propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) - self.message_router.set_outbound_propagation_node(propagation_hash) - else: - log("LXMF - Propagation node identity not known", LOG_ERROR) + if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): + log("LXMF - Propagation node length is invalid", LOG_ERROR) + return False + + try: + dest_hash = bytes.fromhex(dest_str) + except Exception as e: + log("LXMF - Propagation node is invalid", LOG_ERROR) + return False + + node_identity = RNS.Identity.recall(dest_hash) + if node_identity != None: + log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO) + dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity) + self.message_router.set_outbound_propagation_node(dest_hash) + self.propagation_node_active = dest_str + return True + else: + log("LXMF - Propagation node identity not known", LOG_ERROR) + return False + + + def propagation_node_update(self, dest_str): + if self.propagation_node_hash_str() != dest_str: + if self.propagation_node_set(dest_str) and self.config_set_callback is not None: + self.config_set_callback("propagation_node_active", dest_str) + + + def propagation_node_hash(self): + try: + return bytes.fromhex(self.propagation_node_active) + except: + return None + + + def propagation_node_hash_str(self): + if self.propagation_node_active: + return self.propagation_node_active + else: + return "" def client_connected(self, link): @@ -625,6 +679,43 @@ class lxmf_connection: log("- App Data: " + message.app_data, LOG_DEBUG) + + +class lxmf_connection_propagation(): + def __init__(self, owner, aspect_filter=None): + self.owner = owner + self.aspect_filter = aspect_filter + + EMITTED_DELTA_GRACE = 300 + EMITTED_DELTA_IGNORE = 10 + + def received_announce(self, destination_hash, announced_identity, app_data): + if app_data == None: + return + + if len(app_data) == 0: + return + + try: + unpacked = umsgpack.unpackb(app_data) + node_active = unpacked[0] + emitted = unpacked[1] + hop_count = RNS.Transport.hops_to(destination_hash) + age = time.time() - emitted + if age < 0: + if age < -1*PropDetector.EMITTED_DELTA_GRACE: + return + log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO) + if self.owner.propagation_node_active == None: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + else: + prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash()) + if hop_count <= prev_hop_count: + self.owner.propagation_node_update(RNS.hexrep(destination_hash, False)) + except: + return + + ############################################################################################################## # LXMF Functions @@ -839,6 +930,36 @@ def config_getoption(config, section, key, default=False, lng_key=""): +#### Config - Set ##### +def config_set(key=None, value=""): + global PATH + + try: + file = PATH + "/config.cfg.owr" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + + file = PATH + "/config.cfg" + if os.path.isfile(file): + fh = open(file,'r') + data = fh.read() + fh.close() + data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE) + fh = open(file,'w') + fh.write(data) + fh.close() + except: + pass + + + + #### Config - Read ##### def config_read(file=None, file_override=None): global CONFIG @@ -932,15 +1053,15 @@ def config_default(file=None, file_override=None): # Value convert -def val_to_bool(val): +def val_to_bool(val, fallback_true=True, fallback_false=False): if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up": return True elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down": return False elif val != "": - return True + return fallback_true else: - return False + return fallback_false ############################################################################################################## @@ -1106,6 +1227,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) else: config_propagation_node = None + if CONFIG.has_option("lxmf", "propagation_node_active"): + config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"] + else: + config_propagation_node_active = None + if path is None: path = PATH @@ -1114,9 +1240,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) destination_name=CONFIG["lxmf"]["destination_name"], destination_type=CONFIG["lxmf"]["destination_type"], display_name=CONFIG["lxmf"]["display_name"], + announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"), send_delay=CONFIG["lxmf"]["send_delay"], desired_method=CONFIG["lxmf"]["desired_method"], propagation_node=config_propagation_node, + propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"), + propagation_node_active=config_propagation_node_active, try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], @@ -1130,6 +1259,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False) LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) + LXMF_CONNECTION.register_config_set_callback(config_set) log("LXMF - Connected", LOG_DEBUG) @@ -1228,7 +1358,13 @@ display_name = CMD desired_method = direct #direct/propagated # Propagation node address/hash. -#propagation_node = +propagation_node = + +# Set propagation node automatically. +propagation_node_auto = True + +# Current propagation node (Automatically set by the software). +propagation_node_active = # Try to deliver a message via the LXMF propagation network, # if a direct delivery to the recipient is not possible. @@ -1244,6 +1380,10 @@ announce_startup_delay = 0 #Seconds announce_periodic = No announce_periodic_interval = 360 #Minutes +# The announce is hidden for client applications +# but is still used for the routing tables. +announce_hidden = No + # Some waiting time after message send # for LXMF/Reticulum processing. send_delay = 0 #Seconds