Implemented basic LXMF propagation support and controls

This commit is contained in:
Mark Qvist 2021-10-03 18:44:00 +02:00
parent 7043f33dd8
commit df7692d7c6
8 changed files with 122 additions and 19 deletions

View File

@ -196,9 +196,18 @@ class Conversation:
if self.send_destination: if self.send_destination:
dest = self.send_destination dest = self.send_destination
source = self.app.lxmf_destination source = self.app.lxmf_destination
lxm = LXMF.LXMessage(dest, source, content, title=title, desired_method=LXMF.LXMessage.DIRECT) desired_method = LXMF.LXMessage.DIRECT
if self.app.directory.preferred_delivery(dest.hash) == DirectoryEntry.PROPAGATED:
if self.app.message_router.get_outbound_propagation_node() != None:
desired_method = LXMF.LXMessage.PROPAGATED
lxm = LXMF.LXMessage(dest, source, content, title=title, desired_method=desired_method)
lxm.register_delivery_callback(self.message_notification) lxm.register_delivery_callback(self.message_notification)
lxm.register_failed_callback(self.message_notification) lxm.register_failed_callback(self.message_notification)
if self.app.message_router.get_outbound_propagation_node() != None:
lxm.try_propagation_on_fail = self.app.try_propagation_on_fail
self.app.message_router.handle_outbound(lxm) self.app.message_router.handle_outbound(lxm)
message_path = Conversation.ingest(lxm, self.app, originator=True) message_path = Conversation.ingest(lxm, self.app, originator=True)
@ -210,6 +219,15 @@ class Conversation:
return False return False
def message_notification(self, message): def message_notification(self, message):
if message.state == LXMF.LXMessage.FAILED and hasattr(message, "try_propagation_on_fail") and message.try_propagation_on_fail:
RNS.log("Direct delivery of "+str(message)+" failed. Retrying as propagated message.", RNS.LOG_VERBOSE)
message.try_propagation_on_fail = None
message.delivery_attempts = 0
del message.next_delivery_attempt
message.packed = None
message.desired_method = LXMF.LXMessage.PROPAGATED
self.app.message_router.handle_outbound(message)
else:
message_path = Conversation.ingest(message, self.app, originator=True) message_path = Conversation.ingest(message, self.app, originator=True)
def __str__(self): def __str__(self):

View File

