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 ####
import string
#### Regex ####
import re
#### Process ####
import signal
import threading
@ -97,9 +100,10 @@ class lxmf_connection:
message_notification_callback = None
message_notification_success_callback = None
message_notification_failed_callback = None
config_set_callback = None
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
self.storage_path = storage_path
self.identity_file = identity_file
@ -112,6 +116,7 @@ class lxmf_connection:
self.display_name = display_name
self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay)
@ -120,6 +125,8 @@ class lxmf_connection:
else:
self.desired_method_direct = True
self.propagation_node = propagation_node
self.propagation_node_auto = propagation_node_auto
self.propagation_node_active = propagation_node_active
self.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup
@ -134,6 +141,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval)
if not self.storage_path:
log("LXMF - No storage_path parameter", LOG_ERROR)
return
if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE)
@ -188,10 +199,18 @@ class lxmf_connection:
self.destination.set_link_established_callback(self.client_connected)
self.autoselect_propagation_node()
if self.propagation_node_auto:
self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation")
RNS.Transport.register_announce_handler(self.propagation_callback)
if self.propagation_node_active:
self.propagation_node_set(self.propagation_node_active)
elif self.propagation_node:
self.propagation_node_set(self.propagation_node)
else:
self.propagation_node_set(self.propagation_node)
if self.announce_startup or self.announce_periodic:
self.announce(True)
self.announce(initial=True)
if self.sync_startup or self.sync_periodic:
self.sync(True)
@ -218,6 +237,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function
def register_config_set_callback(self, handler_function):
self.config_set_callback = handler_function
def destination_hash(self):
return self.destination.hash
@ -261,7 +284,7 @@ class lxmf_connection:
return ""
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""):
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None):
if type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1]
@ -276,8 +299,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR)
return
if destination_name == None:
destination_name = self.destination_name
if destination_type == None:
destination_type = self.destination_type
destination_identity = RNS.Identity.recall(destination)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type)
self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -346,7 +374,7 @@ class lxmf_connection:
message.desired_method_str = "propagated"
def announce(self, initial=False):
def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0:
@ -363,26 +391,29 @@ class lxmf_connection:
announce_timer.daemon = True
announce_timer.start()
else:
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
return
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None):
if app_data:
def announce_now(self, app_data=None, attached_interface=None):
if self.announce_hidden:
self.destination.announce("".encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG)
elif app_data != None:
if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8"))
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG)
self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else:
self.destination.announce(app_data)
self.destination.announce(app_data, attached_interface=attached_interface)
log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data:
if isinstance(self.announce_data, str):
self.destination.announce(self.announce_data.encode("utf-8"))
self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else:
self.destination.announce(self.announce_data)
self.destination.announce(self.announce_data, attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else:
self.destination.announce()
@ -424,24 +455,50 @@ class lxmf_connection:
return False
def autoselect_propagation_node(self):
if self.propagation_node is not None:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
def propagation_node_set(self, dest_str):
if not dest_str:
return False
if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
log("LXMF - Propagation node length is invalid", LOG_ERROR)
else:
return False
try:
propagation_hash = bytes.fromhex(self.propagation_node)
dest_hash = bytes.fromhex(dest_str)
except Exception as e:
log("LXMF - Propagation node is invalid", LOG_ERROR)
return
return False
node_identity = RNS.Identity.recall(propagation_hash)
node_identity = RNS.Identity.recall(dest_hash)
if node_identity != None:
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash)
log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO)
dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(dest_hash)
self.propagation_node_active = dest_str
return True
else:
log("LXMF - Propagation node identity not known", LOG_ERROR)
return False
def propagation_node_update(self, dest_str):
if self.propagation_node_hash_str() != dest_str:
if self.propagation_node_set(dest_str) and self.config_set_callback is not None:
self.config_set_callback("propagation_node_active", dest_str)
def propagation_node_hash(self):
try:
return bytes.fromhex(self.propagation_node_active)
except:
return None
def propagation_node_hash_str(self):
if self.propagation_node_active:
return self.propagation_node_active
else:
return ""
def client_connected(self, link):
@ -529,6 +586,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG)
class lxmf_connection_propagation():
def __init__(self, owner, aspect_filter=None):
self.owner = owner
self.aspect_filter = aspect_filter
EMITTED_DELTA_GRACE = 300
EMITTED_DELTA_IGNORE = 10
def received_announce(self, destination_hash, announced_identity, app_data):
if app_data == None:
return
if len(app_data) == 0:
return
try:
unpacked = umsgpack.unpackb(app_data)
node_active = unpacked[0]
emitted = unpacked[1]
hop_count = RNS.Transport.hops_to(destination_hash)
age = time.time() - emitted
if age < 0:
if age < -1*PropDetector.EMITTED_DELTA_GRACE:
return
log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO)
if self.owner.propagation_node_active == None:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
else:
prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash())
if hop_count <= prev_hop_count:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
except:
return
##############################################################################################################
# LXMF Functions
@ -772,6 +866,9 @@ def mqtt_message_received_callback_send(client, userdata, message):
if "title" not in message_data:
message_data["title"] = ""
if "fields" not in message_data:
message_data["fields"] = None
timestamp = None
if "timestamp" in message_data and timestamp is None:
message_data["timestamp"] = message_data["timestamp"].strip()
@ -783,7 +880,7 @@ def mqtt_message_received_callback_send(client, userdata, message):
if message_data["date_time"] != "":
timestamp = time.mktime(datetime.datetime.strptime(message_data["date_time"], '%Y-%m-%d %H:%M:%S').timetuple())
LXMF_CONNECTION.send(message_data["destination"].strip(), content, message_data["title"].strip(), None, timestamp)
LXMF_CONNECTION.send(message_data["destination"].strip(), content, message_data["title"].strip(), message_data["fields"], timestamp)
##############################################################################################################
@ -843,6 +940,36 @@ def config_getoption(config, section, key, default=False, lng_key=""):
#### Config - Set #####
def config_set(key=None, value=""):
global PATH
try:
file = PATH + "/config.cfg.owr"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
file = PATH + "/config.cfg"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
except:
pass
#### Config - Read #####
def config_read(file=None, file_override=None):
global CONFIG
@ -936,15 +1063,15 @@ def config_default(file=None, file_override=None):
# Value convert
def val_to_bool(val):
def val_to_bool(val, fallback_true=True, fallback_false=False):
if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up":
return True
elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down":
return False
elif val != "":
return True
return fallback_true
else:
return False
return fallback_false
##############################################################################################################
@ -1110,6 +1237,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
else:
config_propagation_node = None
if CONFIG.has_option("lxmf", "propagation_node_active"):
config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"]
else:
config_propagation_node_active = None
if path is None:
path = PATH
@ -1118,9 +1250,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
destination_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node,
propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"),
propagation_node_active=config_propagation_node_active,
try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"],
@ -1134,6 +1269,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG)
@ -1244,7 +1380,13 @@ display_name =
desired_method = direct #direct/propagated
# Propagation node address/hash.
propagation_node = ca2762fe5283873719aececfb9e18835
propagation_node =
# Set propagation node automatically.
propagation_node_auto = True
# Current propagation node (Automatically set by the software).
propagation_node_active =
# Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible.
@ -1260,6 +1402,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = No
announce_periodic_interval = 360 #Minutes
# The announce is hidden for client applications
# but is still used for the routing tables.
announce_hidden = No
# Some waiting time after message send
# for LXMF/Reticulum processing.
send_delay = 0 #Seconds

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 ####
import string
#### Regex ####
import re
#### Process ####
import signal
import threading
@ -95,9 +98,10 @@ class lxmf_connection:
message_notification_callback = None
message_notification_success_callback = None
message_notification_failed_callback = None
config_set_callback = None
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
self.storage_path = storage_path
self.identity_file = identity_file
@ -110,6 +114,7 @@ class lxmf_connection:
self.display_name = display_name
self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay)
@ -118,6 +123,8 @@ class lxmf_connection:
else:
self.desired_method_direct = True
self.propagation_node = propagation_node
self.propagation_node_auto = propagation_node_auto
self.propagation_node_active = propagation_node_active
self.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup
@ -132,6 +139,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval)
if not self.storage_path:
log("LXMF - No storage_path parameter", LOG_ERROR)
return
if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE)
@ -186,10 +197,18 @@ class lxmf_connection:
self.destination.set_link_established_callback(self.client_connected)
self.autoselect_propagation_node()
if self.propagation_node_auto:
self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation")
RNS.Transport.register_announce_handler(self.propagation_callback)
if self.propagation_node_active:
self.propagation_node_set(self.propagation_node_active)
elif self.propagation_node:
self.propagation_node_set(self.propagation_node)
else:
self.propagation_node_set(self.propagation_node)
if self.announce_startup or self.announce_periodic:
self.announce(True)
self.announce(initial=True)
if self.sync_startup or self.sync_periodic:
self.sync(True)
@ -216,6 +235,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function
def register_config_set_callback(self, handler_function):
self.config_set_callback = handler_function
def destination_hash(self):
return self.destination.hash
@ -259,7 +282,7 @@ class lxmf_connection:
return ""
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""):
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None):
if type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1]
@ -274,8 +297,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR)
return
if destination_name == None:
destination_name = self.destination_name
if destination_type == None:
destination_type = self.destination_type
destination_identity = RNS.Identity.recall(destination)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type)
self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -344,7 +372,7 @@ class lxmf_connection:
message.desired_method_str = "propagated"
def announce(self, initial=False):
def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0:
@ -361,26 +389,29 @@ class lxmf_connection:
announce_timer.daemon = True
announce_timer.start()
else:
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
return
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None):
if app_data:
def announce_now(self, app_data=None, attached_interface=None):
if self.announce_hidden:
self.destination.announce("".encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG)
elif app_data != None:
if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8"))
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG)
self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else:
self.destination.announce(app_data)
self.destination.announce(app_data, attached_interface=attached_interface)
log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data:
if isinstance(self.announce_data, str):
self.destination.announce(self.announce_data.encode("utf-8"))
self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else:
self.destination.announce(self.announce_data)
self.destination.announce(self.announce_data, attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else:
self.destination.announce()
@ -422,24 +453,50 @@ class lxmf_connection:
return False
def autoselect_propagation_node(self):
if self.propagation_node is not None:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
def propagation_node_set(self, dest_str):
if not dest_str:
return False
if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
log("LXMF - Propagation node length is invalid", LOG_ERROR)
else:
return False
try:
propagation_hash = bytes.fromhex(self.propagation_node)
dest_hash = bytes.fromhex(dest_str)
except Exception as e:
log("LXMF - Propagation node is invalid", LOG_ERROR)
return
return False
node_identity = RNS.Identity.recall(propagation_hash)
node_identity = RNS.Identity.recall(dest_hash)
if node_identity != None:
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash)
log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO)
dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(dest_hash)
self.propagation_node_active = dest_str
return True
else:
log("LXMF - Propagation node identity not known", LOG_ERROR)
return False
def propagation_node_update(self, dest_str):
if self.propagation_node_hash_str() != dest_str:
if self.propagation_node_set(dest_str) and self.config_set_callback is not None:
self.config_set_callback("propagation_node_active", dest_str)
def propagation_node_hash(self):
try:
return bytes.fromhex(self.propagation_node_active)
except:
return None
def propagation_node_hash_str(self):
if self.propagation_node_active:
return self.propagation_node_active
else:
return ""
def client_connected(self, link):
@ -527,6 +584,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG)
class lxmf_connection_propagation():
def __init__(self, owner, aspect_filter=None):
self.owner = owner
self.aspect_filter = aspect_filter
EMITTED_DELTA_GRACE = 300
EMITTED_DELTA_IGNORE = 10
def received_announce(self, destination_hash, announced_identity, app_data):
if app_data == None:
return
if len(app_data) == 0:
return
try:
unpacked = umsgpack.unpackb(app_data)
node_active = unpacked[0]
emitted = unpacked[1]
hop_count = RNS.Transport.hops_to(destination_hash)
age = time.time() - emitted
if age < 0:
if age < -1*PropDetector.EMITTED_DELTA_GRACE:
return
log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO)
if self.owner.propagation_node_active == None:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
else:
prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash())
if hop_count <= prev_hop_count:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
except:
return
##############################################################################################################
# LXMF Functions
@ -666,7 +760,37 @@ def config_getoption(config, section, key, default=False, lng_key=""):
#### Config - Read #####
#### Config - Set #####
def config_set(key=None, value=""):
global PATH
try:
file = PATH + "/config.cfg.owr"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
file = PATH + "/config.cfg"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
except:
pass
#### Config
- Read #####
def config_read(file=None, file_override=None):
global CONFIG
@ -759,15 +883,15 @@ def config_default(file=None, file_override=None):
# Value convert
def val_to_bool(val):
def val_to_bool(val, fallback_true=True, fallback_false=False):
if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up":
return True
elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down":
return False
elif val != "":
return True
return fallback_true
else:
return False
return fallback_false
##############################################################################################################
@ -933,6 +1057,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
else:
config_propagation_node = None
if CONFIG.has_option("lxmf", "propagation_node_active"):
config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"]
else:
config_propagation_node_active = None
if path is None:
path = PATH
@ -941,9 +1070,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
destination_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node,
propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"),
propagation_node_active=config_propagation_node_active,
try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"],
@ -957,6 +1089,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG)
@ -1061,7 +1194,13 @@ display_name = Chatbot
desired_method = direct #direct/propagated
# Propagation node address/hash.
#propagation_node =
propagation_node =
# Set propagation node automatically.
propagation_node_auto = True
# Current propagation node (Automatically set by the software).
propagation_node_active =
# Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible.
@ -1077,6 +1216,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = No
announce_periodic_interval = 360 #Minutes
# The announce is hidden for client applications
# but is still used for the routing tables.
announce_hidden = No
# Some waiting time after message send
# for LXMF/Reticulum processing.
send_delay = 0 #Seconds

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 ####
import string
#### Regex ####
import re
#### Process ####
import signal
import threading
@ -93,9 +96,10 @@ class lxmf_connection:
message_notification_callback = None
message_notification_success_callback = None
message_notification_failed_callback = None
config_set_callback = None
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
self.storage_path = storage_path
self.identity_file = identity_file
@ -108,6 +112,7 @@ class lxmf_connection:
self.display_name = display_name
self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay)
@ -116,6 +121,8 @@ class lxmf_connection:
else:
self.desired_method_direct = True
self.propagation_node = propagation_node
self.propagation_node_auto = propagation_node_auto
self.propagation_node_active = propagation_node_active
self.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup
@ -130,6 +137,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval)
if not self.storage_path:
log("LXMF - No storage_path parameter", LOG_ERROR)
return
if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE)
@ -184,10 +195,18 @@ class lxmf_connection:
self.destination.set_link_established_callback(self.client_connected)
self.autoselect_propagation_node()
if self.propagation_node_auto:
self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation")
RNS.Transport.register_announce_handler(self.propagation_callback)
if self.propagation_node_active:
self.propagation_node_set(self.propagation_node_active)
elif self.propagation_node:
self.propagation_node_set(self.propagation_node)
else:
self.propagation_node_set(self.propagation_node)
if self.announce_startup or self.announce_periodic:
self.announce(True)
self.announce(initial=True)
if self.sync_startup or self.sync_periodic:
self.sync(True)
@ -214,6 +233,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function
def register_config_set_callback(self, handler_function):
self.config_set_callback = handler_function
def destination_hash(self):
return self.destination.hash
@ -257,7 +280,7 @@ class lxmf_connection:
return ""
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""):
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None):
if type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1]
@ -272,8 +295,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR)
return
if destination_name == None:
destination_name = self.destination_name
if destination_type == None:
destination_type = self.destination_type
destination_identity = RNS.Identity.recall(destination)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type)
self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -342,7 +370,7 @@ class lxmf_connection:
message.desired_method_str = "propagated"
def announce(self, initial=False):
def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0:
@ -359,26 +387,29 @@ class lxmf_connection:
announce_timer.daemon = True
announce_timer.start()
else:
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
return
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None):
if app_data:
def announce_now(self, app_data=None, attached_interface=None):
if self.announce_hidden:
self.destination.announce("".encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG)
elif app_data != None:
if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8"))
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG)
self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else:
self.destination.announce(app_data)
self.destination.announce(app_data, attached_interface=attached_interface)
log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data:
if isinstance(self.announce_data, str):
self.destination.announce(self.announce_data.encode("utf-8"))
self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else:
self.destination.announce(self.announce_data)
self.destination.announce(self.announce_data, attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else:
self.destination.announce()
@ -420,24 +451,50 @@ class lxmf_connection:
return False
def autoselect_propagation_node(self):
if self.propagation_node is not None:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
def propagation_node_set(self, dest_str):
if not dest_str:
return False
if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
log("LXMF - Propagation node length is invalid", LOG_ERROR)
else:
return False
try:
propagation_hash = bytes.fromhex(self.propagation_node)
dest_hash = bytes.fromhex(dest_str)
except Exception as e:
log("LXMF - Propagation node is invalid", LOG_ERROR)
return
return False
node_identity = RNS.Identity.recall(propagation_hash)
node_identity = RNS.Identity.recall(dest_hash)
if node_identity != None:
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash)
log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO)
dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(dest_hash)
self.propagation_node_active = dest_str
return True
else:
log("LXMF - Propagation node identity not known", LOG_ERROR)
return False
def propagation_node_update(self, dest_str):
if self.propagation_node_hash_str() != dest_str:
if self.propagation_node_set(dest_str) and self.config_set_callback is not None:
self.config_set_callback("propagation_node_active", dest_str)
def propagation_node_hash(self):
try:
return bytes.fromhex(self.propagation_node_active)
except:
return None
def propagation_node_hash_str(self):
if self.propagation_node_active:
return self.propagation_node_active
else:
return ""
def client_connected(self, link):
@ -525,6 +582,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG)
class lxmf_connection_propagation():
def __init__(self, owner, aspect_filter=None):
self.owner = owner
self.aspect_filter = aspect_filter
EMITTED_DELTA_GRACE = 300
EMITTED_DELTA_IGNORE = 10
def received_announce(self, destination_hash, announced_identity, app_data):
if app_data == None:
return
if len(app_data) == 0:
return
try:
unpacked = umsgpack.unpackb(app_data)
node_active = unpacked[0]
emitted = unpacked[1]
hop_count = RNS.Transport.hops_to(destination_hash)
age = time.time() - emitted
if age < 0:
if age < -1*PropDetector.EMITTED_DELTA_GRACE:
return
log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO)
if self.owner.propagation_node_active == None:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
else:
prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash())
if hop_count <= prev_hop_count:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
except:
return
##############################################################################################################
# LXMF Functions
@ -701,6 +795,36 @@ def config_getoption(config, section, key, default=False, lng_key=""):
#### Config - Set #####
def config_set(key=None, value=""):
global PATH
try:
file = PATH + "/config.cfg.owr"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
file = PATH + "/config.cfg"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
except:
pass
#### Config - Read #####
def config_read(file=None, file_override=None):
global CONFIG
@ -794,15 +918,15 @@ def config_default(file=None, file_override=None):
# Value convert
def val_to_bool(val):
def val_to_bool(val, fallback_true=True, fallback_false=False):
if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up":
return True
elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down":
return False
elif val != "":
return True
return fallback_true
else:
return False
return fallback_false
##############################################################################################################
@ -967,6 +1091,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
else:
config_propagation_node = None
if CONFIG.has_option("lxmf", "propagation_node_active"):
config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"]
else:
config_propagation_node_active = None
if path is None:
path = PATH
@ -975,9 +1104,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
destination_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node,
propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"),
propagation_node_active=config_propagation_node_active,
try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"],
@ -991,6 +1123,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG)
@ -1085,7 +1218,13 @@ display_name = CMD
desired_method = direct #direct/propagated
# Propagation node address/hash.
#propagation_node =
propagation_node =
# Set propagation node automatically.
propagation_node_auto = True
# Current propagation node (Automatically set by the software).
propagation_node_active =
# Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible.
@ -1101,6 +1240,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = No
announce_periodic_interval = 360 #Minutes
# The announce is hidden for client applications
# but is still used for the routing tables.
announce_hidden = No
# Some waiting time after message send
# for LXMF/Reticulum processing.
send_delay = 0 #Seconds

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 ####
import string
#### Regex ####
import re
#### Process ####
import signal
import threading
@ -91,9 +94,10 @@ class lxmf_connection:
message_notification_callback = None
message_notification_success_callback = None
message_notification_failed_callback = None
config_set_callback = None
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
self.storage_path = storage_path
self.identity_file = identity_file
@ -106,6 +110,7 @@ class lxmf_connection:
self.display_name = display_name
self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay)
@ -114,6 +119,8 @@ class lxmf_connection:
else:
self.desired_method_direct = True
self.propagation_node = propagation_node
self.propagation_node_auto = propagation_node_auto
self.propagation_node_active = propagation_node_active
self.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup
@ -128,6 +135,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval)
if not self.storage_path:
log("LXMF - No storage_path parameter", LOG_ERROR)
return
if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE)
@ -182,10 +193,18 @@ class lxmf_connection:
self.destination.set_link_established_callback(self.client_connected)
self.autoselect_propagation_node()
if self.propagation_node_auto:
self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation")
RNS.Transport.register_announce_handler(self.propagation_callback)
if self.propagation_node_active:
self.propagation_node_set(self.propagation_node_active)
elif self.propagation_node:
self.propagation_node_set(self.propagation_node)
else:
self.propagation_node_set(self.propagation_node)
if self.announce_startup or self.announce_periodic:
self.announce(True)
self.announce(initial=True)
if self.sync_startup or self.sync_periodic:
self.sync(True)
@ -212,6 +231,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function
def register_config_set_callback(self, handler_function):
self.config_set_callback = handler_function
def destination_hash(self):
return self.destination.hash
@ -255,7 +278,7 @@ class lxmf_connection:
return ""
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""):
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None):
if type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1]
@ -270,8 +293,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR)
return
if destination_name == None:
destination_name = self.destination_name
if destination_type == None:
destination_type = self.destination_type
destination_identity = RNS.Identity.recall(destination)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type)
self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -340,7 +368,7 @@ class lxmf_connection:
message.desired_method_str = "propagated"
def announce(self, initial=False):
def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0:
@ -357,26 +385,29 @@ class lxmf_connection:
announce_timer.daemon = True
announce_timer.start()
else:
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
return
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None):
if app_data:
def announce_now(self, app_data=None, attached_interface=None):
if self.announce_hidden:
self.destination.announce("".encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG)
elif app_data != None:
if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8"))
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG)
self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else:
self.destination.announce(app_data)
self.destination.announce(app_data, attached_interface=attached_interface)
log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data:
if isinstance(self.announce_data, str):
self.destination.announce(self.announce_data.encode("utf-8"))
self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else:
self.destination.announce(self.announce_data)
self.destination.announce(self.announce_data, attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else:
self.destination.announce()
@ -418,24 +449,50 @@ class lxmf_connection:
return False
def autoselect_propagation_node(self):
if self.propagation_node is not None:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
def propagation_node_set(self, dest_str):
if not dest_str:
return False
if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
log("LXMF - Propagation node length is invalid", LOG_ERROR)
else:
return False
try:
propagation_hash = bytes.fromhex(self.propagation_node)
dest_hash = bytes.fromhex(dest_str)
except Exception as e:
log("LXMF - Propagation node is invalid", LOG_ERROR)
return
return False
node_identity = RNS.Identity.recall(propagation_hash)
node_identity = RNS.Identity.recall(dest_hash)
if node_identity != None:
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash)
log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO)
dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(dest_hash)
self.propagation_node_active = dest_str
return True
else:
log("LXMF - Propagation node identity not known", LOG_ERROR)
return False
def propagation_node_update(self, dest_str):
if self.propagation_node_hash_str() != dest_str:
if self.propagation_node_set(dest_str) and self.config_set_callback is not None:
self.config_set_callback("propagation_node_active", dest_str)
def propagation_node_hash(self):
try:
return bytes.fromhex(self.propagation_node_active)
except:
return None
def propagation_node_hash_str(self):
if self.propagation_node_active:
return self.propagation_node_active
else:
return ""
def client_connected(self, link):
@ -523,6 +580,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG)
class lxmf_connection_propagation():
def __init__(self, owner, aspect_filter=None):
self.owner = owner
self.aspect_filter = aspect_filter
EMITTED_DELTA_GRACE = 300
EMITTED_DELTA_IGNORE = 10
def received_announce(self, destination_hash, announced_identity, app_data):
if app_data == None:
return
if len(app_data) == 0:
return
try:
unpacked = umsgpack.unpackb(app_data)
node_active = unpacked[0]
emitted = unpacked[1]
hop_count = RNS.Transport.hops_to(destination_hash)
age = time.time() - emitted
if age < 0:
if age < -1*PropDetector.EMITTED_DELTA_GRACE:
return
log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO)
if self.owner.propagation_node_active == None:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
else:
prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash())
if hop_count <= prev_hop_count:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
except:
return
##############################################################################################################
# LXMF Functions
@ -704,6 +798,36 @@ def config_getoption(config, section, key, default=False, lng_key=""):
#### Config - Set #####
def config_set(key=None, value=""):
global PATH
try:
file = PATH + "/config.cfg.owr"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
file = PATH + "/config.cfg"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
except:
pass
#### Config - Read #####
def config_read(file=None, file_override=None):
global CONFIG
@ -887,15 +1011,15 @@ def data_default(file=None):
# Value convert
def val_to_bool(val):
def val_to_bool(val, fallback_true=True, fallback_false=False):
if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up":
return True
elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down":
return False
elif val != "":
return True
return fallback_true
else:
return False
return fallback_false
##############################################################################################################
@ -1065,6 +1189,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
else:
config_propagation_node = None
if CONFIG.has_option("lxmf", "propagation_node_active"):
config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"]
else:
config_propagation_node_active = None
if path is None:
path = PATH
@ -1073,9 +1202,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
destination_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node,
propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"),
propagation_node_active=config_propagation_node_active,
try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"],
@ -1089,6 +1221,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG)
@ -1162,8 +1295,8 @@ DEFAULT_CONFIG_OVERRIDE = '''# This is the user configuration file to override t
# It is also used in the group description/info.
display_name = Distribution Group
# Propagation node address/hash.
propagation_node = ca2762fe5283873719aececfb9e18835
# Set propagation node automatically.
propagation_node_auto = True
# Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible.
@ -1205,7 +1338,13 @@ display_name = Distribution Group
desired_method = direct #direct/propagated
# Propagation node address/hash.
propagation_node = ca2762fe5283873719aececfb9e18835
propagation_node =
# Set propagation node automatically.
propagation_node_auto = True
# Current propagation node (Automatically set by the software).
propagation_node_active =
# Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible.
@ -1221,6 +1360,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = Yes
announce_periodic_interval = 120 #Minutes
# The announce is hidden for client applications
# but is still used for the routing tables.
announce_hidden = No
# Some waiting time after message send
# for LXMF/Reticulum processing.
send_delay = 0 #Seconds

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 ####
import string
#### Regex ####
import re
#### Process ####
import signal
import threading
@ -89,9 +92,10 @@ class lxmf_connection:
message_notification_callback = None
message_notification_success_callback = None
message_notification_failed_callback = None
config_set_callback = None
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
self.storage_path = storage_path
self.identity_file = identity_file
@ -104,6 +108,7 @@ class lxmf_connection:
self.display_name = display_name
self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay)
@ -112,6 +117,8 @@ class lxmf_connection:
else:
self.desired_method_direct = True
self.propagation_node = propagation_node
self.propagation_node_auto = propagation_node_auto
self.propagation_node_active = propagation_node_active
self.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup
@ -126,6 +133,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval)
if not self.storage_path:
log("LXMF - No storage_path parameter", LOG_ERROR)
return
if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE)
@ -180,10 +191,18 @@ class lxmf_connection:
self.destination.set_link_established_callback(self.client_connected)
self.autoselect_propagation_node()
if self.propagation_node_auto:
self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation")
RNS.Transport.register_announce_handler(self.propagation_callback)
if self.propagation_node_active:
self.propagation_node_set(self.propagation_node_active)
elif self.propagation_node:
self.propagation_node_set(self.propagation_node)
else:
self.propagation_node_set(self.propagation_node)
if self.announce_startup or self.announce_periodic:
self.announce(True)
self.announce(initial=True)
if self.sync_startup or self.sync_periodic:
self.sync(True)
@ -210,6 +229,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function
def register_config_set_callback(self, handler_function):
self.config_set_callback = handler_function
def destination_hash(self):
return self.destination.hash
@ -253,7 +276,7 @@ class lxmf_connection:
return ""
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""):
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None):
if type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1]
@ -268,8 +291,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR)
return
if destination_name == None:
destination_name = self.destination_name
if destination_type == None:
destination_type = self.destination_type
destination_identity = RNS.Identity.recall(destination)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type)
self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -338,7 +366,7 @@ class lxmf_connection:
message.desired_method_str = "propagated"
def announce(self, initial=False):
def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0:
@ -355,26 +383,29 @@ class lxmf_connection:
announce_timer.daemon = True
announce_timer.start()
else:
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
return
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None):
if app_data:
def announce_now(self, app_data=None, attached_interface=None):
if self.announce_hidden:
self.destination.announce("".encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG)
elif app_data != None:
if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8"))
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG)
self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else:
self.destination.announce(app_data)
self.destination.announce(app_data, attached_interface=attached_interface)
log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data:
if isinstance(self.announce_data, str):
self.destination.announce(self.announce_data.encode("utf-8"))
self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else:
self.destination.announce(self.announce_data)
self.destination.announce(self.announce_data, attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else:
self.destination.announce()
@ -416,24 +447,50 @@ class lxmf_connection:
return False
def autoselect_propagation_node(self):
if self.propagation_node is not None:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
def propagation_node_set(self, dest_str):
if not dest_str:
return False
if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
log("LXMF - Propagation node length is invalid", LOG_ERROR)
else:
return False
try:
propagation_hash = bytes.fromhex(self.propagation_node)
dest_hash = bytes.fromhex(dest_str)
except Exception as e:
log("LXMF - Propagation node is invalid", LOG_ERROR)
return
return False
node_identity = RNS.Identity.recall(propagation_hash)
node_identity = RNS.Identity.recall(dest_hash)
if node_identity != None:
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash)
log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO)
dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(dest_hash)
self.propagation_node_active = dest_str
return True
else:
log("LXMF - Propagation node identity not known", LOG_ERROR)
return False
def propagation_node_update(self, dest_str):
if self.propagation_node_hash_str() != dest_str:
if self.propagation_node_set(dest_str) and self.config_set_callback is not None:
self.config_set_callback("propagation_node_active", dest_str)
def propagation_node_hash(self):
try:
return bytes.fromhex(self.propagation_node_active)
except:
return None
def propagation_node_hash_str(self):
if self.propagation_node_active:
return self.propagation_node_active
else:
return ""
def client_connected(self, link):
@ -521,6 +578,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG)
class lxmf_connection_propagation():
def __init__(self, owner, aspect_filter=None):
self.owner = owner
self.aspect_filter = aspect_filter
EMITTED_DELTA_GRACE = 300
EMITTED_DELTA_IGNORE = 10
def received_announce(self, destination_hash, announced_identity, app_data):
if app_data == None:
return
if len(app_data) == 0:
return
try:
unpacked = umsgpack.unpackb(app_data)
node_active = unpacked[0]
emitted = unpacked[1]
hop_count = RNS.Transport.hops_to(destination_hash)
age = time.time() - emitted
if age < 0:
if age < -1*PropDetector.EMITTED_DELTA_GRACE:
return
log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO)
if self.owner.propagation_node_active == None:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
else:
prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash())
if hop_count <= prev_hop_count:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
except:
return
##############################################################################################################
# LXMF Functions
@ -669,6 +763,36 @@ def config_getoption(config, section, key, default=False, lng_key=""):
#### Config - Set #####
def config_set(key=None, value=""):
global PATH
try:
file = PATH + "/config.cfg.owr"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
file = PATH + "/config.cfg"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
except:
pass
#### Config - Read #####
def config_read(file=None, file_override=None):
global CONFIG
@ -762,15 +886,15 @@ def config_default(file=None, file_override=None):
# Value convert
def val_to_bool(val):
def val_to_bool(val, fallback_true=True, fallback_false=False):
if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up":
return True
elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down":
return False
elif val != "":
return True
return fallback_true
else:
return False
return fallback_false
##############################################################################################################
@ -935,6 +1059,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
else:
config_propagation_node = None
if CONFIG.has_option("lxmf", "propagation_node_active"):
config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"]
else:
config_propagation_node_active = None
if path is None:
path = PATH
@ -943,9 +1072,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
destination_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node,
propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"),
propagation_node_active=config_propagation_node_active,
try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"],
@ -959,6 +1091,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG)
@ -1053,7 +1186,13 @@ display_name = Echo Test
desired_method = direct #direct/propagated
# Propagation node address/hash.
#propagation_node =
propagation_node =
# Set propagation node automatically.
propagation_node_auto = True
# Current propagation node (Automatically set by the software).
propagation_node_active =
# Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible.
@ -1069,6 +1208,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = Yes
announce_periodic_interval = 360 #Minutes
# The announce is hidden for client applications
# but is still used for the routing tables.
announce_hidden = No
# Some waiting time after message send
# for LXMF/Reticulum processing.
send_delay = 0 #Seconds

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 ####
import string
#### Regex ####
import re
#### Other ####
import random
import secrets
@ -90,9 +93,10 @@ class lxmf_connection:
message_notification_callback = None
message_notification_success_callback = None
message_notification_failed_callback = None
config_set_callback = None
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
self.storage_path = storage_path
self.identity_file = identity_file
@ -105,6 +109,7 @@ class lxmf_connection:
self.display_name = display_name
self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay)
@ -113,6 +118,8 @@ class lxmf_connection:
else:
self.desired_method_direct = True
self.propagation_node = propagation_node
self.propagation_node_auto = propagation_node_auto
self.propagation_node_active = propagation_node_active
self.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup
@ -127,6 +134,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval)
if not self.storage_path:
log("LXMF - No storage_path parameter", LOG_ERROR)
return
if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE)
@ -181,10 +192,18 @@ class lxmf_connection:
self.destination.set_link_established_callback(self.client_connected)
self.autoselect_propagation_node()
if self.propagation_node_auto:
self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation")
RNS.Transport.register_announce_handler(self.propagation_callback)
if self.propagation_node_active:
self.propagation_node_set(self.propagation_node_active)
elif self.propagation_node:
self.propagation_node_set(self.propagation_node)
else:
self.propagation_node_set(self.propagation_node)
if self.announce_startup or self.announce_periodic:
self.announce(True)
self.announce(initial=True)
if self.sync_startup or self.sync_periodic:
self.sync(True)
@ -211,6 +230,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function
def register_config_set_callback(self, handler_function):
self.config_set_callback = handler_function
def destination_hash(self):
return self.destination.hash
@ -254,7 +277,7 @@ class lxmf_connection:
return ""
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""):
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None):
if type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1]
@ -269,8 +292,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR)
return
if destination_name == None:
destination_name = self.destination_name
if destination_type == None:
destination_type = self.destination_type
destination_identity = RNS.Identity.recall(destination)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type)
self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -339,7 +367,7 @@ class lxmf_connection:
message.desired_method_str = "propagated"
def announce(self, initial=False):
def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0:
@ -356,26 +384,29 @@ class lxmf_connection:
announce_timer.daemon = True
announce_timer.start()
else:
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
return
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None):
if app_data:
def announce_now(self, app_data=None, attached_interface=None):
if self.announce_hidden:
self.destination.announce("".encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG)
elif app_data != None:
if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8"))
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG)
self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else:
self.destination.announce(app_data)
self.destination.announce(app_data, attached_interface=attached_interface)
log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data:
if isinstance(self.announce_data, str):
self.destination.announce(self.announce_data.encode("utf-8"))
self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else:
self.destination.announce(self.announce_data)
self.destination.announce(self.announce_data, attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else:
self.destination.announce()
@ -417,24 +448,50 @@ class lxmf_connection:
return False
def autoselect_propagation_node(self):
if self.propagation_node is not None:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
def propagation_node_set(self, dest_str):
if not dest_str:
return False
if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
log("LXMF - Propagation node length is invalid", LOG_ERROR)
else:
return False
try:
propagation_hash = bytes.fromhex(self.propagation_node)
dest_hash = bytes.fromhex(dest_str)
except Exception as e:
log("LXMF - Propagation node is invalid", LOG_ERROR)
return
return False
node_identity = RNS.Identity.recall(propagation_hash)
node_identity = RNS.Identity.recall(dest_hash)
if node_identity != None:
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash)
log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO)
dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(dest_hash)
self.propagation_node_active = dest_str
return True
else:
log("LXMF - Propagation node identity not known", LOG_ERROR)
return False
def propagation_node_update(self, dest_str):
if self.propagation_node_hash_str() != dest_str:
if self.propagation_node_set(dest_str) and self.config_set_callback is not None:
self.config_set_callback("propagation_node_active", dest_str)
def propagation_node_hash(self):
try:
return bytes.fromhex(self.propagation_node_active)
except:
return None
def propagation_node_hash_str(self):
if self.propagation_node_active:
return self.propagation_node_active
else:
return ""
def client_connected(self, link):
@ -522,6 +579,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG)
class lxmf_connection_propagation():
def __init__(self, owner, aspect_filter=None):
self.owner = owner
self.aspect_filter = aspect_filter
EMITTED_DELTA_GRACE = 300
EMITTED_DELTA_IGNORE = 10
def received_announce(self, destination_hash, announced_identity, app_data):
if app_data == None:
return
if len(app_data) == 0:
return
try:
unpacked = umsgpack.unpackb(app_data)
node_active = unpacked[0]
emitted = unpacked[1]
hop_count = RNS.Transport.hops_to(destination_hash)
age = time.time() - emitted
if age < 0:
if age < -1*PropDetector.EMITTED_DELTA_GRACE:
return
log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO)
if self.owner.propagation_node_active == None:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
else:
prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash())
if hop_count <= prev_hop_count:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
except:
return
##############################################################################################################
# LXMF Functions
@ -575,15 +669,15 @@ def lxmf_failed(message):
# Value convert
def val_to_bool(val):
def val_to_bool(val, fallback_true=True, fallback_false=False):
if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up":
return True
elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down":
return False
elif val != "":
return True
return fallback_true
else:
return False
return fallback_false
##############################################################################################################

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 ####
import string
#### Regex ####
import re
#### UID ####
import uuid
#### ####
import base64
#### Process ####
import signal
import threading
@ -81,7 +90,10 @@ PATH_RNS = None
#### Global Variables - System (Not changeable) ####
CACHE = []
CACHE = {}
CACHE["in"] = {}
CACHE["out"] = {}
CACHE_CHANGE = False
CONFIG = None
RNS_CONNECTION = None
LXMF_CONNECTION = None
@ -97,9 +109,10 @@ class lxmf_connection:
message_notification_callback = None
message_notification_success_callback = None
message_notification_failed_callback = None
config_set_callback = None
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
self.storage_path = storage_path
self.identity_file = identity_file
@ -112,6 +125,7 @@ class lxmf_connection:
self.display_name = display_name
self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay)
@ -120,6 +134,8 @@ class lxmf_connection:
else:
self.desired_method_direct = True
self.propagation_node = propagation_node
self.propagation_node_auto = propagation_node_auto
self.propagation_node_active = propagation_node_active
self.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup
@ -134,6 +150,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval)
if not self.storage_path:
log("LXMF - No storage_path parameter", LOG_ERROR)
return
if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE)
@ -188,10 +208,18 @@ class lxmf_connection:
self.destination.set_link_established_callback(self.client_connected)
self.autoselect_propagation_node()
if self.propagation_node_auto:
self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation")
RNS.Transport.register_announce_handler(self.propagation_callback)
if self.propagation_node_active:
self.propagation_node_set(self.propagation_node_active)
elif self.propagation_node:
self.propagation_node_set(self.propagation_node)
else:
self.propagation_node_set(self.propagation_node)
if self.announce_startup or self.announce_periodic:
self.announce(True)
self.announce(initial=True)
if self.sync_startup or self.sync_periodic:
self.sync(True)
@ -218,6 +246,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function
def register_config_set_callback(self, handler_function):
self.config_set_callback = handler_function
def destination_hash(self):
return self.destination.hash
@ -261,7 +293,7 @@ class lxmf_connection:
return ""
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""):
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None):
if type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1]
@ -276,8 +308,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR)
return
if destination_name == None:
destination_name = self.destination_name
if destination_type == None:
destination_type = self.destination_type
destination_identity = RNS.Identity.recall(destination)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type)
self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -346,7 +383,7 @@ class lxmf_connection:
message.desired_method_str = "propagated"
def announce(self, initial=False):
def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0:
@ -363,26 +400,29 @@ class lxmf_connection:
announce_timer.daemon = True
announce_timer.start()
else:
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
return
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None):
if app_data:
def announce_now(self, app_data=None, attached_interface=None):
if self.announce_hidden:
self.destination.announce("".encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG)
elif app_data != None:
if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8"))
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG)
self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else:
self.destination.announce(app_data)
self.destination.announce(app_data, attached_interface=attached_interface)
log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data:
if isinstance(self.announce_data, str):
self.destination.announce(self.announce_data.encode("utf-8"))
self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else:
self.destination.announce(self.announce_data)
self.destination.announce(self.announce_data, attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else:
self.destination.announce()
@ -424,24 +464,50 @@ class lxmf_connection:
return False
def autoselect_propagation_node(self):
if self.propagation_node is not None:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
def propagation_node_set(self, dest_str):
if not dest_str:
return False
if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
log("LXMF - Propagation node length is invalid", LOG_ERROR)
else:
return False
try:
propagation_hash = bytes.fromhex(self.propagation_node)
dest_hash = bytes.fromhex(dest_str)
except Exception as e:
log("LXMF - Propagation node is invalid", LOG_ERROR)
return
return False
node_identity = RNS.Identity.recall(propagation_hash)
node_identity = RNS.Identity.recall(dest_hash)
if node_identity != None:
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash)
log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO)
dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(dest_hash)
self.propagation_node_active = dest_str
return True
else:
log("LXMF - Propagation node identity not known", LOG_ERROR)
return False
def propagation_node_update(self, dest_str):
if self.propagation_node_hash_str() != dest_str:
if self.propagation_node_set(dest_str) and self.config_set_callback is not None:
self.config_set_callback("propagation_node_active", dest_str)
def propagation_node_hash(self):
try:
return bytes.fromhex(self.propagation_node_active)
except:
return None
def propagation_node_hash_str(self):
if self.propagation_node_active:
return self.propagation_node_active
else:
return ""
def client_connected(self, link):
@ -529,6 +595,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG)
class lxmf_connection_propagation():
def __init__(self, owner, aspect_filter=None):
self.owner = owner
self.aspect_filter = aspect_filter
EMITTED_DELTA_GRACE = 300
EMITTED_DELTA_IGNORE = 10
def received_announce(self, destination_hash, announced_identity, app_data):
if app_data == None:
return
if len(app_data) == 0:
return
try:
unpacked = umsgpack.unpackb(app_data)
node_active = unpacked[0]
emitted = unpacked[1]
hop_count = RNS.Transport.hops_to(destination_hash)
age = time.time() - emitted
if age < 0:
if age < -1*PropDetector.EMITTED_DELTA_GRACE:
return
log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO)
if self.owner.propagation_node_active == None:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
else:
prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash())
if hop_count <= prev_hop_count:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
except:
return
##############################################################################################################
# LXMF Functions
@ -549,7 +652,7 @@ class lxmf_announce_callback:
#### LXMF - Message ####
def lxmf_message_received_callback(message):
global CACHE
global CACHE, CACHE_CHANGE
if CONFIG["lxmf"].getboolean("signature_validated") and not message.signature_validated:
log("LXMF - Source " + RNS.prettyhexrep(message.source_hash) + " have no valid signature", LOG_DEBUG)
@ -558,34 +661,341 @@ def lxmf_message_received_callback(message):
if not message.fields:
return
if not "registration_request" in message.fields and not "telemetry" in message.fields:
return
hash_destination = RNS.hexrep(message.source_hash, delimit=False)
hash_identity = ""
#hash_identity = RNS.Identity.recall(message.source_hash)
#if hash_identity != None:
# hash_identity = RNS.hexrep(hash_identity, delimit=False)
#else:
# hash_identity = ""
for key in message.fields:
try:
data = message.fields[key]
if not isinstance(data, dict):
continue
if "type" not in data:
continue
if data["type"] == "":
continue
data["hash_destination"] = hash_destination
data["hash_identity"] = hash_identity
data["timestamp_client"] = message.timestamp
data["timestamp_server"] = time.time()
if "password" in data:
data["password"] = str(base64.b32encode(data["password"]))
CACHE["in"][str(uuid.uuid4())] = data
CACHE_CHANGE = True
except:
pass
#### LXMF - Notification ####
def lxmf_message_notification_success_callback(message):
global CACHE, CACHE_CHANGE
key = message.app_data
if key in CACHE["out"]:
del CACHE["out"][key]
CACHE_CHANGE = True
#### Jobs ####
def jobs_in():
global CACHE, CACHE_CHANGE
while True:
time.sleep(CONFIG["processing"].getint("interval_in"))
log("Jobs - Loop/Execute", LOG_DEBUG)
if len(CACHE["in"]) > 0:
log("Cache - Available -> Execute", LOG_DEBUG)
CACHE_DEL = []
db = None
try:
db = psycopg2.connect(user=CONFIG["database"]["user"], password=CONFIG["database"]["password"], host=CONFIG["database"]["host"], port=CONFIG["database"]["port"], database=CONFIG["database"]["database"])
dbc = db.cursor()
if CONFIG["features"].getboolean("registration") and "registration_request" in message.fields:
dbc.execute("INSERT INTO "+CONFIG["database"]["table_registration"]+" (hash, data) VALUES(%s, %s)", (
RNS.hexrep(message.source_hash, delimit=False),
umsgpack.packb(message.fields["registration_request"]))
)
for key in CACHE["in"]:
try:
log("-> Execute", LOG_EXTREME)
log(CACHE["in"][key], LOG_EXTREME)
if CONFIG["features"].getboolean("telemetry") and "telemetry" in message.fields:
dbc.execute("INSERT INTO "+CONFIG["database"]["table_telemetry"]+" (hash, data) VALUES(%s, %s)", (
RNS.hexrep(message.source_hash, delimit=False),
umsgpack.packb(message.fields["telemetry"]))
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
##############################################################################################################
# Config
@ -644,6 +1054,36 @@ def config_getoption(config, section, key, default=False, lng_key=""):
#### Config - Set #####
def config_set(key=None, value=""):
global PATH
try:
file = PATH + "/config.cfg.owr"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
file = PATH + "/config.cfg"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
except:
pass
#### Config - Read #####
def config_read(file=None, file_override=None):
global CONFIG
@ -737,15 +1177,15 @@ def config_default(file=None, file_override=None):
# Value convert
def val_to_bool(val):
def val_to_bool(val, fallback_true=True, fallback_false=False):
if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up":
return True
elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down":
return False
elif val != "":
return True
return fallback_true
else:
return False
return fallback_false
##############################################################################################################
@ -881,6 +1321,10 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
print("Config - Error reading config file " + PATH + "/config.cfg")
panic()
if not cache_read(PATH + "/cache.data"):
print("Cache - Error reading cache file " + PATH + "/cache.data")
panic()
if CONFIG["main"].getboolean("default_config"):
print("Exit!")
print("First start with the default config!")
@ -910,6 +1354,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
else:
config_propagation_node = None
if CONFIG.has_option("lxmf", "propagation_node_active"):
config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"]
else:
config_propagation_node_active = None
if path is None:
path = PATH
@ -925,10 +1374,13 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
destination_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
announce_data = umsgpack.packb(announce_data),
send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node,
propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"),
propagation_node_active=config_propagation_node_active,
try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"],
@ -942,6 +1394,8 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_message_notification_success_callback(lxmf_message_notification_success_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG)
@ -949,6 +1403,15 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
log("LXMF - Address: " + RNS.prettyhexrep(LXMF_CONNECTION.destination_hash()), LOG_FORCE)
log("...............................................................................", LOG_FORCE)
log("...............................................................................", LOG_EXTREME)
log(CACHE, LOG_EXTREME)
log("...............................................................................", LOG_EXTREME)
jobs_in_thread = threading.Thread(target=jobs_in, daemon=True)
jobs_in_thread.start()
jobs_out_thread = threading.Thread(target=jobs_out, daemon=True)
jobs_out_thread.start()
while True:
time.sleep(1)
@ -1007,9 +1470,16 @@ announce_periodic_interval = 15 #Minutes
[features]
announce_versions = True
registration = True
account_add = True
account_edit = True
account_del = True
account_prove = True
telemetry = False
[processing]
interval_in = 5 #Seconds
interval_out = 60 #Seconds
[data]
v_s = 0.0.0 #Version software
v_c = 2022-01-01 00:00 #Version config
@ -1017,6 +1487,7 @@ v_d = 2022-01-01 00:00 #Version data
v_a = 2022-01-01 00:00 #Version auth
u_s = #URL Software
i_s = #Info Software
cmd = #CMD
'''
@ -1045,6 +1516,7 @@ name = LXMF Provisioning Server
# to be compatibel with other LXMF programs.
destination_name = lxmf
destination_type = provisioning
destination_type_conv = 11
# The name will be visible to other peers
# on the network, and included in announces.
@ -1054,11 +1526,17 @@ display_name = LXMF Provisioning Server
desired_method = direct #direct/propagated
# Propagation node address/hash.
#propagation_node =
propagation_node =
# Set propagation node automatically.
propagation_node_auto = True
# Current propagation node (Automatically set by the software).
propagation_node_active =
# Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible.
try_propagation_on_fail = No
try_propagation_on_fail = Yes
# The peer is announced at startup
# to let other peers reach it immediately.
@ -1070,6 +1548,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = Yes
announce_periodic_interval = 360 #Minutes
# The announce is hidden for client applications
# but is still used for the routing tables.
announce_hidden = No
# Some waiting time after message send
# for LXMF/Reticulum processing.
send_delay = 0 #Seconds
@ -1091,7 +1573,7 @@ sync_periodic_interval = 360 #Minutes
sync_limit = 8
# Allow only messages with valid signature.
signature_validated = Yes
signature_validated = No
@ -1104,8 +1586,6 @@ port = 5432
user = postgres
password = password
database = database
table_registration = tbl_account
table_telemetry = tbl_telemetry
@ -1114,12 +1594,37 @@ table_telemetry = tbl_telemetry
[features]
announce_versions = True
registration = True
account_add = True
account_add_auth = False
account_add_auth_state = 1
account_add_auth_role = 3
account_edit = True
account_edit_auth = False
account_edit_auth_state = 1
account_edit_auth_role = 3
account_del = True
account_prove = True
account_prove_auth = True
account_prove_auth_state = 1
account_prove_auth_role = 3
telemetry = False
#### Processing ####
[processing]
interval_in = 5 #Seconds
interval_out = 60 #Seconds
#### Data settings ####
[data]
@ -1129,6 +1634,7 @@ v_d = 2022-01-01 00:00 #Version data
v_a = 2022-01-01 00:00 #Version auth
u_s = #URL Software
i_s = #Info Software
cmd = #CMD
'''

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_success_callback = None
message_notification_failed_callback = None
config_set_callback = None
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, send_delay=0, desired_method="direct", propagation_node=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
def __init__(self, storage_path=None, identity_file="identity", identity=None, destination_name="lxmf", destination_type="delivery", display_name="", announce_data=None, announce_hidden=False, send_delay=0, desired_method="direct", propagation_node=None, propagation_node_auto=False, propagation_node_active=None, try_propagation_on_fail=False, announce_startup=False, announce_startup_delay=0, announce_periodic=False, announce_periodic_interval=360, sync_startup=False, sync_startup_delay=0, sync_limit=8, sync_periodic=False, sync_periodic_interval=360):
self.storage_path = storage_path
self.identity_file = identity_file
@ -208,6 +209,7 @@ class lxmf_connection:
self.display_name = display_name
self.announce_data = announce_data
self.announce_hidden = announce_hidden
self.send_delay = int(send_delay)
@ -216,6 +218,8 @@ class lxmf_connection:
else:
self.desired_method_direct = True
self.propagation_node = propagation_node
self.propagation_node_auto = propagation_node_auto
self.propagation_node_active = propagation_node_active
self.try_propagation_on_fail = try_propagation_on_fail
self.announce_startup = announce_startup
@ -230,6 +234,10 @@ class lxmf_connection:
self.sync_periodic = sync_periodic
self.sync_periodic_interval = int(sync_periodic_interval)
if not self.storage_path:
log("LXMF - No storage_path parameter", LOG_ERROR)
return
if not os.path.isdir(self.storage_path):
os.makedirs(self.storage_path)
log("LXMF - Storage path was created", LOG_NOTICE)
@ -284,10 +292,18 @@ class lxmf_connection:
self.destination.set_link_established_callback(self.client_connected)
self.autoselect_propagation_node()
if self.propagation_node_auto:
self.propagation_callback = lxmf_connection_propagation(self, "lxmf.propagation")
RNS.Transport.register_announce_handler(self.propagation_callback)
if self.propagation_node_active:
self.propagation_node_set(self.propagation_node_active)
elif self.propagation_node:
self.propagation_node_set(self.propagation_node)
else:
self.propagation_node_set(self.propagation_node)
if self.announce_startup or self.announce_periodic:
self.announce(True)
self.announce(initial=True)
if self.sync_startup or self.sync_periodic:
self.sync(True)
@ -314,6 +330,10 @@ class lxmf_connection:
self.message_notification_failed_callback = handler_function
def register_config_set_callback(self, handler_function):
self.config_set_callback = handler_function
def destination_hash(self):
return self.destination.hash
@ -357,7 +377,7 @@ class lxmf_connection:
return ""
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data=""):
def send(self, destination, content="", title="", fields=None, timestamp=None, app_data="", destination_name=None, destination_type=None):
if type(destination) is not bytes:
if len(destination) == ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2)+2:
destination = destination[1:-1]
@ -372,8 +392,13 @@ class lxmf_connection:
log("LXMF - Destination is invalid", LOG_ERROR)
return
if destination_name == None:
destination_name = self.destination_name
if destination_type == None:
destination_type = self.destination_type
destination_identity = RNS.Identity.recall(destination)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, self.destination_name, self.destination_type)
destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, destination_name, destination_type)
self.send_message(destination, self.destination, content, title, fields, timestamp, app_data)
@ -442,7 +467,7 @@ class lxmf_connection:
message.desired_method_str = "propagated"
def announce(self, initial=False):
def announce(self, app_data=None, attached_interface=None, initial=False):
announce_timer = None
if self.announce_periodic and self.announce_periodic_interval > 0:
@ -459,26 +484,29 @@ class lxmf_connection:
announce_timer.daemon = True
announce_timer.start()
else:
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
return
self.announce_now()
self.announce_now(app_data=app_data, attached_interface=attached_interface)
def announce_now(self, app_data=None):
if app_data:
def announce_now(self, app_data=None, attached_interface=None):
if self.announce_hidden:
self.destination.announce("".encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +" (Hidden)", LOG_DEBUG)
elif app_data != None:
if isinstance(app_data, str):
self.destination.announce(app_data.encode("utf-8"))
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + announce_data, LOG_DEBUG)
self.destination.announce(app_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + app_data, LOG_DEBUG)
else:
self.destination.announce(app_data)
self.destination.announce(app_data, attached_interface=attached_interface)
log("LMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
elif self.announce_data:
if isinstance(self.announce_data, str):
self.destination.announce(self.announce_data.encode("utf-8"))
self.destination.announce(self.announce_data.encode("utf-8"), attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()) +":" + self.announce_data, LOG_DEBUG)
else:
self.destination.announce(self.announce_data)
self.destination.announce(self.announce_data, attached_interface=attached_interface)
log("LXMF - Announced: " + RNS.prettyhexrep(self.destination_hash()), LOG_DEBUG)
else:
self.destination.announce()
@ -520,24 +548,50 @@ class lxmf_connection:
return False
def autoselect_propagation_node(self):
if self.propagation_node is not None:
if len(self.propagation_node) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
def propagation_node_set(self, dest_str):
if not dest_str:
return False
if len(dest_str) != ((RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2):
log("LXMF - Propagation node length is invalid", LOG_ERROR)
else:
return False
try:
propagation_hash = bytes.fromhex(self.propagation_node)
dest_hash = bytes.fromhex(dest_str)
except Exception as e:
log("LXMF - Propagation node is invalid", LOG_ERROR)
return
return False
node_identity = RNS.Identity.recall(propagation_hash)
node_identity = RNS.Identity.recall(dest_hash)
if node_identity != None:
log("LXMF - Propagation node: " + RNS.prettyhexrep(propagation_hash), LOG_INFO)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(propagation_hash)
log("LXMF - Propagation node: " + RNS.prettyhexrep(dest_hash), LOG_INFO)
dest_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
self.message_router.set_outbound_propagation_node(dest_hash)
self.propagation_node_active = dest_str
return True
else:
log("LXMF - Propagation node identity not known", LOG_ERROR)
return False
def propagation_node_update(self, dest_str):
if self.propagation_node_hash_str() != dest_str:
if self.propagation_node_set(dest_str) and self.config_set_callback is not None:
self.config_set_callback("propagation_node_active", dest_str)
def propagation_node_hash(self):
try:
return bytes.fromhex(self.propagation_node_active)
except:
return None
def propagation_node_hash_str(self):
if self.propagation_node_active:
return self.propagation_node_active
else:
return ""
def client_connected(self, link):
@ -625,6 +679,43 @@ class lxmf_connection:
log("- App Data: " + message.app_data, LOG_DEBUG)
class lxmf_connection_propagation():
def __init__(self, owner, aspect_filter=None):
self.owner = owner
self.aspect_filter = aspect_filter
EMITTED_DELTA_GRACE = 300
EMITTED_DELTA_IGNORE = 10
def received_announce(self, destination_hash, announced_identity, app_data):
if app_data == None:
return
if len(app_data) == 0:
return
try:
unpacked = umsgpack.unpackb(app_data)
node_active = unpacked[0]
emitted = unpacked[1]
hop_count = RNS.Transport.hops_to(destination_hash)
age = time.time() - emitted
if age < 0:
if age < -1*PropDetector.EMITTED_DELTA_GRACE:
return
log("LXMF - Received an propagation node announce from "+RNS.prettyhexrep(destination_hash)+": "+str(age)+" seconds ago, "+str(hop_count)+" hops away", LOG_INFO)
if self.owner.propagation_node_active == None:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
else:
prev_hop_count = RNS.Transport.hops_to(self.owner.propagation_node_hash())
if hop_count <= prev_hop_count:
self.owner.propagation_node_update(RNS.hexrep(destination_hash, False))
except:
return
##############################################################################################################
# LXMF Functions
@ -839,6 +930,36 @@ def config_getoption(config, section, key, default=False, lng_key=""):
#### Config - Set #####
def config_set(key=None, value=""):
global PATH
try:
file = PATH + "/config.cfg.owr"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
file = PATH + "/config.cfg"
if os.path.isfile(file):
fh = open(file,'r')
data = fh.read()
fh.close()
data = re.sub(r'^#?'+key+'( +)?=( +)?(\w+)?', key+" = "+value, data, count=1, flags=re.MULTILINE)
fh = open(file,'w')
fh.write(data)
fh.close()
except:
pass
#### Config - Read #####
def config_read(file=None, file_override=None):
global CONFIG
@ -932,15 +1053,15 @@ def config_default(file=None, file_override=None):
# Value convert
def val_to_bool(val):
def val_to_bool(val, fallback_true=True, fallback_false=False):
if val == "on" or val == "On" or val == "true" or val == "True" or val == "yes" or val == "Yes" or val == "1" or val == "open" or val == "opened" or val == "up":
return True
elif val == "off" or val == "Off" or val == "false" or val == "False" or val == "no" or val == "No" or val == "0" or val == "close" or val == "closed" or val == "down":
return False
elif val != "":
return True
return fallback_true
else:
return False
return fallback_false
##############################################################################################################
@ -1106,6 +1227,11 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
else:
config_propagation_node = None
if CONFIG.has_option("lxmf", "propagation_node_active"):
config_propagation_node_active = CONFIG["lxmf"]["propagation_node_active"]
else:
config_propagation_node_active = None
if path is None:
path = PATH
@ -1114,9 +1240,12 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
destination_name=CONFIG["lxmf"]["destination_name"],
destination_type=CONFIG["lxmf"]["destination_type"],
display_name=CONFIG["lxmf"]["display_name"],
announce_hidden=CONFIG["lxmf"].getboolean("announce_hidden"),
send_delay=CONFIG["lxmf"]["send_delay"],
desired_method=CONFIG["lxmf"]["desired_method"],
propagation_node=config_propagation_node,
propagation_node_auto=CONFIG["lxmf"].getboolean("propagation_node_auto"),
propagation_node_active=config_propagation_node_active,
try_propagation_on_fail=CONFIG["lxmf"].getboolean("try_propagation_on_fail"),
announce_startup=CONFIG["lxmf"].getboolean("announce_startup"),
announce_startup_delay=CONFIG["lxmf"]["announce_startup_delay"],
@ -1130,6 +1259,7 @@ def setup(path=None, path_rns=None, path_log=None, loglevel=None, service=False)
LXMF_CONNECTION.register_announce_callback(lxmf_announce_callback)
LXMF_CONNECTION.register_message_received_callback(lxmf_message_received_callback)
LXMF_CONNECTION.register_config_set_callback(config_set)
log("LXMF - Connected", LOG_DEBUG)
@ -1228,7 +1358,13 @@ display_name = CMD
desired_method = direct #direct/propagated
# Propagation node address/hash.
#propagation_node =
propagation_node =
# Set propagation node automatically.
propagation_node_auto = True
# Current propagation node (Automatically set by the software).
propagation_node_active =
# Try to deliver a message via the LXMF propagation network,
# if a direct delivery to the recipient is not possible.
@ -1244,6 +1380,10 @@ announce_startup_delay = 0 #Seconds
announce_periodic = No
announce_periodic_interval = 360 #Minutes
# The announce is hidden for client applications
# but is still used for the routing tables.
announce_hidden = No
# Some waiting time after message send
# for LXMF/Reticulum processing.
send_delay = 0 #Seconds