Merge branch 'SebastianObi:main' into main

This commit is contained in:
Swissbandit 2023-04-29 11:46:22 +02:00 committed by GitHub
commit a36d8f9a9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 2747 additions and 444 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -48,6 +48,9 @@ import pickle
#### String #### #### String ####
import string import string
#### Regex ####
import re
#### Process #### #### Process ####
import signal import signal
import threading import threading
@ -97,9 +100,10 @@ class lxmf_connection:
message_notification_callback = None message_notification_callback = None
message_notification_success_callback = None message_notification_success_callback = None
message_notification_failed_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.storage_path = storage_path
self.identity_file = identity_file self.identity_file = identity_file
@ -112,6 +116,7 @@ class lxmf_connection:
self.display_name = display_name self.display_name = display_name
self.announce_data = announce_data self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay) self.send_delay = int(send_delay)
@ -120,6 +125,8 @@ class lxmf_connection:
else: else:
self.desired_method_direct = True self.desired_method_direct = True
self.propagation_node = propagation_node 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.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup self.announce_startup = announce_startup
@ -134,6 +141,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval) 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): if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path) os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE) 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.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: if self.announce_startup or self.announce_periodic:
self.announce(True) self.announce(initial=True)
if self.sync_startup or self.sync_periodic: if self.sync_startup or self.sync_periodic:
self.sync(True) self.sync(True)
@ -218,6 +237,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function 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): def destination_hash(self):
return self.destination.hash return self.destination.hash
@ -261,7 +284,7 @@ class lxmf_connection:
return "" 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 type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1] destination = destination[1:-1]
@ -276,8 +299,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR) log("LXMF - Destination is invalid", LOG_ERROR)
return 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_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) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -346,7 +374,7 @@ class lxmf_connection:
message.desired_method_str = "propagated" message.desired_method_str = "propagated"
def announce(self, initial=False): def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0: if self.announce_periodic and self.announce_periodic_interval > 0:
@ -363,26 +391,29 @@ class lxmf_connection:
announce_timer.daemon = True announce_timer.daemon = True
announce_timer.start() announce_timer.start()
else: else:
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
return return
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None): def announce_now(self, app_data=None, attached_interface=None):
if app_data: 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): if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8")) self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else: 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) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data: elif self.announce_data:
if isinstance(self.announce_data, str): 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else: 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else: else:
self.destination.announce() self.destination.announce()
@ -424,24 +455,50 @@ class lxmf_connection:
return False return False
def autoselect_propagation_node(self): def propagation_node_set(self, dest_str):
if self.propagation_node is not None: if not dest_str:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): return False
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
node_identity = RNS.Identity.recall(propagation_hash) if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
if node_identity != None: log("LXMF - Propagation node length is invalid", LOG_ERROR)
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) return False
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash) try:
else: dest_hash = bytes.fromhex(dest_str)
log("LXMF - Propagation node identity not known", LOG_ERROR) 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): def client_connected(self, link):
@ -529,6 +586,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG) 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 # LXMF Functions
@ -772,6 +866,9 @@ def mqtt_message_received_callback_send(client, userdata, message):
if "title" not in message_data: if "title" not in message_data:
message_data["title"] = "" message_data["title"] = ""
if "fields" not in message_data:
message_data["fields"] = None
timestamp = None timestamp = None
if "timestamp" in message_data and timestamp is None: if "timestamp" in message_data and timestamp is None:
message_data["timestamp"] = message_data["timestamp"].strip() 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"] != "": if message_data["date_time"] != "":
timestamp = time.mktime(datetime.datetime.strptime(message_data["date_time"], '%Y-%m-%d %H:%M:%S').timetuple()) 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 ##### #### Config - Read #####
def config_read(file=None, file_override=None): def config_read(file=None, file_override=None):
global CONFIG global CONFIG
@ -936,15 +1063,15 @@ def config_default(file=None, file_override=None):
# Value convert # 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": 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 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": 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 return False
elif val != "": elif val != "":
return True return fallback_true
else: 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: else:
config_propagation_node = None 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: if path is None:
path = PATH 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_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"], destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"], display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"], send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"], desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node, 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"), try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], 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_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG) log("LXMF - Connected", LOG_DEBUG)
@ -1244,7 +1380,13 @@ display_name =
desired_method = direct #direct/propagated desired_method = direct #direct/propagated
# Propagation node address/hash. # 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, # Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible. # if a direct delivery to the recipient is not possible.
@ -1260,6 +1402,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = No announce_periodic = No
announce_periodic_interval = 360 #Minutes 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 # Some waiting time after message send
# for LXMF/Reticulum processing. # for LXMF/Reticulum processing.
send_delay = 0 #Seconds send_delay = 0 #Seconds

