From a8d337a94ef393a34d6bf2cabe62824423bdeea4 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Wed, 1 Feb 2023 15:39:45 +0100 Subject: [PATCH 01/17] Fixed propagation node autoselection not working when None value was set in settings dict --- nomadnet/NomadNetworkApp.py | 11 ++++------- nomadnet/ui/textui/Network.py | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/nomadnet/NomadNetworkApp.py b/nomadnet/NomadNetworkApp.py index 1211e32..701869b 100644 --- a/nomadnet/NomadNetworkApp.py +++ b/nomadnet/NomadNetworkApp.py @@ -456,9 +456,8 @@ class NomadNetworkApp: def autoselect_propagation_node(self): selected_node = None - if "propagation_node" in self.peer_settings: + if "propagation_node" in self.peer_settings and self.peer_settings["propagation_node"] != None: selected_node = self.peer_settings["propagation_node"] - else: nodes = self.directory.known_nodes() trusted_nodes = [] @@ -706,7 +705,7 @@ class NomadNetworkApp: self.config["textui"]["intro_text"] = "Nomad Network" if not "editor" in self.config["textui"]: - self.config["textui"]["editor"] = "editor" + self.config["textui"]["editor"] = "nano" if not "glyphs" in self.config["textui"]: self.config["textui"]["glyphs"] = "unicode" @@ -958,10 +957,8 @@ glyphs = unicode # application. On by default. mouse_enabled = True -# What editor to use for editing text. By -# default the operating systems "editor" -# alias will be used. -editor = editor +# What editor to use for editing text. +editor = nano # If you don't want the Guide section to # show up in the menu, you can disable it. diff --git a/nomadnet/ui/textui/Network.py b/nomadnet/ui/textui/Network.py index 7a8081b..633fb2d 100644 --- a/nomadnet/ui/textui/Network.py +++ b/nomadnet/ui/textui/Network.py @@ -704,7 +704,7 @@ class KnownNodes(urwid.WidgetWrap): else: self.no_content = True widget_style = "inactive_text" - 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.pile = urwid.Pile([urwid.Text(("warning_text", g["info"]+"\n"), align="center"), SelectText(("warning_text", "Currently, no nodes are saved\n\nCtrl+L to view the announce stream\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="Saved Nodes"), widget_style)) From dc64ab26d42f0f9c79006f0ac403555b390747f6 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 11:49:36 +0100 Subject: [PATCH 02/17] Fixed browser cursor position on scrolling --- nomadnet/ui/textui/Browser.py | 36 +++++++++++++++++++++++----- nomadnet/vendor/Scrollable.py | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/nomadnet/ui/textui/Browser.py b/nomadnet/ui/textui/Browser.py index bdecffe..0fbd57c 100644 --- a/nomadnet/ui/textui/Browser.py +++ b/nomadnet/ui/textui/Browser.py @@ -26,13 +26,32 @@ class BrowserFrame(urwid.Frame): elif key == "ctrl g": nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.sub_displays.network_display.toggle_fullscreen() elif self.get_focus() == "body": + if key == "down" or key == "up": + try: + if hasattr(self.delegate, "page_pile") and self.delegate.page_pile: + def df(loop, user_data): + st = None + nf = self.delegate.page_pile.get_focus() + if hasattr(nf, "key_timeout"): + st = nf + elif hasattr(nf, "original_widget"): + no = nf.original_widget + if hasattr(no, "original_widget"): + st = no.original_widget + else: + if hasattr(no, "key_timeout"): + st = no + + if st and hasattr(st, "key_timeout") and hasattr(st, "keypress") and callable(st.keypress): + st.keypress(None, None) + + nomadnet.NomadNetworkApp.get_shared_instance().ui.loop.set_alarm_in(0.25, df) + + except Exception as e: + RNS.log("Error while setting up cursor timeout. The contained exception was: "+str(e), RNS.LOG_ERROR) + return super(BrowserFrame, self).keypress(size, key) - # if key == "up" and self.delegate.messagelist.top_is_visible: - # nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.set_focus("header") - # elif key == "down" and self.delegate.messagelist.bottom_is_visible: - # self.set_focus("footer") - # else: - # return super(ConversationFrame, self).keypress(size, key) + else: return super(BrowserFrame, self).keypress(size, key) @@ -80,6 +99,7 @@ class Browser: self.link_target = None self.frame = None self.attr_maps = [] + self.page_pile = None self.build_display() self.history = [] @@ -219,6 +239,7 @@ class Browser: self.browser_header = urwid.Text("") self.browser_footer = urwid.Text("") + self.page_pile = None self.browser_body = urwid.Filler(urwid.Text("Disconnected\n"+self.g["arrow_l"]+" "+self.g["arrow_r"], align="center"), "middle") self.frame = BrowserFrame(self.browser_body, header=self.browser_header, footer=self.browser_footer) @@ -266,6 +287,7 @@ class Browser: def update_display(self): if self.status == Browser.DISCONECTED: self.display_widget.set_attr_map({None: "inactive_text"}) + self.page_pile = None self.browser_body = urwid.Filler(urwid.Text("Disconnected\n"+self.g["arrow_l"]+" "+self.g["arrow_r"], align="center"), "middle") self.browser_footer = urwid.Text("") self.browser_header = urwid.Text("") @@ -314,6 +336,8 @@ class Browser: def update_page_display(self): pile = urwid.Pile(self.attr_maps) + pile.automove_cursor_on_scroll = True + self.page_pile = pile self.browser_body = urwid.AttrMap(ScrollBar(Scrollable(pile, force_forward_keypress=True), thumb_char="\u2503", trough_char=" "), "scrollbar") def identify(self): diff --git a/nomadnet/vendor/Scrollable.py b/nomadnet/vendor/Scrollable.py index ee28a46..d4eb078 100644 --- a/nomadnet/vendor/Scrollable.py +++ b/nomadnet/vendor/Scrollable.py @@ -107,6 +107,51 @@ class Scrollable(urwid.WidgetDecoration): if canv_full.cursor is not None: # Full canvas contains the cursor, but scrolled out of view self._forward_keypress = False + + # Reset cursor position on page/up down scrolling + try: + if hasattr(ow, "automove_cursor_on_scroll") and ow.automove_cursor_on_scroll: + pwi = 0 + ch = 0 + last_hidden = False + first_visible = False + for w,o in ow.contents: + wcanv = w.render((maxcol,)) + wh = wcanv.rows() + if wh: + ch += wh + + if not last_hidden and ch >= self._trim_top: + last_hidden = True + + elif last_hidden: + if not first_visible: + first_visible = True + + if w.selectable(): + ow.focus_item = pwi + + st = None + nf = ow.get_focus() + if hasattr(nf, "key_timeout"): + st = nf + elif hasattr(nf, "original_widget"): + no = nf.original_widget + if hasattr(no, "original_widget"): + st = no.original_widget + else: + if hasattr(no, "key_timeout"): + st = no + + if st and hasattr(st, "key_timeout") and hasattr(st, "keypress") and callable(st.keypress): + st.keypress(None, None) + + break + + pwi += 1 + except Exception as e: + pass + else: # Original widget does not have a cursor, but may be selectable From 78106b3f8cfe2f48673e7ae4afa14d9d578ee1a7 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 11:50:13 +0100 Subject: [PATCH 03/17] Fixed invalid indent --- nomadnet/vendor/Scrollable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nomadnet/vendor/Scrollable.py b/nomadnet/vendor/Scrollable.py index d4eb078..eb0d2fa 100644 --- a/nomadnet/vendor/Scrollable.py +++ b/nomadnet/vendor/Scrollable.py @@ -149,8 +149,8 @@ class Scrollable(urwid.WidgetDecoration): break pwi += 1 - except Exception as e: - pass + except Exception as e: + pass else: # Original widget does not have a cursor, but may be selectable From 71ff0f9bcdf157029fe4e572870ea90056b7f5ff Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 12:03:46 +0100 Subject: [PATCH 04/17] Added feedback on opening link --- nomadnet/ui/textui/Browser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nomadnet/ui/textui/Browser.py b/nomadnet/ui/textui/Browser.py index 0fbd57c..8b75fbd 100644 --- a/nomadnet/ui/textui/Browser.py +++ b/nomadnet/ui/textui/Browser.py @@ -51,7 +51,7 @@ class BrowserFrame(urwid.Frame): RNS.log("Error while setting up cursor timeout. The contained exception was: "+str(e), RNS.LOG_ERROR) return super(BrowserFrame, self).keypress(size, key) - + else: return super(BrowserFrame, self).keypress(size, key) @@ -175,6 +175,7 @@ class Browser: if destination_type == "nomadnetwork.node": if self.status >= Browser.DISCONECTED: RNS.log("Browser handling link to: "+str(link_target), RNS.LOG_DEBUG) + self.browser_footer = urwid.Text("Opening link to: "+str(link_target)) try: self.retrieve_url(link_target) except Exception as e: From e3074830d64dd27d82db1e3279fd3c01d141c15e Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 12:04:34 +0100 Subject: [PATCH 05/17] Updated dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 880b2a1..fadae5d 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,6 @@ setuptools.setup( entry_points= { 'console_scripts': ['nomadnet=nomadnet.nomadnet:main'] }, - install_requires=["rns>=0.4.7", "lxmf>=0.2.9", "urwid>=2.1.2", "qrcode"], + install_requires=["rns>=0.4.8", "lxmf>=0.3.0", "urwid>=2.1.2", "qrcode"], python_requires=">=3.6", ) From 365a027f9b04b8fdba30f036aee6663c1587cdef Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 12:05:06 +0100 Subject: [PATCH 06/17] Updated version --- nomadnet/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nomadnet/_version.py b/nomadnet/_version.py index f9aa3e1..e19434e 100644 --- a/nomadnet/_version.py +++ b/nomadnet/_version.py @@ -1 +1 @@ -__version__ = "0.3.2" +__version__ = "0.3.3" From 16ca15c82cd2df0f758e14ba9fb93b7b40d3efa9 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 13:16:00 +0100 Subject: [PATCH 07/17] Updated guide --- nomadnet/ui/textui/Guide.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/nomadnet/ui/textui/Guide.py b/nomadnet/ui/textui/Guide.py index 8b178c9..c30bfee 100644 --- a/nomadnet/ui/textui/Guide.py +++ b/nomadnet/ui/textui/Guide.py @@ -312,14 +312,22 @@ The distributed message store is resilient to intermittency, and will remain fun >>Pages -Nomad Network nodes can host pages similar to web pages, that other peers can read and interact with. Pages are written in a compact markup language called `*micron`*. To learn how to write formatted pages with micron, see the `*Markup`* section of this guide (which is, itself, written in micron). Pages can be linked arbitrarily with hyperlinks, that can also link to pages (or other resources) on other nodes. +Nomad Network nodes can host pages similar to web pages, that other peers can read and interact with. Pages are written in a compact markup language called `*micron`*. To learn how to write formatted pages with micron, see the `*Markup`* section of this guide (which is, itself, written in micron). Pages can be linked together with hyperlinks, that can also link to pages (or other resources) on other nodes. To add pages to your node, place micron files in the `*pages`* directory of your Nomad Network programs `*storage`* directory. By default, the path to this will be `!~/.nomadnetwork/storage/pages`!. You should probably create the file `!index.mu`! first, as this is the page that will get served by default to a connecting peer. You can control how long a peer will cache your pages by including the cache header in a page. To do so, the first line of your page must start with `!#!c=X`!, where `!X`! is the cache time in seconds. To tell the peer to always load the page from your node, and never cache it, set the cache time to zero. You should only do this if there is a real need, for example if your page displays dynamic content that `*must`* be updated at every page view. The default caching time is 12 hours. In most cases, you should not need to include the cache control header in your pages. +>> Dynamic Pages + You can use a preprocessor such as PHP, bash, Python (or whatever you prefer) to generate dynamic pages. To do so, just set executable permissions on the relevant page file, and be sure to include the interpreter at the beginning of the file, for example `!#!/usr/bin/python3`!. +In the `!examples`! directory, you can find various small examples for the use of this feature. The currently included examples are: + + - A messageboard that receives messages over LXMF, contributed by trippcheng + +By default, you can find the examples in `!~/.nomadnetwork/examples`!. If you build something neat, that you feel would fit here, you are more than welcome to contribute it. + >>Authenticating Users Sometimes, you don't want everyone to be able to view certain pages or execute certain scripts. In such cases, you can use `*authentication`* to control who gets to run certain requests. @@ -330,9 +338,9 @@ For each user allowed to access the page, add a line to this file, containing th `Faaa `= -24c2dc2248953e0a3c21 -564a476410f4fca2ae93 -8bb864743f12f53395a1 +d454bcdac0e64fb68ba8e267543ae110 +2b9ff3fb5902c9ca5ff97bdfb239ef50 +7106d5abbc7208bfb171f2dd84b36490 `= `` @@ -348,7 +356,7 @@ Like pages, you can place files you want to make available in the `!~/.nomadnetw Links to pages and resources in Nomad Network use a simple URL format. Here is an example: -`!1385edace36466a6b3dd:/page/index.mu`! +`!18176ffddcc8cce1ddf8e3f72068f4a6:/page/index.mu`! The first part is the 10 byte destination address of the node (represented as readable hexadecimal), followed by the `!:`! character. Everything after the `!:`! represents the request path. From 8a1cb25403e21ec288972e69dd33b2195df60761 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 13:16:27 +0100 Subject: [PATCH 08/17] Ensure PN autoselection on node save if trusted --- nomadnet/ui/textui/Network.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nomadnet/ui/textui/Network.py b/nomadnet/ui/textui/Network.py index 633fb2d..b794cd2 100644 --- a/nomadnet/ui/textui/Network.py +++ b/nomadnet/ui/textui/Network.py @@ -610,6 +610,10 @@ class KnownNodeInfo(urwid.WidgetWrap): node_entry = DirectoryEntry(source_hash, display_name=display_str, trust_level=trust_level, hosts_node=True, identify_on_connect=connect_identify_checkbox.get_state()) self.app.directory.remember(node_entry) self.app.ui.main_display.sub_displays.network_display.directory_change_callback() + + if trust_level == DirectoryEntry.TRUSTED: + self.app.autoselect_propagation_node() + show_known_nodes(None) back_button = ("weight", 0.2, urwid.Button("Back", on_press=show_known_nodes)) From f37ce1482242f785104462bfaff128304264f798 Mon Sep 17 00:00:00 2001 From: chengtripp <124098378+chengtripp@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:16:06 +0000 Subject: [PATCH 09/17] Create messageboard.py --- nomadnet/apps/messageboard/messageboard.py | 187 +++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 nomadnet/apps/messageboard/messageboard.py diff --git a/nomadnet/apps/messageboard/messageboard.py b/nomadnet/apps/messageboard/messageboard.py new file mode 100644 index 0000000..8a71cd1 --- /dev/null +++ b/nomadnet/apps/messageboard/messageboard.py @@ -0,0 +1,187 @@ +# Simple message board that can be hosted on a NomadNet node, messages can be posted by 'conversing' with a unique peer, all messages are then forwarded to the message board. +# https://github.com/chengtripp/lxmf_messageboard + +import RNS +import LXMF +import os, time +from queue import Queue +import RNS.vendor.umsgpack as msgpack + +display_name = "NomadNet Message Board" +max_messages = 20 + +def setup_lxmf(): + if os.path.isfile(identitypath): + identity = RNS.Identity.from_file(identitypath) + RNS.log('Loaded identity from file', RNS.LOG_INFO) + else: + RNS.log('No Primary Identity file found, creating new...', RNS.LOG_INFO) + identity = RNS.Identity() + identity.to_file(identitypath) + + return identity + +def lxmf_delivery(message): + # Do something here with a received message + RNS.log("A message was received: "+str(message.content.decode('utf-8'))) + + message_content = message.content.decode('utf-8') + source_hash_text = RNS.hexrep(message.source_hash, delimit=False) + + #Create username (just first 5 char of your addr) + username = source_hash_text[0:5] + + RNS.log('Username: {}'.format(username), RNS.LOG_INFO) + + time_string = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(message.timestamp)) + new_message = '{} {}: {}\n'.format(time_string, username, message_content) + + # Push message to board + # First read message board (if it exists + if os.path.isfile(boardpath): + f = open(boardpath, "rb") + message_board = msgpack.unpack(f) + f.close() + else: + message_board = [] + + #Check we aren't doubling up (this can sometimes happen if there is an error initially and it then gets fixed) + if new_message not in message_board: + # Append our new message to the list + message_board.append(new_message) + + # Prune the message board if needed + while len(message_board) > max_messages: + RNS.log('Pruning Message Board') + message_board.pop(0) + + # Now open the board and write the updated list + f = open(boardpath, "wb") + msgpack.pack(message_board, f) + f.close() + + # Send reply + message_reply = '{}_{}_Your message has been added to the messageboard'.format(source_hash_text, time.time()) + q.put(message_reply) + +def announce_now(lxmf_destination): + lxmf_destination.announce() + +def send_message(destination_hash, message_content): + try: + # Make a binary destination hash from a hexadecimal string + destination_hash = bytes.fromhex(destination_hash) + + except Exception as e: + RNS.log("Invalid destination hash", RNS.LOG_ERROR) + return + + # Check that size is correct + if not len(destination_hash) == RNS.Reticulum.TRUNCATED_HASHLENGTH//8: + RNS.log("Invalid destination hash length", RNS.LOG_ERROR) + + else: + # Length of address was correct, let's try to recall the + # corresponding Identity + destination_identity = RNS.Identity.recall(destination_hash) + + if destination_identity == None: + # No path/identity known, we'll have to abort or request one + RNS.log("Could not recall an Identity for the requested address. You have probably never received an announce from it. Try requesting a path from the network first. In fact, let's do this now :)", RNS.LOG_ERROR) + RNS.Transport.request_path(destination_hash) + RNS.log("OK, a path was requested. If the network knows a path, you will receive an announce with the Identity data shortly.", RNS.LOG_INFO) + + else: + # We know the identity for the destination hash, let's + # reconstruct a destination object. + lxmf_destination = RNS.Destination(destination_identity, RNS.Destination.OUT, RNS.Destination.SINGLE, "lxmf", "delivery") + + # Create a new message object + lxm = LXMF.LXMessage(lxmf_destination, local_lxmf_destination, message_content, title="Reply", desired_method=LXMF.LXMessage.DIRECT) + + # You can optionally tell LXMF to try to send the message + # as a propagated message if a direct link fails + lxm.try_propagation_on_fail = True + + # Send it + message_router.handle_outbound(lxm) + +def announce_check(): + if os.path.isfile(announcepath): + f = open(announcepath, "r") + announce = int(f.readline()) + f.close() + else: + RNS.log('failed to open announcepath', RNS.LOG_DEBUG) + announce = 1 + + if announce > int(time.time()): + RNS.log('Recent announcement', RNS.LOG_DEBUG) + else: + f = open(announcepath, "w") + next_announce = int(time.time()) + 1800 + f.write(str(next_announce)) + f.close() + announce_now(local_lxmf_destination) + RNS.log('Announcement sent, expr set 1800 seconds', RNS.LOG_INFO) + +#Setup Paths and Config Files +userdir = os.path.expanduser("~") + +if os.path.isdir("/etc/nomadmb") and os.path.isfile("/etc/nomadmb/config"): + configdir = "/etc/nomadmb" +elif os.path.isdir(userdir+"/.config/nomadmb") and os.path.isfile(userdir+"/.config/nomadmb/config"): + configdir = userdir+"/.config/nomadmb" +else: + configdir = userdir+"/.nomadmb" + +storagepath = configdir+"/storage" +if not os.path.isdir(storagepath): + os.makedirs(storagepath) + +identitypath = configdir+"/storage/identity" +announcepath = configdir+"/storage/announce" +boardpath = configdir+"/storage/board" + +# Message Queue +q = Queue(maxsize = 5) + +# Start Reticulum and print out all the debug messages +reticulum = RNS.Reticulum(loglevel=RNS.LOG_VERBOSE) + +# Create a Identity. +current_identity = setup_lxmf() + +# Init the LXMF router +message_router = LXMF.LXMRouter(identity = current_identity, storagepath = configdir) + +# Register a delivery destination (for yourself) +# In this example we use the same Identity as we used +# to instantiate the LXMF router. It could be a different one, +# but it can also just be the same, depending on what you want. +local_lxmf_destination = message_router.register_delivery_identity(current_identity, display_name=display_name) + +# Set a callback for when a message is received +message_router.register_delivery_callback(lxmf_delivery) + +# Announce node properties + +RNS.log('LXMF Router ready to receive on: {}'.format(RNS.prettyhexrep(local_lxmf_destination.hash)), RNS.LOG_INFO) +announce_check() + +while True: + + # Work through internal message queue + for i in list(q.queue): + message_id = q.get() + split_message = message_id.split('_') + destination_hash = split_message[0] + message = split_message[2] + RNS.log('{} {}'.format(destination_hash, message), RNS.LOG_INFO) + send_message(destination_hash, message) + + # Check whether we need to make another announcement + announce_check() + + #Sleep + time.sleep(10) From 570b99d814b2a618de74e3310173063eb01320d3 Mon Sep 17 00:00:00 2001 From: chengtripp <124098378+chengtripp@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:17:38 +0000 Subject: [PATCH 10/17] Create messageboard.mu --- nomadnet/apps/messageboard/messageboard.mu | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 nomadnet/apps/messageboard/messageboard.mu diff --git a/nomadnet/apps/messageboard/messageboard.mu b/nomadnet/apps/messageboard/messageboard.mu new file mode 100644 index 0000000..24993d0 --- /dev/null +++ b/nomadnet/apps/messageboard/messageboard.mu @@ -0,0 +1,41 @@ +#!/bin/python3 +import time +import os +import RNS.vendor.umsgpack as msgpack + +message_board_peer = 'please_replace' +userdir = os.path.expanduser("~") + +if os.path.isdir("/etc/nomadmb") and os.path.isfile("/etc/nomadmb/config"): + configdir = "/etc/nomadmb" +elif os.path.isdir(userdir+"/.config/nomadmb") and os.path.isfile(userdir+"/.config/nomadmb/config"): + configdir = userdir+"/.config/nomadmb" +else: + configdir = userdir+"/.nomadmb" + +storagepath = configdir+"/storage" +if not os.path.isdir(storagepath): + os.makedirs(storagepath) + +boardpath = configdir+"/storage/board" + +print('`!`F222`Bddd`cNomadNet Message Board') + +print('-') +print('`a`b`f') +print("") +print("To add a message to the board just converse with the NomadNet Message Board at `[lxmf@{}]".format(message_board_peer)) +time_string = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())) +print("Last Updated: {}".format(time_string)) +print("") +print('>Messages') +print(" Date Time Username Message") +f = open(boardpath, "rb") +board_contents = msgpack.unpack(f) +board_contents.reverse() + +for content in board_contents: + print("`a{}".format(content.rstrip())) + print("") + +f.close() From 02a15f1503c1e9191c5d61f38674b09dcae2c4ec Mon Sep 17 00:00:00 2001 From: chengtripp <124098378+chengtripp@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:20:42 +0000 Subject: [PATCH 11/17] Create README.md --- nomadnet/apps/messageboard/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 nomadnet/apps/messageboard/README.md diff --git a/nomadnet/apps/messageboard/README.md b/nomadnet/apps/messageboard/README.md new file mode 100644 index 0000000..53c637b --- /dev/null +++ b/nomadnet/apps/messageboard/README.md @@ -0,0 +1,18 @@ +# lxmf_messageboard +Simple message board that can be hosted on a NomadNet node, messages can be posted by 'conversing' with a unique peer, all messages are then forwarded to the message board. + +## How Do I Use It? +A user can submit messages to the message board by initiating a chat with the message board peer, they are assigned a username (based on the first 5 characters of their address) and their messages are added directly to the message board. The message board can be viewed on a page hosted by a NomadNet node. + +An example message board can be found on the reticulum testnet hosted on the SolarExpress Node `` and the message board peer `` + +## How Does It Work? +The message board page itself is hosted on a NomadNet node, you can place the message_board.mu into the pages directory. You can then run the message_board.py script which provides the peer that the users can send messages to. The two parts are joined together using umsgpack and a flat file system similar to NomadNet and Reticulum and runs in the background. + +## How Do I Set It Up? +* Turn on node hosting in NomadNet +* Put the `message_board.mu` file into `pages` directory in the config file for `NomadNet`. Edit the file to customise from the default page. +* Run the `message_board.py` script (`python3 message_board.py` either in a `screen` or as a system service), this script uses `NomadNet` and `RNS` libraries and has no additional libraries that need to be installed. Take a note of the message boards address, it is printed on starting the board, you can then place this address in `message_board.mu` file to make it easier for users to interact the board. + +## Credits +* The send and receive functions in message_board.py are based on examples posted on the Reticulum Matrix channel by [Mark](https://github.com/markqvist) From a4f5b2870729eae6533edc4177b2d03ab4b14d14 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 16:42:23 +0100 Subject: [PATCH 12/17] Renamed directory --- nomadnet/{apps => examples}/messageboard/README.md | 2 +- nomadnet/{apps => examples}/messageboard/messageboard.mu | 0 nomadnet/{apps => examples}/messageboard/messageboard.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename nomadnet/{apps => examples}/messageboard/README.md (91%) rename nomadnet/{apps => examples}/messageboard/messageboard.mu (100%) rename nomadnet/{apps => examples}/messageboard/messageboard.py (100%) diff --git a/nomadnet/apps/messageboard/README.md b/nomadnet/examples/messageboard/README.md similarity index 91% rename from nomadnet/apps/messageboard/README.md rename to nomadnet/examples/messageboard/README.md index 53c637b..16e35e0 100644 --- a/nomadnet/apps/messageboard/README.md +++ b/nomadnet/examples/messageboard/README.md @@ -15,4 +15,4 @@ The message board page itself is hosted on a NomadNet node, you can place the me * Run the `message_board.py` script (`python3 message_board.py` either in a `screen` or as a system service), this script uses `NomadNet` and `RNS` libraries and has no additional libraries that need to be installed. Take a note of the message boards address, it is printed on starting the board, you can then place this address in `message_board.mu` file to make it easier for users to interact the board. ## Credits -* The send and receive functions in message_board.py are based on examples posted on the Reticulum Matrix channel by [Mark](https://github.com/markqvist) +* This example application was written and contributed by @chengtripp \ No newline at end of file diff --git a/nomadnet/apps/messageboard/messageboard.mu b/nomadnet/examples/messageboard/messageboard.mu similarity index 100% rename from nomadnet/apps/messageboard/messageboard.mu rename to nomadnet/examples/messageboard/messageboard.mu diff --git a/nomadnet/apps/messageboard/messageboard.py b/nomadnet/examples/messageboard/messageboard.py similarity index 100% rename from nomadnet/apps/messageboard/messageboard.py rename to nomadnet/examples/messageboard/messageboard.py From 681295a818db4cecfc457357e1ddb93d57494722 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 16:43:50 +0100 Subject: [PATCH 13/17] Create examples directory on init --- nomadnet/NomadNetworkApp.py | 4 ++++ nomadnet/vendor/quotes.py | 1 + setup.py | 7 +++++++ 3 files changed, 12 insertions(+) diff --git a/nomadnet/NomadNetworkApp.py b/nomadnet/NomadNetworkApp.py index 701869b..2efa8c1 100644 --- a/nomadnet/NomadNetworkApp.py +++ b/nomadnet/NomadNetworkApp.py @@ -111,6 +111,7 @@ class NomadNetworkApp: self.pagespath = self.configdir+"/storage/pages" self.filespath = self.configdir+"/storage/files" self.cachepath = self.configdir+"/storage/cache" + self.examplespath = self.configdir+"/examples" self.downloads_path = os.path.expanduser("~/Downloads") @@ -169,6 +170,9 @@ class NomadNetworkApp: else: RNS.log("Could not load config file, creating default configuration file...") self.createDefaultConfig() + if not os.path.isdir(self.examplespath): + os.makedirs(self.examplespath) + self.firstrun = True if os.path.isfile(self.identitypath): diff --git a/nomadnet/vendor/quotes.py b/nomadnet/vendor/quotes.py index ee142f7..4c87571 100644 --- a/nomadnet/vendor/quotes.py +++ b/nomadnet/vendor/quotes.py @@ -3,4 +3,5 @@ quotes = [ ("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"), ("The landscape of the future is set, but how one will march across it is not determined", "Terence McKenna") + ("Freedom originates in the division of power, despotism in its concentration.", "John Acton") ] \ No newline at end of file diff --git a/setup.py b/setup.py index fadae5d..b3fadd6 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,12 @@ exec(open("nomadnet/_version.py", "r").read()) with open("README.md", "r") as fh: long_description = fh.read() +package_data = { +"": [ + "examples/messageboard/*", + ] +} + setuptools.setup( name="nomadnet", version=__version__, @@ -15,6 +21,7 @@ setuptools.setup( long_description_content_type="text/markdown", url="https://github.com/markqvist/nomadnet", packages=setuptools.find_packages(), + package_data=package_data, classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", From 6aca6d671a1bdf45651c6fbf99d79cd828d255d9 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 17:04:16 +0100 Subject: [PATCH 14/17] Add node examples on first run --- nomadnet/NomadNetworkApp.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nomadnet/NomadNetworkApp.py b/nomadnet/NomadNetworkApp.py index 2efa8c1..db94ce9 100644 --- a/nomadnet/NomadNetworkApp.py +++ b/nomadnet/NomadNetworkApp.py @@ -168,11 +168,19 @@ class NomadNetworkApp: RNS.log("Check your configuration file for errors!", RNS.LOG_ERROR) nomadnet.panic() else: + if not os.path.isdir(self.examplespath): + try: + os.makedirs(self.examplespath) + from distutils.dir_util import copy_tree + examplespath = os.path.join(os.path.dirname(__file__), "examples") + copy_tree(examplespath, self.examplespath) + + except Exception as e: + RNS.log("Could not copy examples into the "+self.examplespath+" directory.", RNS.LOG_ERROR) + RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) + RNS.log("Could not load config file, creating default configuration file...") self.createDefaultConfig() - if not os.path.isdir(self.examplespath): - os.makedirs(self.examplespath) - self.firstrun = True if os.path.isfile(self.identitypath): From f4e3f1cb46738c47e936d39820abc8c4fa69dd3c Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 4 Feb 2023 19:06:16 +0100 Subject: [PATCH 15/17] Excluded pycache from example inclusions --- nomadnet/NomadNetworkApp.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nomadnet/NomadNetworkApp.py b/nomadnet/NomadNetworkApp.py index db94ce9..cd11bfe 100644 --- a/nomadnet/NomadNetworkApp.py +++ b/nomadnet/NomadNetworkApp.py @@ -170,10 +170,9 @@ class NomadNetworkApp: else: if not os.path.isdir(self.examplespath): try: - os.makedirs(self.examplespath) - from distutils.dir_util import copy_tree + import shutil examplespath = os.path.join(os.path.dirname(__file__), "examples") - copy_tree(examplespath, self.examplespath) + shutil.copytree(examplespath, self.examplespath, ignore=shutil.ignore_patterns("__pycache__")) except Exception as e: RNS.log("Could not copy examples into the "+self.examplespath+" directory.", RNS.LOG_ERROR) From 2d6cd8e33fc9f25a997424cfdf1427bad4d577a4 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Thu, 9 Feb 2023 12:40:35 +0100 Subject: [PATCH 16/17] Catch unhandled Ctrl-E before passing to terminal --- nomadnet/ui/TextUI.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nomadnet/ui/TextUI.py b/nomadnet/ui/TextUI.py index 6389aac..3f38744 100644 --- a/nomadnet/ui/TextUI.py +++ b/nomadnet/ui/TextUI.py @@ -217,6 +217,8 @@ class TextUI: def unhandled_input(self, key): if key == "ctrl q": raise urwid.ExitMainLoop + elif key == "ctrl e": + pass def display_main(self, loop, user_data): self.loop.widget = self.main_display.widget From 4ec1a4979f1ffd95d436ee201b8ccf75d6fb854a Mon Sep 17 00:00:00 2001 From: Dionysis Grigoropoulos Date: Thu, 9 Feb 2023 13:45:37 +0200 Subject: [PATCH 17/17] Fix bug where Nomadnet crashes on empty lists --- nomadnet/ui/textui/Conversations.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/nomadnet/ui/textui/Conversations.py b/nomadnet/ui/textui/Conversations.py index 9aa1978..3929bd1 100644 --- a/nomadnet/ui/textui/Conversations.py +++ b/nomadnet/ui/textui/Conversations.py @@ -110,7 +110,10 @@ class ConversationsDisplay(): def delete_selected_conversation(self): self.dialog_open = True - source_hash = self.ilb.get_selected_item().source_hash + item = self.ilb.get_selected_item() + if item == None: + return + source_hash = item.source_hash def dismiss_dialog(sender): self.update_conversation_list() @@ -140,7 +143,10 @@ class ConversationsDisplay(): def edit_selected_in_directory(self): g = self.app.ui.glyphs self.dialog_open = True - source_hash_text = self.ilb.get_selected_item().source_hash + item = self.ilb.get_selected_item() + if item == None: + return + source_hash_text = item.source_hash display_name = self.ilb.get_selected_item().display_name if display_name == None: display_name = ""