diff --git a/nomadnet/Conversation.py b/nomadnet/Conversation.py index 8a6f623..6a117f0 100644 --- a/nomadnet/Conversation.py +++ b/nomadnet/Conversation.py @@ -26,7 +26,7 @@ class Conversation: # Add the announce to the directory announce # stream logger - app.directory.announce_received(destination_hash, app_data) + app.directory.lxmf_announce_received(destination_hash, app_data) @staticmethod def query_for_peer(source_hash): diff --git a/nomadnet/Directory.py b/nomadnet/Directory.py index fce3ddd..5187ce2 100644 --- a/nomadnet/Directory.py +++ b/nomadnet/Directory.py @@ -5,7 +5,7 @@ import time import RNS.vendor.umsgpack as msgpack class Directory: - ANNOUNCE_STREAM_MAXLENGTH = 256 + ANNOUNCE_STREAM_MAXLENGTH = 64 def __init__(self, app): self.directory_entries = {} @@ -54,7 +54,13 @@ class Directory: except Exception as e: RNS.log("Could not load directory from disk. The contained exception was: "+str(e), RNS.LOG_ERROR) - def announce_received(self, source_hash, app_data): + def lxmf_announce_received(self, source_hash, app_data): + timestamp = time.time() + self.announce_stream.insert(0, (timestamp, source_hash, app_data)) + while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH: + self.announce_stream.pop() + + def node_announce_received(self, source_hash, app_data): timestamp = time.time() self.announce_stream.insert(0, (timestamp, source_hash, app_data)) while len(self.announce_stream) > Directory.ANNOUNCE_STREAM_MAXLENGTH: diff --git a/nomadnet/Node.py b/nomadnet/Node.py new file mode 100644 index 0000000..ad08888 --- /dev/null +++ b/nomadnet/Node.py @@ -0,0 +1,124 @@ +import os +import RNS +import time +import threading +import RNS.vendor.umsgpack as msgpack + +class Node: + JOB_INTERVAL = 5 + + def __init__(self, app): + RNS.log("Nomad Network Node starting...", RNS.LOG_VERBOSE) + self.app = app + self.identity = self.app.identity + self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, "nomadnetwork", "node") + self.last_announce = None + self.announce_interval = self.app.node_announce_interval + self.job_interval = Node.JOB_INTERVAL + self.should_run_jobs = True + self.app_data = None + self.name = self.app.node_name + + self.register_pages() + self.register_files() + + if self.app.node_announce_at_start: + self.announce() + + if self.name == None: + name_string = self.app.peer_settings["display_name"]+"'s Node" + else: + name_string = self.name + + RNS.log("Node \""+name_string+"\" ready for incoming connections on "+RNS.prettyhexrep(self.destination.hash), RNS.LOG_VERBOSE) + + + def register_pages(self): + self.servedpages = [] + self.scan_pages(self.app.pagespath) + + if not self.app.pagespath+"index.mu" in self.servedpages: + self.destination.register_request_handler( + "/page/index.mu", + response_generator = self.serve_default_index, + allow = RNS.Destination.ALLOW_ALL + ) + + for page in self.servedpages: + request_path = "/page"+page.replace(self.app.pagespath, "") + + self.destination.register_request_handler( + request_path, + response_generator = self.serve_page, + allow = RNS.Destination.ALLOW_ALL + ) + + def register_files(self): + self.servedfiles = [] + self.scan_files(self.app.filespath) + + def scan_pages(self, base_path): + files = [file for file in os.listdir(base_path) if os.path.isfile(os.path.join(base_path, file)) and file[:1] != "."] + directories = [file for file in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, file)) and file[:1] != "."] + + for file in files: + self.servedpages.append(base_path+"/"+file) + + for directory in directories: + self.scan_pages(base_path+"/"+directory) + + def scan_files(self, base_path): + files = [file for file in os.listdir(base_path) if os.path.isfile(os.path.join(base_path, file)) and file[:1] != "."] + directories = [file for file in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, file)) and file[:1] != "."] + + for file in files: + self.servedfiles.append(base_path+"/"+file) + + for directory in directories: + self.scan_files(base_path+"/"+directory) + + def serve_page(self, path, data, request_id, remote_identity, requested_at): + RNS.log("Request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_VERBOSE) + file_path = path.replace("/page", self.app.pagespath, 1) + try: + RNS.log("Serving file: "+file_path, RNS.LOG_VERBOSE) + fh = open(file_path, "rb") + response_data = fh.read() + fh.close() + return response_data + + except Exception as e: + RNS.log("Error occurred while handling request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + return None + + + def serve_default_index(self, path, data, request_id, remote_identity, requested_at): + RNS.log("Serving default index for request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_VERBOSE) + return DEFAULT_INDEX.encode("utf-8") + + def announce(self): + self.app_data = self.name.encode("utf-8") + self.last_announce = time.time() + self.destination.announce(app_data=self.app_data) + + def __jobs(self): + while self.should_run_jobs: + now = time.time() + + if now > self.last_announce + self.announce_interval: + self.announce() + + time.sleep(self.job_interval) + + def peer_connected(link): + RNS.log("Peer connected to "+str(self.destination), RNS.LOG_INFO) + link.set_link_closed_callback(self.peer_disconnected) + + +DEFAULT_INDEX = '''>Default Home Page + +This node is serving pages, but no home page file (index.mu) was found in the page storage directory. This is an auto-generated placeholder. + +If you are the node operator, you can define your own home page by creating a file named `*index.mu`* in the page storage directory. +''' \ No newline at end of file diff --git a/nomadnet/NomadNetworkApp.py b/nomadnet/NomadNetworkApp.py index 31bf540..3221a1a 100644 --- a/nomadnet/NomadNetworkApp.py +++ b/nomadnet/NomadNetworkApp.py @@ -51,6 +51,9 @@ class NomadNetworkApp: self.directorypath = self.configdir+"/storage/directory" self.peersettingspath = self.configdir+"/storage/peersettings" + self.pagespath = self.configdir+"/storage/pages" + self.filespath = self.configdir+"/storage/files" + if not os.path.isdir(self.storagepath): os.makedirs(self.storagepath) @@ -63,6 +66,12 @@ class NomadNetworkApp: if not os.path.isdir(self.conversationpath): os.makedirs(self.conversationpath) + if not os.path.isdir(self.pagespath): + os.makedirs(self.pagespath) + + if not os.path.isdir(self.filespath): + os.makedirs(self.filespath) + if os.path.isfile(self.configpath): try: self.config = ConfigObj(self.configpath) @@ -151,6 +160,11 @@ class NomadNetworkApp: self.directory = nomadnet.Directory.Directory(self) + if self.enable_node: + self.node = nomadnet.Node(self) + else: + self.node = None + nomadnet.ui.spawn(self.uimode) def set_display_name(self, display_name): @@ -295,13 +309,40 @@ class NomadNetworkApp: self.uimode = nomadnet.ui.UI_WEB if "node" in self.config: - for option in self.config["node"]: - value = self.config["node"][option] + if not "enable_node" in self.config["node"]: + self.enable_node = False + else: + self.enable_node = self.config["node"].as_bool("enable_node") - if option == "enable_node": - value = self.config["node"].as_bool(option) - self.enable_node = value + if not "node_name" in self.config["node"]: + self.node_name = None + else: + value = self.config["node"]["node_name"] + if value.lower() == "none": + self.node_name = None + else: + self.node_name = self.config["node"]["node_name"] + if not "announce_at_start" in self.config["node"]: + self.node_announce_at_start = False + else: + self.node_announce_at_start = self.config["announce_at_start"].as_bool("announce_at_start") + + if not "announce_interval" in self.config["node"]: + self.node_announce_interval = 720 + else: + value = self.config["announce_interval"].as_int("announce_interval") + if value < 1: + value = 1 + self.node_announce_interval = value + + if "pages_path" in self.config["node"]: + self.pagespath = self.config["node"]["pages_path"] + + if "files_path" in self.config["node"]: + self.filespath = self.config["node"]["files_path"] + + @staticmethod def get_shared_instance(): if NomadNetworkApp._shared_instance != None: @@ -385,6 +426,23 @@ hide_guide = no [node] +# Whether to enable node hosting + enable_node = no +# The node name will be visible to other +# peers on the network, and included in +# announces. + +node_name = None + +# Automatic announce interval in minutes. +# 12 hours by default. + +announce_interval = 720 + +# Whether to announce when the node starts + +announce_at_start = No + '''.splitlines() \ No newline at end of file diff --git a/nomadnet/__init__.py b/nomadnet/__init__.py index 900858b..8b362e5 100644 --- a/nomadnet/__init__.py +++ b/nomadnet/__init__.py @@ -3,6 +3,7 @@ import glob from .NomadNetworkApp import NomadNetworkApp from .Conversation import Conversation +from .Node import Node from .ui import * diff --git a/nomadnet/ui/textui/Guide.py b/nomadnet/ui/textui/Guide.py index 1aa9a7c..997d0b3 100644 --- a/nomadnet/ui/textui/Guide.py +++ b/nomadnet/ui/textui/Guide.py @@ -168,7 +168,7 @@ A `*peer`* refers to another Nomad Network client, which will generally be opera An `*announce`* can be sent by any peer on the network, which will notify other peers of its existence, and contains the cryptographic keys that allows other peers to communicate with it. -In the `![ Network ]`! section of the program, you can monitor announces on the network, initiate conversations with announced peers, and announce your own peer on the network. +In the `![ Network ]`! section of the program, you can monitor announces on the network, initiate conversations with announced peers, and announce your own peer on the network. You can also connect to nodes on the network and browse information shared by them. >>Conversations @@ -188,8 +188,7 @@ If no nodes exist on a network, all peers will still be able to communicate dire ''' -TOPIC_CONVERSATIONS = '''Conversations -============= +TOPIC_CONVERSATIONS = '''>Conversations Conversations in Nomad Network ''' diff --git a/nomadnet/ui/textui/Network.py b/nomadnet/ui/textui/Network.py index 438b63b..d637db9 100644 --- a/nomadnet/ui/textui/Network.py +++ b/nomadnet/ui/textui/Network.py @@ -10,7 +10,7 @@ class NetworkDisplayShortcuts(): self.app = app g = app.ui.glyphs - self.widget = urwid.AttrMap(urwid.Text("[C-"+g["arrow_u"]+g["arrow_d"]+"] Navigate announces"), "shortcutbar") + self.widget = urwid.AttrMap(urwid.Text("[C-l] View Nodes/Announces [C-"+g["arrow_u"]+g["arrow_d"]+"] Navigate Lists"), "shortcutbar") class DialogLineBox(urwid.LineBox): @@ -93,7 +93,7 @@ class AnnounceInfo(urwid.WidgetWrap): def show_announce_stream(sender): options = self.parent.left_pile.options(height_type="weight", height_amount=1) - self.parent.left_pile.contents[1] = (AnnounceStream(self.app, self.parent), options) + self.parent.left_pile.contents[0] = (AnnounceStream(self.app, self.parent), options) def converse(sender): show_announce_stream(None) @@ -176,7 +176,7 @@ class AnnounceStreamEntry(urwid.WidgetWrap): parent = self.app.ui.main_display.sub_displays.network_display info_widget = AnnounceInfo(announce, parent, self.app) options = parent.left_pile.options(height_type="weight", height_amount=1) - parent.left_pile.contents[1] = (info_widget, options) + parent.left_pile.contents[0] = (info_widget, options) class AnnounceStream(urwid.WidgetWrap): def __init__(self, app, parent): @@ -203,6 +203,12 @@ class AnnounceStream(urwid.WidgetWrap): self.display_widget = self.ilb urwid.WidgetWrap.__init__(self, urwid.LineBox(self.display_widget, title="Announce Stream")) + def keypress(self, size, key): + if key == "up": + nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header") + + return super(AnnounceStream, self).keypress(size, key) + def rebuild_widget_list(self): self.added_entries = [] self.widget_list = [] @@ -287,7 +293,8 @@ class KnownNodes(urwid.WidgetWrap): else: self.no_content = True widget_style = "inactive_text" - self.display_widget = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no nodes are known\n\n"), align="center")]) + self.pile = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no nodes are known\n\n"), align="center")]) + self.display_widget = urwid.Filler(self.pile, valign="top", height="pack") urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title="Known Nodes"), widget_style)) @@ -361,7 +368,7 @@ class LocalPeer(urwid.WidgetWrap): def save_query(sender): def dismiss_dialog(sender): self.dialog_open = False - self.parent.left_pile.contents[3] = (LocalPeer(self.app, self.parent), options) + self.parent.left_pile.contents[2] = (LocalPeer(self.app, self.parent), options) self.app.set_display_name(e_name.get_edit_text()) @@ -378,13 +385,13 @@ class LocalPeer(urwid.WidgetWrap): overlay = dialog options = self.parent.left_pile.options(height_type="pack", height_amount=None) self.dialog_open = True - self.parent.left_pile.contents[3] = (overlay, options) + self.parent.left_pile.contents[2] = (overlay, options) def announce_query(sender): def dismiss_dialog(sender): self.dialog_open = False options = self.parent.left_pile.options(height_type="pack", height_amount=None) - self.parent.left_pile.contents[3] = (LocalPeer(self.app, self.parent), options) + self.parent.left_pile.contents[2] = (LocalPeer(self.app, self.parent), options) self.app.announce_now() @@ -402,11 +409,11 @@ class LocalPeer(urwid.WidgetWrap): self.dialog_open = True options = self.parent.left_pile.options(height_type="pack", height_amount=None) - self.parent.left_pile.contents[3] = (overlay, options) + self.parent.left_pile.contents[2] = (overlay, options) def node_settings_query(sender): options = self.parent.left_pile.options(height_type="pack", height_amount=None) - self.parent.left_pile.contents[3] = (self.parent.node_settings_display, options) + self.parent.left_pile.contents[2] = (self.parent.node_settings_display, options) if LocalPeer.announce_timer == None: self.t_last_announce = AnnounceTime(self.app) @@ -443,7 +450,7 @@ class NodeSettings(urwid.WidgetWrap): def show_peer_info(sender): options = self.parent.left_pile.options(height_type="pack", height_amount=None) - self.parent.left_pile.contents[3] = (LocalPeer(self.app, self.parent), options) + self.parent.left_pile.contents[2] = (LocalPeer(self.app, self.parent), options) widget_style = "inactive_text" pile = urwid.Pile([ @@ -518,6 +525,16 @@ class NetworkStats(urwid.WidgetWrap): self.w_heard_peers.start() self.w_known_nodes.start() + + +class NetworkLeftPile(urwid.Pile): + def keypress(self, size, key): + if key == "ctrl l": + self.parent.toggle_list() + else: + return super(NetworkLeftPile, self).keypress(size, key) + + class NetworkDisplay(): list_width = 0.33 @@ -525,19 +542,21 @@ class NetworkDisplay(): self.app = app g = self.app.ui.glyphs - self.known_nodes_display = KnownNodes(self.app) + self.known_nodes_display = None self.network_stats_display = NetworkStats(self.app, self) self.announce_stream_display = AnnounceStream(self.app, self) self.local_peer_display = LocalPeer(self.app, self) self.node_settings_display = NodeSettings(self.app, self) - self.left_pile = urwid.Pile([ - ("pack", self.known_nodes_display), + self.list_display = 0 + self.left_pile = NetworkLeftPile([ ("weight", 1, self.announce_stream_display), ("pack", self.network_stats_display), ("pack", self.local_peer_display), ]) + self.left_pile.parent = self + self.left_area = self.left_pile self.right_area = urwid.AttrMap(urwid.LineBox(urwid.Filler(urwid.Text("Disconnected\n"+g["arrow_l"]+" "+g["arrow_r"], align="center"), "middle"), title="Remote Node"), "inactive_text") @@ -552,6 +571,18 @@ class NetworkDisplay(): self.shortcuts_display = NetworkDisplayShortcuts(self.app) self.widget = self.columns + def toggle_list(self): + if self.list_display != 0: + self.announce_stream_display = AnnounceStream(self.app, self) + options = self.left_pile.options(height_type="weight", height_amount=1) + self.left_pile.contents[0] = (self.announce_stream_display, options) + self.list_display = 0 + else: + self.known_nodes_display = KnownNodes(self.app) + options = self.left_pile.options(height_type="weight", height_amount=1) + self.left_pile.contents[0] = (self.known_nodes_display, options) + self.list_display = 1 + def start(self): self.local_peer_display.start() self.network_stats_display.start()