View File

@ -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.

View File

@ -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.

View File

@ -47,6 +47,9 @@ import pickle
#### String #### #### String ####
import string import string
#### Regex ####
import re
#### Process #### #### Process ####
import signal import signal
import threading import threading
@ -95,9 +98,10 @@ class lxmf_connection:
message_notification_callback = None message_notification_callback = None
message_notification_success_callback = None message_notification_success_callback = None
message_notification_failed_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.storage_path = storage_path
self.identity_file = identity_file self.identity_file = identity_file
@ -110,6 +114,7 @@ class lxmf_connection:
self.display_name = display_name self.display_name = display_name
self.announce_data = announce_data self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay) self.send_delay = int(send_delay)
@ -118,6 +123,8 @@ class lxmf_connection:
else: else:
self.desired_method_direct = True self.desired_method_direct = True
self.propagation_node = propagation_node 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.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup self.announce_startup = announce_startup
@ -132,6 +139,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval) 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): if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path) os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE) 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.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: if self.announce_startup or self.announce_periodic:
self.announce(True) self.announce(initial=True)
if self.sync_startup or self.sync_periodic: if self.sync_startup or self.sync_periodic:
self.sync(True) self.sync(True)
@ -216,6 +235,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function 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): def destination_hash(self):
return self.destination.hash return self.destination.hash
@ -259,7 +282,7 @@ class lxmf_connection:
return "" 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 type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1] destination = destination[1:-1]
@ -274,8 +297,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR) log("LXMF - Destination is invalid", LOG_ERROR)
return 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_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) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -344,7 +372,7 @@ class lxmf_connection:
message.desired_method_str = "propagated" message.desired_method_str = "propagated"
def announce(self, initial=False): def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0: if self.announce_periodic and self.announce_periodic_interval > 0:
@ -361,26 +389,29 @@ class lxmf_connection:
announce_timer.daemon = True announce_timer.daemon = True
announce_timer.start() announce_timer.start()
else: else:
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
return return
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None): def announce_now(self, app_data=None, attached_interface=None):
if app_data: 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): if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8")) self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else: 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) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data: elif self.announce_data:
if isinstance(self.announce_data, str): 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else: 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else: else:
self.destination.announce() self.destination.announce()
@ -422,24 +453,50 @@ class lxmf_connection:
return False return False
def autoselect_propagation_node(self): def propagation_node_set(self, dest_str):
if self.propagation_node is not None: if not dest_str:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): return False
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
node_identity = RNS.Identity.recall(propagation_hash) if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
if node_identity != None: log("LXMF - Propagation node length is invalid", LOG_ERROR)
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) return False
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash) try:
else: dest_hash = bytes.fromhex(dest_str)
log("LXMF - Propagation node identity not known", LOG_ERROR) 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): def client_connected(self, link):
@ -527,6 +584,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG) 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 # 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): def config_read(file=None, file_override=None):
global CONFIG global CONFIG
@ -759,15 +883,15 @@ def config_default(file=None, file_override=None):
# Value convert # 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": 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 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": 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 return False
elif val != "": elif val != "":
return True return fallback_true
else: 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: else:
config_propagation_node = None 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: if path is None:
path = PATH 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_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"], destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"], display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"], send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"], desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node, 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"), try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], 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_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG) log("LXMF - Connected", LOG_DEBUG)
@ -1061,7 +1194,13 @@ display_name = Chatbot
desired_method = direct #direct/propagated desired_method = direct #direct/propagated
# Propagation node address/hash. # 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, # Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible. # if a direct delivery to the recipient is not possible.
@ -1077,6 +1216,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = No announce_periodic = No
announce_periodic_interval = 360 #Minutes 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 # Some waiting time after message send
# for LXMF/Reticulum processing. # for LXMF/Reticulum processing.
send_delay = 0 #Seconds send_delay = 0 #Seconds

View File

@ -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.

View File

