Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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.
|
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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|||||||
|
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 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,10 +88,14 @@ 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)
|
||||||
fh = open(file_path, "rb")
|
if os.access(file_path, os.X_OK):
|
||||||
response_data = fh.read()
|
generated = subprocess.run([file_path], stdout=subprocess.PIPE)
|
||||||
fh.close()
|
return generated.stdout
|
||||||
return response_data
|
else:
|
||||||
|
fh = open(file_path, "rb")
|
||||||
|
response_data = fh.read()
|
||||||
|
fh.close()
|
||||||
|
return response_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
RNS.log("Error occurred while handling request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_ERROR)
|
RNS.log("Error occurred while handling request "+RNS.prettyhexrep(request_id)+" for: "+str(path), RNS.LOG_ERROR)
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.0.8"
|
__version__ = "0.0.9"
|
||||||
@@ -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,11 +166,21 @@ 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("", RNS.LOG_WARNING, _override_destination = True)
|
||||||
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("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)
|
||||||
|
|
||||||
self.loop.run()
|
self.loop.run()
|
||||||
|
|||||||
@@ -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,10 +100,18 @@ 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):
|
||||||
self.link_target = link_target
|
if self.status == Browser.DONE:
|
||||||
self.app.ui.loop.set_alarm_in(0.1, self.marked_link_job)
|
self.link_target = link_target
|
||||||
|
self.app.ui.loop.set_alarm_in(0.1, self.marked_link_job)
|
||||||
|
|
||||||
def marked_link_job(self, sender, event):
|
def marked_link_job(self, sender, event):
|
||||||
link_target = self.link_target
|
link_target = self.link_target
|
||||||
@@ -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 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.destination_hash:
|
if destination_hash != self.loopback:
|
||||||
self.download_file(destination_hash, path)
|
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:
|
else:
|
||||||
RNS.log("Cannot request file download from a node that is not currently connected.", RNS.LOG_ERROR)
|
self.download_local_file(path)
|
||||||
RNS.log("The requested URL was: "+str(url), RNS.LOG_ERROR)
|
|
||||||
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):
|
||||||
load_thread = threading.Thread(target=self.__load)
|
cached = self.get_cached(self.current_url())
|
||||||
load_thread.setDaemon(True)
|
if cached:
|
||||||
load_thread.start()
|
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):
|
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 = ""
|
||||||
|
|
||||||
@@ -568,4 +846,18 @@ def size_str(num, suffix='B'):
|
|||||||
return "%.2f%s%s" % (num, unit, suffix)
|
return "%.2f%s%s" % (num, unit, suffix)
|
||||||
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)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
@@ -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":
|
||||||
|
|||||||
@@ -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,102 @@ 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
|
||||||
|
display_name = self.app.node.name
|
||||||
|
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 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"
|
def connect_query(sender):
|
||||||
pile = urwid.Pile([
|
self.parent.browser.retrieve_url(RNS.hexrep(self.app.node.destination.hash, delimit=False))
|
||||||
urwid.Text("\n"+g["info"], align="center"),
|
|
||||||
urwid.Text("\nNode Hosting currently unavailable\n\n", align="center"),
|
if NodeInfo.announce_timer == None:
|
||||||
urwid.Padding(urwid.Button("Back", on_press=show_peer_info), "center", "pack")
|
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)
|
||||||
|
|
||||||
|
widget_style = ""
|
||||||
|
if self.app.enable_node:
|
||||||
|
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("\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
|
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):
|
||||||
|
self.t_last_announce.start()
|
||||||
|
self.t_active_links.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -712,14 +849,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 +870,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 +892,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 +934,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
|
||||||
|
|||||||
3
nomadnet/vendor/quotes.py
vendored
@@ -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")
|
||||||
]
|
]
|
||||||
2
setup.py
@@ -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',
|
||||||
)
|
)
|
||||||