Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
953c8c94c7 | ||
|
|
0c94dfa482 | ||
|
|
08f1d4cda7 | ||
|
|
718e2ca859 | ||
|
|
9dc09be4ae | ||
|
|
41ce3e8800 | ||
|
|
29489353fc | ||
|
|
c27b2e06b8 | ||
|
|
f41d1eecb0 | ||
|
|
012721ee57 | ||
|
|
959cc13167 | ||
|
|
48a5001009 | ||
|
|
a77f423362 | ||
|
|
b8e67721b8 | ||
|
|
913c7935c0 | ||
|
|
0d93e7e8d2 | ||
|
|
150076be6b | ||
|
|
a6376c716d | ||
|
|
7d5bb8e016 |
30
README.md
@@ -1,8 +1,6 @@
|
||||
Nomad Network
|
||||
Nomad Network - Communicate Freely
|
||||
==========
|
||||
|
||||
Communicate Freely.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
- Zero-configuration, minimal-infrastructure mesh communication
|
||||
- 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
|
||||
- An easy to use and bandwidth efficient markup language for writing pages
|
||||
- Page caching in browser
|
||||
|
||||
## 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.
|
||||
|
||||
### Feature roadmap
|
||||
- Page caching in browser
|
||||
- Node-side generated pages with PHP, Python, bash or others
|
||||
- Access control and authentication for pages and files
|
||||
- Propagated messaging and discussion threads
|
||||
- Access control and authentication for nodes, pages and files
|
||||
- Network-wide propagated messaging and discussion threads
|
||||
- Geospatial information sharing
|
||||
- Distributed Marketplace
|
||||
|
||||
## Dependencies:
|
||||
- Python 3
|
||||
@@ -43,7 +43,21 @@ pip3 install 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.
|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 86 KiB |
BIN
docs/screenshots/4.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
docs/screenshots/5.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
@@ -2,6 +2,7 @@ import os
|
||||
import RNS
|
||||
import time
|
||||
import threading
|
||||
import subprocess
|
||||
import RNS.vendor.umsgpack as msgpack
|
||||
|
||||
class Node:
|
||||
@@ -87,6 +88,10 @@ class Node:
|
||||
file_path = path.replace("/page", self.app.pagespath, 1)
|
||||
try:
|
||||
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")
|
||||
response_data = fh.read()
|
||||
fh.close()
|
||||
@@ -121,6 +126,7 @@ class Node:
|
||||
def announce(self):
|
||||
self.app_data = self.name.encode("utf-8")
|
||||
self.last_announce = time.time()
|
||||
self.app.peer_settings["node_last_announce"] = self.last_announce
|
||||
self.destination.announce(app_data=self.app_data)
|
||||
|
||||
def __jobs(self):
|
||||
|
||||
@@ -53,11 +53,14 @@ class NomadNetworkApp:
|
||||
|
||||
self.pagespath = self.configdir+"/storage/pages"
|
||||
self.filespath = self.configdir+"/storage/files"
|
||||
self.cachepath = self.configdir+"/storage/cache"
|
||||
|
||||
self.downloads_path = os.path.expanduser("~/Downloads")
|
||||
|
||||
self.firstrun = False
|
||||
|
||||
self.peer_announce_at_start = True
|
||||
|
||||
if not os.path.isdir(self.storagepath):
|
||||
os.makedirs(self.storagepath)
|
||||
|
||||
@@ -76,6 +79,9 @@ class NomadNetworkApp:
|
||||
if not os.path.isdir(self.filespath):
|
||||
os.makedirs(self.filespath)
|
||||
|
||||
if not os.path.isdir(self.cachepath):
|
||||
os.makedirs(self.cachepath)
|
||||
|
||||
if os.path.isfile(self.configpath):
|
||||
try:
|
||||
self.config = ConfigObj(self.configpath)
|
||||
@@ -123,6 +129,10 @@ class NomadNetworkApp:
|
||||
file = open(self.peersettingspath, "rb")
|
||||
self.peer_settings = msgpack.unpackb(file.read())
|
||||
file.close()
|
||||
|
||||
if not "node_last_announce" in self.peer_settings:
|
||||
self.peer_settings["node_last_announce"] = None
|
||||
|
||||
except Exception as e:
|
||||
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)
|
||||
@@ -134,6 +144,7 @@ class NomadNetworkApp:
|
||||
"display_name": "Anonymous Peer",
|
||||
"announce_interval": None,
|
||||
"last_announce": None,
|
||||
"node_last_announce": None,
|
||||
}
|
||||
self.save_peer_settings()
|
||||
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.Directory)
|
||||
|
||||
if self.peer_announce_at_start:
|
||||
self.announce_now()
|
||||
|
||||
nomadnet.ui.spawn(self.uimode)
|
||||
|
||||
def set_display_name(self, display_name):
|
||||
@@ -262,6 +276,10 @@ class NomadNetworkApp:
|
||||
value = self.config["client"]["downloads_path"]
|
||||
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":
|
||||
value = value.lower()
|
||||
if value == "none":
|
||||
@@ -347,7 +365,8 @@ class NomadNetworkApp:
|
||||
if not "announce_at_start" in self.config["node"]:
|
||||
self.node_announce_at_start = False
|
||||
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"]:
|
||||
self.node_announce_interval = 720
|
||||
@@ -401,11 +420,19 @@ enable_client = yes
|
||||
user_interface = text
|
||||
downloads_path = ~/Downloads
|
||||
|
||||
# By default, the peer is announced at startup
|
||||
# to let other peers reach it immediately.
|
||||
announce_at_start = yes
|
||||
|
||||
[textui]
|
||||
|
||||
# Amount of time to show intro screen
|
||||
intro_time = 1
|
||||
|
||||
# You can specify the display theme.
|
||||
# theme = dark
|
||||
theme = light
|
||||
|
||||
# Specify the number of colors to use
|
||||
# valid colormodes are:
|
||||
# monochrome, 16, 88, 256 and 24bit
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.0.8"
|
||||
__version__ = "0.1.0"
|
||||
@@ -1,6 +1,7 @@
|
||||
import RNS
|
||||
import importlib
|
||||
import urwid
|
||||
import time
|
||||
import os
|
||||
|
||||
import nomadnet
|
||||
from nomadnet.ui.textui import *
|
||||
@@ -47,6 +48,39 @@ THEMES = {
|
||||
("progress_full", "black", "light gray", "standout", "#111", "#bbb"),
|
||||
("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"),
|
||||
("cross", "X", "\u2715", "\u2715"),
|
||||
("unknown", "?", "?", "?"),
|
||||
("lock", "E", "\U0001f512", "\uf023"),
|
||||
("unlock", "!", "\U0001f513", "\uf09c"),
|
||||
("encrypted", "", "\u26BF", "\uf023"),
|
||||
("plaintext", "!", "!", "\uf06e "),
|
||||
("arrow_r", "->", "\u2192", "\u2192"),
|
||||
("arrow_l", "<-", "\u2190", "\u2190"),
|
||||
("arrow_u", "/\\", "\u2191", "\u2191"),
|
||||
("arrow_d", "\\/", "\u2193", "\u2193"),
|
||||
("warning", "!", "\u26a0", "\uf12a"),
|
||||
("info", "i", "\u2139", "\ufb4d"),
|
||||
("unread", "N", "\u2709", "\uf003 "),
|
||||
("unread", "[U]", "\u2709", "\uf003 "),
|
||||
("divider1", "-", "\u2504", "\u2504"),
|
||||
("peer", "P", "\U0001F464", "\uf415"),
|
||||
("node", "N", "\U0001F5A5", "\uf502"),
|
||||
("page", "", "\U0001F5B9", "\uf719 "),
|
||||
("speed", "", "\U0001F5B9", "\uf9c4"),
|
||||
("peer", "[P]", "\u24c5 ", "\uf415"),
|
||||
("node", "[N]", "\u24c3 ", "\uf502"),
|
||||
("page", "", "\u25a4", "\uf719 "),
|
||||
("speed", "", "\u25F7", "\uf9c4"),
|
||||
("decoration_menu", "", "", " \uf93a"),
|
||||
("globe", "", "", "\uf484"),
|
||||
}
|
||||
|
||||
class TextUI:
|
||||
@@ -85,12 +120,13 @@ class TextUI:
|
||||
self.app.ui = self
|
||||
self.loop = None
|
||||
|
||||
if importlib.util.find_spec("urwid") != None:
|
||||
import urwid
|
||||
else:
|
||||
RNS.log("The text-mode user interface requires Urwid to be installed on your system.", RNS.LOG_ERROR)
|
||||
RNS.log("You can install it with the command: pip3 install urwid", RNS.LOG_ERROR)
|
||||
nomadnet.panic()
|
||||
# TODO: Remove
|
||||
# if importlib.util.find_spec("urwid") != None:
|
||||
# import urwid
|
||||
# else:
|
||||
# RNS.log("The text-mode user interface requires Urwid to be installed on your system.", RNS.LOG_ERROR)
|
||||
# RNS.log("You can install it with the command: pip3 install urwid", RNS.LOG_ERROR)
|
||||
# nomadnet.panic()
|
||||
|
||||
urwid.set_encoding("UTF-8")
|
||||
|
||||
@@ -130,10 +166,20 @@ class TextUI:
|
||||
if intro_timeout > 0:
|
||||
self.loop.set_alarm_in(intro_timeout, self.display_main)
|
||||
|
||||
# TODO: Probably remove this at some point when better terminal
|
||||
# color capability detection has been implemented
|
||||
if "KONSOLE_VERSION" in os.environ:
|
||||
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)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import os
|
||||
import time
|
||||
import urwid
|
||||
import nomadnet
|
||||
import subprocess
|
||||
import threading
|
||||
from .MicronParser import markup_to_attrmaps
|
||||
from nomadnet.vendor.Scrollable import *
|
||||
@@ -11,6 +12,14 @@ class BrowserFrame(urwid.Frame):
|
||||
def keypress(self, size, key):
|
||||
if key == "ctrl w":
|
||||
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":
|
||||
return super(BrowserFrame, self).keypress(size, key)
|
||||
# if key == "up" and self.delegate.messagelist.top_is_visible:
|
||||
@@ -25,6 +34,7 @@ class BrowserFrame(urwid.Frame):
|
||||
class Browser:
|
||||
DEFAULT_PATH = "/page/index.mu"
|
||||
DEFAULT_TIMEOUT = 10
|
||||
DEFAULT_CACHE_TIME = 12*60*60
|
||||
|
||||
NO_PATH = 0x00
|
||||
PATH_REQUESTED = 0x01
|
||||
@@ -51,6 +61,7 @@ class Browser:
|
||||
self.last_keypress = None
|
||||
|
||||
self.link = None
|
||||
self.loopback = None
|
||||
self.status = Browser.DISCONECTED
|
||||
self.response_progress = 0
|
||||
self.response_size = None
|
||||
@@ -66,6 +77,13 @@ class Browser:
|
||||
self.attr_maps = []
|
||||
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:
|
||||
self.path = Browser.DEFAULT_PATH
|
||||
|
||||
@@ -82,8 +100,16 @@ class Browser:
|
||||
path = self.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):
|
||||
if self.status == Browser.DONE:
|
||||
self.link_target = link_target
|
||||
self.app.ui.loop.set_alarm_in(0.1, self.marked_link_job)
|
||||
|
||||
@@ -175,7 +201,11 @@ class Browser:
|
||||
else:
|
||||
self.display_widget.set_attr_map({None: "body_text"})
|
||||
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:
|
||||
self.browser_footer = self.make_status_widget()
|
||||
@@ -226,6 +256,11 @@ class Browser:
|
||||
self.response_size = None
|
||||
self.response_transfer_size = None
|
||||
|
||||
self.history = []
|
||||
self.history_ptr = 0
|
||||
self.history_inc = False
|
||||
self.history_dec = False
|
||||
|
||||
self.update_display()
|
||||
|
||||
|
||||
@@ -271,11 +306,14 @@ class Browser:
|
||||
|
||||
if destination_hash != None and path != None:
|
||||
if path.startswith("/file/"):
|
||||
if destination_hash != self.loopback:
|
||||
if destination_hash == self.destination_hash:
|
||||
self.download_file(destination_hash, path)
|
||||
else:
|
||||
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)
|
||||
else:
|
||||
self.download_local_file(path)
|
||||
else:
|
||||
self.set_destination_hash(destination_hash)
|
||||
self.set_path(path)
|
||||
@@ -296,6 +334,35 @@ class Browser:
|
||||
def set_timeout(self, 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):
|
||||
if self.link != None and self.link.destination.hash == self.destination_hash:
|
||||
@@ -323,11 +390,133 @@ class Browser:
|
||||
else:
|
||||
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):
|
||||
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.setDaemon(True)
|
||||
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):
|
||||
@@ -436,11 +625,98 @@ class Browser:
|
||||
self.markup = self.page_data.decode("utf-8")
|
||||
self.attr_maps = markup_to_attrmaps(self.markup, url_delegate=self)
|
||||
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()
|
||||
|
||||
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:
|
||||
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):
|
||||
try:
|
||||
@@ -463,7 +739,7 @@ class Browser:
|
||||
|
||||
self.update_display()
|
||||
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):
|
||||
@@ -511,6 +787,8 @@ class Browser:
|
||||
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 += "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:
|
||||
stats_string = ""
|
||||
|
||||
@@ -569,3 +847,17 @@ def size_str(num, suffix='B'):
|
||||
num /= 1000.0
|
||||
|
||||
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)
|
||||
@@ -702,9 +702,9 @@ class LXMessageWidget(urwid.WidgetWrap):
|
||||
message_time = datetime.fromtimestamp(self.timestamp)
|
||||
encryption_string = ""
|
||||
if message.get_transport_encrypted():
|
||||
encryption_string = " ["+g["lock"]+" "+str(message.get_transport_encryption())+"]"
|
||||
encryption_string = " ["+g["encrypted"]+" "+str(message.get_transport_encryption())+"]"
|
||||
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
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ class TopicList(urwid.WidgetWrap):
|
||||
GuideEntry(self.app, self, guide_display, "Hosting a Node"),
|
||||
GuideEntry(self.app, self, guide_display, "Markup"),
|
||||
self.first_run_entry,
|
||||
GuideEntry(self.app, self, guide_display, "Display Test"),
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
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.
|
||||
|
||||
>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
|
||||
|
||||
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,
|
||||
"Markup": TOPIC_MARKUP,
|
||||
"First Run": TOPIC_FIRST_RUN,
|
||||
"Display Test": TOPIC_DISPLAYTEST,
|
||||
"Credits & Licenses": TOPIC_LICENSES,
|
||||
}
|
||||
@@ -5,16 +5,26 @@ from urwid.util import is_mouse_press
|
||||
from urwid.text_layout import calc_coords
|
||||
import re
|
||||
|
||||
DEFAULT_FG = "ddd"
|
||||
DEFAULT_FG_DARK = "ddd"
|
||||
DEFAULT_FG_LIGHT = "222"
|
||||
DEFAULT_BG = "default"
|
||||
|
||||
STYLES = {
|
||||
"plain": { "fg": DEFAULT_FG, "bg": DEFAULT_BG, "bold": False, "underline": False, "italic": False },
|
||||
SELECTED_STYLES = None
|
||||
|
||||
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 },
|
||||
"heading2": { "fg": "111", "bg": "999", "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_SPECS = {}
|
||||
|
||||
@@ -22,12 +32,18 @@ SECTION_INDENT = 2
|
||||
INDENT_RIGHT = 1
|
||||
|
||||
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 = []
|
||||
|
||||
state = {
|
||||
"literal": False,
|
||||
"depth": 0,
|
||||
"fg_color": DEFAULT_FG,
|
||||
"fg_color": SELECTED_STYLES["plain"]["fg"],
|
||||
"bg_color": DEFAULT_BG,
|
||||
"formatting": {
|
||||
"bold": False,
|
||||
@@ -72,6 +88,10 @@ def parse_line(line, state, url_delegate):
|
||||
if first_char == "\\":
|
||||
line = line[1:]
|
||||
|
||||
# Check for comments
|
||||
elif first_char == "#":
|
||||
return None
|
||||
|
||||
# Check for section heading reset
|
||||
elif first_char == "<":
|
||||
state["depth"] = 0
|
||||
@@ -86,8 +106,8 @@ def parse_line(line, state, url_delegate):
|
||||
|
||||
for j in range(1, i+1):
|
||||
wanted_style = "heading"+str(i)
|
||||
if wanted_style in STYLES:
|
||||
style = STYLES[wanted_style]
|
||||
if wanted_style in SELECTED_STYLES:
|
||||
style = SELECTED_STYLES[wanted_style]
|
||||
|
||||
line = line[state["depth"]:]
|
||||
if len(line) > 0:
|
||||
@@ -229,7 +249,7 @@ def make_output(state, line, url_delegate):
|
||||
state["fg_color"] = color
|
||||
skip = 3
|
||||
elif c == "f":
|
||||
state["fg_color"] = DEFAULT_FG
|
||||
state["fg_color"] = SELECTED_STYLES["plain"]["fg"]
|
||||
elif c == "B":
|
||||
if len(line) >= 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"]["underline"] = False
|
||||
state["formatting"]["italic"] = False
|
||||
state["fg_color"] = DEFAULT_FG
|
||||
state["fg_color"] = SELECTED_STYLES["plain"]["fg"]
|
||||
state["bg_color"] = DEFAULT_BG
|
||||
state["align"] = state["default_align"]
|
||||
elif c == "c":
|
||||
|
||||
@@ -13,7 +13,7 @@ class NetworkDisplayShortcuts():
|
||||
self.app = app
|
||||
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"
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ class AnnounceInfo(urwid.WidgetWrap):
|
||||
is_node = announce[3]
|
||||
|
||||
if is_node:
|
||||
type_string = g["node"] + " Node"
|
||||
type_string = "Node " + g["node"]
|
||||
else:
|
||||
type_string = g["peer"] + " Peer"
|
||||
type_string = "Peer " + g["peer"]
|
||||
|
||||
try:
|
||||
data_str = announce[2].decode("utf-8")
|
||||
@@ -461,8 +461,6 @@ class KnownNodes(urwid.WidgetWrap):
|
||||
# TODO: Sort list
|
||||
return widget_list
|
||||
|
||||
|
||||
|
||||
class NodeEntry(urwid.WidgetWrap):
|
||||
def __init__(self, app, node, delegate):
|
||||
source_hash = node.source_hash
|
||||
@@ -537,6 +535,69 @@ class AnnounceTime(urwid.WidgetWrap):
|
||||
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):
|
||||
announce_timer = None
|
||||
|
||||
@@ -598,9 +659,9 @@ class LocalPeer(urwid.WidgetWrap):
|
||||
options = self.parent.left_pile.options(height_type="pack", height_amount=None)
|
||||
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)
|
||||
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:
|
||||
self.t_last_announce = AnnounceTime(self.app)
|
||||
@@ -619,7 +680,7 @@ class LocalPeer(urwid.WidgetWrap):
|
||||
self.t_last_announce,
|
||||
announce_button,
|
||||
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()
|
||||
|
||||
|
||||
class NodeSettings(urwid.WidgetWrap):
|
||||
class NodeInfo(urwid.WidgetWrap):
|
||||
announce_timer = None
|
||||
links_timer = None
|
||||
|
||||
def __init__(self, app, parent):
|
||||
self.app = app
|
||||
self.parent = parent
|
||||
g = self.app.ui.glyphs
|
||||
|
||||
self.dialog_open = False
|
||||
|
||||
widget_style = ""
|
||||
|
||||
def show_peer_info(sender):
|
||||
options = self.parent.left_pile.options(height_type="pack", height_amount=None)
|
||||
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([
|
||||
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")
|
||||
])
|
||||
|
||||
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_known_nodes.start()
|
||||
|
||||
|
||||
|
||||
class NetworkLeftPile(urwid.Pile):
|
||||
def keypress(self, size, key):
|
||||
if key == "ctrl l":
|
||||
self.parent.toggle_list()
|
||||
elif key == "ctrl w":
|
||||
self.parent.browser.disconnect()
|
||||
elif key == "ctrl u":
|
||||
self.parent.browser.url_dialog()
|
||||
else:
|
||||
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)
|
||||
|
||||
if self.app.node != None:
|
||||
self.browser.loopback = self.app.node.destination.hash
|
||||
|
||||
self.known_nodes_display = KnownNodes(self.app)
|
||||
self.network_stats_display = NetworkStats(self.app, self)
|
||||
self.announce_stream_display = AnnounceStream(self.app, self)
|
||||
self.local_peer_display = LocalPeer(self.app, self)
|
||||
self.node_settings_display = NodeSettings(self.app, self)
|
||||
self.node_info_display = NodeInfo(self.app, self)
|
||||
|
||||
self.known_nodes_display.delegate = self
|
||||
|
||||
@@ -752,11 +899,12 @@ class NetworkDisplay():
|
||||
|
||||
self.left_area = self.left_pile
|
||||
self.right_area = self.browser.display_widget
|
||||
self.right_area_width = 1-NetworkDisplay.list_width
|
||||
|
||||
self.columns = urwid.Columns(
|
||||
[
|
||||
("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
|
||||
)
|
||||
@@ -793,8 +941,12 @@ class NetworkDisplay():
|
||||
|
||||
def start(self):
|
||||
self.local_peer_display.start()
|
||||
self.node_info_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):
|
||||
return self.shortcuts_display
|
||||
|
||||
3
nomadnet/vendor/quotes.py
vendored
@@ -1,4 +1,5 @@
|
||||
quotes = [
|
||||
("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")
|
||||
]
|
||||