Compare commits

..

19 Commits
0.0.8 ... 0.1.0

Author SHA1 Message Date
Mark Qvist
953c8c94c7 Fixed browser view on non node hosting instances. 2021-09-19 00:30:07 +02:00
Mark Qvist
0c94dfa482 Updated version 2021-09-19 00:27:19 +02:00
Mark Qvist
08f1d4cda7 Fixed node info view on non node hosting instances. 2021-09-19 00:25:13 +02:00
Mark Qvist
718e2ca859 Fixed None reference 2021-09-19 00:15:26 +02:00
Mark Qvist
9dc09be4ae Updated RNS version. 2021-09-18 23:35:30 +02:00
Mark Qvist
41ce3e8800 Updated readme 2021-09-18 15:29:40 +02:00
Mark Qvist
29489353fc Update README.md 2021-09-18 15:14:54 +02:00
Mark Qvist
c27b2e06b8 Updated readme 2021-09-18 15:11:40 +02:00
Mark Qvist
f41d1eecb0 Updated screenshots. Fixed URL dialog keypress handler. 2021-09-18 15:09:41 +02:00
Mark Qvist
012721ee57 Updated readme. Fixed a potential memory leak. 2021-09-18 14:01:14 +02:00
Mark Qvist
959cc13167 Added light theme 2021-09-17 21:16:30 +02:00
Mark Qvist
48a5001009 Added display test 2021-09-17 20:35:38 +02:00
Mark Qvist
a77f423362 Added page preprocessing 2021-09-17 19:04:23 +02:00
Mark Qvist
b8e67721b8 Implemented page caching and cache control headers 2021-09-17 18:45:08 +02:00
Mark Qvist
913c7935c0 Added URL entry dialog 2021-09-17 12:23:13 +02:00
Mark Qvist
0d93e7e8d2 Added back, forward and reload actions to browser. 2021-09-17 11:47:41 +02:00
Mark Qvist
150076be6b Implemented local node browsing. 2021-09-16 21:56:59 +02:00
Mark Qvist
a6376c716d Added local node info box 2021-09-16 20:44:15 +02:00
Mark Qvist
7d5bb8e016 User interface enhancements 2021-09-16 19:54:32 +02:00
17 changed files with 700 additions and 80 deletions

View File