@ -47,6 +47,9 @@ import pickle
#### String #### #### String ####
import string import string
#### Regex ####
import re
#### Process #### #### Process ####
import signal import signal
import threading import threading
@ -93,9 +96,10 @@ class lxmf_connection:
message_notification_callback = None message_notification_callback = None
message_notification_success_callback = None message_notification_success_callback = None
message_notification_failed_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.storage_path = storage_path
self.identity_file = identity_file self.identity_file = identity_file
@ -108,6 +112,7 @@ class lxmf_connection:
self.display_name = display_name self.display_name = display_name
self.announce_data = announce_data self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay) self.send_delay = int(send_delay)
@ -116,6 +121,8 @@ class lxmf_connection:
else: else:
self.desired_method_direct = True self.desired_method_direct = True
self.propagation_node = propagation_node 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.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup self.announce_startup = announce_startup
@ -130,6 +137,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval) 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): if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path) os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE) 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.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: if self.announce_startup or self.announce_periodic:
self.announce(True) self.announce(initial=True)
if self.sync_startup or self.sync_periodic: if self.sync_startup or self.sync_periodic:
self.sync(True) self.sync(True)
@ -214,6 +233,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function 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): def destination_hash(self):
return self.destination.hash return self.destination.hash
@ -257,7 +280,7 @@ class lxmf_connection:
return "" 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 type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1] destination = destination[1:-1]
@ -272,8 +295,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR) log("LXMF - Destination is invalid", LOG_ERROR)
return 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_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) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -342,7 +370,7 @@ class lxmf_connection:
message.desired_method_str = "propagated" message.desired_method_str = "propagated"
def announce(self, initial=False): def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0: if self.announce_periodic and self.announce_periodic_interval > 0:
@ -359,26 +387,29 @@ class lxmf_connection:
announce_timer.daemon = True announce_timer.daemon = True
announce_timer.start() announce_timer.start()
else: else:
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
return return
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None): def announce_now(self, app_data=None, attached_interface=None):
if app_data: 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): if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8")) self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else: 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) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data: elif self.announce_data:
if isinstance(self.announce_data, str): 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else: 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else: else:
self.destination.announce() self.destination.announce()
@ -420,24 +451,50 @@ class lxmf_connection:
return False return False
def autoselect_propagation_node(self): def propagation_node_set(self, dest_str):
if self.propagation_node is not None: if not dest_str:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): return False
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
node_identity = RNS.Identity.recall(propagation_hash) if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
if node_identity != None: log("LXMF - Propagation node length is invalid", LOG_ERROR)
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) return False
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash) try:
else: dest_hash = bytes.fromhex(dest_str)
log("LXMF - Propagation node identity not known", LOG_ERROR) 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): def client_connected(self, link):
@ -525,6 +582,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG) 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 # 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 ##### #### Config - Read #####
def config_read(file=None, file_override=None): def config_read(file=None, file_override=None):
global CONFIG global CONFIG
@ -794,15 +918,15 @@ def config_default(file=None, file_override=None):
# Value convert # 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": 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 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": 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 return False
elif val != "": elif val != "":
return True return fallback_true
else: 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: else:
config_propagation_node = None 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: if path is None:
path = PATH 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_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"], destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"], display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"], send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"], desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node, 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"), try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], 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_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG) log("LXMF - Connected", LOG_DEBUG)
@ -1085,7 +1218,13 @@ display_name = CMD
desired_method = direct #direct/propagated desired_method = direct #direct/propagated
# Propagation node address/hash. # 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, # Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible. # if a direct delivery to the recipient is not possible.
@ -1101,6 +1240,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = No announce_periodic = No
announce_periodic_interval = 360 #Minutes 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 # Some waiting time after message send
# for LXMF/Reticulum processing. # for LXMF/Reticulum processing.
send_delay = 0 #Seconds send_delay = 0 #Seconds

View File

@ -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! <!source_address!>
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.

View File

@ -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]

View File

@ -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! <!source_address!>
send_prefix =
cluster_receive_title_prefix =
cluster_receive_prefix = @!cluster_source!->
cluster_send_title_prefix = !source_name! <!source_address!>
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.

View File

@ -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]

View File

@ -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.

File diff suppressed because it is too large Load Diff

View File

@ -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.

View File