@ -17,6 +17,7 @@ class Directory:
associated_peer = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", announced_identity) associated_peer = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", announced_identity)
app.directory.node_announce_received(destination_hash, app_data, associated_peer) app.directory.node_announce_received(destination_hash, app_data, associated_peer)
app.autoselect_propagation_node()
def __init__(self, app): def __init__(self, app):
@ -31,7 +32,7 @@ class Directory:
packed_list = [] packed_list = []
for source_hash in self.directory_entries: for source_hash in self.directory_entries:
e = self.directory_entries[source_hash] e = self.directory_entries[source_hash]
packed_list.append((e.source_hash, e.display_name, e.trust_level, e.hosts_node)) packed_list.append((e.source_hash, e.display_name, e.trust_level, e.hosts_node, e.preferred_delivery))
directory = { directory = {
"entry_list": packed_list, "entry_list": packed_list,
@ -59,7 +60,12 @@ class Directory:
else: else:
hosts_node = False hosts_node = False
entries[e[0]] = DirectoryEntry(e[0], e[1], e[2], hosts_node) if len(e) > 4:
preferred_delivery = e[4]
else:
preferred_delivery = None
entries[e[0]] = DirectoryEntry(e[0], e[1], e[2], hosts_node, preferred_delivery=preferred_delivery)
self.directory_entries = entries self.directory_entries = entries
@ -130,6 +136,12 @@ class Directory:
else: else:
return DirectoryEntry.UNKNOWN return DirectoryEntry.UNKNOWN
def preferred_delivery(self, source_hash):
if source_hash in self.directory_entries:
return self.directory_entries[source_hash].preferred_delivery
else:
return DirectoryEntry.DIRECT
def remember(self, entry): def remember(self, entry):
self.directory_entries[entry.source_hash] = entry self.directory_entries[entry.source_hash] = entry
@ -189,13 +201,21 @@ class DirectoryEntry:
UNKNOWN = 0x02 UNKNOWN = 0x02
TRUSTED = 0xFF TRUSTED = 0xFF
def __init__(self, source_hash, display_name=None, trust_level=UNKNOWN, hosts_node=False): DIRECT = 0x01
PROPAGATED = 0x02
def __init__(self, source_hash, display_name=None, trust_level=UNKNOWN, hosts_node=False, preferred_delivery=None):
if len(source_hash) == RNS.Identity.TRUNCATED_HASHLENGTH//8: if len(source_hash) == RNS.Identity.TRUNCATED_HASHLENGTH//8:
self.source_hash = source_hash self.source_hash = source_hash
self.display_name = display_name self.display_name = display_name
if display_name == None: if display_name == None:
display_name = source_hash display_name = source_hash
if preferred_delivery == None:
self.preferred_delivery = DirectoryEntry.DIRECT
else:
self.preferred_delivery = preferred_delivery
self.trust_level = trust_level self.trust_level = trust_level
self.hosts_node = hosts_node self.hosts_node = hosts_node
else: else:

View File

@ -6,6 +6,8 @@ import RNS
import LXMF import LXMF
import nomadnet import nomadnet
from nomadnet.Directory import DirectoryEntry
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
from ._version import __version__ from ._version import __version__
@ -60,6 +62,7 @@ class NomadNetworkApp:
self.firstrun = False self.firstrun = False
self.peer_announce_at_start = True self.peer_announce_at_start = True
self.try_propagation_on_fail = True
if not os.path.isdir(self.storagepath): if not os.path.isdir(self.storagepath):
os.makedirs(self.storagepath) os.makedirs(self.storagepath)
@ -154,9 +157,9 @@ class NomadNetworkApp:
nomadnet.panic() nomadnet.panic()
atexit.register(self.exit_handler) self.directory = nomadnet.Directory(self)
self.message_router = LXMF.LXMRouter() self.message_router = LXMF.LXMRouter(identity = self.identity, autopeer = True)
self.message_router.register_delivery_callback(self.lxmf_delivery) self.message_router.register_delivery_callback(self.lxmf_delivery)
self.lxmf_destination = self.message_router.register_delivery_identity(self.identity, display_name=self.peer_settings["display_name"]) self.lxmf_destination = self.message_router.register_delivery_identity(self.identity, display_name=self.peer_settings["display_name"])
@ -171,9 +174,9 @@ class NomadNetworkApp:
RNS.log("LXMF Router ready to receive on: "+RNS.prettyhexrep(self.lxmf_destination.hash)) RNS.log("LXMF Router ready to receive on: "+RNS.prettyhexrep(self.lxmf_destination.hash))
self.directory = nomadnet.Directory(self)
if self.enable_node: if self.enable_node:
self.message_router.enable_propagation(self.storagepath)
RNS.log("LXMF Propagation Node started on: "+RNS.prettyhexrep(self.message_router.propagation_destination.hash))
self.node = nomadnet.Node(self) self.node = nomadnet.Node(self)
else: else:
self.node = None self.node = None
@ -181,9 +184,13 @@ class NomadNetworkApp:
RNS.Transport.register_announce_handler(nomadnet.Conversation) RNS.Transport.register_announce_handler(nomadnet.Conversation)
RNS.Transport.register_announce_handler(nomadnet.Directory) RNS.Transport.register_announce_handler(nomadnet.Directory)
self.autoselect_propagation_node()
if self.peer_announce_at_start: if self.peer_announce_at_start:
self.announce_now() self.announce_now()
atexit.register(self.exit_handler)
nomadnet.ui.spawn(self.uimode) nomadnet.ui.spawn(self.uimode)
def set_display_name(self, display_name): def set_display_name(self, display_name):
@ -202,6 +209,30 @@ class NomadNetworkApp:
self.peer_settings["last_announce"] = time.time() self.peer_settings["last_announce"] = time.time()
self.save_peer_settings() self.save_peer_settings()
def autoselect_propagation_node(self):
nodes = self.directory.known_nodes()
trusted_nodes = []
selected_node = None
best_hops = RNS.Transport.PATHFINDER_M+1
for node in nodes:
if node.trust_level == DirectoryEntry.TRUSTED:
hops = RNS.Transport.hops_to(node.source_hash)
if hops < best_hops:
best_hops = hops
selected_node = node
if selected_node == None:
RNS.log("Could not autoselect a prepagation node! LXMF propagation will not be available until a trusted node announces on the network.", RNS.LOG_WARNING)
else:
node_identity = RNS.Identity.recall(selected_node.source_hash)
propagation_hash = RNS.Destination.hash_from_name_and_identity("lxmf.propagation", node_identity)
RNS.log("Selecting "+selected_node.display_name+" "+RNS.prettyhexrep(propagation_hash)+" as default LXMF propagation node", RNS.LOG_INFO)
self.message_router.set_outbound_propagation_node(propagation_hash)
def save_peer_settings(self): def save_peer_settings(self):
file = open(self.peersettingspath, "wb") file = open(self.peersettingspath, "wb")
file.write(msgpack.packb(self.peer_settings)) file.write(msgpack.packb(self.peer_settings))
@ -286,6 +317,10 @@ class NomadNetworkApp:
value = self.config["client"].as_bool(option) value = self.config["client"].as_bool(option)
self.peer_announce_at_start = value self.peer_announce_at_start = value
if option == "try_propagation_on_send_fail":
value = self.config["client"].as_bool(option)
self.try_propagation_on_fail = value
if option == "user_interface": if option == "user_interface":
value = value.lower() value = value.lower()
if value == "none": if value == "none":
@ -430,6 +465,12 @@ downloads_path = ~/Downloads
# to let other peers reach it immediately. # to let other peers reach it immediately.
announce_at_start = yes announce_at_start = yes
# By default, the client will try to deliver a
# message via the LXMF propagation network, if
# a direct delivery to the recipient is not
# possible.
try_propagation_on_send_fail = yes
[textui] [textui]
# Amount of time to show intro screen # Amount of time to show intro screen

View File

@ -1 +1 @@
__version__ = "0.1.1" __version__ = "0.1.2"

View File

@ -120,6 +120,7 @@ GLYPHS = {
("decoration_menu", " +", " +", " \uf93a"), ("decoration_menu", " +", " +", " \uf93a"),
("unread_menu", " !", " \u2709", urm_char), ("unread_menu", " !", " \u2709", urm_char),
("globe", "", "", "\uf484"), ("globe", "", "", "\uf484"),
("sent", "/\\", "\u2191", "\ufbf4")
} }
class TextUI: class TextUI:

View File

@ -144,6 +144,9 @@ class ConversationsDisplay():
unknown_selected = True unknown_selected = True
trusted_selected = False trusted_selected = False
direct_selected = True
propagated_selected = False
try: try:
if self.app.directory.find(bytes.fromhex(source_hash_text)): if self.app.directory.find(bytes.fromhex(source_hash_text)):
trust_level = self.app.directory.trust_level(bytes.fromhex(source_hash_text)) trust_level = self.app.directory.trust_level(bytes.fromhex(source_hash_text))
@ -159,6 +162,11 @@ class ConversationsDisplay():
untrusted_selected = False untrusted_selected = False
unknown_selected = False unknown_selected = False
trusted_selected = True trusted_selected = True
if self.app.directory.preferred_delivery(bytes.fromhex(source_hash_text)) == DirectoryEntry.PROPAGATED:
direct_selected = False
propagated_selected = True
except Exception as e: except Exception as e:
pass pass
@ -167,6 +175,10 @@ class ConversationsDisplay():
r_unknown = urwid.RadioButton(trust_button_group, "Unknown", state=unknown_selected) r_unknown = urwid.RadioButton(trust_button_group, "Unknown", state=unknown_selected)
r_trusted = urwid.RadioButton(trust_button_group, "Trusted", state=trusted_selected) r_trusted = urwid.RadioButton(trust_button_group, "Trusted", state=trusted_selected)
method_button_group = []
r_direct = urwid.RadioButton(method_button_group, "Deliver directly", state=direct_selected)
r_propagated = urwid.RadioButton(method_button_group, "Use propagation nodes", state=propagated_selected)
def dismiss_dialog(sender): def dismiss_dialog(sender):
self.update_conversation_list() self.update_conversation_list()
self.dialog_open = False self.dialog_open = False
@ -181,7 +193,11 @@ class ConversationsDisplay():
elif r_trusted.state == True: elif r_trusted.state == True:
trust_level = DirectoryEntry.TRUSTED trust_level = DirectoryEntry.TRUSTED
entry = DirectoryEntry(source_hash, display_name, trust_level) delivery = DirectoryEntry.DIRECT
if r_propagated.state == True:
delivery = DirectoryEntry.PROPAGATED
entry = DirectoryEntry(source_hash, display_name, trust_level, preferred_delivery=delivery)
self.app.directory.remember(entry) self.app.directory.remember(entry)
self.update_conversation_list() self.update_conversation_list()
self.dialog_open = False self.dialog_open = False
@ -216,6 +232,9 @@ class ConversationsDisplay():
r_untrusted, r_untrusted,
r_unknown, r_unknown,
r_trusted, r_trusted,
urwid.Divider(g["divider1"]),
r_direct,
r_propagated,
known_section, known_section,
urwid.Columns([("weight", 0.45, urwid.Button("Save", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Back", on_press=dismiss_dialog))]) urwid.Columns([("weight", 0.45, urwid.Button("Save", on_press=confirmed)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Back", on_press=dismiss_dialog))])
]) ])
@ -718,6 +737,9 @@ class LXMessageWidget(urwid.WidgetWrap):
elif message.lxm.state == LXMF.LXMessage.FAILED: elif message.lxm.state == LXMF.LXMessage.FAILED:
header_style = "msg_header_failed" header_style = "msg_header_failed"
title_string = g["cross"]+" "+title_string title_string = g["cross"]+" "+title_string
elif message.lxm.state == LXMF.LXMessage.SENT:
header_style = "msg_header_sent"
title_string = g["sent"]+" "+title_string
else: else:
header_style = "msg_header_sent" header_style = "msg_header_sent"
title_string = g["arrow_r"]+" "+title_string title_string = g["arrow_r"]+" "+title_string

View File

@ -1,5 +1,6 @@
quotes = [ quotes = [
("I want the wisdom that wise men revere. I want more.", "Faithless"), ("I want the wisdom that wise men revere. I want more.", "Faithless"),
("That's enough entropy for you my friend", "Unknown"), ("That's enough entropy for you my friend", "Unknown"),
("Any time two people connect online, it's financed by a third person who believes they can manipulate the first two", "Jaron Lanier") ("Any time two people connect online, it's financed by a third person who believes they can manipulate the first two", "Jaron Lanier"),
("The landscape of the future is set, but how one will march across it is not determined", "Terence McKenna")
] ]

View File

@ -23,6 +23,6 @@ setuptools.setup(
entry_points= { entry_points= {
'console_scripts': ['nomadnet=nomadnet.nomadnet:main'] 'console_scripts': ['nomadnet=nomadnet.nomadnet:main']
}, },
install_requires=['rns>=0.2.6', 'lxmf>=0.0.9', 'urwid>=2.1.2'], install_requires=['rns>=0.2.7', 'lxmf>=0.1.0', 'urwid>=2.1.2'],
python_requires='>=3.6', python_requires='>=3.6',
) )