@@ -1,8 +1,6 @@
Nomad Network Nomad Network - Communicate Freely
========== ==========
Communicate Freely.
![Screenshot](https://github.com/markqvist/NomadNet/raw/master/docs/screenshots/1.png) ![Screenshot](https://github.com/markqvist/NomadNet/raw/master/docs/screenshots/1.png)
The intention with this program is to provide a tool to that allows you to build private and resilient communications platforms that are in complete control and ownership of the people that use them. The intention with this program is to provide a tool to that allows you to build private and resilient communications platforms that are in complete control and ownership of the people that use them.
@@ -15,17 +13,19 @@ Nomad Network does not need any connections to the public internet to work. In f
- Encrypted messaging over packet-radio, LoRa, WiFi or anything else [Reticulum](https://github.com/markqvist/Reticulum) supports. - Encrypted messaging over packet-radio, LoRa, WiFi or anything else [Reticulum](https://github.com/markqvist/Reticulum) supports.
- Zero-configuration, minimal-infrastructure mesh communication - Zero-configuration, minimal-infrastructure mesh communication
- Connectable nodes that can host pages and files - Connectable nodes that can host pages and files
- Node-side generated pages with PHP, Python, bash or others
- Built-in text-based browser for interacting with contents on nodes - Built-in text-based browser for interacting with contents on nodes
- An easy to use and bandwidth efficient markup language for writing pages - An easy to use and bandwidth efficient markup language for writing pages
- Page caching in browser
## Current Status ## Current Status
The current version of the program should be considered a beta release. The program works well, but there will most probably be bugs and possibly sub-optimal performance in some scenarios. On the other hand, this is the ideal time to have an influence on the direction of the development of Nomad Network. To do so, join the discussion, report bugs and request features here on the GitHub project. The current version of the program should be considered a beta release. The program works well, but there will most probably be bugs and possibly sub-optimal performance in some scenarios. On the other hand, this is the ideal time to have an influence on the direction of the development of Nomad Network. To do so, join the discussion, report bugs and request features here on the GitHub project.
### Feature roadmap ### Feature roadmap
- Page caching in browser - Access control and authentication for nodes, pages and files
- Node-side generated pages with PHP, Python, bash or others - Network-wide propagated messaging and discussion threads
- Access control and authentication for pages and files - Geospatial information sharing
- Propagated messaging and discussion threads - Distributed Marketplace
## Dependencies: ## Dependencies:
- Python 3 - Python 3
@@ -43,7 +43,21 @@ pip3 install nomadnet
nomadnet nomadnet
``` ```
The first time the program is running, you will be presented with the guide section, which contains all the information you need to start using Nomad Network.
To use Nomad Network on packet radio or LoRa, you will need to configure your Reticulum installation to use any relevant packet radio TNCs or LoRa devices on your system. See the Reticulum documentation for info. To use Nomad Network on packet radio or LoRa, you will need to configure your Reticulum installation to use any relevant packet radio TNCs or LoRa devices on your system. See the Reticulum documentation for info.
## Caveat Emptor ## Caveat Emptor
Nomad Network is experimental software, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy-breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch. Nomad Network is beta software, and should be considered as such. While it has been built with cryptography best-practices very foremost in mind, it _has not_ been externally security audited, and there could very well be privacy-breaking bugs. If you want to help out, or help sponsor an audit, please do get in touch.
## Screenshots
![Screenshot 1](https://github.com/markqvist/NomadNet/raw/master/docs/screenshots/1.png)
![Screenshot 2](https://github.com/markqvist/NomadNet/raw/master/docs/screenshots/2.png)
![Screenshot 3](https://github.com/markqvist/NomadNet/raw/master/docs/screenshots/3.png)
![Screenshot 4](https://github.com/markqvist/NomadNet/raw/master/docs/screenshots/4.png)
![Screenshot 5](https://github.com/markqvist/NomadNet/raw/master/docs/screenshots/5.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 86 KiB

BIN
docs/screenshots/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
docs/screenshots/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -2,6 +2,7 @@ import os
import RNS import RNS
import time import time
import threading import threading
import subprocess
import RNS.vendor.umsgpack as msgpack import RNS.vendor.umsgpack as msgpack
class Node: class Node:
@@ -87,6 +88,10 @@ class Node:
file_path = path.replace("/page", self.app.pagespath, 1) file_path = path.replace("/page", self.app.pagespath, 1)
try: try:
RNS.log("Serving page: "+file_path, RNS.LOG_VERBOSE) RNS.log("Serving page: "+file_path, RNS.LOG_VERBOSE)
if os.access(file_path, os.X_OK):
generated = subprocess.run([file_path], stdout=subprocess.PIPE)
return generated.stdout
else:
fh = open(file_path, "rb") fh = open(file_path, "rb")
response_data = fh.read() response_data = fh.read()
fh.close() fh.close()
@@ -121,6 +126,7 @@ class Node:
def announce(self): def announce(self):
self.app_data = self.name.encode("utf-8") self.app_data = self.name.encode("utf-8")
self.last_announce = time.time() self.last_announce = time.time()
self.app.peer_settings["node_last_announce"] = self.last_announce
self.destination.announce(app_data=self.app_data) self.destination.announce(app_data=self.app_data)
def __jobs(self): def __jobs(self):

View File

@@ -53,11 +53,14 @@ class NomadNetworkApp:
self.pagespath = self.configdir+"/storage/pages" self.pagespath = self.configdir+"/storage/pages"
self.filespath = self.configdir+"/storage/files" self.filespath = self.configdir+"/storage/files"
self.cachepath = self.configdir+"/storage/cache"
self.downloads_path = os.path.expanduser("~/Downloads") self.downloads_path = os.path.expanduser("~/Downloads")
self.firstrun = False self.firstrun = False
self.peer_announce_at_start = True
if not os.path.isdir(self.storagepath): if not os.path.isdir(self.storagepath):
os.makedirs(self.storagepath) os.makedirs(self.storagepath)
@@ -76,6 +79,9 @@ class NomadNetworkApp:
if not os.path.isdir(self.filespath): if not os.path.isdir(self.filespath):
os.makedirs(self.filespath) os.makedirs(self.filespath)
if not os.path.isdir(self.cachepath):
os.makedirs(self.cachepath)
if os.path.isfile(self.configpath): if os.path.isfile(self.configpath):
try: try:
self.config = ConfigObj(self.configpath) self.config = ConfigObj(self.configpath)
@@ -123,6 +129,10 @@ class NomadNetworkApp:
file = open(self.peersettingspath, "rb") file = open(self.peersettingspath, "rb")
self.peer_settings = msgpack.unpackb(file.read()) self.peer_settings = msgpack.unpackb(file.read())
file.close() file.close()
if not "node_last_announce" in self.peer_settings:
self.peer_settings["node_last_announce"] = None
except Exception as e: except Exception as e:
RNS.log("Could not load local peer settings from "+self.peersettingspath, RNS.LOG_ERROR) RNS.log("Could not load local peer settings from "+self.peersettingspath, RNS.LOG_ERROR)
RNS.log("The contained exception was: %s" % (str(e)), RNS.LOG_ERROR) RNS.log("The contained exception was: %s" % (str(e)), RNS.LOG_ERROR)
@@ -134,6 +144,7 @@ class NomadNetworkApp:
"display_name": "Anonymous Peer", "display_name": "Anonymous Peer",
"announce_interval": None, "announce_interval": None,
"last_announce": None, "last_announce": None,
"node_last_announce": None,
} }
self.save_peer_settings() self.save_peer_settings()
RNS.log("Created new peer settings file") RNS.log("Created new peer settings file")
@@ -170,6 +181,9 @@ 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)
if self.peer_announce_at_start:
self.announce_now()
nomadnet.ui.spawn(self.uimode) nomadnet.ui.spawn(self.uimode)
def set_display_name(self, display_name): def set_display_name(self, display_name):
@@ -262,6 +276,10 @@ class NomadNetworkApp:
value = self.config["client"]["downloads_path"] value = self.config["client"]["downloads_path"]
self.downloads_path = os.path.expanduser(value) self.downloads_path = os.path.expanduser(value)
if option == "announce_at_start":
value = self.config["client"].as_bool(option)
self.peer_announce_at_start = value
if option == "user_interface": if option == "user_interface":
value = value.lower() value = value.lower()
if value == "none": if value == "none":
@@ -347,7 +365,8 @@ class NomadNetworkApp:
if not "announce_at_start" in self.config["node"]: if not "announce_at_start" in self.config["node"]:
self.node_announce_at_start = False self.node_announce_at_start = False
else: else:
self.node_announce_at_start = self.config["node"]["announce_at_start"] value = self.config["node"].as_bool("announce_at_start")
self.node_announce_at_start = value
if not "announce_interval" in self.config["node"]: if not "announce_interval" in self.config["node"]:
self.node_announce_interval = 720 self.node_announce_interval = 720
@@ -401,11 +420,19 @@ enable_client = yes
user_interface = text user_interface = text
downloads_path = ~/Downloads downloads_path = ~/Downloads
# By default, the peer is announced at startup
# to let other peers reach it immediately.
announce_at_start = yes
[textui] [textui]
# Amount of time to show intro screen # Amount of time to show intro screen
intro_time = 1 intro_time = 1
# You can specify the display theme.
# theme = dark
theme = light
# Specify the number of colors to use # Specify the number of colors to use
# valid colormodes are: # valid colormodes are:
# monochrome, 16, 88, 256 and 24bit # monochrome, 16, 88, 256 and 24bit

View File

@@ -1 +1 @@
__version__ = "0.0.8" __version__ = "0.1.0"

View File

@@ -1,6 +1,7 @@
import RNS import RNS
import importlib import urwid
import time import time
import os
import nomadnet import nomadnet
from nomadnet.ui.textui import * from nomadnet.ui.textui import *
@@ -47,6 +48,39 @@ THEMES = {
("progress_full", "black", "light gray", "standout", "#111", "#bbb"), ("progress_full", "black", "light gray", "standout", "#111", "#bbb"),
("progress_empty", "light gray", "default", "default", "#ddd", "default"), ("progress_empty", "light gray", "default", "default", "#ddd", "default"),
], ],
},
THEME_LIGHT: {
"urwid_theme": [
# Style name # 16-color style # Monochrome style # 88, 256 and true-color style
("heading", "dark gray,underline", "default", "underline", "g93,underline", "default"),
("menubar", "black", "dark gray", "standout", "#111", "#bbb"),
("scrollbar", "dark gray", "default", "standout", "#444", "default"),
("shortcutbar", "black", "dark gray", "standout", "#111", "#bbb"),
("body_text", "black", "default", "default", "#222", "default"),
("error_text", "dark red", "default", "default", "dark red", "default"),
("warning_text", "yellow", "default", "default", "#ba4", "default"),
("inactive_text", "light gray", "default", "default", "dark gray", "default"),
("buttons", "light green,bold", "default", "default", "#00a533", "default"),
("msg_editor", "black", "dark cyan", "standout", "#111", "#0bb"),
("msg_header_ok", "black", "dark green", "standout", "#111", "#6b2"),
("msg_header_caution", "black", "yellow", "standout", "#111", "#fd3"),
("msg_header_sent", "black", "dark gray", "standout", "#111", "#ddd"),
("msg_header_delivered", "black", "light blue", "standout", "#111", "#28b"),
("msg_header_failed", "black", "dark gray", "standout", "#000", "#777"),
("msg_warning_untrusted", "black", "dark red", "standout", "#111", "dark red"),
("list_focus", "black", "dark gray", "standout", "#111", "#aaa"),
("list_off_focus", "black", "dark gray", "standout", "#111", "#777"),
("list_trusted", "dark green", "default", "default", "#4a0", "default"),
("list_focus_trusted", "black", "dark gray", "standout", "#150", "#aaa"),
("list_unknown", "dark gray", "default", "default", "#444", "default"),
("list_normal", "dark gray", "default", "default", "#444", "default"),
("list_untrusted", "dark red", "default", "default", "#a22", "default"),
("list_focus_untrusted", "black", "dark gray", "standout", "#810", "#aaa"),
("topic_list_normal", "black", "default", "default", "#222", "default"),
("browser_controls", "dark gray", "default", "default", "#444", "default"),
("progress_full", "black", "dark gray", "standout", "#111", "#bbb"),
("progress_empty", "dark gray", "default", "default", "#ddd", "default"),
],
} }
} }
@@ -61,21 +95,22 @@ GLYPHS = {
("check", "=", "\u2713", "\u2713"), ("check", "=", "\u2713", "\u2713"),
("cross", "X", "\u2715", "\u2715"), ("cross", "X", "\u2715", "\u2715"),
("unknown", "?", "?", "?"), ("unknown", "?", "?", "?"),
("lock", "E", "\U0001f512", "\uf023"), ("encrypted", "", "\u26BF", "\uf023"),
("unlock", "!", "\U0001f513", "\uf09c"), ("plaintext", "!", "!", "\uf06e "),
("arrow_r", "->", "\u2192", "\u2192"), ("arrow_r", "->", "\u2192", "\u2192"),
("arrow_l", "<-", "\u2190", "\u2190"), ("arrow_l", "<-", "\u2190", "\u2190"),
("arrow_u", "/\\", "\u2191", "\u2191"), ("arrow_u", "/\\", "\u2191", "\u2191"),
("arrow_d", "\\/", "\u2193", "\u2193"), ("arrow_d", "\\/", "\u2193", "\u2193"),
("warning", "!", "\u26a0", "\uf12a"), ("warning", "!", "\u26a0", "\uf12a"),
("info", "i", "\u2139", "\ufb4d"), ("info", "i", "\u2139", "\ufb4d"),
("unread", "N", "\u2709", "\uf003 "), ("unread", "[U]", "\u2709", "\uf003 "),
("divider1", "-", "\u2504", "\u2504"), ("divider1", "-", "\u2504", "\u2504"),
("peer", "P", "\U0001F464", "\uf415"), ("peer", "[P]", "\u24c5 ", "\uf415"),
("node", "N", "\U0001F5A5", "\uf502"), ("node", "[N]", "\u24c3 ", "\uf502"),
("page", "", "\U0001F5B9", "\uf719 "), ("page", "", "\u25a4", "\uf719 "),
("speed", "", "\U0001F5B9", "\uf9c4"), ("speed", "", "\u25F7", "\uf9c4"),
("decoration_menu", "", "", " \uf93a"), ("decoration_menu", "", "", " \uf93a"),
("globe", "", "", "\uf484"),
} }
class TextUI: class TextUI:
@@ -85,12 +120,13 @@ class TextUI:
self.app.ui = self self.app.ui = self
self.loop = None self.loop = None
if importlib.util.find_spec("urwid") != None: # TODO: Remove
import urwid # if importlib.util.find_spec("urwid") != None:
else: # import urwid
RNS.log("The text-mode user interface requires Urwid to be installed on your system.", RNS.LOG_ERROR) # else:
RNS.log("You can install it with the command: pip3 install urwid", RNS.LOG_ERROR) # RNS.log("The text-mode user interface requires Urwid to be installed on your system.", RNS.LOG_ERROR)
nomadnet.panic() # RNS.log("You can install it with the command: pip3 install urwid", RNS.LOG_ERROR)
# nomadnet.panic()
urwid.set_encoding("UTF-8") urwid.set_encoding("UTF-8")
@@ -130,10 +166,20 @@ class TextUI:
if intro_timeout > 0: if intro_timeout > 0:
self.loop.set_alarm_in(intro_timeout, self.display_main) self.loop.set_alarm_in(intro_timeout, self.display_main)
# TODO: Probably remove this at some point when better terminal if "KONSOLE_VERSION" in os.environ:
# color capability detection has been implemented
if colormode > 16: if colormode > 16:
RNS.log("Starting Text UI in "+str(colormode)+" color mode. If no UI appears, try adjusting your color settings in "+str(self.app.configdir)+"/config", RNS.LOG_INFO, _override_destination = True) RNS.log("", RNS.LOG_WARNING, _override_destination = True)
RNS.log("", RNS.LOG_WARNING, _override_destination = True)
RNS.log("You are using the terminal emulator Konsole.", RNS.LOG_WARNING, _override_destination = True)
RNS.log("If you are not seeing the user interface, it is due to a bug in Konsole/urwid.", RNS.LOG_WARNING, _override_destination = True)
RNS.log("", RNS.LOG_WARNING, _override_destination = True)
RNS.log("To circumvent this, use another terminal emulator, or launch nomadnet within a", RNS.LOG_WARNING, _override_destination = True)
RNS.log("screen session, using a command like the following:", RNS.LOG_WARNING, _override_destination = True)
RNS.log("", RNS.LOG_WARNING, _override_destination = True)
RNS.log("screen nomadnet", RNS.LOG_WARNING, _override_destination = True)
RNS.log("", RNS.LOG_WARNING, _override_destination = True)
RNS.log("Press ctrl-c to exit now and try again.", RNS.LOG_WARNING, _override_destination = True)
self.set_colormode(colormode) self.set_colormode(colormode)