@ -48,6 +48,9 @@ import pickle
#### String #### #### String ####
import string import string
#### Regex ####
import re
#### Process #### #### Process ####
import signal import signal
import threading import threading
@ -91,9 +94,10 @@ class lxmf_connection:
message_notification_callback = None message_notification_callback = None
message_notification_success_callback = None message_notification_success_callback = None
message_notification_failed_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.storage_path = storage_path
self.identity_file = identity_file self.identity_file = identity_file
@ -106,6 +110,7 @@ class lxmf_connection:
self.display_name = display_name self.display_name = display_name
self.announce_data = announce_data self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay) self.send_delay = int(send_delay)
@ -114,6 +119,8 @@ class lxmf_connection:
else: else:
self.desired_method_direct = True self.desired_method_direct = True
self.propagation_node = propagation_node 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.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup self.announce_startup = announce_startup
@ -128,6 +135,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval) 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): if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path) os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE) 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.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: if self.announce_startup or self.announce_periodic:
self.announce(True) self.announce(initial=True)
if self.sync_startup or self.sync_periodic: if self.sync_startup or self.sync_periodic:
self.sync(True) self.sync(True)
@ -212,6 +231,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function 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): def destination_hash(self):
return self.destination.hash return self.destination.hash
@ -255,7 +278,7 @@ class lxmf_connection:
return "" 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 type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1] destination = destination[1:-1]
@ -270,8 +293,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR) log("LXMF - Destination is invalid", LOG_ERROR)
return 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_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) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -340,7 +368,7 @@ class lxmf_connection:
message.desired_method_str = "propagated" message.desired_method_str = "propagated"
def announce(self, initial=False): def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0: if self.announce_periodic and self.announce_periodic_interval > 0:
@ -357,26 +385,29 @@ class lxmf_connection:
announce_timer.daemon = True announce_timer.daemon = True
announce_timer.start() announce_timer.start()
else: else:
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
return return
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None): def announce_now(self, app_data=None, attached_interface=None):
if app_data: 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): if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8")) self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else: 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) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data: elif self.announce_data:
if isinstance(self.announce_data, str): 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else: 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else: else:
self.destination.announce() self.destination.announce()
@ -418,24 +449,50 @@ class lxmf_connection:
return False return False
def autoselect_propagation_node(self): def propagation_node_set(self, dest_str):
if self.propagation_node is not None: if not dest_str:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): return False
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
node_identity = RNS.Identity.recall(propagation_hash) if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
if node_identity != None: log("LXMF - Propagation node length is invalid", LOG_ERROR)
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) return False
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash) try:
else: dest_hash = bytes.fromhex(dest_str)
log("LXMF - Propagation node identity not known", LOG_ERROR) 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): def client_connected(self, link):
@ -523,6 +580,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG) 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 # 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 ##### #### Config - Read #####
def config_read(file=None, file_override=None): def config_read(file=None, file_override=None):
global CONFIG global CONFIG
@ -887,15 +1011,15 @@ def data_default(file=None):
# Value convert # 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": 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 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": 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 return False
elif val != "": elif val != "":
return True return fallback_true
else: 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: else:
config_propagation_node = None 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: if path is None:
path = PATH 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_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"], destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"], display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"], send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"], desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node, 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"), try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], 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_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG) 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. # It is also used in the group description/info.
display_name = Distribution Group display_name = Distribution Group
# Propagation node address/hash. # Set propagation node automatically.
propagation_node = ca2762fe5283873719aececfb9e18835 propagation_node_auto = True
# Try to deliver a message via the LXMF propagation network, # Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible. # if a direct delivery to the recipient is not possible.
@ -1205,7 +1338,13 @@ display_name = Distribution Group
desired_method = direct #direct/propagated desired_method = direct #direct/propagated
# Propagation node address/hash. # 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, # Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible. # if a direct delivery to the recipient is not possible.
@ -1221,6 +1360,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = Yes announce_periodic = Yes
announce_periodic_interval = 120 #Minutes 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 # Some waiting time after message send
# for LXMF/Reticulum processing. # for LXMF/Reticulum processing.
send_delay = 0 #Seconds send_delay = 0 #Seconds

View File

@ -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.

View File

@ -47,6 +47,9 @@ import pickle
#### String #### #### String ####
import string import string
#### Regex ####
import re
#### Process #### #### Process ####
import signal import signal
import threading import threading
@ -89,9 +92,10 @@ class lxmf_connection:
message_notification_callback = None message_notification_callback = None
message_notification_success_callback = None message_notification_success_callback = None
message_notification_failed_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.storage_path = storage_path
self.identity_file = identity_file self.identity_file = identity_file
@ -104,6 +108,7 @@ class lxmf_connection:
self.display_name = display_name self.display_name = display_name
self.announce_data = announce_data self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay) self.send_delay = int(send_delay)
@ -112,6 +117,8 @@ class lxmf_connection:
else: else:
self.desired_method_direct = True self.desired_method_direct = True
self.propagation_node = propagation_node 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.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup self.announce_startup = announce_startup
@ -126,6 +133,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval) 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): if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path) os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE) 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.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: if self.announce_startup or self.announce_periodic:
self.announce(True) self.announce(initial=True)
if self.sync_startup or self.sync_periodic: if self.sync_startup or self.sync_periodic:
self.sync(True) self.sync(True)
@ -210,6 +229,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function 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): def destination_hash(self):
return self.destination.hash return self.destination.hash
@ -253,7 +276,7 @@ class lxmf_connection:
return "" 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 type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1] destination = destination[1:-1]
@ -268,8 +291,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR) log("LXMF - Destination is invalid", LOG_ERROR)
return 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_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) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -338,7 +366,7 @@ class lxmf_connection:
message.desired_method_str = "propagated" message.desired_method_str = "propagated"
def announce(self, initial=False): def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0: if self.announce_periodic and self.announce_periodic_interval > 0:
@ -355,26 +383,29 @@ class lxmf_connection:
announce_timer.daemon = True announce_timer.daemon = True
announce_timer.start() announce_timer.start()
else: else:
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
return return
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None): def announce_now(self, app_data=None, attached_interface=None):
if app_data: 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): if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8")) self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else: 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) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data: elif self.announce_data:
if isinstance(self.announce_data, str): 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else: 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else: else:
self.destination.announce() self.destination.announce()
@ -416,24 +447,50 @@ class lxmf_connection:
return False return False
def autoselect_propagation_node(self): def propagation_node_set(self, dest_str):
if self.propagation_node is not None: if not dest_str:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): return False
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
node_identity = RNS.Identity.recall(propagation_hash) if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
if node_identity != None: log("LXMF - Propagation node length is invalid", LOG_ERROR)
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) return False
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash) try:
else: dest_hash = bytes.fromhex(dest_str)
log("LXMF - Propagation node identity not known", LOG_ERROR) 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): def client_connected(self, link):
@ -521,6 +578,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG) 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 # 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 ##### #### Config - Read #####
def config_read(file=None, file_override=None): def config_read(file=None, file_override=None):
global CONFIG global CONFIG
@ -762,15 +886,15 @@ def config_default(file=None, file_override=None):
# Value convert # 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": 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 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": 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 return False
elif val != "": elif val != "":
return True return fallback_true
else: 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: else:
config_propagation_node = None 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: if path is None:
path = PATH 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_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"], destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"], display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"], send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"], desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node, 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"), try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], 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_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG) log("LXMF - Connected", LOG_DEBUG)
@ -1053,7 +1186,13 @@ display_name = Echo Test
desired_method = direct #direct/propagated desired_method = direct #direct/propagated
# Propagation node address/hash. # 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, # Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible. # if a direct delivery to the recipient is not possible.
@ -1069,6 +1208,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = Yes announce_periodic = Yes
announce_periodic_interval = 360 #Minutes 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 # Some waiting time after message send
# for LXMF/Reticulum processing. # for LXMF/Reticulum processing.
send_delay = 0 #Seconds send_delay = 0 #Seconds

View File

@ -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.

View File

@ -44,6 +44,9 @@ import pickle
#### String #### #### String ####
import string import string
#### Regex ####
import re
#### Other #### #### Other ####
import random import random
import secrets import secrets
@ -90,9 +93,10 @@ class lxmf_connection:
message_notification_callback = None message_notification_callback = None
message_notification_success_callback = None message_notification_success_callback = None
message_notification_failed_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.storage_path = storage_path
self.identity_file = identity_file self.identity_file = identity_file
@ -105,6 +109,7 @@ class lxmf_connection:
self.display_name = display_name self.display_name = display_name
self.announce_data = announce_data self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay) self.send_delay = int(send_delay)
@ -113,6 +118,8 @@ class lxmf_connection:
else: else:
self.desired_method_direct = True self.desired_method_direct = True
self.propagation_node = propagation_node 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.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup self.announce_startup = announce_startup
@ -127,6 +134,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval) 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): if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path) os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE) 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.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: if self.announce_startup or self.announce_periodic:
self.announce(True) self.announce(initial=True)
if self.sync_startup or self.sync_periodic: if self.sync_startup or self.sync_periodic:
self.sync(True) self.sync(True)
@ -211,6 +230,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function 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): def destination_hash(self):
return self.destination.hash return self.destination.hash
@ -254,7 +277,7 @@ class lxmf_connection:
return "" 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 type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1] destination = destination[1:-1]
@ -269,8 +292,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR) log("LXMF - Destination is invalid", LOG_ERROR)
return 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_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) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -339,7 +367,7 @@ class lxmf_connection:
message.desired_method_str = "propagated" message.desired_method_str = "propagated"
def announce(self, initial=False): def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0: if self.announce_periodic and self.announce_periodic_interval > 0:
@ -356,26 +384,29 @@ class lxmf_connection:
announce_timer.daemon = True announce_timer.daemon = True
announce_timer.start() announce_timer.start()
else: else:
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
return return
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None): def announce_now(self, app_data=None, attached_interface=None):
if app_data: 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): if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8")) self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else: 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) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data: elif self.announce_data:
if isinstance(self.announce_data, str): 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else: 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else: else:
self.destination.announce() self.destination.announce()
@ -417,24 +448,50 @@ class lxmf_connection:
return False return False
def autoselect_propagation_node(self): def propagation_node_set(self, dest_str):
if self.propagation_node is not None: if not dest_str:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): return False
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
node_identity = RNS.Identity.recall(propagation_hash) if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
if node_identity != None: log("LXMF - Propagation node length is invalid", LOG_ERROR)
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) return False
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash) try:
else: dest_hash = bytes.fromhex(dest_str)
log("LXMF - Propagation node identity not known", LOG_ERROR) 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): def client_connected(self, link):
@ -522,6 +579,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG) 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 # LXMF Functions
@ -575,15 +669,15 @@ def lxmf_failed(message):
# Value convert # 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": 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 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": 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 return False
elif val != "": elif val != "":
return True return fallback_true
else: else:
return False return fallback_false
############################################################################################################## ##############################################################################################################