View File

@@ -3,6 +3,7 @@ import os
import time import time
import urwid import urwid
import nomadnet import nomadnet
import subprocess
import threading import threading
from .MicronParser import markup_to_attrmaps from .MicronParser import markup_to_attrmaps
from nomadnet.vendor.Scrollable import * from nomadnet.vendor.Scrollable import *
@@ -11,6 +12,14 @@ class BrowserFrame(urwid.Frame):
def keypress(self, size, key): def keypress(self, size, key):
if key == "ctrl w": if key == "ctrl w":
self.delegate.disconnect() self.delegate.disconnect()
elif key == "ctrl d":
self.delegate.back()
elif key == "ctrl f":
self.delegate.forward()
elif key == "ctrl r":
self.delegate.reload()
elif key == "ctrl u":
self.delegate.url_dialog()
elif self.get_focus() == "body": elif self.get_focus() == "body":
return super(BrowserFrame, self).keypress(size, key) return super(BrowserFrame, self).keypress(size, key)
# if key == "up" and self.delegate.messagelist.top_is_visible: # if key == "up" and self.delegate.messagelist.top_is_visible:
@@ -25,6 +34,7 @@ class BrowserFrame(urwid.Frame):
class Browser: class Browser:
DEFAULT_PATH = "/page/index.mu" DEFAULT_PATH = "/page/index.mu"
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
DEFAULT_CACHE_TIME = 12*60*60
NO_PATH = 0x00 NO_PATH = 0x00
PATH_REQUESTED = 0x01 PATH_REQUESTED = 0x01
@@ -51,6 +61,7 @@ class Browser:
self.last_keypress = None self.last_keypress = None
self.link = None self.link = None
self.loopback = None
self.status = Browser.DISCONECTED self.status = Browser.DISCONECTED
self.response_progress = 0 self.response_progress = 0
self.response_size = None self.response_size = None
@@ -66,6 +77,13 @@ class Browser:
self.attr_maps = [] self.attr_maps = []
self.build_display() self.build_display()
self.history = []
self.history_ptr = 0
self.history_inc = False
self.history_dec = False
self.reloading = False
self.loaded_from_cache = False
if self.path == None: if self.path == None:
self.path = Browser.DEFAULT_PATH self.path = Browser.DEFAULT_PATH
@@ -82,8 +100,16 @@ class Browser:
path = self.path path = self.path
return RNS.hexrep(self.destination_hash, delimit=False)+":"+path return RNS.hexrep(self.destination_hash, delimit=False)+":"+path
def url_hash(self, url):
if url == None:
return None
else:
url = url.encode("utf-8")
return RNS.hexrep(RNS.Identity.full_hash(url), delimit=False)
def marked_link(self, link_target): def marked_link(self, link_target):
if self.status == Browser.DONE:
self.link_target = link_target self.link_target = link_target
self.app.ui.loop.set_alarm_in(0.1, self.marked_link_job) self.app.ui.loop.set_alarm_in(0.1, self.marked_link_job)
@@ -175,7 +201,11 @@ class Browser:
else: else:
self.display_widget.set_attr_map({None: "body_text"}) self.display_widget.set_attr_map({None: "body_text"})
self.browser_header = self.make_control_widget() self.browser_header = self.make_control_widget()
self.linebox.set_title(self.app.directory.simplest_display_str(self.destination_hash)) remote_display_string = self.app.directory.simplest_display_str(self.destination_hash)
if self.loopback != None and remote_display_string == RNS.prettyhexrep(self.loopback):
remote_display_string = self.app.node.name
self.linebox.set_title(remote_display_string)
if self.status == Browser.DONE: if self.status == Browser.DONE:
self.browser_footer = self.make_status_widget() self.browser_footer = self.make_status_widget()
@@ -226,6 +256,11 @@ class Browser:
self.response_size = None self.response_size = None
self.response_transfer_size = None self.response_transfer_size = None
self.history = []
self.history_ptr = 0
self.history_inc = False
self.history_dec = False
self.update_display() self.update_display()
@@ -271,11 +306,14 @@ class Browser:
if destination_hash != None and path != None: if destination_hash != None and path != None:
if path.startswith("/file/"): if path.startswith("/file/"):
if destination_hash != self.loopback:
if destination_hash == self.destination_hash: if destination_hash == self.destination_hash:
self.download_file(destination_hash, path) self.download_file(destination_hash, path)
else: else:
RNS.log("Cannot request file download from a node that is not currently connected.", RNS.LOG_ERROR) RNS.log("Cannot request file download from a node that is not currently connected.", RNS.LOG_ERROR)
RNS.log("The requested URL was: "+str(url), RNS.LOG_ERROR) RNS.log("The requested URL was: "+str(url), RNS.LOG_ERROR)
else:
self.download_local_file(path)
else: else:
self.set_destination_hash(destination_hash) self.set_destination_hash(destination_hash)
self.set_path(path) self.set_path(path)
@@ -296,6 +334,35 @@ class Browser:
def set_timeout(self, timeout): def set_timeout(self, timeout):
self.timeout = timeout self.timeout = timeout
def download_local_file(self, path):
try:
file_path = self.app.filespath+path.replace("/file", "", 1)
if os.path.isfile(file_path):
file_name = os.path.basename(file_path)
file_destination = self.app.downloads_path+"/"+file_name
counter = 0
while os.path.isfile(file_destination):
counter += 1
file_destination = self.app.downloads_path+"/"+file_name+"."+str(counter)
fs = open(file_path, "rb")
fd = open(file_destination, "wb")
fd.write(fs.read())
fd.close()
fs.close()
self.saved_file_name = file_destination.replace(self.app.downloads_path+"/", "", 1)
self.update_display()
else:
RNS.log("The requested local download file does not exist: "+str(file_path), RNS.LOG_ERROR)
self.status = Browser.DONE
self.response_progress = 0
except Exception as e:
RNS.log("An error occurred while handling file response. The contained exception was: "+str(e), RNS.LOG_ERROR)
def download_file(self, destination_hash, path): def download_file(self, destination_hash, path):
if self.link != None and self.link.destination.hash == self.destination_hash: if self.link != None and self.link.destination.hash == self.destination_hash:
@@ -323,11 +390,133 @@ class Browser:
else: else:
self.link.teardown() self.link.teardown()
def write_history(self):
entry = [self.destination_hash, self.path]
self.history.insert(self.history_ptr, entry)
self.history_ptr += 1
if len(self.history) > self.history_ptr:
self.history = self.history[:self.history_ptr]
def back(self):
target_ptr = self.history_ptr-1
if not self.history_inc and not self.history_dec:
if target_ptr > 0:
self.history_dec = True
entry = self.history[target_ptr-1]
url = RNS.hexrep(entry[0], delimit=False)+":"+entry[1]
self.history_ptr = target_ptr
self.retrieve_url(url)
def forward(self):
target_ptr = self.history_ptr+1
if not self.history_inc and not self.history_dec:
if target_ptr <= len(self.history):
self.history_dec = True
entry = self.history[target_ptr-1]
url = RNS.hexrep(entry[0], delimit=False)+":"+entry[1]
self.history_ptr = target_ptr
self.retrieve_url(url)
def reload(self):
if not self.reloading and self.status == Browser.DONE:
self.reloading = True
self.uncache_page(self.current_url())
self.load_page()
def close_dialogs(self):
options = self.delegate.columns.options("weight", self.delegate.right_area_width)
self.delegate.columns.contents[1] = (self.display_widget, options)
def url_dialog(self):
e_url = UrlEdit(caption="URL : ", edit_text=self.current_url())
def dismiss_dialog(sender):
self.close_dialogs()
def confirmed(sender):
self.retrieve_url(e_url.get_edit_text())
self.close_dialogs()
dialog = UrlDialogLineBox(
urwid.Pile([
e_url,
urwid.Columns([("weight", 0.45, urwid.Button("Cancel", on_press=dismiss_dialog)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Go", on_press=confirmed))])
]), title="Enter URL"
)
e_url.confirmed = confirmed
dialog.confirmed = confirmed
dialog.delegate = self
bottom = self.display_widget
overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 65), valign="middle", height="pack", left=2, right=2)
options = self.delegate.columns.options("weight", self.delegate.right_area_width)
self.delegate.columns.contents[1] = (overlay, options)
self.delegate.columns.focus_position = 1
def load_page(self): def load_page(self):
cached = self.get_cached(self.current_url())
if cached:
self.status = Browser.DONE
self.page_data = cached
self.markup = self.page_data.decode("utf-8")
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self)
self.response_progress = 0
self.response_size = None
self.response_transfer_size = None
self.saved_file_name = None
self.loaded_from_cache = True
self.update_display()
if not self.history_inc and not self.history_dec and not self.reloading:
self.write_history()
else:
self.history_dec = False
self.history_inc = False
self.reloading = False
else:
if self.destination_hash != self.loopback:
load_thread = threading.Thread(target=self.__load) load_thread = threading.Thread(target=self.__load)
load_thread.setDaemon(True) load_thread.setDaemon(True)
load_thread.start() load_thread.start()
else:
RNS.log("Browser handling local page: "+str(self.path), RNS.LOG_DEBUG)
page_path = self.app.pagespath+self.path.replace("/page", "", 1)
page_data = b"The requested local page did not exist in the file system"
if os.path.isfile(page_path):
if os.access(page_path, os.X_OK):
generated = subprocess.run([page_path], stdout=subprocess.PIPE)
page_data = generated.stdout
else:
file = open(page_path, "rb")
page_data = file.read()
file.close()
self.status = Browser.DONE
self.page_data = page_data
self.markup = self.page_data.decode("utf-8")
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self)
self.response_progress = 0
self.response_size = None
self.response_transfer_size = None
self.saved_file_name = None
self.loaded_from_cache = False
self.update_display()
if not self.history_inc and not self.history_dec and not self.reloading:
self.write_history()
else:
self.history_dec = False
self.history_inc = False
self.reloading = False
def __load(self): def __load(self):
@@ -436,11 +625,98 @@ class Browser:
self.markup = self.page_data.decode("utf-8") self.markup = self.page_data.decode("utf-8")
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self) self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self)
self.response_progress = 0 self.response_progress = 0
self.loaded_from_cache = False
# Simple header handling. Should be expanded when more
# header tags are added.
cache_time = Browser.DEFAULT_CACHE_TIME
if self.markup[:4] == "#!c=":
endpos = self.markup.find("\n")
if endpos == -1:
endpos = len(self.markup)
cache_time = int(self.markup[4:endpos])
self.update_display() self.update_display()
if not self.history_inc and not self.history_dec and not self.reloading:
self.write_history()
else:
self.history_dec = False
self.history_inc = False
self.reloading = False
if cache_time == 0:
RNS.log("Received page "+str(self.current_url())+", not caching due to header.", RNS.LOG_DEBUG)
else:
RNS.log("Received page "+str(self.current_url())+", caching for %.3f hours." % (cache_time/60/60), RNS.LOG_DEBUG)
self.cache_page(cache_time)
except Exception as e: except Exception as e:
RNS.log("An error occurred while handling response. The contained exception was: "+str(e)) RNS.log("An error occurred while handling response. The contained exception was: "+str(e))
def uncache_page(self, url):
url_hash = self.url_hash(url)
files = os.listdir(self.app.cachepath)
for file in files:
if file.startswith(url_hash):
cachefile = self.app.cachepath+"/"+file
os.unlink(cachefile)
RNS.log("Removed "+str(cachefile)+" from cache.", RNS.LOG_DEBUG)
def get_cached(self, url):
url_hash = self.url_hash(url)
files = os.listdir(self.app.cachepath)
for file in files:
cachepath = self.app.cachepath+"/"+file
try:
components = file.split("_")
if len(components) == 2 and len(components[0]) == 64 and len(components[1]) > 0:
expires = float(components[1])
if time.time() > expires:
RNS.log("Removing stale cache entry "+str(file), RNS.LOG_DEBUG)
os.unlink(cachepath)
else:
if file.startswith(url_hash):
RNS.log("Found "+str(file)+" in cache.", RNS.LOG_DEBUG)
RNS.log("Returning cached page", RNS.LOG_DEBUG)
file = open(cachepath, "rb")
data = file.read()
file.close()
return data
except Exception as e:
RNS.log("Error while parsing cache entry "+str(cachepath)+", removing it.", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
try:
os.unlink(cachepath)
except Exception as e:
RNS.log("Additionally, an exception occurred while unlinking the entry: "+str(e), RNS.LOG_ERROR)
RNS.log("You will probably need to remove this entry manually by deleting the file: "+str(cachepath), RNS.LOG_ERROR)
return None
def cache_page(self, cache_time):
url_hash = self.url_hash(self.current_url())
if url_hash == None:
RNS.log("Could not cache page "+str(self.current_url()), RNS.LOG_ERROR)
else:
try:
self.uncache_page(self.current_url())
cache_expires = time.time()+cache_time
filename = url_hash+"_"+str(cache_expires)
cachefile = self.app.cachepath+"/"+filename
file = open(cachefile, "wb")
file.write(self.page_data)
file.close()
RNS.log("Cached page "+str(self.current_url())+" to "+str(cachefile), RNS.LOG_DEBUG)
except Exception as e:
RNS.log("Could not write cache file for page "+str(self.current_url()), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
def file_received(self, request_receipt): def file_received(self, request_receipt):
try: try:
@@ -463,7 +739,7 @@ class Browser:
self.update_display() self.update_display()
except Exception as e: except Exception as e:
RNS.log("An error occurred while handling file response. The contained exception was: "+str(e)) RNS.log("An error occurred while handling file response. The contained exception was: "+str(e), RNS.LOG_ERROR)
def request_failed(self, request_receipt=None): def request_failed(self, request_receipt=None):
@@ -511,6 +787,8 @@ class Browser:
stats_string = " "+self.g["page"]+size_str(self.response_size) stats_string = " "+self.g["page"]+size_str(self.response_size)
stats_string += " "+self.g["arrow_d"]+size_str(self.response_transfer_size)+" in "+response_time_str stats_string += " "+self.g["arrow_d"]+size_str(self.response_transfer_size)+" in "+response_time_str
stats_string += "s "+self.g["speed"]+size_str(self.response_transfer_size/self.response_time, suffix="b")+"/s" stats_string += "s "+self.g["speed"]+size_str(self.response_transfer_size/self.response_time, suffix="b")+"/s"
elif self.loaded_from_cache:
stats_string = " (cached)"
else: else:
stats_string = "" stats_string = ""
@@ -569,3 +847,17 @@ def size_str(num, suffix='B'):
num /= 1000.0 num /= 1000.0
return "%.2f%s%s" % (num, last_unit, suffix) return "%.2f%s%s" % (num, last_unit, suffix)
class UrlDialogLineBox(urwid.LineBox):
def keypress(self, size, key):
if key == "esc":
self.delegate.close_dialogs()
else:
return super(UrlDialogLineBox, self).keypress(size, key)
class UrlEdit(urwid.Edit):
def keypress(self, size, key):
if key == "enter":
self.confirmed(self)
else:
return super(UrlEdit, self).keypress(size, key)

View File

@@ -702,9 +702,9 @@ class LXMessageWidget(urwid.WidgetWrap):
message_time = datetime.fromtimestamp(self.timestamp) message_time = datetime.fromtimestamp(self.timestamp)
encryption_string = "" encryption_string = ""
if message.get_transport_encrypted(): if message.get_transport_encrypted():
encryption_string = " ["+g["lock"]+" "+str(message.get_transport_encryption())+"]" encryption_string = " ["+g["encrypted"]+" "+str(message.get_transport_encryption())+"]"
else: else:
encryption_string = " ["+g["unlock"]+" "+str(message.get_transport_encryption())+"]" encryption_string = " ["+g["plaintext"]+" "+str(message.get_transport_encryption())+"]"
title_string = message_time.strftime(time_format)+encryption_string title_string = message_time.strftime(time_format)+encryption_string

View File

@@ -111,6 +111,7 @@ class TopicList(urwid.WidgetWrap):
GuideEntry(self.app, self, guide_display, "Hosting a Node"), GuideEntry(self.app, self, guide_display, "Hosting a Node"),
GuideEntry(self.app, self, guide_display, "Markup"), GuideEntry(self.app, self, guide_display, "Markup"),
self.first_run_entry, self.first_run_entry,
GuideEntry(self.app, self, guide_display, "Display Test"),
GuideEntry(self.app, self, guide_display, "Credits & Licenses"), GuideEntry(self.app, self, guide_display, "Credits & Licenses"),
] ]
@@ -182,7 +183,7 @@ TOPIC_INTRODUCTION = '''>Nomad Network
The intention with this program is to provide a tool to that allows you to build private and resilient communications platforms that are in complete control and ownership of the people that use them. The intention with this program is to provide a tool to that allows you to build private and resilient communications platforms that are in complete control and ownership of the people that use them.
Nomad Network is build on LXMF and Reticulum, which together provides the cryptographic mesh functionality and peer-to-peer message routing that Nomad Network relies on. This foundation also makes it possible to use the program over a very wide variety of communication mediums, from packet radio to gigabit fiber. Nomad Network is build on LXMF and Reticulum, which together provides the cryptographic mesh functionality and peer-to-peer message routing that Nomad Network relies on. This foundation also makes it possible to use the program over a very wide variety of communication mediums, from packet radio to fiber.
Nomad Network does not need any connections to the public internet to work. In fact, it doesn't even need an IP or Ethernet network. You can use it entirely over packet radio, LoRa or even serial lines. But if you wish, you can bridge islanded Reticulum networks over the Internet or private ethernet networks, or you can build networks running completely over the Internet. The choice is yours. Nomad Network does not need any connections to the public internet to work. In fact, it doesn't even need an IP or Ethernet network. You can use it entirely over packet radio, LoRa or even serial lines. But if you wish, you can bridge islanded Reticulum networks over the Internet or private ethernet networks, or you can build networks running completely over the Internet. The choice is yours.
@@ -232,7 +233,9 @@ Nomad Network nodes can host pages similar to web pages, that other peers can re
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. 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.
Pages are static in this version, but the next release of Nomad Network will add the ability to use a preprocessor such as PHP, bash, Python (or whatever you prefer) to generate dynamic pages. 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.
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`!.
>>Files >>Files
@@ -290,6 +293,41 @@ Now go out there and explore. This is still early days. See what you can find an
''' '''
TOPIC_DISPLAYTEST = '''>Markup & Color Display Test
`cYou can use this section to gauge how well your terminal reproduces the various types of formatting used by Nomad Network.
``
>>>>>>>>>>>>>>>>>>>>
-\u223f
<
>>
`a`!This line should be bold, and aligned to the left`!
`c`*This one should be italic and centered`*
`r`_And this one should be underlined, aligned right`_
``
The following line should contain a red gradient bar:
`B100 `B200 `B300 `B400 `B500 `B600 `B700 `B800 `B900 `Ba00 `Bb00 `Bc00 `Bd00 `Be00 `Bf00`b
The following line should contain a green gradient bar:
`B010 `B020 `B030 `B040 `B050 `B060 `B070 `B080 `B090 `B0a0 `B0b0 `B0c0 `B0d0 `B0e0 `B0f0`b
The following line should contain a blue gradient bar:
`B001 `B002 `B003 `B004 `B005 `B006 `B007 `B008 `B009 `B00a `B00b `B00c `B00d `B00e `B00f`b
Unicode Glyphs : \u2713 \u2715 \u26a0 \u24c3 \u2193
Nerd Font Glyphs : \uf484 \uf9c4 \uf719 \uf502 \uf415 \uf023 \uf06e
'''
TOPIC_LICENSES = '''>Thanks, Acknowledgements and Licenses TOPIC_LICENSES = '''>Thanks, Acknowledgements and Licenses
This program uses various other software components, without which Nomad Network would not have been possible. Sincere thanks to the authors and contributors of the following projects This program uses various other software components, without which Nomad Network would not have been possible. Sincere thanks to the authors and contributors of the following projects
@@ -553,6 +591,29 @@ Here is `F00f`_`[a more visible link`1385edace36466a6b3dd:/page/index.mu]`_`f
When links like these are displayed in the built-in browser, clicking on them or activating them using the keyboard will cause the browser to load the specified URL. When links like these are displayed in the built-in browser, clicking on them or activating them using the keyboard will cause the browser to load the specified URL.
>Comments
You can insert comments that will not be displayed in the output by starting a line with the # character.
Here's an example:
`Faaa
`=
# This line will not be displayed
This line will
`=
``
The above markup produces the following output:
`Faaa`B333
# This line will not be displayed
This line will
``
>Literals >Literals
To display literal content, for example source-code, or blocks of text that should not be interpreted by micron, you can use literal blocks, specified by the \\`= tag. Below is the source code of this entire document, presented as a literal block. To display literal content, for example source-code, or blocks of text that should not be interpreted by micron, you can use literal blocks, specified by the \\`= tag. Below is the source code of this entire document, presented as a literal block.
@@ -571,5 +632,6 @@ TOPICS = {
"Hosting a Node": TOPIC_HOSTING, "Hosting a Node": TOPIC_HOSTING,
"Markup": TOPIC_MARKUP, "Markup": TOPIC_MARKUP,
"First Run": TOPIC_FIRST_RUN, "First Run": TOPIC_FIRST_RUN,
"Display Test": TOPIC_DISPLAYTEST,
"Credits & Licenses": TOPIC_LICENSES, "Credits & Licenses": TOPIC_LICENSES,
} }

View File

@@ -5,16 +5,26 @@ from urwid.util import is_mouse_press
from urwid.text_layout import calc_coords from urwid.text_layout import calc_coords
import re import re
DEFAULT_FG = "ddd" DEFAULT_FG_DARK = "ddd"
DEFAULT_FG_LIGHT = "222"
DEFAULT_BG = "default" DEFAULT_BG = "default"
STYLES = { SELECTED_STYLES = None
"plain": { "fg": DEFAULT_FG, "bg": DEFAULT_BG, "bold": False, "underline": False, "italic": False },
STYLES_DARK = {
"plain": { "fg": DEFAULT_FG_DARK, "bg": DEFAULT_BG, "bold": False, "underline": False, "italic": False },
"heading1": { "fg": "222", "bg": "bbb", "bold": False, "underline": False, "italic": False }, "heading1": { "fg": "222", "bg": "bbb", "bold": False, "underline": False, "italic": False },
"heading2": { "fg": "111", "bg": "999", "bold": False, "underline": False, "italic": False }, "heading2": { "fg": "111", "bg": "999", "bold": False, "underline": False, "italic": False },
"heading3": { "fg": "000", "bg": "777", "bold": False, "underline": False, "italic": False }, "heading3": { "fg": "000", "bg": "777", "bold": False, "underline": False, "italic": False },
} }
STYLES_LIGHT = {
"plain": { "fg": DEFAULT_FG_LIGHT, "bg": DEFAULT_BG, "bold": False, "underline": False, "italic": False },
"heading1": { "fg": "000", "bg": "777", "bold": False, "underline": False, "italic": False },
"heading2": { "fg": "111", "bg": "aaa", "bold": False, "underline": False, "italic": False },
"heading3": { "fg": "222", "bg": "ccc", "bold": False, "underline": False, "italic": False },
}
SYNTH_STYLES = [] SYNTH_STYLES = []
SYNTH_SPECS = {} SYNTH_SPECS = {}
@@ -22,12 +32,18 @@ SECTION_INDENT = 2
INDENT_RIGHT = 1 INDENT_RIGHT = 1
def markup_to_attrmaps(markup, url_delegate = None): def markup_to_attrmaps(markup, url_delegate = None):
global SELECTED_STYLES
if nomadnet.NomadNetworkApp.get_shared_instance().config["textui"]["theme"] == nomadnet.ui.TextUI.THEME_DARK:
SELECTED_STYLES = STYLES_DARK
else:
SELECTED_STYLES = STYLES_LIGHT
attrmaps = [] attrmaps = []
state = { state = {
"literal": False, "literal": False,
"depth": 0, "depth": 0,
"fg_color": DEFAULT_FG, "fg_color": SELECTED_STYLES["plain"]["fg"],
"bg_color": DEFAULT_BG, "bg_color": DEFAULT_BG,
"formatting": { "formatting": {
"bold": False, "bold": False,
@@ -72,6 +88,10 @@ def parse_line(line, state, url_delegate):
if first_char == "\\": if first_char == "\\":
line = line[1:] line = line[1:]
# Check for comments
elif first_char == "#":
return None
# Check for section heading reset # Check for section heading reset
elif first_char == "<": elif first_char == "<":
state["depth"] = 0 state["depth"] = 0
@@ -86,8 +106,8 @@ def parse_line(line, state, url_delegate):
for j in range(1, i+1): for j in range(1, i+1):
wanted_style = "heading"+str(i) wanted_style = "heading"+str(i)
if wanted_style in STYLES: if wanted_style in SELECTED_STYLES:
style = STYLES[wanted_style] style = SELECTED_STYLES[wanted_style]
line = line[state["depth"]:] line = line[state["depth"]:]
if len(line) > 0: if len(line) > 0:
@@ -229,7 +249,7 @@ def make_output(state, line, url_delegate):
state["fg_color"] = color state["fg_color"] = color
skip = 3 skip = 3
elif c == "f": elif c == "f":
state["fg_color"] = DEFAULT_FG state["fg_color"] = SELECTED_STYLES["plain"]["fg"]
elif c == "B": elif c == "B":
if len(line) >= i+4: if len(line) >= i+4:
color = line[i+1:i+4] color = line[i+1:i+4]
@@ -241,7 +261,7 @@ def make_output(state, line, url_delegate):
state["formatting"]["bold"] = False state["formatting"]["bold"] = False
state["formatting"]["underline"] = False state["formatting"]["underline"] = False
state["formatting"]["italic"] = False state["formatting"]["italic"] = False
state["fg_color"] = DEFAULT_FG state["fg_color"] = SELECTED_STYLES["plain"]["fg"]
state["bg_color"] = DEFAULT_BG state["bg_color"] = DEFAULT_BG
state["align"] = state["default_align"] state["align"] = state["default_align"]
elif c == "c": elif c == "c":

View File

@@ -13,7 +13,7 @@ class NetworkDisplayShortcuts():
self.app = app self.app = app
g = app.ui.glyphs g = app.ui.glyphs
self.widget = urwid.AttrMap(urwid.Text("[C-l] Toggle Nodes/Announces view [C-x] Remove entry [C-w] Disconnect remote"), "shortcutbar") self.widget = urwid.AttrMap(urwid.Text("[C-l] Toggle Nodes/Announces view [C-x] Remove entry [C-w] Disconnect [C-d] Back [C-f] Forward [C-r] Reload [C-u] Enter URL"), "shortcutbar")
# "[C-"+g["arrow_u"]+g["arrow_d"]+"] Navigate Lists" # "[C-"+g["arrow_u"]+g["arrow_d"]+"] Navigate Lists"
@@ -67,9 +67,9 @@ class AnnounceInfo(urwid.WidgetWrap):
is_node = announce[3] is_node = announce[3]
if is_node: if is_node:
type_string = g["node"] + " Node" type_string = "Node " + g["node"]
else: else:
type_string = g["peer"] + " Peer" type_string = "Peer " + g["peer"]
try: try:
data_str = announce[2].decode("utf-8") data_str = announce[2].decode("utf-8")
@@ -461,8 +461,6 @@ class KnownNodes(urwid.WidgetWrap):
# TODO: Sort list # TODO: Sort list
return widget_list return widget_list
class NodeEntry(urwid.WidgetWrap): class NodeEntry(urwid.WidgetWrap):
def __init__(self, app, node, delegate): def __init__(self, app, node, delegate):
source_hash = node.source_hash source_hash = node.source_hash
@@ -537,6 +535,69 @@ class AnnounceTime(urwid.WidgetWrap):
self.started = False self.started = False
class NodeAnnounceTime(urwid.WidgetWrap):
def __init__(self, app):
self.started = False
self.app = app
self.timeout = self.app.config["textui"]["animation_interval"]
self.display_widget = urwid.Text("")
self.update_time()
urwid.WidgetWrap.__init__(self, self.display_widget)
def update_time(self):
self.last_announce_string = "Never"
if self.app.peer_settings["node_last_announce"] != None:
self.last_announce_string = pretty_date(int(self.app.peer_settings["node_last_announce"]))
self.display_widget.set_text("Last Announce : "+self.last_announce_string)
def update_time_callback(self, loop=None, user_data=None):
self.update_time()
if self.started:
self.app.ui.loop.set_alarm_in(self.timeout, self.update_time_callback)
def start(self):
was_started = self.started
self.started = True
if not was_started:
self.update_time_callback()
def stop(self):
self.started = False
class NodeActiveConnections(urwid.WidgetWrap):
def __init__(self, app):
self.started = False
self.app = app
self.timeout = self.app.config["textui"]["animation_interval"]
self.display_widget = urwid.Text("")
self.update_stat()
urwid.WidgetWrap.__init__(self, self.display_widget)
def update_stat(self):
self.stat_string = "None"
if self.app.node != None:
self.stat_string = str(len(self.app.node.destination.links))
self.display_widget.set_text("Connected Peers : "+self.stat_string)
def update_stat_callback(self, loop=None, user_data=None):
self.update_stat()
if self.started:
self.app.ui.loop.set_alarm_in(self.timeout, self.update_stat_callback)
def start(self):
was_started = self.started
self.started = True
if not was_started:
self.update_stat_callback()
def stop(self):
self.started = False
class LocalPeer(urwid.WidgetWrap): class LocalPeer(urwid.WidgetWrap):
announce_timer = None announce_timer = None
@@ -598,9 +659,9 @@ class LocalPeer(urwid.WidgetWrap):
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type="pack", height_amount=None)
self.parent.left_pile.contents[2] = (overlay, options) self.parent.left_pile.contents[2] = (overlay, options)
def node_settings_query(sender): def node_info_query(sender):
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type="pack", height_amount=None)
self.parent.left_pile.contents[2] = (self.parent.node_settings_display, options) self.parent.left_pile.contents[2] = (self.parent.node_info_display, options)
if LocalPeer.announce_timer == None: if LocalPeer.announce_timer == None:
self.t_last_announce = AnnounceTime(self.app) self.t_last_announce = AnnounceTime(self.app)
@@ -619,7 +680,7 @@ class LocalPeer(urwid.WidgetWrap):
self.t_last_announce, self.t_last_announce,
announce_button, announce_button,
urwid.Divider(g["divider1"]), urwid.Divider(g["divider1"]),
urwid.Columns([("weight", 0.45, urwid.Button("Save", on_press=save_query)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Node Cfg", on_press=node_settings_query))]) urwid.Columns([("weight", 0.45, urwid.Button("Save", on_press=save_query)), ("weight", 0.1, urwid.Text("")), ("weight", 0.45, urwid.Button("Node Info", on_press=node_info_query))])
] ]
) )
@@ -629,26 +690,109 @@ class LocalPeer(urwid.WidgetWrap):
self.t_last_announce.start() self.t_last_announce.start()
class NodeSettings(urwid.WidgetWrap): class NodeInfo(urwid.WidgetWrap):
announce_timer = None
links_timer = None
def __init__(self, app, parent): def __init__(self, app, parent):
self.app = app self.app = app
self.parent = parent self.parent = parent
g = self.app.ui.glyphs g = self.app.ui.glyphs
self.dialog_open = False
widget_style = ""
def show_peer_info(sender): def show_peer_info(sender):
options = self.parent.left_pile.options(height_type="pack", height_amount=None) options = self.parent.left_pile.options(height_type="pack", height_amount=None)
self.parent.left_pile.contents[2] = (LocalPeer(self.app, self.parent), options) self.parent.left_pile.contents[2] = (LocalPeer(self.app, self.parent), options)
widget_style = "inactive_text" if self.app.enable_node:
if self.app.node != None:
display_name = self.app.node.name
else:
display_name = None
if display_name == None:
display_name = ""
t_id = urwid.Text("Addr : "+RNS.hexrep(self.app.node.destination.hash, delimit=False))
e_name = urwid.Text("Name : "+display_name)
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[2] = (NodeInfo(self.app, self.parent), options)
self.app.node.announce()
dialog = DialogLineBox(
urwid.Pile([
urwid.Text("\n\n\nAnnounce Sent\n\n", align="center"),
urwid.Button("OK", on_press=dismiss_dialog)
]), title=g["info"]
)
dialog.delegate = self
bottom = self
#overlay = urwid.Overlay(dialog, bottom, align="center", width=("relative", 100), valign="middle", height="pack", left=4, right=4)
overlay = dialog
self.dialog_open = True
options = self.parent.left_pile.options(height_type="pack", height_amount=None)
self.parent.left_pile.contents[2] = (overlay, options)
def connect_query(sender):
self.parent.browser.retrieve_url(RNS.hexrep(self.app.node.destination.hash, delimit=False))
if NodeInfo.announce_timer == None:
self.t_last_announce = NodeAnnounceTime(self.app)
NodeInfo.announce_timer = self.t_last_announce
else:
self.t_last_announce = NodeInfo.announce_timer
self.t_last_announce.update_time()
if NodeInfo.links_timer == None:
self.t_active_links = NodeActiveConnections(self.app)
NodeInfo.links_timer = self.t_active_links
else:
self.t_active_links = NodeInfo.links_timer
self.t_active_links.update_stat()
announce_button = urwid.Button("Announce Now", on_press=announce_query)
connect_button = urwid.Button("Browse", on_press=connect_query)
pile = urwid.Pile([
t_id,
e_name,
urwid.Divider(g["divider1"]),
self.t_last_announce,
self.t_active_links,
urwid.Divider(g["divider1"]),
urwid.Columns([
("weight", 0.3, urwid.Button("Back", on_press=show_peer_info)),
("weight", 0.1, urwid.Text("")),
("weight", 0.3, connect_button),
("weight", 0.1, urwid.Text("")),
("weight", 0.3, announce_button)
])
])
else:
pile = urwid.Pile([ pile = urwid.Pile([
urwid.Text("\n"+g["info"], align="center"), urwid.Text("\n"+g["info"], align="center"),
urwid.Text("\nNode Hosting currently unavailable\n\n", align="center"), urwid.Text("\nThis instance is not hosting a node\n\n", align="center"),
urwid.Padding(urwid.Button("Back", on_press=show_peer_info), "center", "pack") urwid.Padding(urwid.Button("Back", on_press=show_peer_info), "center", "pack")
]) ])
self.display_widget = pile self.display_widget = pile
urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title="Node Settings"), widget_style)) urwid.WidgetWrap.__init__(self, urwid.AttrMap(urwid.LineBox(self.display_widget, title="Local Node Info"), widget_style))
def start(self):
if self.app.node != None:
self.t_last_announce.start()
self.t_active_links.start()
@@ -712,14 +856,14 @@ class NetworkStats(urwid.WidgetWrap):
self.w_heard_peers.start() self.w_heard_peers.start()
self.w_known_nodes.start() self.w_known_nodes.start()
class NetworkLeftPile(urwid.Pile): class NetworkLeftPile(urwid.Pile):
def keypress(self, size, key): def keypress(self, size, key):
if key == "ctrl l": if key == "ctrl l":
self.parent.toggle_list() self.parent.toggle_list()
elif key == "ctrl w": elif key == "ctrl w":
self.parent.browser.disconnect() self.parent.browser.disconnect()
elif key == "ctrl u":
self.parent.browser.url_dialog()
else: else:
return super(NetworkLeftPile, self).keypress(size, key) return super(NetworkLeftPile, self).keypress(size, key)
@@ -733,11 +877,14 @@ class NetworkDisplay():
self.browser = Browser(self.app, "nomadnetwork", "node", auth_identity = self.app.identity, delegate = self) self.browser = Browser(self.app, "nomadnetwork", "node", auth_identity = self.app.identity, delegate = self)
if self.app.node != None:
self.browser.loopback = self.app.node.destination.hash
self.known_nodes_display = KnownNodes(self.app) self.known_nodes_display = KnownNodes(self.app)
self.network_stats_display = NetworkStats(self.app, self) self.network_stats_display = NetworkStats(self.app, self)
self.announce_stream_display = AnnounceStream(self.app, self) self.announce_stream_display = AnnounceStream(self.app, self)
self.local_peer_display = LocalPeer(self.app, self) self.local_peer_display = LocalPeer(self.app, self)
self.node_settings_display = NodeSettings(self.app, self) self.node_info_display = NodeInfo(self.app, self)
self.known_nodes_display.delegate = self self.known_nodes_display.delegate = self
@@ -752,11 +899,12 @@ class NetworkDisplay():
self.left_area = self.left_pile self.left_area = self.left_pile
self.right_area = self.browser.display_widget self.right_area = self.browser.display_widget
self.right_area_width = 1-NetworkDisplay.list_width
self.columns = urwid.Columns( self.columns = urwid.Columns(
[ [
("weight", NetworkDisplay.list_width, self.left_area), ("weight", NetworkDisplay.list_width, self.left_area),
("weight", 1-NetworkDisplay.list_width, self.right_area) ("weight", self.right_area_width, self.right_area)
], ],
dividechars=0, focus_column=0 dividechars=0, focus_column=0
) )
@@ -793,8 +941,12 @@ class NetworkDisplay():
def start(self): def start(self):
self.local_peer_display.start() self.local_peer_display.start()
self.node_info_display.start()
self.network_stats_display.start() self.network_stats_display.start()
self.announce_stream_display.start() # There seems to be an intermittent memory leak somewhere
# in the periodic updating here. The periodic updater should
# not be needed anymore, so dis
#self.announce_stream_display.start()
def shortcuts(self): def shortcuts(self):
return self.shortcuts_display return self.shortcuts_display

View File

@@ -1,4 +1,5 @@
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")
] ]

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.4', 'lxmf>=0.0.9', 'urwid>=2.1.2'], install_requires=['rns>=0.2.5', 'lxmf>=0.0.9', 'urwid>=2.1.2'],
python_requires='>=3.6', python_requires='>=3.6',
) )