View File

@ -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.

View File

@ -47,6 +47,15 @@ import pickle
#### String #### #### String ####
import string import string
#### Regex ####
import re
#### UID ####
import uuid
#### ####
import base64
#### Process #### #### Process ####
import signal import signal
import threading import threading
@ -81,7 +90,10 @@ PATH_RNS = None
#### Global Variables - System (Not changeable) #### #### Global Variables - System (Not changeable) ####
CACHE = [] CACHE = {}
CACHE["in"] = {}
CACHE["out"] = {}
CACHE_CHANGE = False
CONFIG = None CONFIG = None
RNS_CONNECTION = None RNS_CONNECTION = None
LXMF_CONNECTION = None LXMF_CONNECTION = None
@ -97,9 +109,10 @@ class lxmf_connection:
message_notification_callback = None message_notification_callback = None
message_notification_success_callback = None message_notification_success_callback = None
message_notification_failed_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.storage_path = storage_path
self.identity_file = identity_file self.identity_file = identity_file
@ -112,6 +125,7 @@ class lxmf_connection:
self.display_name = display_name self.display_name = display_name
self.announce_data = announce_data self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay) self.send_delay = int(send_delay)
@ -120,6 +134,8 @@ class lxmf_connection:
else: else:
self.desired_method_direct = True self.desired_method_direct = True
self.propagation_node = propagation_node 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.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup self.announce_startup = announce_startup
@ -134,6 +150,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval) 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): if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path) os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE) 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.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: if self.announce_startup or self.announce_periodic:
self.announce(True) self.announce(initial=True)
if self.sync_startup or self.sync_periodic: if self.sync_startup or self.sync_periodic:
self.sync(True) self.sync(True)
@ -218,6 +246,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function 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): def destination_hash(self):
return self.destination.hash return self.destination.hash
@ -261,7 +293,7 @@ class lxmf_connection:
return "" 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 type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1] destination = destination[1:-1]
@ -276,8 +308,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR) log("LXMF - Destination is invalid", LOG_ERROR)
return 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_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) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -346,7 +383,7 @@ class lxmf_connection:
message.desired_method_str = "propagated" message.desired_method_str = "propagated"
def announce(self, initial=False): def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0: if self.announce_periodic and self.announce_periodic_interval > 0:
@ -363,26 +400,29 @@ class lxmf_connection:
announce_timer.daemon = True announce_timer.daemon = True
announce_timer.start() announce_timer.start()
else: else:
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
return return
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None): def announce_now(self, app_data=None, attached_interface=None):
if app_data: 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): if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8")) self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else: 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) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data: elif self.announce_data:
if isinstance(self.announce_data, str): 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else: 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else: else:
self.destination.announce() self.destination.announce()
@ -424,24 +464,50 @@ class lxmf_connection:
return False return False
def autoselect_propagation_node(self): def propagation_node_set(self, dest_str):
if self.propagation_node is not None: if not dest_str:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): return False
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
node_identity = RNS.Identity.recall(propagation_hash) if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
if node_identity != None: log("LXMF - Propagation node length is invalid", LOG_ERROR)
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) return False
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash) try:
else: dest_hash = bytes.fromhex(dest_str)
log("LXMF - Propagation node identity not known", LOG_ERROR) 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): def client_connected(self, link):
@ -529,6 +595,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG) 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 # LXMF Functions
@ -549,7 +652,7 @@ class lxmf_announce_callback:
#### LXMF - Message #### #### LXMF - Message ####
def lxmf_message_received_callback(message): def lxmf_message_received_callback(message):
global CACHE global CACHE, CACHE_CHANGE
if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated: 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) 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: if not message.fields:
return return
if not "registration_request" in message.fields and not "telemetry" in message.fields: hash_destination = RNS.hexrep(message.source_hash, delimit=False)
return 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 for key in message.fields:
try: try:
db = psycopg2.connect(user=CONFIG["database"]["user"], password=CONFIG["database"]["password"], host=CONFIG["database"]["host"], port=CONFIG["database"]["port"], database=CONFIG["database"]["database"]) data = message.fields[key]
dbc = db.cursor() 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: data["hash_destination"] = hash_destination
dbc.execute("INSERT INTO "+CONFIG["database"]["table_registration"]+" (hash, data) VALUES(%s, %s)", ( data["hash_identity"] = hash_identity
RNS.hexrep(message.source_hash, delimit=False), data["timestamp_client"] = message.timestamp
umsgpack.packb(message.fields["registration_request"])) data["timestamp_server"] = time.time()
)
if CONFIG["features"].getboolean("telemetry") and "telemetry" in message.fields: if "password" in data:
dbc.execute("INSERT INTO "+CONFIG["database"]["table_telemetry"]+" (hash, data) VALUES(%s, %s)", ( data["password"] = str(base64.b32encode(data["password"]))
RNS.hexrep(message.source_hash, delimit=False),
umsgpack.packb(message.fields["telemetry"]))
)
db.commit() CACHE["in"][str(uuid.uuid4())] = data
except psycopg2.DatabaseError as e: CACHE_CHANGE = True
log("DB - Error: "+str(e), LOG_ERROR) except:
if db: pass
dbc.close()
db.close()
db = None
#### 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 ##### #### Config - Read #####
def config_read(file=None, file_override=None): def config_read(file=None, file_override=None):
global CONFIG global CONFIG
@ -737,15 +1177,15 @@ def config_default(file=None, file_override=None):
# Value convert # 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": 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 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": 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 return False
elif val != "": elif val != "":
return True return fallback_true
else: 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") print("Config - Error reading config file " + PATH + "/config.cfg")
panic() panic()
if not cache_read(PATH + "/cache.data"):
print("Cache - Error reading cache file " + PATH + "/cache.data")
panic()
if CONFIG["main"].getboolean("default_config"): if CONFIG["main"].getboolean("default_config"):
print("Exit!") print("Exit!")
print("First start with the default config!") 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: else:
config_propagation_node = None 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: if path is None:
path = PATH 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_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"], destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"], display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
announce_data = umsgpack.packb(announce_data), announce_data = umsgpack.packb(announce_data),
send_delay=CONFIG["lxmf"]["send_delay"], send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"], desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node, 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"), try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], 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_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_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) 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("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE)
log("...............................................................................", 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: while True:
time.sleep(1) time.sleep(1)
@ -1007,9 +1470,16 @@ announce_periodic_interval = 15 #Minutes
[features] [features]
announce_versions = True announce_versions = True
registration = True account_add = True
account_edit = True
account_del = True
account_prove = True
telemetry = False telemetry = False
[processing]
interval_in = 5 #Seconds
interval_out = 60 #Seconds
[data] [data]
v_s = 0.0.0 #Version software v_s = 0.0.0 #Version software
v_c = 2022-01-01 00:00 #Version config 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 v_a = 2022-01-01 00:00 #Version auth
u_s = #URL Software u_s = #URL Software
i_s = #Info Software i_s = #Info Software
cmd = #CMD
''' '''
@ -1045,6 +1516,7 @@ name = LXMF Provisioning Server
# to be compatibel with other LXMF programs. # to be compatibel with other LXMF programs.
destination_name = lxmf destination_name = lxmf
destination_type = provisioning destination_type = provisioning
destination_type_conv = 11
# The name will be visible to other peers # The name will be visible to other peers
# on the network, and included in announces. # on the network, and included in announces.
@ -1054,11 +1526,17 @@ display_name = LXMF Provisioning Server
desired_method = direct #direct/propagated desired_method = direct #direct/propagated
# Propagation node address/hash. # 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, # Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible. # 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 # The peer is announced at startup
# to let other peers reach it immediately. # to let other peers reach it immediately.
@ -1070,6 +1548,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = Yes announce_periodic = Yes
announce_periodic_interval = 360 #Minutes 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 # Some waiting time after message send
# for LXMF/Reticulum processing. # for LXMF/Reticulum processing.
send_delay = 0 #Seconds send_delay = 0 #Seconds
@ -1091,7 +1573,7 @@ sync_periodic_interval = 360 #Minutes
sync_limit = 8 sync_limit = 8
# Allow only messages with valid signature. # Allow only messages with valid signature.
signature_validated = Yes signature_validated = No
@ -1104,8 +1586,6 @@ port = 5432
user = postgres user = postgres
password = password password = password
database = database database = database
table_registration = tbl_account
table_telemetry = tbl_telemetry
@ -1114,12 +1594,37 @@ table_telemetry = tbl_telemetry
[features] [features]
announce_versions = True 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 telemetry = False
#### Processing ####
[processing]
interval_in = 5 #Seconds
interval_out = 60 #Seconds
#### Data settings #### #### Data settings ####
[data] [data]
@ -1129,6 +1634,7 @@ v_d = 2022-01-01 00:00 #Version data
v_a = 2022-01-01 00:00 #Version auth v_a = 2022-01-01 00:00 #Version auth
u_s = #URL Software u_s = #URL Software
i_s = #Info Software i_s = #Info Software
cmd = #CMD
''' '''

View File

@ -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.

View File

@ -193,9 +193,10 @@ class lxmf_connection:
message_notification_callback = None message_notification_callback = None
message_notification_success_callback = None message_notification_success_callback = None
message_notification_failed_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.storage_path = storage_path
self.identity_file = identity_file self.identity_file = identity_file
@ -208,6 +209,7 @@ class lxmf_connection:
self.display_name = display_name self.display_name = display_name
self.announce_data = announce_data self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay) self.send_delay = int(send_delay)
@ -216,6 +218,8 @@ class lxmf_connection:
else: else:
self.desired_method_direct = True self.desired_method_direct = True
self.propagation_node = propagation_node 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.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup self.announce_startup = announce_startup
@ -230,6 +234,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval) 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): if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path) os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE) 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.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: if self.announce_startup or self.announce_periodic:
self.announce(True) self.announce(initial=True)
if self.sync_startup or self.sync_periodic: if self.sync_startup or self.sync_periodic:
self.sync(True) self.sync(True)
@ -314,6 +330,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function 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): def destination_hash(self):
return self.destination.hash return self.destination.hash
@ -357,7 +377,7 @@ class lxmf_connection:
return "" 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 type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2: if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1] destination = destination[1:-1]
@ -372,8 +392,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR) log("LXMF - Destination is invalid", LOG_ERROR)
return 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_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) self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -442,7 +467,7 @@ class lxmf_connection:
message.desired_method_str = "propagated" message.desired_method_str = "propagated"
def announce(self, initial=False): def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0: if self.announce_periodic and self.announce_periodic_interval > 0:
@ -459,26 +484,29 @@ class lxmf_connection:
announce_timer.daemon = True announce_timer.daemon = True
announce_timer.start() announce_timer.start()
else: else:
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
return return
self.announce_now() self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None): def announce_now(self, app_data=None, attached_interface=None):
if app_data: 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): if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8")) self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else: 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) log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data: elif self.announce_data:
if isinstance(self.announce_data, str): 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else: 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) log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else: else:
self.destination.announce() self.destination.announce()
@ -520,24 +548,50 @@ class lxmf_connection:
return False return False
def autoselect_propagation_node(self): def propagation_node_set(self, dest_str):
if self.propagation_node is not None: if not dest_str:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2): return False
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
node_identity = RNS.Identity.recall(propagation_hash) if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
if node_identity != None: log("LXMF - Propagation node length is invalid", LOG_ERROR)
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO) return False
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash) try:
else: dest_hash = bytes.fromhex(dest_str)
log("LXMF - Propagation node identity not known", LOG_ERROR) 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): def client_connected(self, link):
@ -625,6 +679,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG) 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 # 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 ##### #### Config - Read #####
def config_read(file=None, file_override=None): def config_read(file=None, file_override=None):
global CONFIG global CONFIG
@ -932,15 +1053,15 @@ def config_default(file=None, file_override=None):
# Value convert # 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": 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 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": 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 return False
elif val != "": elif val != "":
return True return fallback_true
else: 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: else:
config_propagation_node = None 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: if path is None:
path = PATH 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_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"], destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"], display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"], send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"], desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node, 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"), try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"), announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"], 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_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback) LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG) log("LXMF - Connected", LOG_DEBUG)
@ -1228,7 +1358,13 @@ display_name = CMD
desired_method = direct #direct/propagated desired_method = direct #direct/propagated
# Propagation node address/hash. # 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, # Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible. # if a direct delivery to the recipient is not possible.
@ -1244,6 +1380,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = No announce_periodic = No
announce_periodic_interval = 360 #Minutes 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 # Some waiting time after message send
# for LXMF/Reticulum processing. # for LXMF/Reticulum processing.
send_delay = 0 #Seconds send_delay = 0 #Seconds